diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bff49ac4..0d23b1f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: pull_request: branches: - main - - fix/update-acctests + - "hotfix/**" paths-ignore: - 'README.md' @@ -16,7 +16,8 @@ on: - 'README.md' branches: - main - - fix/update-acctests + - fix/update-acctests-v2 + # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: @@ -126,7 +127,8 @@ jobs: fail-fast: false matrix: terraform: - - '1.3.*' + - '1.4.*' + - '1.5.*' - 'latest' steps: diff --git a/docs/data-sources/connectors.md b/docs/data-sources/connectors.md index 75887715..36e104a3 100644 --- a/docs/data-sources/connectors.md +++ b/docs/data-sources/connectors.md @@ -13,7 +13,14 @@ Connectors provide connectivity to Remote Networks. For more information, see Tw ## Example Usage ```terraform -data "twingate_connectors" "all" {} +data "twingate_connectors" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} ``` @@ -22,6 +29,12 @@ data "twingate_connectors" "all" {} ### Optional - `connectors` (Attributes List) List of Connectors (see [below for nested schema](#nestedatt--connectors)) +- `name` (String) Returns only connectors that exactly match this name. If no options are passed it will return all connectors. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the connector. +- `name_exclude` (String) Match when the value does not exist in the name of the connector. +- `name_prefix` (String) The name of the connector must start with the value. +- `name_regexp` (String) The regular expression match of the name of the connector. +- `name_suffix` (String) The name of the connector must end with the value. ### Read-Only diff --git a/docs/data-sources/groups.md b/docs/data-sources/groups.md index b5496be0..ea9e5e4f 100644 --- a/docs/data-sources/groups.md +++ b/docs/data-sources/groups.md @@ -15,6 +15,11 @@ Groups are how users are authorized to access Resources. For more information, s ```terraform data "twingate_groups" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } # Group names are not constrained to be unique within Twingate, @@ -28,8 +33,13 @@ data "twingate_groups" "foo" { - `groups` (Attributes List) List of Groups (see [below for nested schema](#nestedatt--groups)) - `is_active` (Boolean) Returns only Groups matching the specified state. -- `name` (String) Returns only Groups that exactly match this name. -- `type` (String) Returns only Groups of the specified type (valid: `MANUAL`, `SYNCED`, `SYSTEM`). +- `name` (String) Returns only groups that exactly match this name. If no options are passed it will return all resources. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the group. +- `name_exclude` (String) Match when the value does not exist in the name of the group. +- `name_prefix` (String) The name of the group must start with the value. +- `name_regexp` (String) The regular expression match of the name of the group. +- `name_suffix` (String) The name of the group must end with the value. +- `types` (Set of String) Returns groups that match a list of types. valid types: `MANUAL`, `SYNCED`, `SYSTEM`. ### Read-Only diff --git a/docs/data-sources/remote_networks.md b/docs/data-sources/remote_networks.md index eb79a4a9..266eddcb 100644 --- a/docs/data-sources/remote_networks.md +++ b/docs/data-sources/remote_networks.md @@ -19,6 +19,15 @@ data "twingate_remote_networks" "all" {} ## Schema +### Optional + +- `name` (String) Returns only remote networks that exactly match this name. If no options are passed it will return all remote networks. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the remote network. +- `name_exclude` (String) Match when the value does not exist in the name of the remote network. +- `name_prefix` (String) The name of the remote network must start with the value. +- `name_regexp` (String) The regular expression match of the name of the remote network. +- `name_suffix` (String) The name of the remote network must end with the value. + ### Read-Only - `id` (String) The ID of this resource. @@ -27,8 +36,11 @@ data "twingate_remote_networks" "all" {} ### Nested Schema for `remote_networks` +Optional: + +- `name` (String) The name of the Remote Network. + Read-Only: - `id` (String) The ID of the Remote Network. - `location` (String) The location of the Remote Network. Must be one of the following: AWS, AZURE, GOOGLE_CLOUD, ON_PREMISE, OTHER. -- `name` (String) The name of the Remote Network diff --git a/docs/data-sources/resources.md b/docs/data-sources/resources.md index 9db92f72..85c064ab 100644 --- a/docs/data-sources/resources.md +++ b/docs/data-sources/resources.md @@ -15,6 +15,11 @@ Resources in Twingate represent servers on the private network that clients can ```terraform data "twingate_resources" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } # Resource names are not constrained to be unique within Twingate, @@ -24,9 +29,14 @@ data "twingate_resources" "foo" { ## Schema -### Required +### Optional -- `name` (String) The name of the Resource +- `name` (String) Returns only resources that exactly match this name. If no options are passed it will return all resources. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the resource. +- `name_exclude` (String) Match when the value does not exist in the name of the resource. +- `name_prefix` (String) The name of the resource must start with the value. +- `name_regexp` (String) The regular expression match of the name of the resource. +- `name_suffix` (String) The name of the resource must end with the value. ### Read-Only diff --git a/docs/data-sources/security_policies.md b/docs/data-sources/security_policies.md index df4c8dd4..b720ea15 100644 --- a/docs/data-sources/security_policies.md +++ b/docs/data-sources/security_policies.md @@ -13,7 +13,14 @@ Security Policies are defined in the Twingate Admin Console and determine user a ## Example Usage ```terraform -data "twingate_security_policies" "all" {} +data "twingate_security_policies" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} ``` @@ -21,6 +28,12 @@ data "twingate_security_policies" "all" {} ### Optional +- `name` (String) Returns only security policies that exactly match this name. If no options are passed it will return all security policies. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the security policy. +- `name_exclude` (String) Match when the value does not exist in the name of the security policy. +- `name_prefix` (String) The name of the security policy must start with the value. +- `name_regexp` (String) The regular expression match of the name of the security policy. +- `name_suffix` (String) The name of the security policy must end with the value. - `security_policies` (Attributes List) (see [below for nested schema](#nestedatt--security_policies)) ### Read-Only diff --git a/docs/data-sources/service_accounts.md b/docs/data-sources/service_accounts.md index dcbb3996..ffcc1394 100644 --- a/docs/data-sources/service_accounts.md +++ b/docs/data-sources/service_accounts.md @@ -15,6 +15,11 @@ Service Accounts offer a way to provide programmatic, centrally-controlled, and ```terraform data "twingate_service_accounts" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } ``` @@ -23,7 +28,12 @@ data "twingate_service_accounts" "foo" { ### Optional -- `name` (String) Filter results by the name of the Service Account. +- `name` (String) Returns only service accounts that exactly match this name. If no options are passed it will return all service accounts. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the service account. +- `name_exclude` (String) Match when the value does not exist in the name of the service account. +- `name_prefix` (String) The name of the service account must start with the value. +- `name_regexp` (String) The regular expression match of the name of the service account. +- `name_suffix` (String) The name of the service account must end with the value. ### Read-Only diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index 6097a835..4f97f008 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -29,7 +29,6 @@ data "twingate_user" "foo" { - `email` (String) The email address of the User - `first_name` (String) The first name of the User -- `is_admin` (Boolean, Deprecated) Indicates whether the User is an admin - `last_name` (String) The last name of the User - `role` (String) Indicates the User's role. Either ADMIN, DEVOPS, SUPPORT, or MEMBER - `type` (String) Indicates the User's type. Either MANUAL or SYNCED. diff --git a/docs/data-sources/users.md b/docs/data-sources/users.md index b8192533..40352a81 100644 --- a/docs/data-sources/users.md +++ b/docs/data-sources/users.md @@ -13,12 +13,57 @@ Users in Twingate can be given access to Twingate Resources and may either be ad ## Example Usage ```terraform -data "twingate_users" "all" {} +data "twingate_users" "all" { + # email = "" + # email_regexp = "" + # email_contains = "" + # email_exclude = "" + # email_prefix = "" + # email_suffix = "" + + # first_name = "" + # first_name_regexp = "" + # first_name_contains = "" + # first_name_exclude = "" + # first_name_prefix = "" + # first_name_suffix = "" + + # last_name = "" + # last_name_regexp = "" + # last_name_contains = "" + # last_name_exclude = "" + # last_name_prefix = "" + # last_name_suffix = "" + + # roles = ["ADMIN", "DEVOPS", "SUPPORT", "MEMBER"] +} ``` ## Schema +### Optional + +- `email` (String) Returns only users that exactly match this email. +- `email_contains` (String) Match when the value exist in the email of the user. +- `email_exclude` (String) Match when the value does not exist in the email of the user. +- `email_prefix` (String) The email of the user must start with the value. +- `email_regexp` (String) The regular expression match of the email of the user. +- `email_suffix` (String) The email of the user must end with the value. +- `first_name` (String) Returns only users that exactly match the first name. +- `first_name_contains` (String) Match when the value exist in the first name of the user. +- `first_name_exclude` (String) Match when the value does not exist in the first name of the user. +- `first_name_prefix` (String) The first name of the user must start with the value. +- `first_name_regexp` (String) The regular expression match of the first name of the user. +- `first_name_suffix` (String) The first name of the user must end with the value. +- `last_name` (String) Returns only users that exactly match the last name. +- `last_name_contains` (String) Match when the value exist in the last name of the user. +- `last_name_exclude` (String) Match when the value does not exist in the last name of the user. +- `last_name_prefix` (String) The last name of the user must start with the value. +- `last_name_regexp` (String) The regular expression match of the last name of the user. +- `last_name_suffix` (String) The last name of the user must end with the value. +- `roles` (Set of String) Returns users that match a list of roles. Valid roles: `ADMIN`, `DEVOPS`, `SUPPORT`, `MEMBER`. + ### Read-Only - `id` (String) The ID of this resource. @@ -32,7 +77,6 @@ Read-Only: - `email` (String) The email address of the User - `first_name` (String) The first name of the User - `id` (String) The ID of the User -- `is_admin` (Boolean, Deprecated) Indicates whether the User is an admin - `last_name` (String) The last name of the User - `role` (String) Indicates the User's role. Either ADMIN, DEVOPS, SUPPORT, or MEMBER. - `type` (String) Indicates the User's type. Either MANUAL or SYNCED. diff --git a/docs/guides/migration-v1-to-v2-guide.md b/docs/guides/migration-v1-to-v2-guide.md new file mode 100644 index 00000000..fbecac4a --- /dev/null +++ b/docs/guides/migration-v1-to-v2-guide.md @@ -0,0 +1,73 @@ +--- +subcategory: "migration" +page_title: "v1 to v2 Migration Guide" +description: |- +This document covers how to migrate from v1 to v2 of the Twingate Terraform provider. +--- + +# Migration Guide +j +This guide covers how to migrate from v1 to v2 of the Twingate Terraform provider. Migration needs to be done for the following objects: +- Resources + - `twingate_resource` +- Data sources + - `twingate_user` + - `twingate_users` + +## Migrating Resources + +The `protocols` attribute in the `twingate_resource` Resource has been changed from a block to an object. + +In v1, the following was valid: + +```terraform +resource "twingate_resource" "resource" { + name = "resource" + address = "internal.int" + remote_network_id = twingate_remote_network.aws_network.id + + protocols { + allow_icmp = true + tcp { + policy = "RESTRICTED" + ports = ["80", "82-83"] + } + udp { + policy = "ALLOW_ALL" + } + } +} +``` + +The `protocols`, `tcp` and `udp` attributes were blocks and not objects. In v2, these are now objects: + +``` +protocols { -> protocols = { +tcp { -> tcp = { +udp { -> udp = { +``` + +In v2, the above resource needs to be rewritten like this: + +```terraform +resource "twingate_resource" "resource" { + name = "resource" + address = "internal.int" + remote_network_id = twingate_remote_network.aws_network.id + + protocols = { + allow_icmp = true + tcp = { + policy = "RESTRICTED" + ports = ["80", "82-83"] + } + udp = { + policy = "ALLOW_ALL" + } + } +} +``` + +## Migrating data sources + +The attribute `is_admin` has been removed from the `twingate_user` and `twingate_users` data sources. Similar information is now available via the [`role` attribute](https://registry.terraform.io/providers/Twingate/twingate/latest/docs/data-sources/users#role). diff --git a/docs/resources/connector.md b/docs/resources/connector.md index 8754a145..fcafcc75 100644 --- a/docs/resources/connector.md +++ b/docs/resources/connector.md @@ -24,6 +24,7 @@ resource "twingate_remote_network" "aws_network" { resource "twingate_connector" "aws_connector" { remote_network_id = twingate_remote_network.aws_network.id + status_updates_enabled = true } ``` @@ -37,7 +38,7 @@ resource "twingate_connector" "aws_connector" { ### Optional - `name` (String) Name of the Connector, if not provided one will be generated. -- `status_updates_enabled` (Boolean) Determines whether status notifications are enabled for the Connector. +- `status_updates_enabled` (Boolean) Determines whether status notifications are enabled for the Connector. Default is `true`. ### Read-Only diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 7c5a03e1..7d85643f 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -30,18 +30,24 @@ resource "twingate_service_account" "github_actions_prod" { name = "Github Actions PROD" } +data "twingate_security_policy" "test_policy" { + name = "Test Policy" +} + resource "twingate_resource" "resource" { name = "network" address = "internal.int" remote_network_id = twingate_remote_network.aws_network.id - protocols { + security_policy_id = data.twingate_security_policy.test_policy.id + + protocols = { allow_icmp = true - tcp { + tcp = { policy = "RESTRICTED" ports = ["80", "82-83"] } - udp { + udp = { policy = "ALLOW_ALL" } } @@ -50,6 +56,8 @@ resource "twingate_resource" "resource" { group_ids = [twingate_group.aws.id] service_account_ids = [twingate_service_account.github_actions_prod.id] } + + is_active = true } ``` @@ -64,12 +72,14 @@ resource "twingate_resource" "resource" { ### Optional -- `access` (Block List, Max: 1) Restrict access to certain groups or service accounts (see [below for nested schema](#nestedblock--access)) +- `access` (Block List) Restrict access to certain groups or service accounts (see [below for nested schema](#nestedblock--access)) - `alias` (String) Set a DNS alias address for the Resource. Must be a DNS-valid name string. +- `is_active` (Boolean) Set the resource as active or inactive. Default is `true`. - `is_authoritative` (Boolean) Determines whether assignments in the access block will override any existing assignments. Default is `true`. If set to `false`, assignments made outside of Terraform will be ignored. -- `is_browser_shortcut_enabled` (Boolean) Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client. -- `is_visible` (Boolean) Controls whether this Resource will be visible in the main Resource list in the Twingate Client. -- `protocols` (Block List, Max: 1) Restrict access to certain protocols and ports. By default or when this argument is not defined, there is no restriction, and all protocols and ports are allowed. (see [below for nested schema](#nestedblock--protocols)) +- `is_browser_shortcut_enabled` (Boolean) Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client. Default is `false`. +- `is_visible` (Boolean) Controls whether this Resource will be visible in the main Resource list in the Twingate Client. Default is `true`. +- `protocols` (Attributes) Restrict access to certain protocols and ports. By default or when this argument is not defined, there is no restriction, and all protocols and ports are allowed. (see [below for nested schema](#nestedatt--protocols)) +- `security_policy_id` (String) The ID of a `twingate_security_policy` to set as this Resource's Security Policy. Default is `Default Policy`. ### Read-Only @@ -84,40 +94,31 @@ Optional: - `service_account_ids` (Set of String) List of Service Account IDs that will have permission to access the Resource. - + ### Nested Schema for `protocols` -Required: - -- `tcp` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--protocols--tcp)) -- `udp` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--protocols--udp)) - Optional: - `allow_icmp` (Boolean) Whether to allow ICMP (ping) traffic +- `tcp` (Attributes) (see [below for nested schema](#nestedatt--protocols--tcp)) +- `udp` (Attributes) (see [below for nested schema](#nestedatt--protocols--udp)) - + ### Nested Schema for `protocols.tcp` -Required: - -- `policy` (String) Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `RESTRICTED` (only listed ports are allowed), `ALLOW_ALL`, or `DENY_ALL` - Optional: -- `ports` (List of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port +- `policy` (String) Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `RESTRICTED` (only listed ports are allowed), `ALLOW_ALL`, or `DENY_ALL` +- `ports` (Set of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port - + ### Nested Schema for `protocols.udp` -Required: - -- `policy` (String) Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `RESTRICTED` (only listed ports are allowed), `ALLOW_ALL`, or `DENY_ALL` - Optional: -- `ports` (List of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port +- `policy` (String) Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `RESTRICTED` (only listed ports are allowed), `ALLOW_ALL`, or `DENY_ALL` +- `ports` (Set of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port ## Import diff --git a/examples/data-sources/twingate_connectors/data-source.tf b/examples/data-sources/twingate_connectors/data-source.tf index 5b9e6a33..ea592c4d 100644 --- a/examples/data-sources/twingate_connectors/data-source.tf +++ b/examples/data-sources/twingate_connectors/data-source.tf @@ -1 +1,8 @@ -data "twingate_connectors" "all" {} +data "twingate_connectors" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} diff --git a/examples/data-sources/twingate_groups/data-source.tf b/examples/data-sources/twingate_groups/data-source.tf index ae9a45b2..51b79a12 100644 --- a/examples/data-sources/twingate_groups/data-source.tf +++ b/examples/data-sources/twingate_groups/data-source.tf @@ -1,5 +1,10 @@ data "twingate_groups" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } # Group names are not constrained to be unique within Twingate, diff --git a/examples/data-sources/twingate_resources/data-source.tf b/examples/data-sources/twingate_resources/data-source.tf index bb0b7c39..89091f1b 100644 --- a/examples/data-sources/twingate_resources/data-source.tf +++ b/examples/data-sources/twingate_resources/data-source.tf @@ -1,5 +1,10 @@ data "twingate_resources" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } # Resource names are not constrained to be unique within Twingate, diff --git a/examples/data-sources/twingate_security_policies/data-source.tf b/examples/data-sources/twingate_security_policies/data-source.tf index e6f30b17..24e3a604 100644 --- a/examples/data-sources/twingate_security_policies/data-source.tf +++ b/examples/data-sources/twingate_security_policies/data-source.tf @@ -1 +1,8 @@ -data "twingate_security_policies" "all" {} +data "twingate_security_policies" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} diff --git a/examples/data-sources/twingate_service_accounts/data-source.tf b/examples/data-sources/twingate_service_accounts/data-source.tf index 17924121..93be3d44 100644 --- a/examples/data-sources/twingate_service_accounts/data-source.tf +++ b/examples/data-sources/twingate_service_accounts/data-source.tf @@ -1,3 +1,8 @@ data "twingate_service_accounts" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } diff --git a/examples/data-sources/twingate_users/data-source.tf b/examples/data-sources/twingate_users/data-source.tf index d3b1e5a5..fa707720 100644 --- a/examples/data-sources/twingate_users/data-source.tf +++ b/examples/data-sources/twingate_users/data-source.tf @@ -1 +1,24 @@ -data "twingate_users" "all" {} +data "twingate_users" "all" { + # email = "" + # email_regexp = "" + # email_contains = "" + # email_exclude = "" + # email_prefix = "" + # email_suffix = "" + + # first_name = "" + # first_name_regexp = "" + # first_name_contains = "" + # first_name_exclude = "" + # first_name_prefix = "" + # first_name_suffix = "" + + # last_name = "" + # last_name_regexp = "" + # last_name_contains = "" + # last_name_exclude = "" + # last_name_prefix = "" + # last_name_suffix = "" + + # roles = ["ADMIN", "DEVOPS", "SUPPORT", "MEMBER"] +} diff --git a/examples/resources/twingate_connector/resource.tf b/examples/resources/twingate_connector/resource.tf index 1fceaed3..49bd461c 100644 --- a/examples/resources/twingate_connector/resource.tf +++ b/examples/resources/twingate_connector/resource.tf @@ -9,4 +9,5 @@ resource "twingate_remote_network" "aws_network" { resource "twingate_connector" "aws_connector" { remote_network_id = twingate_remote_network.aws_network.id + status_updates_enabled = true } \ No newline at end of file diff --git a/examples/resources/twingate_resource/resource.tf b/examples/resources/twingate_resource/resource.tf index 7c5ae842..d1cbb6f4 100644 --- a/examples/resources/twingate_resource/resource.tf +++ b/examples/resources/twingate_resource/resource.tf @@ -15,18 +15,24 @@ resource "twingate_service_account" "github_actions_prod" { name = "Github Actions PROD" } +data "twingate_security_policy" "test_policy" { + name = "Test Policy" +} + resource "twingate_resource" "resource" { name = "network" address = "internal.int" remote_network_id = twingate_remote_network.aws_network.id - protocols { + security_policy_id = data.twingate_security_policy.test_policy.id + + protocols = { allow_icmp = true - tcp { + tcp = { policy = "RESTRICTED" ports = ["80", "82-83"] } - udp { + udp = { policy = "ALLOW_ALL" } } @@ -35,5 +41,7 @@ resource "twingate_resource" "resource" { group_ids = [twingate_group.aws.id] service_account_ids = [twingate_service_account.github_actions_prod.id] } + + is_active = true } diff --git a/go.mod b/go.mod index ca45cb5c..d58f1abb 100644 --- a/go.mod +++ b/go.mod @@ -5,67 +5,67 @@ go 1.21 require ( github.com/client9/misspell v0.3.4 github.com/google/go-cmp v0.6.0 - github.com/hashicorp/go-retryablehttp v0.7.4 + github.com/hashicorp/go-retryablehttp v0.7.5 github.com/hashicorp/go-uuid v1.0.3 - github.com/hashicorp/terraform-plugin-docs v0.16.0 - github.com/hashicorp/terraform-plugin-framework v1.4.2 + github.com/hashicorp/terraform-plugin-docs v0.18.0 + github.com/hashicorp/terraform-plugin-framework v1.5.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 - github.com/hashicorp/terraform-plugin-go v0.19.0 - github.com/hashicorp/terraform-plugin-mux v0.12.0 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 - github.com/hashicorp/terraform-plugin-testing v1.5.1 - github.com/hasura/go-graphql-client v0.10.0 + github.com/hashicorp/terraform-plugin-go v0.21.0 + github.com/hashicorp/terraform-plugin-testing v1.6.0 + github.com/hasura/go-graphql-client v0.11.0 github.com/iancoleman/strcase v0.3.0 github.com/jarcoal/httpmock v1.3.1 github.com/mattn/goveralls v0.0.12 - github.com/securego/gosec/v2 v2.18.2 + github.com/securego/gosec/v2 v2.19.0 github.com/stretchr/testify v1.8.4 gotest.tools/gotestsum v1.11.0 ) require ( + github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.2 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bitfield/gotestdox v0.2.1 // indirect - github.com/ccojocar/zxcvbn-go v1.0.1 // indirect - github.com/cloudflare/circl v1.3.3 // indirect + github.com/ccojocar/zxcvbn-go v1.0.2 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dnephin/pflag v1.0.7 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect + github.com/hashicorp/cli v1.1.6 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.5.1 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hc-install v0.6.0 // indirect - github.com/hashicorp/hcl/v2 v2.18.0 // indirect + github.com/hashicorp/hc-install v0.6.2 // indirect + github.com/hashicorp/hcl/v2 v2.19.1 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.19.0 // indirect - github.com/hashicorp/terraform-json v0.17.1 // indirect + github.com/hashicorp/terraform-exec v0.20.0 // indirect + github.com/hashicorp/terraform-json v0.21.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.2 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 // indirect + github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect - github.com/huandu/xstrings v1.3.2 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.15 // indirect - github.com/klauspost/compress v1.16.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mitchellh/cli v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect @@ -78,24 +78,26 @@ require ( github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - github.com/zclconf/go-cty v1.14.0 // indirect - golang.org/x/crypto v0.14.0 // indirect + github.com/yuin/goldmark v1.6.0 // indirect + github.com/yuin/goldmark-meta v1.1.0 // indirect + github.com/zclconf/go-cty v1.14.1 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.14.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect - google.golang.org/grpc v1.57.1 // indirect - google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/grpc v1.61.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - nhooyr.io/websocket v1.8.7 // indirect + nhooyr.io/websocket v1.8.10 // indirect ) diff --git a/go.sum b/go.sum index 7b84e27a..76c3e66e 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,22 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= +github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= -github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= -github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= @@ -28,12 +26,15 @@ github.com/bitfield/gotestdox v0.2.1/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8 github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4= -github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= +github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -41,76 +42,55 @@ github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= -github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= -github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk= +github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo= github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g= +github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8= +github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -127,52 +107,49 @@ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k= -github.com/hashicorp/go-plugin v1.5.1/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= -github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= -github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.6.0 h1:fDHnU7JNFNSQebVKYhHZ0va1bC6SrPQ8fpebsvNr2w4= -github.com/hashicorp/hc-install v0.6.0/go.mod h1:10I912u3nntx9Umo1VAeYPUUuehk0aRQJYpMwbX5wQA= -github.com/hashicorp/hcl/v2 v2.18.0 h1:wYnG7Lt31t2zYkcquwgKo6MWXzRUDIeIVU5naZwHLl8= -github.com/hashicorp/hcl/v2 v2.18.0/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +github.com/hashicorp/hc-install v0.6.2 h1:V1k+Vraqz4olgZ9UzKiAcbman9i9scg9GgSt/U3mw/M= +github.com/hashicorp/hc-install v0.6.2/go.mod h1:2JBpd+NCFKiHiu/yYCGaPyPHhZLxXTpz8oreHa/a3Ps= +github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= +github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM= -github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= -github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA= -github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= -github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI= -github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA= -github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI= -github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY= +github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo= +github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= +github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= +github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= +github.com/hashicorp/terraform-plugin-docs v0.18.0 h1:2bINhzXc+yDeAcafurshCrIjtdu1XHn9zZ3ISuEhgpk= +github.com/hashicorp/terraform-plugin-docs v0.18.0/go.mod h1:iIUfaJpdUmpi+rI42Kgq+63jAjI8aZVTyxp3Bvk9Hg8= +github.com/hashicorp/terraform-plugin-framework v1.5.0 h1:8kcvqJs/x6QyOFSdeAyEgsenVOUeC/IyKpi2ul4fjTg= +github.com/hashicorp/terraform-plugin-framework v1.5.0/go.mod h1:6waavirukIlFpVpthbGd2PUNYaFedB0RwW3MDzJ/rtc= github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= -github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= -github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec= +github.com/hashicorp/terraform-plugin-go v0.21.0 h1:VSjdVQYNDKR0l2pi3vsFK1PdMQrw6vGOshJXMNFeVc0= +github.com/hashicorp/terraform-plugin-go v0.21.0/go.mod h1:piJp8UmO1uupCvC9/H74l2C6IyKG0rW4FDedIpwW5RQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-mux v0.12.0 h1:TJlmeslQ11WlQtIFAfth0vXx+gSNgvMEng2Rn9z3WZY= -github.com/hashicorp/terraform-plugin-mux v0.12.0/go.mod h1:8MR0AgmV+Q03DIjyrAKxXyYlq2EUnYBQP8gxAAA0zeM= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 h1:wcOKYwPI9IorAJEBLzgclh3xVolO7ZorYd6U1vnok14= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0/go.mod h1:qH/34G25Ugdj5FcM95cSoXzUgIbgfhVLXCcEcYaMwq8= -github.com/hashicorp/terraform-plugin-testing v1.5.1 h1:T4aQh9JAhmWo4+t1A7x+rnxAJHCDIYW9kXyo4sVO92c= -github.com/hashicorp/terraform-plugin-testing v1.5.1/go.mod h1:dg8clO6K59rZ8w9EshBmDp1CxTIPu3yA4iaDpX1h5u0= -github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= -github.com/hashicorp/terraform-registry-address v0.2.2/go.mod h1:LtwNbCihUoUZ3RYriyS2wF/lGPB6gF9ICLRtuDk7hSo= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 h1:Bl3e2ei2j/Z3Hc2HIS15Gal2KMKyLAZ2om1HCEvK6es= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0/go.mod h1:i2C41tszDjiWfziPQDL5R/f3Zp0gahXe5No/MIO9rCE= +github.com/hashicorp/terraform-plugin-testing v1.6.0 h1:Wsnfh+7XSVRfwcr2jZYHsnLOnZl7UeaOBvsx6dl/608= +github.com/hashicorp/terraform-plugin-testing v1.6.0/go.mod h1:cJGG0/8j9XhHaJZRC+0sXFI4uzqQZ9Az4vh6C4GJpFE= +github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= +github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hasura/go-graphql-client v0.10.0 h1:eQm/ap/rqxMG6yAGe6J+FkXu1VqJ9p21E63vz0A7zLQ= -github.com/hasura/go-graphql-client v0.10.0/go.mod h1:z9UPkMmCBMuJjvBEtdE6F+oTR2r15AcjirVNq/8P+Ig= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hasura/go-graphql-client v0.11.0 h1:EFEkpMZlkq5gLZj9oiI6TnHCOHV1oErxOroMc5qUHQI= +github.com/hasura/go-graphql-client v0.11.0/go.mod h1:eNNnmHAp6NgwKZ4xRbZEfywxr07qk34Y0QhbPsYIfhw= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -184,13 +161,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -200,26 +172,23 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/goveralls v0.0.12 h1:PEEeF0k1SsTjOBQ8FOmrOAoCu4ytuMaWCnWe94zxbCg= github.com/mattn/goveralls v0.0.12/go.mod h1:44ImGEUfmqH8bBtaMrYKsM65LXfNLWmwaxFGjZwgMSQ= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= -github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= -github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -232,58 +201,48 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= -github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= -github.com/securego/gosec/v2 v2.18.2 h1:DkDt3wCiOtAHf1XkiXZBhQ6m6mK/b9T/wD257R3/c+I= -github.com/securego/gosec/v2 v2.18.2/go.mod h1:xUuqSF6i0So56Y2wwohWAmB07EdBkUN6crbLlHwbyJs= +github.com/securego/gosec/v2 v2.19.0 h1:gl5xMkOI0/E6Hxx0XCY2XujA3V7SNSefA8sC+3f1gnk= +github.com/securego/gosec/v2 v2.19.0/go.mod h1:hOkDcHz9J/XIgIlPDXalxjeVYsHxoWUc5zJSHxcB8YM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= -github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -292,19 +251,22 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHg github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc= -github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= +github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= +github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= +github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U= golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -313,10 +275,9 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -327,16 +288,16 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -359,8 +320,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -369,20 +330,20 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= @@ -390,39 +351,36 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg= -google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/gotestsum v1.11.0 h1:A88/QWw7acMjZH1dMe6KZFhw32odUOIjCiAU/Q4n3mI= gotest.tools/gotestsum v1.11.0/go.mod h1:cUOKgFEvWAP0twchmiOvdzX0SBZX0UI58bGRpRIu4xs= gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= +nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/golangci.yml b/golangci.yml index df04e6c0..4ac2e03a 100644 --- a/golangci.yml +++ b/golangci.yml @@ -22,6 +22,8 @@ linters-settings: - github.com/hashicorp/terraform-plugin-framework/datasource.DataSource - github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.Set - github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.Bool + - github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.String + - github.com/hashicorp/terraform-plugin-testing/plancheck.PlanCheck errcheck: check-type-assertions: false check-blank: false diff --git a/main.go b/main.go index 561a0aaa..e2805d77 100644 --- a/main.go +++ b/main.go @@ -6,52 +6,27 @@ import ( "log" "github.com/Twingate/terraform-provider-twingate/twingate" - twingateV2 "github.com/Twingate/terraform-provider-twingate/twingate/v2" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server" - "github.com/hashicorp/terraform-plugin-framework/providerserver" - "github.com/hashicorp/terraform-plugin-mux/tf5to6server" - "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" ) var ( version = "dev" ) +const registry = "registry.terraform.io/Twingate/twingate" + func main() { var debug bool flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers") flag.Parse() - ctx := context.Background() - upgradedSdkProvider, err := tf5to6server.UpgradeServer(ctx, twingate.Provider(version).GRPCProvider) - if err != nil { - log.Fatal(err) - } - providers := []func() tfprotov6.ProviderServer{ - func() tfprotov6.ProviderServer { - return upgradedSdkProvider + err := providerserver.Serve(context.Background(), twingate.New(version), + providerserver.ServeOpts{ + Debug: debug, + Address: registry, + ProtocolVersion: 6, }, - providerserver.NewProtocol6(twingateV2.New(version)()), - } - - muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) - - if err != nil { - log.Fatal(err) - } - - var serveOpts []tf6server.ServeOpt - if debug { - serveOpts = append(serveOpts, tf6server.WithManagedDebug()) - } - - err = tf6server.Serve( - "registry.terraform.io/Twingate/twingate", - muxServer.ProviderServer, - serveOpts..., ) if err != nil { diff --git a/templates/guides/migration-v1-to-v2-guide.md.tmpl b/templates/guides/migration-v1-to-v2-guide.md.tmpl new file mode 100644 index 00000000..fbecac4a --- /dev/null +++ b/templates/guides/migration-v1-to-v2-guide.md.tmpl @@ -0,0 +1,73 @@ +--- +subcategory: "migration" +page_title: "v1 to v2 Migration Guide" +description: |- +This document covers how to migrate from v1 to v2 of the Twingate Terraform provider. +--- + +# Migration Guide +j +This guide covers how to migrate from v1 to v2 of the Twingate Terraform provider. Migration needs to be done for the following objects: +- Resources + - `twingate_resource` +- Data sources + - `twingate_user` + - `twingate_users` + +## Migrating Resources + +The `protocols` attribute in the `twingate_resource` Resource has been changed from a block to an object. + +In v1, the following was valid: + +```terraform +resource "twingate_resource" "resource" { + name = "resource" + address = "internal.int" + remote_network_id = twingate_remote_network.aws_network.id + + protocols { + allow_icmp = true + tcp { + policy = "RESTRICTED" + ports = ["80", "82-83"] + } + udp { + policy = "ALLOW_ALL" + } + } +} +``` + +The `protocols`, `tcp` and `udp` attributes were blocks and not objects. In v2, these are now objects: + +``` +protocols { -> protocols = { +tcp { -> tcp = { +udp { -> udp = { +``` + +In v2, the above resource needs to be rewritten like this: + +```terraform +resource "twingate_resource" "resource" { + name = "resource" + address = "internal.int" + remote_network_id = twingate_remote_network.aws_network.id + + protocols = { + allow_icmp = true + tcp = { + policy = "RESTRICTED" + ports = ["80", "82-83"] + } + udp = { + policy = "ALLOW_ALL" + } + } +} +``` + +## Migrating data sources + +The attribute `is_admin` has been removed from the `twingate_user` and `twingate_users` data sources. Similar information is now available via the [`role` attribute](https://registry.terraform.io/providers/Twingate/twingate/latest/docs/data-sources/users#role). diff --git a/twingate/internal/attr/common.go b/twingate/internal/attr/common.go index c10e2d15..2adebf6c 100644 --- a/twingate/internal/attr/common.go +++ b/twingate/internal/attr/common.go @@ -6,4 +6,10 @@ const ( RemoteNetworkID = "remote_network_id" Type = "type" IsActive = "is_active" + + FilterByRegexp = "_regexp" + FilterByContains = "_contains" + FilterByExclude = "_exclude" + FilterByPrefix = "_prefix" + FilterBySuffix = "_suffix" ) diff --git a/twingate/internal/attr/group.go b/twingate/internal/attr/group.go index 6666e582..bec304c6 100644 --- a/twingate/internal/attr/group.go +++ b/twingate/internal/attr/group.go @@ -5,4 +5,5 @@ const ( SecurityPolicyID = "security_policy_id" Groups = "groups" Alias = "alias" + Types = "types" ) diff --git a/twingate/internal/attr/helper.go b/twingate/internal/attr/helper.go index 788e7d72..8500f8a5 100644 --- a/twingate/internal/attr/helper.go +++ b/twingate/internal/attr/helper.go @@ -5,6 +5,7 @@ import "strings" const ( attrFirstElement = ".0" attrPathSeparator = ".0." + attrSeparator = "." attrLenSymbol = ".#" ) @@ -18,10 +19,24 @@ func First(attributes ...string) string { return attr + attrFirstElement } +func FirstAttr(attributes ...string) string { + attr := PathAttr(attributes...) + + if attr == "" { + return "" + } + + return attr + attrFirstElement +} + func Path(attributes ...string) string { return strings.Join(attributes, attrPathSeparator) } +func PathAttr(attributes ...string) string { + return strings.Join(attributes, attrSeparator) +} + func Len(attributes ...string) string { attr := Path(attributes...) @@ -31,3 +46,13 @@ func Len(attributes ...string) string { return attr + attrLenSymbol } + +func LenAttr(attributes ...string) string { + attr := PathAttr(attributes...) + + if attr == "" { + return "" + } + + return attr + attrLenSymbol +} diff --git a/twingate/internal/attr/user.go b/twingate/internal/attr/user.go index f9a9804a..1259f27d 100644 --- a/twingate/internal/attr/user.go +++ b/twingate/internal/attr/user.go @@ -4,8 +4,8 @@ const ( FirstName = "first_name" LastName = "last_name" Email = "email" - IsAdmin = "is_admin" Role = "role" + Roles = "roles" Users = "users" SendInvite = "send_invite" State = "state" diff --git a/twingate/internal/client/client.go b/twingate/internal/client/client.go index ddcf7eac..d29480f1 100644 --- a/twingate/internal/client/client.go +++ b/twingate/internal/client/client.go @@ -92,15 +92,15 @@ func newTransport(underlineRoundTripper http.RoundTripper, apiToken, version, co } func twingateAgentVersion(version string) string { - return fmt.Sprintf("TwingateTF/%s", version) + return "TwingateTF/" + version } func (s *serverURL) newGraphqlServerURL() string { - return fmt.Sprintf("%s/api/graphql/", s.url) + return s.url + "/api/graphql/" } func (s *serverURL) newAPIServerURL() string { - return fmt.Sprintf("%s/api/v4", s.url) + return s.url + "/api/v4" } type serverURL struct { diff --git a/twingate/internal/client/connector-tokens.go b/twingate/internal/client/connector-tokens.go index a24c2d33..8da17adf 100644 --- a/twingate/internal/client/connector-tokens.go +++ b/twingate/internal/client/connector-tokens.go @@ -2,7 +2,6 @@ package client import ( "context" - "fmt" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client/query" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" @@ -16,7 +15,7 @@ func (client *Client) VerifyConnectorTokens(ctx context.Context, refreshToken, a } headers := map[string]string{ - "Authorization": fmt.Sprintf("Bearer %s", accessToken), + "Authorization": "Bearer " + accessToken, } _, err := client.post(ctx, "/connector/validate_tokens", payload, headers) diff --git a/twingate/internal/client/connector.go b/twingate/internal/client/connector.go index 3c37c2e9..efc0d7d6 100644 --- a/twingate/internal/client/connector.go +++ b/twingate/internal/client/connector.go @@ -76,10 +76,11 @@ func (client *Client) ReadConnector(ctx context.Context, connectorID string) (*m return response.ToModel(), nil } -func (client *Client) ReadConnectors(ctx context.Context) ([]*model.Connector, error) { +func (client *Client) ReadConnectors(ctx context.Context, name, filter string) ([]*model.Connector, error) { opr := resourceConnector.read() variables := newVars( + gqlNullable(query.NewConnectorFilterInput(name, filter), "filter"), cursor(query.CursorConnectors), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/query/connectors-read.go b/twingate/internal/client/query/connectors-read.go index fdeb5fb1..815e841c 100644 --- a/twingate/internal/client/query/connectors-read.go +++ b/twingate/internal/client/query/connectors-read.go @@ -8,7 +8,7 @@ import ( const CursorConnectors = "connectorsEndCursor" type ReadConnectors struct { - Connectors `graphql:"connectors(after: $connectorsEndCursor, first: $pageLimit)"` + Connectors `graphql:"connectors(filter: $filter, after: $connectorsEndCursor, first: $pageLimit)"` } type Connectors struct { @@ -36,3 +36,13 @@ func (c Connectors) ToModel() []*model.Connector { return edge.Node.ToModel() }) } + +type ConnectorFilterInput struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewConnectorFilterInput(name, filter string) *ConnectorFilterInput { + return &ConnectorFilterInput{ + Name: NewStringFilterOperationInput(name, filter), + } +} diff --git a/twingate/internal/client/query/groups-read.go b/twingate/internal/client/query/groups-read.go index e9c255d2..31f42aab 100644 --- a/twingate/internal/client/query/groups-read.go +++ b/twingate/internal/client/query/groups-read.go @@ -1,6 +1,7 @@ package query import ( + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" ) @@ -36,7 +37,38 @@ type GroupFilterInput struct { } type StringFilterOperationInput struct { - Eq string `json:"eq"` + Eq *string `json:"eq"` + Ne *string `json:"ne"` + StartsWith *string `json:"startsWith"` + EndsWith *string `json:"endsWith"` + Regexp *string `json:"regexp"` + Contains *string `json:"contains"` + In []string `json:"in"` +} + +func NewStringFilterOperationInput(name, filter string) *StringFilterOperationInput { + if filter == "" && name == "" { + return nil + } + + var stringFilter StringFilterOperationInput + + switch filter { + case attr.FilterByRegexp: + stringFilter.Regexp = &name + case attr.FilterByContains: + stringFilter.Contains = &name + case attr.FilterByExclude: + stringFilter.Ne = &name + case attr.FilterByPrefix: + stringFilter.StartsWith = &name + case attr.FilterBySuffix: + stringFilter.EndsWith = &name + default: + stringFilter.Eq = &name + } + + return &stringFilter } type GroupTypeFilterOperatorInput struct { @@ -65,13 +97,11 @@ func NewGroupFilterInput(input *model.GroupsFilter) *GroupFilterInput { } if input.Name != nil { - filter.Name = &StringFilterOperationInput{ - Eq: *input.Name, - } + filter.Name = NewStringFilterOperationInput(*input.Name, input.NameFilter) } - if input.Type != nil { - filter.Type.In = []string{*input.Type} + if len(input.Types) > 0 { + filter.Type.In = input.Types } if input.IsActive != nil { diff --git a/twingate/internal/client/query/query_test.go b/twingate/internal/client/query/query_test.go index 9720cbae..5bec2b09 100644 --- a/twingate/internal/client/query/query_test.go +++ b/twingate/internal/client/query/query_test.go @@ -847,14 +847,14 @@ func TestBuildGroupsFilter(t *testing.T) { filter: &model.GroupsFilter{Name: optionalString("Group")}, expected: &GroupFilterInput{ Name: &StringFilterOperationInput{ - Eq: "Group", + Eq: optionalString("Group"), }, Type: defaultType, IsActive: defaultActive, }, }, { - filter: &model.GroupsFilter{Type: optionalString("MANUAL")}, + filter: &model.GroupsFilter{Types: []string{"MANUAL"}}, expected: &GroupFilterInput{ Type: GroupTypeFilterOperatorInput{ In: []string{model.GroupTypeManual}, @@ -863,7 +863,7 @@ func TestBuildGroupsFilter(t *testing.T) { }, }, { - filter: &model.GroupsFilter{Type: optionalString("SYSTEM")}, + filter: &model.GroupsFilter{Types: []string{"SYSTEM"}}, expected: &GroupFilterInput{ Type: GroupTypeFilterOperatorInput{ In: []string{model.GroupTypeSystem}, @@ -872,7 +872,7 @@ func TestBuildGroupsFilter(t *testing.T) { }, }, { - filter: &model.GroupsFilter{Type: optionalString("SYNCED")}, + filter: &model.GroupsFilter{Types: []string{"SYNCED"}}, expected: &GroupFilterInput{ Type: GroupTypeFilterOperatorInput{ In: []string{model.GroupTypeSynced}, @@ -896,7 +896,7 @@ func TestBuildGroupsFilter(t *testing.T) { }, { filter: &model.GroupsFilter{ - Type: optionalString("SYSTEM"), + Types: []string{"SYSTEM"}, IsActive: optionalBool(false), }, expected: &GroupFilterInput{ @@ -908,7 +908,7 @@ func TestBuildGroupsFilter(t *testing.T) { }, { filter: &model.GroupsFilter{ - Type: optionalString("MANUAL"), + Types: []string{"MANUAL"}, IsActive: optionalBool(true), }, expected: &GroupFilterInput{ @@ -920,7 +920,7 @@ func TestBuildGroupsFilter(t *testing.T) { }, { filter: &model.GroupsFilter{ - Type: optionalString("MANUAL"), + Types: []string{"MANUAL"}, IsActive: optionalBool(false), }, expected: &GroupFilterInput{ diff --git a/twingate/internal/client/query/remote-networks-read.go b/twingate/internal/client/query/remote-networks-read.go index 7f014c5d..cf6bdc2c 100644 --- a/twingate/internal/client/query/remote-networks-read.go +++ b/twingate/internal/client/query/remote-networks-read.go @@ -8,7 +8,7 @@ import ( const CursorRemoteNetworks = "remoteNetworksEndCursor" type ReadRemoteNetworks struct { - RemoteNetworks `graphql:"remoteNetworks(after: $remoteNetworksEndCursor, first: $pageLimit)"` + RemoteNetworks `graphql:"remoteNetworks(filter: $filter, after: $remoteNetworksEndCursor, first: $pageLimit)"` } func (q ReadRemoteNetworks) IsEmpty() bool { @@ -28,3 +28,13 @@ func (r RemoteNetworks) ToModel() []*model.RemoteNetwork { return edge.Node.ToModel() }) } + +type RemoteNetworkFilterInput struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewRemoteNetworkFilterInput(name, filter string) *RemoteNetworkFilterInput { + return &RemoteNetworkFilterInput{ + Name: NewStringFilterOperationInput(name, filter), + } +} diff --git a/twingate/internal/client/query/resource-create.go b/twingate/internal/client/query/resource-create.go index 17c697b5..7611a8e2 100644 --- a/twingate/internal/client/query/resource-create.go +++ b/twingate/internal/client/query/resource-create.go @@ -1,7 +1,7 @@ package query type CreateResource struct { - ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, groupIds: $groupIds, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias)"` + ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, groupIds: $groupIds, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` } func (q CreateResource) IsEmpty() bool { diff --git a/twingate/internal/client/query/resource-read.go b/twingate/internal/client/query/resource-read.go index d641b291..83087561 100644 --- a/twingate/internal/client/query/resource-read.go +++ b/twingate/internal/client/query/resource-read.go @@ -56,6 +56,7 @@ type ResourceNode struct { IsVisible bool IsBrowserShortcutEnabled bool Alias string + SecurityPolicy *gqlSecurityPolicy } type Protocols struct { @@ -90,6 +91,11 @@ func (r gqlResource) ToModel() *model.Resource { } func (r ResourceNode) ToModel() *model.Resource { + var securityPolicy string + if r.SecurityPolicy != nil { + securityPolicy = string(r.SecurityPolicy.ID) + } + return &model.Resource{ ID: string(r.ID), Name: r.Name, @@ -100,6 +106,7 @@ func (r ResourceNode) ToModel() *model.Resource { IsVisible: &r.IsVisible, IsBrowserShortcutEnabled: &r.IsBrowserShortcutEnabled, Alias: optionalString(r.Alias), + SecurityPolicyID: optionalString(securityPolicy), } } diff --git a/twingate/internal/client/query/resource-update.go b/twingate/internal/client/query/resource-update.go index 419b4f57..48ae871b 100644 --- a/twingate/internal/client/query/resource-update.go +++ b/twingate/internal/client/query/resource-update.go @@ -1,7 +1,7 @@ package query type UpdateResource struct { - ResourceEntityResponse `graphql:"resourceUpdate(id: $id, name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias)"` + ResourceEntityResponse `graphql:"resourceUpdate(id: $id, name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId, isActive: $isActive)"` } func (q UpdateResource) IsEmpty() bool { diff --git a/twingate/internal/client/query/resources-by-name-read.go b/twingate/internal/client/query/resources-by-name-read.go index 09a9220a..82331937 100644 --- a/twingate/internal/client/query/resources-by-name-read.go +++ b/twingate/internal/client/query/resources-by-name-read.go @@ -1,9 +1,19 @@ package query type ReadResourcesByName struct { - Resources `graphql:"resources(filter: {name: {eq: $name}}, after: $resourcesEndCursor, first: $pageLimit)"` + Resources `graphql:"resources(filter: $filter, after: $resourcesEndCursor, first: $pageLimit)"` } func (q ReadResourcesByName) IsEmpty() bool { return len(q.Edges) == 0 } + +type ResourceFilterInput struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewResourceFilterInput(name, filter string) *ResourceFilterInput { + return &ResourceFilterInput{ + Name: NewStringFilterOperationInput(name, filter), + } +} diff --git a/twingate/internal/client/query/security-policies-read.go b/twingate/internal/client/query/security-policies-read.go index b3319b62..70e14a3c 100644 --- a/twingate/internal/client/query/security-policies-read.go +++ b/twingate/internal/client/query/security-policies-read.go @@ -8,7 +8,7 @@ import ( const CursorPolicies = "policiesEndCursor" type ReadSecurityPolicies struct { - SecurityPolicies `graphql:"securityPolicies(after: $policiesEndCursor, first: $pageLimit)"` + SecurityPolicies `graphql:"securityPolicies(filter: $filter, after: $policiesEndCursor, first: $pageLimit)"` } func (q ReadSecurityPolicies) IsEmpty() bool { @@ -29,3 +29,13 @@ func (q ReadSecurityPolicies) ToModel() []*model.SecurityPolicy { return edge.Node.ToModel() }) } + +type SecurityPolicyFilterField struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewSecurityPolicyFilterField(name, filter string) *SecurityPolicyFilterField { + return &SecurityPolicyFilterField{ + Name: NewStringFilterOperationInput(name, filter), + } +} diff --git a/twingate/internal/client/query/service-accounts-read.go b/twingate/internal/client/query/service-accounts-read.go index 9e861ab5..1f0f0cc3 100644 --- a/twingate/internal/client/query/service-accounts-read.go +++ b/twingate/internal/client/query/service-accounts-read.go @@ -99,21 +99,15 @@ func IsGqlKeyActive(item *GqlKeyIDEdge) bool { } type ServiceAccountFilterInput struct { - Name StringFilter `json:"name"` + Name *StringFilterOperationInput `json:"name"` } -type StringFilter struct { - Eq string `json:"eq"` -} - -func NewServiceAccountFilterInput(name string) *ServiceAccountFilterInput { +func NewServiceAccountFilterInput(name, filter string) *ServiceAccountFilterInput { if name == "" { return nil } return &ServiceAccountFilterInput{ - Name: StringFilter{ - Eq: name, - }, + Name: NewStringFilterOperationInput(name, filter), } } diff --git a/twingate/internal/client/query/users-read.go b/twingate/internal/client/query/users-read.go index f4b79336..1d60e6d4 100644 --- a/twingate/internal/client/query/users-read.go +++ b/twingate/internal/client/query/users-read.go @@ -8,7 +8,7 @@ import ( const CursorUsers = "usersEndCursor" type ReadUsers struct { - Users `graphql:"users(after: $usersEndCursor, first: $pageLimit)"` + Users `graphql:"users(filter: $filter, after: $usersEndCursor, first: $pageLimit)"` } func (q ReadUsers) IsEmpty() bool { @@ -28,3 +28,14 @@ func (u Users) ToModel() []*model.User { return edge.Node.ToModel() }) } + +type UserFilterInput struct { + FirstName *StringFilterOperationInput `json:"firstName"` + LastName *StringFilterOperationInput `json:"lastName"` + Email *StringFilterOperationInput `json:"email"` + Role *UserRoleFilterOperationInput `json:"role"` +} + +type UserRoleFilterOperationInput struct { + In []UserRole `json:"in"` +} diff --git a/twingate/internal/client/remote-network.go b/twingate/internal/client/remote-network.go index 32a59cfe..0b4e51d2 100644 --- a/twingate/internal/client/remote-network.go +++ b/twingate/internal/client/remote-network.go @@ -30,10 +30,11 @@ func (client *Client) CreateRemoteNetwork(ctx context.Context, req *model.Remote return response.ToModel(), nil } -func (client *Client) ReadRemoteNetworks(ctx context.Context) ([]*model.RemoteNetwork, error) { +func (client *Client) ReadRemoteNetworks(ctx context.Context, name, filter string) ([]*model.RemoteNetwork, error) { opr := resourceRemoteNetwork.read() variables := newVars( + gqlNullable(query.NewRemoteNetworkFilterInput(name, filter), "filter"), cursor(query.CursorRemoteNetworks), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index 8518c30e..ae07541c 100644 --- a/twingate/internal/client/resource.go +++ b/twingate/internal/client/resource.go @@ -70,6 +70,7 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), + gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -92,6 +93,10 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) resource.IsBrowserShortcutEnabled = nil } + if input.SecurityPolicyID == nil { + resource.SecurityPolicyID = nil + } + return resource, nil } @@ -177,9 +182,11 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) gqlVar(input.Name, "name"), gqlVar(input.Address, "address"), gqlVar(newProtocolsInput(input.Protocols), "protocols"), + gqlVar(input.IsActive, "isActive"), gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), + gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -204,6 +211,10 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) resource.IsBrowserShortcutEnabled = nil } + if input.SecurityPolicyID == nil { + resource.SecurityPolicyID = nil + } + return resource, nil } @@ -232,11 +243,11 @@ func (client *Client) UpdateResourceActiveState(ctx context.Context, resource *m return client.mutate(ctx, &response, variables, opr, attr{id: resource.ID}) } -func (client *Client) ReadResourcesByName(ctx context.Context, name string) ([]*model.Resource, error) { +func (client *Client) ReadResourcesByName(ctx context.Context, name, filter string) ([]*model.Resource, error) { opr := resourceResource.read() variables := newVars( - gqlVar(name, "name"), + gqlNullable(query.NewResourceFilterInput(name, filter), "filter"), cursor(query.CursorResources), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/security-policy.go b/twingate/internal/client/security-policy.go index e51c1f1f..405e4bd4 100644 --- a/twingate/internal/client/security-policy.go +++ b/twingate/internal/client/security-policy.go @@ -30,10 +30,11 @@ func (client *Client) ReadSecurityPolicy(ctx context.Context, securityPolicyID, return response.ToModel(), nil } -func (client *Client) ReadSecurityPolicies(ctx context.Context) ([]*model.SecurityPolicy, error) { +func (client *Client) ReadSecurityPolicies(ctx context.Context, name, filter string) ([]*model.SecurityPolicy, error) { opr := resourceSecurityPolicy.read() variables := newVars( + gqlNullable(query.NewSecurityPolicyFilterField(name, filter), "filter"), cursor(query.CursorPolicies), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/service-account.go b/twingate/internal/client/service-account.go index f8ef61e7..7f85163d 100644 --- a/twingate/internal/client/service-account.go +++ b/twingate/internal/client/service-account.go @@ -119,13 +119,17 @@ func (client *Client) readServiceAccountsAfter(ctx context.Context, variables ma func (client *Client) ReadServiceAccounts(ctx context.Context, input ...string) ([]*model.ServiceAccount, error) { opr := resourceServiceAccount.read() - var name string + var name, filter string if len(input) > 0 { name = input[0] } + if len(input) > 1 { + filter = input[1] + } + variables := newVars( - gqlNullable(query.NewServiceAccountFilterInput(name), "filter"), + gqlNullable(query.NewServiceAccountFilterInput(name, filter), "filter"), cursor(query.CursorServices), cursor(query.CursorResources), cursor(query.CursorServiceKeys), diff --git a/twingate/internal/client/user.go b/twingate/internal/client/user.go index 49e0b309..5fa91d82 100644 --- a/twingate/internal/client/user.go +++ b/twingate/internal/client/user.go @@ -6,12 +6,56 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/client/query" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" ) -func (client *Client) ReadUsers(ctx context.Context) ([]*model.User, error) { +type StringFilter struct { + Name string + Filter string +} + +type UsersFilter struct { + Email *StringFilter + FirstName *StringFilter + LastName *StringFilter + Roles []string +} + +func NewUserFilterInput(filter *UsersFilter) *query.UserFilterInput { + if filter == nil { + return nil + } + + queryFilter := &query.UserFilterInput{} + + if filter.FirstName != nil { + queryFilter.FirstName = query.NewStringFilterOperationInput(filter.FirstName.Name, filter.FirstName.Filter) + } + + if filter.LastName != nil { + queryFilter.LastName = query.NewStringFilterOperationInput(filter.LastName.Name, filter.LastName.Filter) + } + + if filter.Email != nil { + queryFilter.Email = query.NewStringFilterOperationInput(filter.Email.Name, filter.Email.Filter) + } + + if len(filter.Roles) > 0 { + queryFilter.Role = &query.UserRoleFilterOperationInput{ + In: utils.Map(filter.Roles, func(item string) query.UserRole { + return query.UserRole(item) + }), + } + } + + return queryFilter +} + +func (client *Client) ReadUsers(ctx context.Context, filter *UsersFilter) ([]*model.User, error) { opr := resourceUser.read() variables := newVars( + gqlNullable(NewUserFilterInput(filter), "filter"), cursor(query.CursorUsers), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/variables.go b/twingate/internal/client/variables.go index b9e6a945..e76daa91 100644 --- a/twingate/internal/client/variables.go +++ b/twingate/internal/client/variables.go @@ -105,6 +105,7 @@ func getValue(val any) any { } } +//nolint:unparam func gqlNullableID(val interface{}, name string) gqlVarOption { return func(values map[string]interface{}) map[string]interface{} { var ( @@ -112,6 +113,10 @@ func gqlNullableID(val interface{}, name string) gqlVarOption { defaultID *graphql.ID ) + if value, ok := val.(*string); ok && value != nil { + val = *value + } + if isZeroValue(val) { gqlValue = defaultID } else { diff --git a/twingate/internal/model/group.go b/twingate/internal/model/group.go index f25dcdca..9d6538e0 100644 --- a/twingate/internal/model/group.go +++ b/twingate/internal/model/group.go @@ -37,9 +37,10 @@ func (g Group) ToTerraform() interface{} { } type GroupsFilter struct { - Name *string - Type *string - IsActive *bool + Name *string + NameFilter string + Types []string + IsActive *bool } func (f *GroupsFilter) HasName() bool { diff --git a/twingate/internal/model/resource.go b/twingate/internal/model/resource.go index c073bb60..8990ca17 100644 --- a/twingate/internal/model/resource.go +++ b/twingate/internal/model/resource.go @@ -34,6 +34,7 @@ type Resource struct { IsVisible *bool IsBrowserShortcutEnabled *bool Alias *string + SecurityPolicyID *string } func (r Resource) AccessToTerraform() []interface{} { diff --git a/twingate/internal/model/user.go b/twingate/internal/model/user.go index 8bbe20ce..02b17c78 100644 --- a/twingate/internal/model/user.go +++ b/twingate/internal/model/user.go @@ -42,17 +42,12 @@ func (u User) GetName() string { return u.Email } -func (u User) IsAdmin() bool { - return u.Role == UserRoleAdmin -} - func (u User) ToTerraform() interface{} { return map[string]interface{}{ attr.ID: u.ID, attr.FirstName: u.FirstName, attr.LastName: u.LastName, attr.Email: u.Email, - attr.IsAdmin: u.IsAdmin(), attr.Role: u.Role, attr.Type: u.Type, } diff --git a/twingate/internal/provider/datasource/connectors.go b/twingate/internal/provider/datasource/connectors.go index a00b986a..fd12f5e9 100644 --- a/twingate/internal/provider/datasource/connectors.go +++ b/twingate/internal/provider/datasource/connectors.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrConnectorsDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.") + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &connectors{} @@ -24,8 +26,14 @@ type connectors struct { } type connectorsModel struct { - ID types.String `tfsdk:"id"` - Connectors []connectorModel `tfsdk:"connectors"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + NameRegexp types.String `tfsdk:"name_regexp"` + NameContains types.String `tfsdk:"name_contains"` + NameExclude types.String `tfsdk:"name_exclude"` + NamePrefix types.String `tfsdk:"name_prefix"` + NameSuffix types.String `tfsdk:"name_suffix"` + Connectors []connectorModel `tfsdk:"connectors"` } func (d *connectors) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -50,6 +58,7 @@ func (d *connectors) Configure(ctx context.Context, req datasource.ConfigureRequ d.client = client } +//nolint:funlen func (d *connectors) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Connectors provide connectivity to Remote Networks. For more information, see Twingate's [documentation](https://docs.twingate.com/docs/understanding-access-nodes).", @@ -59,6 +68,31 @@ func (d *connectors) Schema(ctx context.Context, req datasource.SchemaRequest, r Description: computedDatasourceIDDescription, }, + attr.Name: schema.StringAttribute{ + Optional: true, + Description: "Returns only connectors that exactly match this name. If no options are passed it will return all connectors. Only one option can be used at a time.", + }, + attr.Name + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the name of the connector.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the connector.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the connector.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the connector must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the connector must end with the value.", + }, + // computed attr.Connectors: schema.ListNestedAttribute{ Computed: true, @@ -89,18 +123,63 @@ func (d *connectors) Schema(ctx context.Context, req datasource.SchemaRequest, r } } +//nolint:cyclop func (d *connectors) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - connectors, err := d.client.ReadConnectors(ctx) + var data connectorsModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var name, filter string + + if data.Name.ValueString() != "" { + name = data.Name.ValueString() + } + + if data.NameRegexp.ValueString() != "" { + name = data.NameRegexp.ValueString() + filter = attr.FilterByRegexp + } + + if data.NameContains.ValueString() != "" { + name = data.NameContains.ValueString() + filter = attr.FilterByContains + } + + if data.NameExclude.ValueString() != "" { + name = data.NameExclude.ValueString() + filter = attr.FilterByExclude + } + + if data.NamePrefix.ValueString() != "" { + name = data.NamePrefix.ValueString() + filter = attr.FilterByPrefix + } + + if data.NameSuffix.ValueString() != "" { + name = data.NameSuffix.ValueString() + filter = attr.FilterBySuffix + } + + if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrConnectorsDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) + + return + } + + connectors, err := d.client.ReadConnectors(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateConnectors) return } - data := connectorsModel{ - ID: types.StringValue("all-connectors"), - Connectors: convertConnectorsToTerraform(connectors), - } + data.ID = types.StringValue("all-connectors") + data.Connectors = convertConnectorsToTerraform(connectors) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/twingate/internal/provider/datasource/converter.go b/twingate/internal/provider/datasource/converter.go index 7a519fa3..9ed03a3e 100644 --- a/twingate/internal/provider/datasource/converter.go +++ b/twingate/internal/provider/datasource/converter.go @@ -47,7 +47,6 @@ func convertUsersToTerraform(users []*model.User) []userModel { FirstName: types.StringValue(user.FirstName), LastName: types.StringValue(user.LastName), Email: types.StringValue(user.Email), - IsAdmin: types.BoolValue(user.IsAdmin()), Role: types.StringValue(user.Role), Type: types.StringValue(user.Type), } diff --git a/twingate/internal/provider/datasource/converter_test.go b/twingate/internal/provider/datasource/converter_test.go index c767268f..72fda413 100644 --- a/twingate/internal/provider/datasource/converter_test.go +++ b/twingate/internal/provider/datasource/converter_test.go @@ -107,7 +107,6 @@ func TestConverterUsersToTerraform(t *testing.T) { FirstName: types.StringValue("Name"), LastName: types.StringValue("Last"), Email: types.StringValue("user@email.com"), - IsAdmin: types.BoolValue(false), Role: types.StringValue("USER"), Type: types.StringValue("SYNCED"), }, @@ -116,7 +115,6 @@ func TestConverterUsersToTerraform(t *testing.T) { FirstName: types.StringValue("Admin"), LastName: types.StringValue("Last"), Email: types.StringValue("admin@email.com"), - IsAdmin: types.BoolValue(true), Role: types.StringValue(model.UserRoleAdmin), Type: types.StringValue("MANUAL"), }, diff --git a/twingate/internal/provider/datasource/groups.go b/twingate/internal/provider/datasource/groups.go index c0370a5e..0a6b4dca 100644 --- a/twingate/internal/provider/datasource/groups.go +++ b/twingate/internal/provider/datasource/groups.go @@ -8,13 +8,18 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + tfattr "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrGroupsDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.") + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &groups{} @@ -27,11 +32,16 @@ type groups struct { } type groupsModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Type types.String `tfsdk:"type"` - IsActive types.Bool `tfsdk:"is_active"` - Groups []groupModel `tfsdk:"groups"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + NameRegexp types.String `tfsdk:"name_regexp"` + NameContains types.String `tfsdk:"name_contains"` + NameExclude types.String `tfsdk:"name_exclude"` + NamePrefix types.String `tfsdk:"name_prefix"` + NameSuffix types.String `tfsdk:"name_suffix"` + Types types.Set `tfsdk:"types"` + IsActive types.Bool `tfsdk:"is_active"` + Groups []groupModel `tfsdk:"groups"` } func (d *groups) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -56,6 +66,7 @@ func (d *groups) Configure(ctx context.Context, req datasource.ConfigureRequest, d.client = client } +//nolint:funlen func (d *groups) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Groups are how users are authorized to access Resources. For more information, see Twingate's [documentation](https://docs.twingate.com/docs/groups).", @@ -66,17 +77,38 @@ func (d *groups) Schema(ctx context.Context, req datasource.SchemaRequest, resp }, attr.Name: schema.StringAttribute{ Optional: true, - Description: "Returns only Groups that exactly match this name.", + Description: "Returns only groups that exactly match this name. If no options are passed it will return all resources. Only one option can be used at a time.", + }, + attr.Name + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the name of the group.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the group.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the group.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the group must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the group must end with the value.", }, attr.IsActive: schema.BoolAttribute{ Optional: true, Description: "Returns only Groups matching the specified state.", }, - attr.Type: schema.StringAttribute{ + attr.Types: schema.SetAttribute{ Optional: true, - Description: fmt.Sprintf("Returns only Groups of the specified type (valid: `%s`, `%s`, `%s`).", model.GroupTypeManual, model.GroupTypeSynced, model.GroupTypeSystem), - Validators: []validator.String{ - stringvalidator.OneOf(model.GroupTypeManual, model.GroupTypeSynced, model.GroupTypeSystem), + ElementType: types.StringType, + Description: fmt.Sprintf("Returns groups that match a list of types. valid types: `%s`, `%s`, `%s`.", model.GroupTypeManual, model.GroupTypeSynced, model.GroupTypeSystem), + Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf(model.GroupTypeManual, model.GroupTypeSynced, model.GroupTypeSystem)), }, }, @@ -123,6 +155,12 @@ func (d *groups) Read(ctx context.Context, req datasource.ReadRequest, resp *dat return } + if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrGroupsDatasourceShouldSetOneOptionalNameAttribute, TwingateGroups) + + return + } + filter := buildFilter(&data) groups, err := d.client.ReadGroups(ctx, filter) @@ -145,16 +183,54 @@ func (d *groups) Read(ctx context.Context, req datasource.ReadRequest, resp *dat resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } +//nolint:cyclop func buildFilter(data *groupsModel) *model.GroupsFilter { - filter := &model.GroupsFilter{ - Name: data.Name.ValueStringPointer(), - Type: data.Type.ValueStringPointer(), - IsActive: data.IsActive.ValueBoolPointer(), + var name, filter string + + if data.Name.ValueString() != "" { + name = data.Name.ValueString() + } + + if data.NameRegexp.ValueString() != "" { + name = data.NameRegexp.ValueString() + filter = attr.FilterByRegexp + } + + if data.NameContains.ValueString() != "" { + name = data.NameContains.ValueString() + filter = attr.FilterByContains + } + + if data.NameExclude.ValueString() != "" { + name = data.NameExclude.ValueString() + filter = attr.FilterByExclude + } + + if data.NamePrefix.ValueString() != "" { + name = data.NamePrefix.ValueString() + filter = attr.FilterByPrefix + } + + if data.NameSuffix.ValueString() != "" { + name = data.NameSuffix.ValueString() + filter = attr.FilterBySuffix + } + + groupFilter := &model.GroupsFilter{ + Name: &name, + NameFilter: filter, + IsActive: data.IsActive.ValueBoolPointer(), + } + + if len(data.Types.Elements()) > 0 { + groupFilter.Types = utils.Map(data.Types.Elements(), func(item tfattr.Value) string { + return item.(types.String).ValueString() + }) } - if filter.Name == nil && filter.Type == nil && filter.IsActive == nil { + if groupFilter.Name == nil && len(groupFilter.Types) == 0 && groupFilter.IsActive == nil { return nil } - return filter + return groupFilter } diff --git a/twingate/internal/provider/datasource/helper.go b/twingate/internal/provider/datasource/helper.go index 150a1de9..6dc0d13c 100644 --- a/twingate/internal/provider/datasource/helper.go +++ b/twingate/internal/provider/datasource/helper.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" ) func addErr(diagnostics *diag.Diagnostics, err error, resource string) { @@ -16,3 +17,15 @@ func addErr(diagnostics *diag.Diagnostics, err error, resource string) { err.Error(), ) } + +func countOptionalAttributes(attributes ...types.String) int { + var count int + + for _, attr := range attributes { + if attr.ValueString() != "" { + count++ + } + } + + return count +} diff --git a/twingate/internal/provider/datasource/remote-networks.go b/twingate/internal/provider/datasource/remote-networks.go index d4c906f9..4efa3343 100644 --- a/twingate/internal/provider/datasource/remote-networks.go +++ b/twingate/internal/provider/datasource/remote-networks.go @@ -14,6 +14,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrRemoteNetworksDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.") + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &remoteNetworks{} @@ -27,6 +29,12 @@ type remoteNetworks struct { type remoteNetworksModel struct { ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + NameRegexp types.String `tfsdk:"name_regexp"` + NameContains types.String `tfsdk:"name_contains"` + NameExclude types.String `tfsdk:"name_exclude"` + NamePrefix types.String `tfsdk:"name_prefix"` + NameSuffix types.String `tfsdk:"name_suffix"` RemoteNetworks []remoteNetworkModel `tfsdk:"remote_networks"` } @@ -61,6 +69,31 @@ func (d *remoteNetworks) Schema(ctx context.Context, req datasource.SchemaReques Description: computedDatasourceIDDescription, }, + attr.Name: schema.StringAttribute{ + Optional: true, + Description: "Returns only remote networks that exactly match this name. If no options are passed it will return all remote networks. Only one option can be used at a time.", + }, + attr.Name + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the name of the remote network.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the remote network.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the remote network.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the remote network must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the remote network must end with the value.", + }, + attr.RemoteNetworks: schema.ListNestedAttribute{ Computed: true, Description: "List of Remote Networks", @@ -71,8 +104,8 @@ func (d *remoteNetworks) Schema(ctx context.Context, req datasource.SchemaReques Description: "The ID of the Remote Network.", }, attr.Name: schema.StringAttribute{ - Computed: true, - Description: "The name of the Remote Network", + Optional: true, + Description: "The name of the Remote Network.", }, attr.Location: schema.StringAttribute{ Computed: true, @@ -85,6 +118,7 @@ func (d *remoteNetworks) Schema(ctx context.Context, req datasource.SchemaReques } } +//nolint:cyclop func (d *remoteNetworks) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data remoteNetworksModel @@ -95,7 +129,44 @@ func (d *remoteNetworks) Read(ctx context.Context, req datasource.ReadRequest, r return } - networks, err := d.client.ReadRemoteNetworks(ctx) + var name, filter string + + if data.Name.ValueString() != "" { + name = data.Name.ValueString() + } + + if data.NameRegexp.ValueString() != "" { + name = data.NameRegexp.ValueString() + filter = attr.FilterByRegexp + } + + if data.NameContains.ValueString() != "" { + name = data.NameContains.ValueString() + filter = attr.FilterByContains + } + + if data.NameExclude.ValueString() != "" { + name = data.NameExclude.ValueString() + filter = attr.FilterByExclude + } + + if data.NamePrefix.ValueString() != "" { + name = data.NamePrefix.ValueString() + filter = attr.FilterByPrefix + } + + if data.NameSuffix.ValueString() != "" { + name = data.NameSuffix.ValueString() + filter = attr.FilterBySuffix + } + + if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrRemoteNetworksDatasourceShouldSetOneOptionalNameAttribute, TwingateRemoteNetworks) + + return + } + + networks, err := d.client.ReadRemoteNetworks(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateRemoteNetworks) diff --git a/twingate/internal/provider/datasource/resources.go b/twingate/internal/provider/datasource/resources.go index 83969bda..27a24330 100644 --- a/twingate/internal/provider/datasource/resources.go +++ b/twingate/internal/provider/datasource/resources.go @@ -13,6 +13,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrResourcesDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.") + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &resources{} @@ -25,9 +27,14 @@ type resources struct { } type resourcesModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Resources []resourceModel `tfsdk:"resources"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + NameRegexp types.String `tfsdk:"name_regexp"` + NameContains types.String `tfsdk:"name_contains"` + NameExclude types.String `tfsdk:"name_exclude"` + NamePrefix types.String `tfsdk:"name_prefix"` + NameSuffix types.String `tfsdk:"name_suffix"` + Resources []resourceModel `tfsdk:"resources"` } func (d *resources) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -69,6 +76,7 @@ func protocolSchema() schema.SingleNestedAttribute { } } +//nolint:funlen func (d *resources) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Resources in Twingate represent servers on the private network that clients can connect to. Resources can be defined by IP, CIDR range, FQDN, or DNS zone. For more information, see the Twingate [documentation](https://docs.twingate.com/docs/resources-and-access-nodes).", @@ -78,10 +86,29 @@ func (d *resources) Schema(ctx context.Context, req datasource.SchemaRequest, re Description: computedDatasourceIDDescription, }, attr.Name: schema.StringAttribute{ - Required: true, - Description: "The name of the Resource", + Optional: true, + Description: "Returns only resources that exactly match this name. If no options are passed it will return all resources. Only one option can be used at a time.", + }, + attr.Name + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the name of the resource.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the resource.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the resource.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the resource must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the resource must end with the value.", }, - // computed attr.Resources: schema.ListNestedAttribute{ Computed: true, @@ -123,6 +150,7 @@ func (d *resources) Schema(ctx context.Context, req datasource.SchemaRequest, re } } +//nolint:cyclop func (d *resources) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data resourcesModel @@ -133,14 +161,51 @@ func (d *resources) Read(ctx context.Context, req datasource.ReadRequest, resp * return } - resources, err := d.client.ReadResourcesByName(ctx, data.Name.ValueString()) + var name, filter string + + if data.Name.ValueString() != "" { + name = data.Name.ValueString() + } + + if data.NameRegexp.ValueString() != "" { + name = data.NameRegexp.ValueString() + filter = attr.FilterByRegexp + } + + if data.NameContains.ValueString() != "" { + name = data.NameContains.ValueString() + filter = attr.FilterByContains + } + + if data.NameExclude.ValueString() != "" { + name = data.NameExclude.ValueString() + filter = attr.FilterByExclude + } + + if data.NamePrefix.ValueString() != "" { + name = data.NamePrefix.ValueString() + filter = attr.FilterByPrefix + } + + if data.NameSuffix.ValueString() != "" { + name = data.NameSuffix.ValueString() + filter = attr.FilterBySuffix + } + + if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrResourcesDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) + + return + } + + resources, err := d.client.ReadResourcesByName(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateResources) return } - data.ID = types.StringValue("query resources by name: " + data.Name.ValueString()) + data.ID = types.StringValue("query resources by name: " + name) data.Resources = convertResourcesToTerraform(resources) // Save data into Terraform state diff --git a/twingate/internal/provider/datasource/security-policies.go b/twingate/internal/provider/datasource/security-policies.go index c814f672..d531a017 100644 --- a/twingate/internal/provider/datasource/security-policies.go +++ b/twingate/internal/provider/datasource/security-policies.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrSecurityPoliciesDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.") + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &securityPolicies{} @@ -25,6 +27,12 @@ type securityPolicies struct { type securityPoliciesModel struct { ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + NameRegexp types.String `tfsdk:"name_regexp"` + NameContains types.String `tfsdk:"name_contains"` + NameExclude types.String `tfsdk:"name_exclude"` + NamePrefix types.String `tfsdk:"name_prefix"` + NameSuffix types.String `tfsdk:"name_suffix"` SecurityPolicies []securityPolicyModel `tfsdk:"security_policies"` } @@ -58,6 +66,30 @@ func (d *securityPolicies) Schema(ctx context.Context, req datasource.SchemaRequ Computed: true, Description: computedDatasourceIDDescription, }, + attr.Name: schema.StringAttribute{ + Optional: true, + Description: "Returns only security policies that exactly match this name. If no options are passed it will return all security policies. Only one option can be used at a time.", + }, + attr.Name + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the name of the security policy.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the security policy.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the security policy.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the security policy must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the security policy must end with the value.", + }, attr.SecurityPolicies: schema.ListNestedAttribute{ Computed: true, Optional: true, @@ -78,18 +110,63 @@ func (d *securityPolicies) Schema(ctx context.Context, req datasource.SchemaRequ } } +//nolint:cyclop func (d *securityPolicies) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - policies, err := d.client.ReadSecurityPolicies(ctx) + var data securityPoliciesModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var name, filter string + + if data.Name.ValueString() != "" { + name = data.Name.ValueString() + } + + if data.NameRegexp.ValueString() != "" { + name = data.NameRegexp.ValueString() + filter = attr.FilterByRegexp + } + + if data.NameContains.ValueString() != "" { + name = data.NameContains.ValueString() + filter = attr.FilterByContains + } + + if data.NameExclude.ValueString() != "" { + name = data.NameExclude.ValueString() + filter = attr.FilterByExclude + } + + if data.NamePrefix.ValueString() != "" { + name = data.NamePrefix.ValueString() + filter = attr.FilterByPrefix + } + + if data.NameSuffix.ValueString() != "" { + name = data.NameSuffix.ValueString() + filter = attr.FilterBySuffix + } + + if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrSecurityPoliciesDatasourceShouldSetOneOptionalNameAttribute, TwingateSecurityPolicies) + + return + } + + policies, err := d.client.ReadSecurityPolicies(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateSecurityPolicy) return } - data := securityPoliciesModel{ - ID: types.StringValue("security-policies-all"), - SecurityPolicies: convertSecurityPoliciesToTerraform(policies), - } + data.ID = types.StringValue("security-policies-all") + data.SecurityPolicies = convertSecurityPoliciesToTerraform(policies) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/twingate/internal/provider/datasource/service-accounts.go b/twingate/internal/provider/datasource/service-accounts.go index cc9b2342..0528fdef 100644 --- a/twingate/internal/provider/datasource/service-accounts.go +++ b/twingate/internal/provider/datasource/service-accounts.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrServiceAccountsDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.") + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &serviceAccounts{} @@ -26,6 +28,11 @@ type serviceAccounts struct { type serviceAccountsModel struct { ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` + NameRegexp types.String `tfsdk:"name_regexp"` + NameContains types.String `tfsdk:"name_contains"` + NameExclude types.String `tfsdk:"name_exclude"` + NamePrefix types.String `tfsdk:"name_prefix"` + NameSuffix types.String `tfsdk:"name_suffix"` ServiceAccounts []serviceAccountModel `tfsdk:"service_accounts"` } @@ -68,7 +75,27 @@ func (d *serviceAccounts) Schema(ctx context.Context, req datasource.SchemaReque }, attr.Name: schema.StringAttribute{ Optional: true, - Description: "Filter results by the name of the Service Account.", + Description: "Returns only service accounts that exactly match this name. If no options are passed it will return all service accounts. Only one option can be used at a time.", + }, + attr.Name + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the name of the service account.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the service account.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the service account.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the service account must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the service account must end with the value.", }, attr.ServiceAccounts: schema.ListNestedAttribute{ Computed: true, @@ -100,6 +127,7 @@ func (d *serviceAccounts) Schema(ctx context.Context, req datasource.SchemaReque } } +//nolint:cyclop func (d *serviceAccounts) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data serviceAccountsModel @@ -110,7 +138,44 @@ func (d *serviceAccounts) Read(ctx context.Context, req datasource.ReadRequest, return } - accounts, err := d.client.ReadServiceAccounts(ctx, data.Name.ValueString()) + var name, filter string + + if data.Name.ValueString() != "" { + name = data.Name.ValueString() + } + + if data.NameRegexp.ValueString() != "" { + name = data.NameRegexp.ValueString() + filter = attr.FilterByRegexp + } + + if data.NameContains.ValueString() != "" { + name = data.NameContains.ValueString() + filter = attr.FilterByContains + } + + if data.NameExclude.ValueString() != "" { + name = data.NameExclude.ValueString() + filter = attr.FilterByExclude + } + + if data.NamePrefix.ValueString() != "" { + name = data.NamePrefix.ValueString() + filter = attr.FilterByPrefix + } + + if data.NameSuffix.ValueString() != "" { + name = data.NameSuffix.ValueString() + filter = attr.FilterBySuffix + } + + if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrServiceAccountsDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) + + return + } + + accounts, err := d.client.ReadServiceAccounts(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateServiceAccounts) diff --git a/twingate/internal/provider/datasource/user.go b/twingate/internal/provider/datasource/user.go index 6dbdea8f..6e3532a8 100644 --- a/twingate/internal/provider/datasource/user.go +++ b/twingate/internal/provider/datasource/user.go @@ -31,7 +31,6 @@ type userModel struct { FirstName types.String `tfsdk:"first_name"` LastName types.String `tfsdk:"last_name"` Email types.String `tfsdk:"email"` - IsAdmin types.Bool `tfsdk:"is_admin"` Role types.String `tfsdk:"role"` Type types.String `tfsdk:"type"` } @@ -79,11 +78,6 @@ func (d *user) Schema(ctx context.Context, req datasource.SchemaRequest, resp *d Computed: true, Description: "The email address of the User", }, - attr.IsAdmin: schema.BoolAttribute{ - Computed: true, - Description: "Indicates whether the User is an admin", - DeprecationMessage: "This read-only Boolean value will be deprecated in a future release. You may use the `role` value instead.", - }, attr.Role: schema.StringAttribute{ Computed: true, Description: fmt.Sprintf("Indicates the User's role. Either %s, %s, %s, or %s", model.UserRoleAdmin, model.UserRoleDevops, model.UserRoleSupport, model.UserRoleMember), @@ -117,7 +111,6 @@ func (d *user) Read(ctx context.Context, req datasource.ReadRequest, resp *datas data.FirstName = types.StringValue(user.FirstName) data.LastName = types.StringValue(user.LastName) data.Email = types.StringValue(user.Email) - data.IsAdmin = types.BoolValue(user.IsAdmin()) data.Role = types.StringValue(user.Role) data.Type = types.StringValue(user.Type) diff --git a/twingate/internal/provider/datasource/users.go b/twingate/internal/provider/datasource/users.go index 7c1b3705..7f80bce3 100644 --- a/twingate/internal/provider/datasource/users.go +++ b/twingate/internal/provider/datasource/users.go @@ -9,11 +9,21 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + tfattr "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) +var ( + ErrUsersDatasourceShouldSetOneOptionalEmailAttribute = errors.New("Only one of email, email_regex, email_contains, email_exclude, email_prefix or email_suffix must be set.") + ErrUsersDatasourceShouldSetOneOptionalFirstNameAttribute = errors.New("Only one of first_name, first_name_regex, first_name_contains, first_name_exclude, first_name_prefix or first_name_suffix must be set.") + ErrUsersDatasourceShouldSetOneOptionalLastNameAttribute = errors.New("Only one of last_name, last_name_regex, last_name_contains, last_name_exclude, last_name_prefix or last_name_suffix must be set.") +) + // Ensure the implementation satisfies the desired interfaces. var _ datasource.DataSource = &users{} @@ -26,8 +36,27 @@ type users struct { } type usersModel struct { - ID types.String `tfsdk:"id"` - Users []userModel `tfsdk:"users"` + ID types.String `tfsdk:"id"` + Email types.String `tfsdk:"email"` + EmailRegexp types.String `tfsdk:"email_regexp"` + EmailContains types.String `tfsdk:"email_contains"` + EmailExclude types.String `tfsdk:"email_exclude"` + EmailPrefix types.String `tfsdk:"email_prefix"` + EmailSuffix types.String `tfsdk:"email_suffix"` + FirstName types.String `tfsdk:"first_name"` + FirstNameRegexp types.String `tfsdk:"first_name_regexp"` + FirstNameContains types.String `tfsdk:"first_name_contains"` + FirstNameExclude types.String `tfsdk:"first_name_exclude"` + FirstNamePrefix types.String `tfsdk:"first_name_prefix"` + FirstNameSuffix types.String `tfsdk:"first_name_suffix"` + LastName types.String `tfsdk:"last_name"` + LastNameRegexp types.String `tfsdk:"last_name_regexp"` + LastNameContains types.String `tfsdk:"last_name_contains"` + LastNameExclude types.String `tfsdk:"last_name_exclude"` + LastNamePrefix types.String `tfsdk:"last_name_prefix"` + LastNameSuffix types.String `tfsdk:"last_name_suffix"` + Roles types.Set `tfsdk:"roles"` + Users []userModel `tfsdk:"users"` } func (d *users) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -52,6 +81,7 @@ func (d *users) Configure(ctx context.Context, req datasource.ConfigureRequest, d.client = client } +//nolint:funlen func (d *users) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: userDescription, @@ -61,6 +91,96 @@ func (d *users) Schema(ctx context.Context, req datasource.SchemaRequest, resp * Description: computedDatasourceIDDescription, }, + // email + + attr.Email: schema.StringAttribute{ + Optional: true, + Description: "Returns only users that exactly match this email.", + }, + attr.Email + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the email of the user.", + }, + attr.Email + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the email of the user.", + }, + attr.Email + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the email of the user.", + }, + attr.Email + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The email of the user must start with the value.", + }, + attr.Email + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The email of the user must end with the value.", + }, + + // first name + + attr.FirstName: schema.StringAttribute{ + Optional: true, + Description: "Returns only users that exactly match the first name.", + }, + attr.FirstName + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the first name of the user.", + }, + attr.FirstName + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the first name of the user.", + }, + attr.FirstName + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the first name of the user.", + }, + attr.FirstName + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The first name of the user must start with the value.", + }, + attr.FirstName + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The first name of the user must end with the value.", + }, + + // last name + + attr.LastName: schema.StringAttribute{ + Optional: true, + Description: "Returns only users that exactly match the last name.", + }, + attr.LastName + attr.FilterByRegexp: schema.StringAttribute{ + Optional: true, + Description: "The regular expression match of the last name of the user.", + }, + attr.LastName + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the last name of the user.", + }, + attr.LastName + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the last name of the user.", + }, + attr.LastName + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The last name of the user must start with the value.", + }, + attr.LastName + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The last name of the user must end with the value.", + }, + + attr.Roles: schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Description: "Returns users that match a list of roles. Valid roles: `ADMIN`, `DEVOPS`, `SUPPORT`, `MEMBER`.", + Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf(model.UserRoles...)), + }, + }, + attr.Users: schema.ListNestedAttribute{ Computed: true, NestedObject: schema.NestedAttributeObject{ @@ -81,11 +201,6 @@ func (d *users) Schema(ctx context.Context, req datasource.SchemaRequest, resp * Computed: true, Description: "The email address of the User", }, - attr.IsAdmin: schema.BoolAttribute{ - Computed: true, - Description: "Indicates whether the User is an admin", - DeprecationMessage: "This read-only Boolean value will be deprecated in a future release. You may use the `role` value instead.", - }, attr.Role: schema.StringAttribute{ Computed: true, Description: fmt.Sprintf("Indicates the User's role. Either %s, %s, %s, or %s.", model.UserRoleAdmin, model.UserRoleDevops, model.UserRoleSupport, model.UserRoleMember), @@ -101,18 +216,162 @@ func (d *users) Schema(ctx context.Context, req datasource.SchemaRequest, resp * } } -func (d *users) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - users, err := d.client.ReadUsers(ctx) +func (d *users) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint + var data usersModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var email, emailFilter, firstName, firstNameFilter, lastName, lastNameFilter string + + // email + + if data.Email.ValueString() != "" { + email = data.Email.ValueString() + } + + if data.EmailRegexp.ValueString() != "" { + email = data.EmailRegexp.ValueString() + emailFilter = attr.FilterByRegexp + } + + if data.EmailContains.ValueString() != "" { + email = data.EmailContains.ValueString() + emailFilter = attr.FilterByContains + } + + if data.EmailExclude.ValueString() != "" { + email = data.EmailExclude.ValueString() + emailFilter = attr.FilterByExclude + } + + if data.EmailPrefix.ValueString() != "" { + email = data.EmailPrefix.ValueString() + emailFilter = attr.FilterByPrefix + } + + if data.EmailSuffix.ValueString() != "" { + email = data.EmailSuffix.ValueString() + emailFilter = attr.FilterBySuffix + } + + // first name + + if data.FirstName.ValueString() != "" { + firstName = data.FirstName.ValueString() + } + + if data.FirstNameRegexp.ValueString() != "" { + firstName = data.FirstNameRegexp.ValueString() + firstNameFilter = attr.FilterByRegexp + } + + if data.FirstNameContains.ValueString() != "" { + firstName = data.FirstNameContains.ValueString() + firstNameFilter = attr.FilterByContains + } + + if data.FirstNameExclude.ValueString() != "" { + firstName = data.FirstNameExclude.ValueString() + firstNameFilter = attr.FilterByExclude + } + + if data.FirstNamePrefix.ValueString() != "" { + firstName = data.FirstNamePrefix.ValueString() + firstNameFilter = attr.FilterByPrefix + } + + if data.FirstNameSuffix.ValueString() != "" { + firstName = data.FirstNameSuffix.ValueString() + firstNameFilter = attr.FilterBySuffix + } + + // last name + + if data.LastName.ValueString() != "" { + lastName = data.LastName.ValueString() + } + + if data.LastNameRegexp.ValueString() != "" { + lastName = data.LastNameRegexp.ValueString() + lastNameFilter = attr.FilterByRegexp + } + + if data.LastNameContains.ValueString() != "" { + lastName = data.LastNameContains.ValueString() + lastNameFilter = attr.FilterByContains + } + + if data.LastNameExclude.ValueString() != "" { + lastName = data.LastNameExclude.ValueString() + lastNameFilter = attr.FilterByExclude + } + + if data.LastNamePrefix.ValueString() != "" { + lastName = data.LastNamePrefix.ValueString() + lastNameFilter = attr.FilterByPrefix + } + + if data.LastNameSuffix.ValueString() != "" { + lastName = data.LastNameSuffix.ValueString() + lastNameFilter = attr.FilterBySuffix + } + + if countOptionalAttributes(data.Email, data.EmailRegexp, data.EmailContains, data.EmailExclude, data.EmailPrefix, data.EmailSuffix) > 1 { + addErr(&resp.Diagnostics, ErrUsersDatasourceShouldSetOneOptionalEmailAttribute, TwingateResources) + + return + } + + if countOptionalAttributes(data.FirstName, data.FirstNameRegexp, data.FirstNameContains, data.FirstNameExclude, data.FirstNamePrefix, data.FirstNameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrUsersDatasourceShouldSetOneOptionalFirstNameAttribute, TwingateResources) + + return + } + + if countOptionalAttributes(data.LastName, data.LastNameRegexp, data.LastNameContains, data.LastNameExclude, data.LastNamePrefix, data.LastNameSuffix) > 1 { + addErr(&resp.Diagnostics, ErrUsersDatasourceShouldSetOneOptionalLastNameAttribute, TwingateResources) + + return + } + + var filter *client.UsersFilter + + if email != "" || firstName != "" || lastName != "" || len(data.Roles.Elements()) > 0 { + filter = &client.UsersFilter{} + } + + if email != "" { + filter.Email = &client.StringFilter{Name: email, Filter: emailFilter} + } + + if firstName != "" { + filter.FirstName = &client.StringFilter{Name: firstName, Filter: firstNameFilter} + } + + if lastName != "" { + filter.LastName = &client.StringFilter{Name: lastName, Filter: lastNameFilter} + } + + if len(data.Roles.Elements()) > 0 { + filter.Roles = utils.Map(data.Roles.Elements(), func(item tfattr.Value) string { + return item.(types.String).ValueString() + }) + } + + users, err := d.client.ReadUsers(ctx, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateUsers) return } - data := usersModel{ - ID: types.StringValue("users-all"), - Users: convertUsersToTerraform(users), - } + data.ID = types.StringValue("users-all") + data.Users = convertUsersToTerraform(users) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/twingate/internal/provider/resource/connector.go b/twingate/internal/provider/resource/connector.go index f22f4f95..e9337368 100644 --- a/twingate/internal/provider/resource/connector.go +++ b/twingate/internal/provider/resource/connector.go @@ -91,7 +91,7 @@ func (r *connector) Schema(_ context.Context, _ resource.SchemaRequest, resp *re attr.StatusUpdatesEnabled: schema.BoolAttribute{ Optional: true, Computed: true, - Description: "Determines whether status notifications are enabled for the Connector.", + Description: "Determines whether status notifications are enabled for the Connector. Default is `true`.", }, // computed attr.ID: schema.StringAttribute{ @@ -115,8 +115,9 @@ func (r *connector) Create(ctx context.Context, req resource.CreateRequest, resp } conn, err := r.client.CreateConnector(ctx, &model.Connector{ - Name: plan.Name.ValueString(), - NetworkID: plan.RemoteNetworkID.ValueString(), + Name: plan.Name.ValueString(), + NetworkID: plan.RemoteNetworkID.ValueString(), + StatusUpdatesEnabled: getOptionalBool(plan.StatusUpdatesEnabled), }) r.helper(ctx, conn, &plan, &resp.State, &resp.Diagnostics, err, operationCreate) diff --git a/twingate/internal/provider/resource/converter_test.go b/twingate/internal/provider/resource/converter_test.go index 1bc533de..065c87dc 100644 --- a/twingate/internal/provider/resource/converter_test.go +++ b/twingate/internal/provider/resource/converter_test.go @@ -7,37 +7,31 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" + tfattr "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stretchr/testify/assert" ) func TestConvertProtocol(t *testing.T) { cases := []struct { - input []interface{} + input types.Object expected *model.Protocol expectedErr error }{ {}, { - input: []interface{}{ - map[string]interface{}{ - attr.Policy: model.PolicyAllowAll, - attr.Ports: []interface{}{ - "-", - }, - }, - }, + input: types.ObjectValueMust(protocolAttributeTypes(), map[string]tfattr.Value{ + attr.Policy: types.StringValue(model.PolicyAllowAll), + attr.Ports: makeTestSet("-"), + }), expectedErr: errors.New("failed to parse protocols port range"), }, { - input: []interface{}{ - map[string]interface{}{ - attr.Policy: model.PolicyRestricted, - attr.Ports: []interface{}{ - "80-88", - }, - }, - }, + input: types.ObjectValueMust(protocolAttributeTypes(), map[string]tfattr.Value{ + attr.Policy: types.StringValue(model.PolicyRestricted), + attr.Ports: makeTestSet("80-88"), + }), expected: &model.Protocol{ Policy: model.PolicyRestricted, Ports: []*model.PortRange{ @@ -125,40 +119,49 @@ func TestConvertPortsRangeToMap(t *testing.T) { } } +func makeTestSet(values ...string) types.Set { + elements := make([]tfattr.Value, 0, len(values)) + for _, val := range values { + elements = append(elements, types.StringValue(val)) + } + + return types.SetValueMust(types.StringType, elements) +} + func TestEqualPorts(t *testing.T) { cases := []struct { - inputA []interface{} - inputB []interface{} + inputA types.Set + inputB types.Set expected bool }{ { - inputA: []interface{}{""}, - inputB: []interface{}{""}, + inputA: makeTestSet(""), + inputB: makeTestSet(""), expected: false, }, { - inputA: []interface{}{"80"}, - inputB: []interface{}{""}, + inputA: makeTestSet("80"), + inputB: makeTestSet(""), expected: false, }, { - inputA: []interface{}{"80"}, - inputB: []interface{}{"90"}, + inputA: makeTestSet("80"), + inputB: makeTestSet("90"), expected: false, }, { - inputA: []interface{}{"80"}, - inputB: []interface{}{"80"}, + inputA: makeTestSet("80"), + inputB: makeTestSet("80"), expected: true, }, { - inputA: []interface{}{"80-81"}, - inputB: []interface{}{"80", "81"}, + inputA: makeTestSet("80-81"), + inputB: makeTestSet("80", "81"), expected: true, }, { - inputA: []interface{}{"80-81", "70"}, - inputB: []interface{}{"70", "80", "81"}, + inputA: makeTestSet("80-81", "70"), + inputB: makeTestSet("70", "80", "81"), expected: true, }, } diff --git a/twingate/internal/provider/resource/helper.go b/twingate/internal/provider/resource/helper.go index feb14fe5..1e013c00 100644 --- a/twingate/internal/provider/resource/helper.go +++ b/twingate/internal/provider/resource/helper.go @@ -4,28 +4,9 @@ import ( "fmt" "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" - tfDiag "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" ) -func ErrAttributeSet(err error, attribute string) diag.Diagnostics { - return diag.FromErr(fmt.Errorf("error setting %s: %w ", attribute, err)) -} - -func convertIDs(data interface{}) []string { - return utils.Map[interface{}, string]( - data.(*schema.Set).List(), - func(elem interface{}) string { - return elem.(string) - }, - ) -} - -func castToStrings(a, b interface{}) (string, string) { - return a.(string), b.(string) -} - // setIntersection - for given two sets A and B, // A ∩ B (read as A intersection B) is the set of common elements that belong to set A and B. // If A = {1, 2, 3, 4} and B = {3, 4, 5, 7}, then the intersection of A and B is given by A ∩ B = {3, 4}. @@ -76,7 +57,7 @@ func withDefaultValue(str, defaultValue string) string { return defaultValue } -func addErr(diagnostics *tfDiag.Diagnostics, err error, operation, resource string) { +func addErr(diagnostics *diag.Diagnostics, err error, operation, resource string) { if err == nil { return } diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 82ba876b..b730cc71 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "reflect" "regexp" "strings" @@ -12,369 +11,529 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + tfattr "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) +const DefaultSecurityPolicyName = "Default Policy" + var ( + DefaultSecurityPolicyID string //nolint:gochecknoglobals ErrPortsWithPolicyAllowAll = errors.New(model.PolicyAllowAll + " policy does not allow specifying ports.") ErrPortsWithPolicyDenyAll = errors.New(model.PolicyDenyAll + " policy does not allow specifying ports.") ErrPolicyRestrictedWithoutPorts = errors.New(model.PolicyRestricted + " policy requires specifying ports.") + ErrInvalidAttributeCombination = errors.New("invalid attribute combination") ErrWildcardAddressWithEnabledShortcut = errors.New("Resources with a CIDR range or wildcard can't have the browser shortcut enabled.") + ErrDefaultPolicyNotSet = errors.New("default policy not set") ) -func Resource() *schema.Resource { //nolint:funlen - portsSchema := &schema.Resource{ - Schema: map[string]*schema.Schema{ - attr.Policy: { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(model.Policies, false), - Description: fmt.Sprintf("Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `%s` (only listed ports are allowed), `%s`, or `%s`", model.PolicyRestricted, model.PolicyAllowAll, model.PolicyDenyAll), - }, - attr.Ports: { - Type: schema.TypeList, - Optional: true, - Description: "List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - DiffSuppressFunc: portsNotChanged, - }, - }, +// Ensure the implementation satisfies the desired interfaces. +var _ resource.Resource = &twingateResource{} + +func NewResourceResource() resource.Resource { + return &twingateResource{} +} + +type twingateResource struct { + client *client.Client +} + +type resourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Address types.String `tfsdk:"address"` + RemoteNetworkID types.String `tfsdk:"remote_network_id"` + IsAuthoritative types.Bool `tfsdk:"is_authoritative"` + Protocols types.Object `tfsdk:"protocols"` + Access types.List `tfsdk:"access"` + IsActive types.Bool `tfsdk:"is_active"` + IsVisible types.Bool `tfsdk:"is_visible"` + IsBrowserShortcutEnabled types.Bool `tfsdk:"is_browser_shortcut_enabled"` + Alias types.String `tfsdk:"alias"` + SecurityPolicyID types.String `tfsdk:"security_policy_id"` +} + +type resourceModelV0 struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Address types.String `tfsdk:"address"` + RemoteNetworkID types.String `tfsdk:"remote_network_id"` + IsAuthoritative types.Bool `tfsdk:"is_authoritative"` + Protocols types.List `tfsdk:"protocols"` + Access types.List `tfsdk:"access"` + IsActive types.Bool `tfsdk:"is_active"` + IsVisible types.Bool `tfsdk:"is_visible"` + IsBrowserShortcutEnabled types.Bool `tfsdk:"is_browser_shortcut_enabled"` + Alias types.String `tfsdk:"alias"` + SecurityPolicyID types.String `tfsdk:"security_policy_id"` +} + +func (r *twingateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = TwingateResource +} + +func (r *twingateResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return } - protocolsSchema := &schema.Resource{ - Schema: map[string]*schema.Schema{ - attr.AllowIcmp: { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Whether to allow ICMP (ping) traffic", - }, - attr.TCP: { - Type: schema.TypeList, - Required: true, - MaxItems: 1, - Elem: portsSchema, - }, - attr.UDP: { - Type: schema.TypeList, - Required: true, - MaxItems: 1, - Elem: portsSchema, - }, - }, + r.client = req.ProviderData.(*client.Client) +} + +func (r *twingateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(attr.ID), req, resp) + + res, err := r.client.ReadResource(ctx, req.ID) + if err != nil { + resp.Diagnostics.AddError("failed to import state", err.Error()) + + return } - accessSchema := &schema.Resource{ - Schema: map[string]*schema.Schema{ - attr.GroupIDs: { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - MinItems: 1, - Optional: true, - AtLeastOneOf: []string{attr.Path(attr.Access, attr.ServiceAccountIDs)}, - Description: "List of Group IDs that will have permission to access the Resource.", - }, - attr.ServiceAccountIDs: { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - MinItems: 1, - Optional: true, - AtLeastOneOf: []string{attr.Path(attr.Access, attr.GroupIDs)}, - Description: "List of Service Account IDs that will have permission to access the Resource.", - }, - }, + if res.Protocols != nil { + protocols, diags := convertProtocolsToTerraform(res.Protocols, nil) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.State.SetAttribute(ctx, path.Root(attr.Protocols), protocols) } - return &schema.Resource{ - Description: "Resources in Twingate represent servers on the private network that clients can connect to. Resources can be defined by IP, CIDR range, FQDN, or DNS zone. For more information, see the Twingate [documentation](https://docs.twingate.com/docs/resources-and-access-nodes).", - CreateContext: resourceCreate, - UpdateContext: resourceUpdate, - ReadContext: resourceRead, - DeleteContext: resourceDelete, + if len(res.Groups) > 0 || len(res.ServiceAccounts) > 0 { + access, diags := convertAccessBlockToTerraform(ctx, res) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.State.SetAttribute(ctx, path.Root(attr.Access), access) + } +} - Schema: map[string]*schema.Schema{ - // required - attr.Name: { - Type: schema.TypeString, +//nolint:funlen +func (r *twingateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Description: "Resources in Twingate represent servers on the private network that clients can connect to. Resources can be defined by IP, CIDR range, FQDN, or DNS zone. For more information, see the Twingate [documentation](https://docs.twingate.com/docs/resources-and-access-nodes).", + Attributes: map[string]schema.Attribute{ + attr.Name: schema.StringAttribute{ Required: true, Description: "The name of the Resource", }, - attr.Address: { - Type: schema.TypeString, + attr.Address: schema.StringAttribute{ Required: true, Description: "The Resource's IP/CIDR or FQDN/DNS zone", }, - attr.RemoteNetworkID: { - Type: schema.TypeString, + attr.RemoteNetworkID: schema.StringAttribute{ Required: true, Description: "Remote Network ID where the Resource lives", }, // optional - attr.IsAuthoritative: { - Type: schema.TypeBool, + attr.IsActive: schema.BoolAttribute{ Optional: true, Computed: true, - Description: "Determines whether assignments in the access block will override any existing assignments. Default is `true`. If set to `false`, assignments made outside of Terraform will be ignored.", + Description: "Set the resource as active or inactive. Default is `true`.", + Default: booldefault.StaticBool(true), }, - attr.Protocols: { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Restrict access to certain protocols and ports. By default or when this argument is not defined, there is no restriction, and all protocols and ports are allowed.", - Elem: protocolsSchema, - DiffSuppressOnRefresh: true, - DiffSuppressFunc: protocolsNotChanged, + attr.IsAuthoritative: schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: "Determines whether assignments in the access block will override any existing assignments. Default is `true`. If set to `false`, assignments made outside of Terraform will be ignored.", + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, }, - attr.Access: { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Restrict access to certain groups or service accounts", - Elem: accessSchema, + attr.Alias: schema.StringAttribute{ + Optional: true, + Description: "Set a DNS alias address for the Resource. Must be a DNS-valid name string.", + PlanModifiers: []planmodifier.String{CaseInsensitiveDiff()}, }, + attr.Protocols: protocols(), // computed - attr.IsVisible: { - Type: schema.TypeBool, - Optional: true, - Computed: true, - Description: "Controls whether this Resource will be visible in the main Resource list in the Twingate Client.", + attr.SecurityPolicyID: schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of a `twingate_security_policy` to set as this Resource's Security Policy. Default is `Default Policy`.", + Default: stringdefault.StaticString(DefaultSecurityPolicyID), + PlanModifiers: []planmodifier.String{UseDefaultPolicyForUnknownModifier()}, }, - attr.IsBrowserShortcutEnabled: { - Type: schema.TypeBool, + attr.IsVisible: schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: "Controls whether this Resource will be visible in the main Resource list in the Twingate Client. Default is `true`.", + Default: booldefault.StaticBool(true), + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + attr.IsBrowserShortcutEnabled: schema.BoolAttribute{ Optional: true, Computed: true, - Description: `Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client.`, - }, - attr.Alias: { - Type: schema.TypeString, - Optional: true, - Description: "Set a DNS alias address for the Resource. Must be a DNS-valid name string.", - DiffSuppressFunc: aliasDiff, + Description: "Controls whether an \"Open in Browser\" shortcut will be shown for this Resource in the Twingate Client. Default is `false`.", + Default: booldefault.StaticBool(false), }, - attr.ID: { - Type: schema.TypeString, - Computed: true, - Description: "Autogenerated ID of the Resource, encoded in base64", + attr.ID: schema.StringAttribute{ + Computed: true, + Description: "Autogenerated ID of the Resource, encoded in base64", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, }, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - } -} - -func resourceCreate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*client.Client) - - resource, err := convertResource(resourceData) - if err != nil { - return diag.FromErr(err) - } - - resource, err = client.CreateResource(ctx, resource) - if err != nil { - return diag.FromErr(err) - } - if err = client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { - return diag.FromErr(err) + Blocks: map[string]schema.Block{attr.Access: accessBlock()}, } - - log.Printf("[INFO] Created resource %s", resource.Name) - - return resourceResourceReadHelper(ctx, client, resourceData, resource, nil) } -func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*client.Client) - - resource, err := convertResource(resourceData) - if err != nil { - return diag.FromErr(err) - } - - resource.ID = resourceData.Id() - - if resourceData.HasChange(attr.Access) { - idsToDelete, idsToAdd, err := getChangedAccessIDs(ctx, resourceData, resource, client) - if err != nil { - return diag.FromErr(err) - } - - if err := client.RemoveResourceAccess(ctx, resource.ID, idsToDelete); err != nil { - return diag.FromErr(err) - } - - if err = client.AddResourceAccess(ctx, resource.ID, idsToAdd); err != nil { - return diag.FromErr(err) - } - } - - if resourceData.HasChanges( - attr.RemoteNetworkID, - attr.Name, - attr.Address, - attr.Protocols, - attr.IsVisible, - attr.IsBrowserShortcutEnabled, - attr.Alias, - ) { - resource, err = client.UpdateResource(ctx, resource) - } else { - resource, err = client.ReadResource(ctx, resource.ID) - } +func (r *twingateResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { //nolint + return map[int64]resource.StateUpgrader{ + // State upgrade implementation from 0 (prior state version) to 1 (Schema.Version) + 0: { + PriorSchema: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + attr.ID: schema.StringAttribute{ + Computed: true, + }, + attr.Name: schema.StringAttribute{ + Required: true, + }, + attr.Address: schema.StringAttribute{ + Required: true, + }, + attr.RemoteNetworkID: schema.StringAttribute{ + Required: true, + }, + attr.IsActive: schema.BoolAttribute{ + Optional: true, + Computed: true, + }, + attr.IsAuthoritative: schema.BoolAttribute{ + Optional: true, + Computed: true, + }, + attr.Alias: schema.StringAttribute{ + Optional: true, + }, + attr.SecurityPolicyID: schema.StringAttribute{ + Optional: true, + Computed: true, + }, + attr.IsVisible: schema.BoolAttribute{ + Optional: true, + Computed: true, + }, + attr.IsBrowserShortcutEnabled: schema.BoolAttribute{ + Optional: true, + Computed: true, + }, + }, - if resource != nil { - resource.IsAuthoritative = convertAuthoritativeFlagLegacy(resourceData) - log.Printf("[INFO] Updated resource %s", resource.Name) + Blocks: map[string]schema.Block{ + attr.Access: schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + attr.GroupIDs: schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + attr.ServiceAccountIDs: schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + }, + }, + }, + attr.Protocols: schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + attr.AllowIcmp: schema.BoolAttribute{ + Optional: true, + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + attr.UDP: schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + attr.Policy: schema.StringAttribute{ + Optional: true, + Computed: true, + }, + attr.Ports: schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + }, + }, + }, + attr.TCP: schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + attr.Policy: schema.StringAttribute{ + Optional: true, + Computed: true, + }, + attr.Ports: schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + }, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorState resourceModelV0 + + resp.Diagnostics.Append(req.State.Get(ctx, &priorState)...) + + if resp.Diagnostics.HasError() { + return + } + + protocols, err := convertProtocolsV0(priorState.Protocols) + if err != nil { + resp.Diagnostics.AddError( + "failed to convert protocols for prior state version 0", + err.Error(), + ) + + return + } + + protocolsState, diags := convertProtocolsToTerraform(protocols, nil) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + upgradedState := resourceModel{ + ID: priorState.ID, + Name: priorState.Name, + Address: priorState.Address, + RemoteNetworkID: priorState.RemoteNetworkID, + Protocols: protocolsState, + Access: priorState.Access, + IsActive: priorState.IsActive, + } + + if !priorState.IsAuthoritative.IsNull() { + upgradedState.IsAuthoritative = priorState.IsAuthoritative + } + + if !priorState.IsVisible.IsNull() { + upgradedState.IsVisible = priorState.IsVisible + } + + if !priorState.IsBrowserShortcutEnabled.IsNull() { + upgradedState.IsBrowserShortcutEnabled = priorState.IsBrowserShortcutEnabled + } + + if !priorState.Alias.IsNull() && priorState.Alias.ValueString() != "" { + upgradedState.Alias = priorState.Alias + } + + if !priorState.SecurityPolicyID.IsNull() && priorState.SecurityPolicyID.ValueString() != "" { + upgradedState.SecurityPolicyID = priorState.SecurityPolicyID + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedState)...) + + resp.Diagnostics.AddWarning("Please update the protocols sections format from a block to an object", + "See the v1 to v2 migration guide in the Twingate Terraform Provider documentation https://registry.terraform.io/providers/Twingate/twingate/latest/docs/guides/migration-v1-to-v2-guide") + }, + }, } - - return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } -func resourceRead(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*client.Client) +func protocols() schema.SingleNestedAttribute { + return schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + Default: objectdefault.StaticValue(defaultProtocolsObject()), + Attributes: map[string]schema.Attribute{ + attr.AllowIcmp: schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + Description: "Whether to allow ICMP (ping) traffic", + }, - resource, err := client.ReadResource(ctx, resourceData.Id()) - if resource != nil { - resource.IsAuthoritative = convertAuthoritativeFlagLegacy(resourceData) + attr.UDP: protocol(), + attr.TCP: protocol(), + }, + Description: "Restrict access to certain protocols and ports. By default or when this argument is not defined, there is no restriction, and all protocols and ports are allowed.", } - - return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } -func resourceDelete(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { - c := meta.(*client.Client) - resourceID := resourceData.Id() - - err := c.DeleteResource(ctx, resourceID) - if err != nil { - return diag.FromErr(err) +func protocol() schema.SingleNestedAttribute { + return schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + Default: objectdefault.StaticValue(defaultProtocolObject()), + Attributes: map[string]schema.Attribute{ + attr.Policy: schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf(model.Policies...), + }, + Default: stringdefault.StaticString(model.PolicyAllowAll), + Description: fmt.Sprintf("Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `%s` (only listed ports are allowed), `%s`, or `%s`", model.PolicyRestricted, model.PolicyAllowAll, model.PolicyDenyAll), + }, + attr.Ports: schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: "List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port", + PlanModifiers: []planmodifier.Set{ + PortsDiff(), + }, + Default: setdefault.StaticValue(defaultEmptyPorts()), + }, + }, } - - log.Printf("[INFO] Deleted resource id %s", resourceData.Id()) - - return nil } -func resourceResourceReadHelper(ctx context.Context, resourceClient *client.Client, resourceData *schema.ResourceData, resource *model.Resource, err error) diag.Diagnostics { - if err != nil { - if errors.Is(err, client.ErrGraphqlResultIsEmpty) { - // clear state - resourceData.SetId("") - - return nil - } - - return diag.FromErr(err) - } - - if resource.Protocols == nil { - resource.Protocols = model.DefaultProtocols() - } - - if !resource.IsActive { - // fix set active state for the resource on `terraform apply` - err = resourceClient.UpdateResourceActiveState(ctx, &model.Resource{ - ID: resource.ID, - IsActive: true, - }) - - if err != nil { - return diag.FromErr(err) - } - } - - if !resource.IsAuthoritative { - groups, serviceAccounts := convertAccess(resourceData) - resource.ServiceAccounts = setIntersection(serviceAccounts, resource.ServiceAccounts) - resource.Groups = setIntersection(groups, resource.Groups) +func accessBlock() schema.ListNestedBlock { + return schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + Description: "Restrict access to certain groups or service accounts", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + attr.GroupIDs: schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: "List of Group IDs that will have permission to access the Resource.", + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + PlanModifiers: []planmodifier.Set{ + EmptySetDiff(), + }, + Default: setdefault.StaticValue(types.SetNull(types.StringType)), + }, + attr.ServiceAccountIDs: schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: "List of Service Account IDs that will have permission to access the Resource.", + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + PlanModifiers: []planmodifier.Set{ + EmptySetDiff(), + }, + Default: setdefault.StaticValue(types.SetNull(types.StringType)), + }, + }, + }, } +} - resourceData.SetId(resource.ID) - - return readDiagnostics(resourceData, resource) +func EmptySetDiff() planmodifier.Set { + return emptySetDiff{} } -func readDiagnostics(resourceData *schema.ResourceData, resource *model.Resource) diag.Diagnostics { //nolint:cyclop - if err := resourceData.Set(attr.Name, resource.Name); err != nil { - return ErrAttributeSet(err, attr.Name) - } +type emptySetDiff struct{} - if err := resourceData.Set(attr.RemoteNetworkID, resource.RemoteNetworkID); err != nil { - return ErrAttributeSet(err, attr.RemoteNetworkID) - } +// Description returns a human-readable description of the plan modifier. +func (m emptySetDiff) Description(_ context.Context) string { + return "" +} - if err := resourceData.Set(attr.Address, resource.Address); err != nil { - return ErrAttributeSet(err, attr.Address) - } +// MarkdownDescription returns a markdown description of the plan modifier. +func (m emptySetDiff) MarkdownDescription(_ context.Context) string { + return "" +} - if err := resourceData.Set(attr.IsAuthoritative, resource.IsAuthoritative); err != nil { - return ErrAttributeSet(err, attr.IsAuthoritative) +// PlanModifySet implements the plan modification logic. +func (m emptySetDiff) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + if req.StateValue.IsNull() { + return } - if err := resourceData.Set(attr.Access, resource.AccessToTerraform()); err != nil { - return ErrAttributeSet(err, attr.Access) + if req.ConfigValue.IsNull() && len(req.StateValue.Elements()) == 0 { + resp.PlanValue = req.StateValue } +} - protocols, err := convertProtocols(resourceData) - if err == nil && protocols != nil && protocols.TCP != nil && protocols.UDP != nil { - if portRangeEqual(protocols.TCP.Ports, resource.Protocols.TCP.Ports) { - resource.Protocols.TCP.Ports = protocols.TCP.Ports - } - - if portRangeEqual(protocols.UDP.Ports, resource.Protocols.UDP.Ports) { - resource.Protocols.UDP.Ports = protocols.UDP.Ports - } - } +func PortsDiff() planmodifier.Set { + return portsDiff{} +} - if err := resourceData.Set(attr.Protocols, resource.Protocols.ToTerraform()); err != nil { - return ErrAttributeSet(err, attr.Protocols) - } +type portsDiff struct{} - if resource.IsVisible != nil { - if err := resourceData.Set(attr.IsVisible, *resource.IsVisible); err != nil { - return ErrAttributeSet(err, attr.IsVisible) - } - } +// Description returns a human-readable description of the plan modifier. +func (m portsDiff) Description(_ context.Context) string { + return "Handles ports difference." +} - if resource.IsBrowserShortcutEnabled != nil { - if err := resourceData.Set(attr.IsBrowserShortcutEnabled, *resource.IsBrowserShortcutEnabled); err != nil { - return ErrAttributeSet(err, attr.IsBrowserShortcutEnabled) - } - } +// MarkdownDescription returns a markdown description of the plan modifier. +func (m portsDiff) MarkdownDescription(_ context.Context) string { + return "Handles ports difference." +} - var alias interface{} - if resource.Alias != nil { - alias = *resource.Alias +// PlanModifySet implements the plan modification logic. +func (m portsDiff) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + if req.StateValue.IsNull() { + return } - if err := resourceData.Set(attr.Alias, alias); err != nil { - return ErrAttributeSet(err, attr.Alias) + if equalPorts(req.StateValue, req.PlanValue) { + resp.PlanValue = req.StateValue } - - return nil -} - -func aliasDiff(key, _, _ string, resourceData *schema.ResourceData) bool { - oldVal, newVal := castToStrings(resourceData.GetChange(key)) - - return oldVal == newVal } -func equalPorts(a, b interface{}) bool { - oldPorts, newPorts := a.([]interface{}), b.([]interface{}) - - oldPortsRange, err := convertPorts(oldPorts) +func equalPorts(one, another types.Set) bool { + oldPortsRange, err := convertPorts(one) if err != nil { return false } - newPortsRange, err := convertPorts(newPorts) + newPortsRange, err := convertPorts(another) if err != nil { return false } @@ -382,11 +541,28 @@ func equalPorts(a, b interface{}) bool { return portRangeEqual(oldPortsRange, newPortsRange) } -func portRangeEqual(portsA, portsB []*model.PortRange) bool { - mapA := convertPortsRangeToMap(portsA) - mapB := convertPortsRangeToMap(portsB) +func portRangeEqual(one, another []*model.PortRange) bool { + oneMap := convertPortsRangeToMap(one) + anotherMap := convertPortsRangeToMap(another) + + return reflect.DeepEqual(oneMap, anotherMap) +} + +func convertPorts(list types.Set) ([]*model.PortRange, error) { + items := list.Elements() + + var ports = make([]*model.PortRange, 0, len(items)) + + for _, port := range items { + portRange, err := model.NewPortRange(port.(types.String).ValueString()) + if err != nil { + return nil, err //nolint:wrapcheck + } + + ports = append(ports, portRange) + } - return reflect.DeepEqual(mapA, mapB) + return ports, nil } func convertPortsRangeToMap(portsRange []*model.PortRange) map[int]struct{} { @@ -407,201 +583,187 @@ func convertPortsRangeToMap(portsRange []*model.PortRange) map[int]struct{} { return out } -func portsNotChanged(attribute, oldValue, newValue string, data *schema.ResourceData) bool { - keys := []string{ - attr.Path(attr.Protocols, attr.TCP, attr.Ports), - attr.Path(attr.Protocols, attr.UDP, attr.Ports), - } +func (r *twingateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan resourceModel - if strings.HasSuffix(attribute, "#") && newValue == "0" { - return newValue == oldValue - } + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - for _, key := range keys { - if strings.HasPrefix(attribute, key) { - return equalPorts(data.GetChange(key)) - } + if resp.Diagnostics.HasError() { + return } - return false -} + input, err := convertResource(&plan) + if err != nil { + addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) -// protocolsNotChanged - suppress protocols change when uses default value. -func protocolsNotChanged(attribute, oldValue, newValue string, data *schema.ResourceData) bool { - switch attribute { - case attr.Len(attr.Protocols): - return newValue == "0" - case attr.Len(attr.Protocols, attr.TCP), attr.Len(attr.Protocols, attr.UDP): - return newValue == "0" - case attr.Path(attr.Protocols, attr.TCP, attr.Policy), attr.Path(attr.Protocols, attr.UDP, attr.Policy): - return oldValue == model.PolicyAllowAll && newValue == "" + return } - return false -} - -func getChangedAccessIDs(ctx context.Context, resourceData *schema.ResourceData, resource *model.Resource, client *client.Client) ([]string, []string, error) { - remote, err := client.ReadResource(ctx, resource.ID) + resource, err := r.client.CreateResource(ctx, input) if err != nil { - return nil, nil, fmt.Errorf("failed to get changedIDs: %w", err) + addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) + + return } - var oldGroups, oldServiceAccounts []string - if resource.IsAuthoritative { - oldGroups, oldServiceAccounts = remote.Groups, remote.ServiceAccounts - } else { - oldGroups = getOldIDsNonAuthoritative(resourceData, attr.GroupIDs) - oldServiceAccounts = getOldIDsNonAuthoritative(resourceData, attr.ServiceAccountIDs) + if err = r.client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { + addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) + + return } - // ids to delete - groupsToDelete := setDifference(oldGroups, resource.Groups) - serviceAccountsToDelete := setDifference(oldServiceAccounts, resource.ServiceAccounts) + if !input.IsActive { + if err := r.client.UpdateResourceActiveState(ctx, &model.Resource{ + ID: resource.ID, + IsActive: false, + }); err != nil { + addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) - // ids to add - groupsToAdd := setDifference(resource.Groups, remote.Groups) - serviceAccountsToAdd := setDifference(resource.ServiceAccounts, remote.ServiceAccounts) + return + } - return append(groupsToDelete, serviceAccountsToDelete...), append(groupsToAdd, serviceAccountsToAdd...), nil + resource.IsActive = false + } + + r.helper(ctx, resource, &plan, &plan, &resp.State, &resp.Diagnostics, err, operationCreate) } -func getOldIDsNonAuthoritative(resourceData *schema.ResourceData, attribute string) []string { - if resourceData.HasChange(attr.Path(attr.Access, attribute)) { - old, _ := resourceData.GetChange(attr.Path(attr.Access, attribute)) +func getAccessAttribute(list types.List, attribute string) []string { + if list.IsNull() || list.IsUnknown() || len(list.Elements()) == 0 { + return nil + } - return convertIDs(old) + obj := list.Elements()[0].(types.Object) + if obj.IsNull() || obj.IsUnknown() { + return nil } - return nil + val := obj.Attributes()[attribute] + if val == nil || val.IsNull() || val.IsUnknown() { + return nil + } + + return convertIDs(val.(types.Set)) } -func convertResource(data *schema.ResourceData) (*model.Resource, error) { - protocols, err := convertProtocols(data) +func convertResource(plan *resourceModel) (*model.Resource, error) { + protocols, err := convertProtocols(&plan.Protocols) if err != nil { return nil, err } - groups, serviceAccounts := convertAccess(data) - res := &model.Resource{ - Name: data.Get(attr.Name).(string), - RemoteNetworkID: data.Get(attr.RemoteNetworkID).(string), - Address: data.Get(attr.Address).(string), - Protocols: protocols, - Groups: groups, - ServiceAccounts: serviceAccounts, - IsAuthoritative: convertAuthoritativeFlagLegacy(data), - Alias: getOptionalString(data, attr.Alias), - } + groupIDs := getAccessAttribute(plan.Access, attr.GroupIDs) + serviceAccountIDs := getAccessAttribute(plan.Access, attr.ServiceAccountIDs) - isVisible, ok := data.GetOkExists(attr.IsVisible) //nolint - if val := isVisible.(bool); ok { - res.IsVisible = &val + if !plan.Access.IsNull() && groupIDs == nil && serviceAccountIDs == nil { + return nil, ErrInvalidAttributeCombination } - isBrowserShortcutEnabled, ok := data.GetOkExists(attr.IsBrowserShortcutEnabled) //nolint - if val := isBrowserShortcutEnabled.(bool); ok && isAttrKnown(data, attr.IsBrowserShortcutEnabled) { - res.IsBrowserShortcutEnabled = &val - } + isBrowserShortcutEnabled := getOptionalBool(plan.IsBrowserShortcutEnabled) - if res.IsBrowserShortcutEnabled != nil && *res.IsBrowserShortcutEnabled && isWildcardAddress(res.Address) { + if isBrowserShortcutEnabled != nil && *isBrowserShortcutEnabled && isWildcardAddress(plan.Address.ValueString()) { return nil, ErrWildcardAddressWithEnabledShortcut } - return res, nil + return &model.Resource{ + Name: plan.Name.ValueString(), + RemoteNetworkID: plan.RemoteNetworkID.ValueString(), + Address: plan.Address.ValueString(), + Protocols: protocols, + Groups: groupIDs, + ServiceAccounts: serviceAccountIDs, + IsActive: plan.IsActive.ValueBool(), + IsAuthoritative: convertAuthoritativeFlag(plan.IsAuthoritative), + Alias: getOptionalString(plan.Alias), + IsVisible: getOptionalBool(plan.IsVisible), + IsBrowserShortcutEnabled: isBrowserShortcutEnabled, + SecurityPolicyID: plan.SecurityPolicyID.ValueStringPointer(), + }, nil } -var cidrRgxp = regexp.MustCompile(`(\d{1,3}\.){3}\d{1,3}(/\d+)?`) +func getOptionalBool(val types.Bool) *bool { + if !val.IsUnknown() { + return val.ValueBoolPointer() + } -func isWildcardAddress(address string) bool { - return strings.ContainsAny(address, "*?") || cidrRgxp.MatchString(address) + return nil } -func isAttrKnown(data *schema.ResourceData, attr string) bool { - cfg := data.GetRawConfig() - val := cfg.GetAttr(attr) +func getOptionalString(val types.String) *string { + if !val.IsUnknown() && !val.IsNull() { + return val.ValueStringPointer() + } - return !val.IsNull() && val.IsKnown() + return nil } -func getOptionalString(data *schema.ResourceData, attr string) *string { - var result *string - - cfg := data.GetRawConfig() - val := cfg.GetAttr(attr) +func convertIDs(list types.Set) []string { + return utils.Map(list.Elements(), func(item tfattr.Value) string { + return item.(types.String).ValueString() + }) +} - if !val.IsNull() { - str := val.AsString() - result = &str +func equalProtocolsState(objA, objB *types.Object) bool { + if objA.IsNull() != objB.IsNull() || objA.IsUnknown() != objB.IsUnknown() { + return false } - return result -} - -func convertAccess(data *schema.ResourceData) ([]string, []string) { - rawList := data.Get(attr.Access).([]interface{}) - if len(rawList) == 0 || rawList[0] == nil { - return nil, nil + protocolsA, err := convertProtocols(objA) + if err != nil { + return false } - rawMap := rawList[0].(map[string]interface{}) + protocolsB, err := convertProtocols(objB) + if err != nil { + return false + } - return convertIDs(rawMap[attr.GroupIDs]), convertIDs(rawMap[attr.ServiceAccountIDs]) + return equalProtocols(protocolsA, protocolsB) } -func convertAuthoritativeFlagLegacy(data *schema.ResourceData) bool { - flag, hasFlag := data.GetOkExists(attr.IsAuthoritative) //nolint - - if hasFlag { - return flag.(bool) - } +func equalProtocols(one, another *model.Protocols) bool { + return one.AllowIcmp == another.AllowIcmp && equalProtocol(one.TCP, another.TCP) && equalProtocol(one.UDP, another.UDP) +} - // default value - return true +func equalProtocol(one, another *model.Protocol) bool { + return one.Policy == another.Policy && portRangeEqual(one.Ports, another.Ports) } -func convertProtocols(data *schema.ResourceData) (*model.Protocols, error) { - rawList := data.Get(attr.Protocols).([]interface{}) - if len(rawList) == 0 { +func convertProtocols(protocols *types.Object) (*model.Protocols, error) { + if protocols == nil || protocols.IsNull() || protocols.IsUnknown() { return model.DefaultProtocols(), nil } - rawMap := rawList[0].(map[string]interface{}) - - udp, err := convertProtocol(rawMap[attr.UDP].([]interface{})) + udp, err := convertProtocol(protocols.Attributes()[attr.UDP]) if err != nil { return nil, err } - tcp, err := convertProtocol(rawMap[attr.TCP].([]interface{})) + tcp, err := convertProtocol(protocols.Attributes()[attr.TCP]) if err != nil { return nil, err } return &model.Protocols{ + AllowIcmp: protocols.Attributes()[attr.AllowIcmp].(types.Bool).ValueBool(), UDP: udp, TCP: tcp, - AllowIcmp: rawMap[attr.AllowIcmp].(bool), }, nil } -func convertProtocol(rawList []interface{}) (*model.Protocol, error) { - if len(rawList) == 0 { +func convertProtocol(protocol tfattr.Value) (*model.Protocol, error) { + obj := convertProtocolObj(protocol) + if obj.IsNull() { return nil, nil //nolint:nilnil } - rawMap := rawList[0].(map[string]interface{}) - policy := rawMap[attr.Policy].(string) - - if policy == "" { - policy = model.PolicyAllowAll - } - - ports, err := convertPorts(rawMap[attr.Ports].([]interface{})) + ports, err := decodePorts(obj) if err != nil { return nil, err } - if err := validateProtocol(policy, ports); err != nil { + policy := obj.Attributes()[attr.Policy].(types.String).ValueString() + if err := isValidPolicy(policy, ports); err != nil { return nil, err } @@ -612,7 +774,34 @@ func convertProtocol(rawList []interface{}) (*model.Protocol, error) { return model.NewProtocol(policy, ports), nil } -func validateProtocol(policy string, ports []*model.PortRange) error { +func convertProtocolObj(protocol tfattr.Value) types.Object { + if protocol == nil || protocol.IsNull() { + return types.ObjectNull(nil) + } + + obj, ok := protocol.(types.Object) + if !ok || obj.IsNull() { + return types.ObjectNull(nil) + } + + return obj +} + +func decodePorts(obj types.Object) ([]*model.PortRange, error) { + portsVal := obj.Attributes()[attr.Ports] + if portsVal == nil || portsVal.IsNull() { + return nil, nil + } + + portsList, ok := portsVal.(types.Set) + if !ok { + return nil, nil + } + + return convertPorts(portsList) +} + +func isValidPolicy(policy string, ports []*model.PortRange) error { switch policy { case model.PolicyAllowAll: if len(ports) > 0 { @@ -633,22 +822,713 @@ func validateProtocol(policy string, ports []*model.PortRange) error { return nil } -func convertPorts(rawList []interface{}) ([]*model.PortRange, error) { - var ports = make([]*model.PortRange, 0, len(rawList)) +func convertProtocolsV0(protocols types.List) (*model.Protocols, error) { + if protocols.IsNull() || protocols.IsUnknown() || len(protocols.Elements()) == 0 { + return model.DefaultProtocols(), nil + } - for _, port := range rawList { - var str string - if port != nil { - str = port.(string) - } + obj := protocols.Elements()[0].(types.Object) + if obj.IsNull() || obj.IsUnknown() { + return model.DefaultProtocols(), nil + } - portRange, err := model.NewPortRange(str) - if err != nil { - return nil, err //nolint:wrapcheck - } + udp, err := convertProtocolV0(obj.Attributes()[attr.UDP]) + if err != nil { + return nil, err + } - ports = append(ports, portRange) + tcp, err := convertProtocolV0(obj.Attributes()[attr.TCP]) + if err != nil { + return nil, err } - return ports, nil + return &model.Protocols{ + AllowIcmp: obj.Attributes()[attr.AllowIcmp].(types.Bool).ValueBool(), + UDP: udp, + TCP: tcp, + }, nil +} + +func convertProtocolV0(protocol tfattr.Value) (*model.Protocol, error) { + obj := convertProtocolObjV0(protocol) + if obj.IsNull() { + return nil, nil //nolint:nilnil + } + + ports, err := decodePortsV0(obj) + if err != nil { + return nil, err + } + + policy := obj.Attributes()[attr.Policy].(types.String).ValueString() + if err := isValidPolicyV0(policy, ports); err != nil { + return nil, err + } + + if policy == model.PolicyDenyAll { + policy = model.PolicyRestricted + } + + return model.NewProtocol(policy, ports), nil +} + +func convertProtocolObjV0(protocol tfattr.Value) types.Object { + if protocol == nil || protocol.IsNull() { + return types.ObjectNull(nil) + } + + list, ok := protocol.(types.List) + if !ok || list.IsNull() || list.IsUnknown() || len(list.Elements()) == 0 { + return types.ObjectNull(nil) + } + + obj := list.Elements()[0].(types.Object) + if obj.IsNull() || obj.IsUnknown() { + return types.ObjectNull(nil) + } + + return obj +} + +func decodePortsV0(obj types.Object) ([]*model.PortRange, error) { + portsVal := obj.Attributes()[attr.Ports] + if portsVal == nil || portsVal.IsNull() { + return nil, nil + } + + portsList, ok := portsVal.(types.Set) + if !ok { + return nil, nil + } + + return convertPortsV0(portsList) +} + +func convertPortsV0(list types.Set) ([]*model.PortRange, error) { + items := list.Elements() + + var ports = make([]*model.PortRange, 0, len(items)) + + for _, port := range items { + portRange, err := model.NewPortRange(port.(types.String).ValueString()) + if err != nil { + return nil, err //nolint:wrapcheck + } + + ports = append(ports, portRange) + } + + return ports, nil +} + +func isValidPolicyV0(policy string, ports []*model.PortRange) error { + switch policy { + case model.PolicyAllowAll: + if len(ports) > 0 { + return ErrPortsWithPolicyAllowAll + } + + case model.PolicyDenyAll: + if len(ports) > 0 { + return ErrPortsWithPolicyDenyAll + } + + case model.PolicyRestricted: + if len(ports) == 0 { + return ErrPolicyRestrictedWithoutPorts + } + } + + return nil +} + +func (r *twingateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state resourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + resource, err := r.client.ReadResource(ctx, state.ID.ValueString()) + if resource != nil { + resource.IsAuthoritative = convertAuthoritativeFlag(state.IsAuthoritative) + + if state.SecurityPolicyID.ValueString() == "" { + s := "" + resource.SecurityPolicyID = &s + } + } + + r.helper(ctx, resource, &state, &state, &resp.State, &resp.Diagnostics, err, operationRead) +} + +func (r *twingateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state resourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + input, err := convertResource(&plan) + if err != nil { + addErr(&resp.Diagnostics, err, operationUpdate, TwingateResource) + + return + } + + planSecurityPolicy := input.SecurityPolicyID + input.ID = state.ID.ValueString() + + if !plan.Access.Equal(state.Access) { + if err := r.updateResourceAccess(ctx, &plan, &state, input); err != nil { + addErr(&resp.Diagnostics, err, operationUpdate, TwingateResource) + + return + } + } + + var resource *model.Resource + + if isResourceChanged(&plan, &state) { + if err := r.setDefaultSecurityPolicy(ctx, input); err != nil { + addErr(&resp.Diagnostics, err, operationUpdate, TwingateResource) + + return + } + + resource, err = r.client.UpdateResource(ctx, input) + } else { + resource, err = r.client.ReadResource(ctx, input.ID) + } + + if resource != nil { + resource.IsAuthoritative = input.IsAuthoritative + } + + if planSecurityPolicy != nil && *planSecurityPolicy == "" { + resource.SecurityPolicyID = planSecurityPolicy + } + + r.helper(ctx, resource, &state, &plan, &resp.State, &resp.Diagnostics, err, operationUpdate) +} + +func (r *twingateResource) setDefaultSecurityPolicy(ctx context.Context, resource *model.Resource) error { + if DefaultSecurityPolicyID == "" { + policy, _ := r.client.ReadSecurityPolicy(ctx, "", DefaultSecurityPolicyName) + if policy != nil { + DefaultSecurityPolicyID = policy.ID + } + } + + if DefaultSecurityPolicyID == "" { + return ErrDefaultPolicyNotSet + } + + remoteResource, err := r.client.ReadResource(ctx, resource.ID) + if err != nil { + return err //nolint:wrapcheck + } + + if remoteResource.SecurityPolicyID != nil && (resource.SecurityPolicyID == nil || *resource.SecurityPolicyID == "") && + *remoteResource.SecurityPolicyID != DefaultSecurityPolicyID { + resource.SecurityPolicyID = &DefaultSecurityPolicyID + } + + return nil +} + +func isResourceChanged(plan, state *resourceModel) bool { + return !plan.RemoteNetworkID.Equal(state.RemoteNetworkID) || + !plan.Name.Equal(state.Name) || + !plan.Address.Equal(state.Address) || + !equalProtocolsState(&plan.Protocols, &state.Protocols) || + !plan.IsActive.Equal(state.IsActive) || + !plan.IsVisible.Equal(state.IsVisible) || + !plan.IsBrowserShortcutEnabled.Equal(state.IsBrowserShortcutEnabled) || + !plan.Alias.Equal(state.Alias) || + !plan.SecurityPolicyID.Equal(state.SecurityPolicyID) +} + +func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state *resourceModel, input *model.Resource) error { + idsToDelete, idsToAdd, err := r.getChangedAccessIDs(ctx, plan, state, input) + if err != nil { + return fmt.Errorf("failed to update resource access: %w", err) + } + + if err := r.client.RemoveResourceAccess(ctx, input.ID, idsToDelete); err != nil { + return fmt.Errorf("failed to update resource access: %w", err) + } + + if err := r.client.AddResourceAccess(ctx, input.ID, idsToAdd); err != nil { + return fmt.Errorf("failed to update resource access: %w", err) + } + + return nil +} + +func (r *twingateResource) getChangedAccessIDs(ctx context.Context, plan, state *resourceModel, resource *model.Resource) ([]string, []string, error) { + remote, err := r.client.ReadResource(ctx, resource.ID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get changedIDs: %w", err) + } + + var oldGroups, oldServiceAccounts []string + if resource.IsAuthoritative { + oldGroups, oldServiceAccounts = remote.Groups, remote.ServiceAccounts + } else { + oldGroups = getOldIDsNonAuthoritative(plan, state, attr.GroupIDs) + oldServiceAccounts = getOldIDsNonAuthoritative(plan, state, attr.ServiceAccountIDs) + } + + // ids to delete + groupsToDelete := setDifference(oldGroups, resource.Groups) + serviceAccountsToDelete := setDifference(oldServiceAccounts, resource.ServiceAccounts) + + // ids to add + groupsToAdd := setDifference(resource.Groups, remote.Groups) + serviceAccountsToAdd := setDifference(resource.ServiceAccounts, remote.ServiceAccounts) + + return append(groupsToDelete, serviceAccountsToDelete...), append(groupsToAdd, serviceAccountsToAdd...), nil +} + +func getOldIDsNonAuthoritative(plan, state *resourceModel, attribute string) []string { + if !plan.Access.Equal(state.Access) { + return getAccessAttribute(state.Access, attribute) + } + + return nil +} + +func (r *twingateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state resourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteResource(ctx, state.ID.ValueString()) + addErr(&resp.Diagnostics, err, operationDelete, TwingateResource) +} + +func (r *twingateResource) helper(ctx context.Context, resource *model.Resource, state, reference *resourceModel, respState *tfsdk.State, diagnostics *diag.Diagnostics, err error, operation string) { + if err != nil { + if errors.Is(err, client.ErrGraphqlResultIsEmpty) { + // clear state + respState.RemoveResource(ctx) + + return + } + + addErr(diagnostics, err, operation, TwingateResource) + + return + } + + if resource.Protocols == nil { + resource.Protocols = model.DefaultProtocols() + } + + if !resource.IsAuthoritative { + resource.Groups = setIntersection(getAccessAttribute(reference.Access, attr.GroupIDs), resource.Groups) + resource.ServiceAccounts = setIntersection(getAccessAttribute(reference.Access, attr.ServiceAccountIDs), resource.ServiceAccounts) + } + + setState(ctx, state, reference, resource, diagnostics) + + if diagnostics.HasError() { + return + } + + // Set refreshed state + diagnostics.Append(respState.Set(ctx, state)...) +} + +func setState(ctx context.Context, state, reference *resourceModel, resource *model.Resource, diagnostics *diag.Diagnostics) { //nolint:cyclop + state.ID = types.StringValue(resource.ID) + state.Name = types.StringValue(resource.Name) + state.RemoteNetworkID = types.StringValue(resource.RemoteNetworkID) + state.Address = types.StringValue(resource.Address) + state.IsActive = types.BoolValue(resource.IsActive) + state.IsAuthoritative = types.BoolValue(resource.IsAuthoritative) + state.SecurityPolicyID = types.StringPointerValue(resource.SecurityPolicyID) + + if !state.IsVisible.IsNull() || !reference.IsVisible.IsUnknown() { + state.IsVisible = types.BoolPointerValue(resource.IsVisible) + } + + if !state.IsBrowserShortcutEnabled.IsNull() || !reference.IsBrowserShortcutEnabled.IsUnknown() { + state.IsBrowserShortcutEnabled = types.BoolPointerValue(resource.IsBrowserShortcutEnabled) + } + + if !state.Alias.IsNull() || !reference.Alias.IsUnknown() { + state.Alias = reference.Alias + } + + if !state.Protocols.IsNull() || !reference.Protocols.IsUnknown() { + protocols, diags := convertProtocolsToTerraform(resource.Protocols, &reference.Protocols) + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return + } + + if !equalProtocolsState(&state.Protocols, &protocols) { + state.Protocols = protocols + } + } + + access, diags := convertAccessBlockToTerraform(ctx, resource) + + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return + } + + state.Access = access +} + +func convertProtocolsToTerraform(protocols *model.Protocols, reference *types.Object) (types.Object, diag.Diagnostics) { + var diagnostics diag.Diagnostics + + if protocols == nil || reference != nil && (reference.IsUnknown() || reference.IsNull()) { + return defaultProtocolsModelToTerraform() + } + + var referenceTCP, referenceUDP tfattr.Value + if reference != nil { + referenceTCP = reference.Attributes()[attr.TCP] + referenceUDP = reference.Attributes()[attr.UDP] + } + + tcp, diags := convertProtocolModelToTerraform(protocols.TCP, referenceTCP) + diagnostics.Append(diags...) + + udp, diags := convertProtocolModelToTerraform(protocols.UDP, referenceUDP) + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return types.ObjectNull(protocolsAttributeTypes()), diagnostics + } + + attributes := map[string]tfattr.Value{ + attr.AllowIcmp: types.BoolValue(protocols.AllowIcmp), + attr.TCP: tcp, + attr.UDP: udp, + } + + obj := types.ObjectValueMust(protocolsAttributeTypes(), attributes) + + return obj, diagnostics +} + +func convertPortsToTerraform(ports []*model.PortRange) types.Set { + if len(ports) == 0 { + return defaultEmptyPorts() + } + + elements := make([]tfattr.Value, 0, len(ports)) + for _, port := range ports { + elements = append(elements, types.StringValue(port.String())) + } + + return types.SetValueMust(types.StringType, elements) +} + +func convertProtocolModelToTerraform(protocol *model.Protocol, _ tfattr.Value) (types.Object, diag.Diagnostics) { + if protocol == nil { + return types.ObjectNull(protocolAttributeTypes()), nil + } + + ports := convertPortsToTerraform(protocol.Ports) + + policy := protocol.Policy + if policy == model.PolicyRestricted && len(ports.Elements()) == 0 { + policy = model.PolicyDenyAll + } + + attributes := map[string]tfattr.Value{ + attr.Policy: types.StringValue(policy), + attr.Ports: ports, + } + + return types.ObjectValue(protocolAttributeTypes(), attributes) +} + +func defaultProtocolsModelToTerraform() (types.Object, diag.Diagnostics) { + attributeTypes := protocolsAttributeTypes() + + var diagnostics diag.Diagnostics + + defaultPorts, diags := defaultProtocolModelToTerraform() + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return makeNullObject(attributeTypes), diagnostics + } + + attributes := map[string]tfattr.Value{ + attr.AllowIcmp: types.BoolValue(true), + attr.TCP: defaultPorts, + attr.UDP: defaultPorts, + } + + obj, diags := types.ObjectValue(attributeTypes, attributes) + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return makeNullObject(attributeTypes), diagnostics + } + + return obj, diagnostics +} + +func defaultProtocolsObject() types.Object { + attributeTypes := protocolsAttributeTypes() + + var diagnostics diag.Diagnostics + + defaultPorts, diags := defaultProtocolModelToTerraform() + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return makeNullObject(attributeTypes) + } + + attributes := map[string]tfattr.Value{ + attr.AllowIcmp: types.BoolValue(true), + attr.TCP: defaultPorts, + attr.UDP: defaultPorts, + } + + obj, diags := types.ObjectValue(attributeTypes, attributes) + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return makeNullObject(attributeTypes) + } + + return obj +} + +func defaultEmptyPorts() types.Set { + return types.SetValueMust(types.StringType, []tfattr.Value{}) +} + +func defaultProtocolModelToTerraform() (basetypes.ObjectValue, diag.Diagnostics) { + attributes := map[string]tfattr.Value{ + attr.Policy: types.StringValue(model.PolicyAllowAll), + attr.Ports: defaultEmptyPorts(), + } + + return types.ObjectValue(protocolAttributeTypes(), attributes) +} + +func defaultProtocolObject() basetypes.ObjectValue { + obj, _ := defaultProtocolModelToTerraform() + + return obj +} + +func protocolsAttributeTypes() map[string]tfattr.Type { + return map[string]tfattr.Type{ + attr.AllowIcmp: types.BoolType, + attr.TCP: types.ObjectType{ + AttrTypes: protocolAttributeTypes(), + }, + attr.UDP: types.ObjectType{ + AttrTypes: protocolAttributeTypes(), + }, + } +} + +func protocolAttributeTypes() map[string]tfattr.Type { + return map[string]tfattr.Type{ + attr.Policy: types.StringType, + attr.Ports: types.SetType{ + ElemType: types.StringType, + }, + } +} + +func convertAccessBlockToTerraform(ctx context.Context, resource *model.Resource) (types.List, diag.Diagnostics) { + var diagnostics, diags diag.Diagnostics + + if len(resource.Groups) == 0 && len(resource.ServiceAccounts) == 0 { + return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + } + + groupIDs, serviceAccountIDs := types.SetNull(types.StringType), types.SetNull(types.StringType) + + if len(resource.Groups) > 0 { + groupIDs, diags = makeSet(resource.Groups) + diagnostics.Append(diags...) + } + + if len(resource.ServiceAccounts) > 0 { + serviceAccountIDs, diags = makeSet(resource.ServiceAccounts) + diagnostics.Append(diags...) + } + + if diagnostics.HasError() { + return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + } + + attributes := map[string]tfattr.Value{ + attr.GroupIDs: groupIDs, + attr.ServiceAccountIDs: serviceAccountIDs, + } + + obj, diags := types.ObjectValue(accessAttributeTypes(), attributes) + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + } + + return makeObjectsList(ctx, obj) +} + +func accessAttributeTypes() map[string]tfattr.Type { + return map[string]tfattr.Type{ + attr.GroupIDs: types.SetType{ + ElemType: types.StringType, + }, + attr.ServiceAccountIDs: types.SetType{ + ElemType: types.StringType, + }, + } +} + +func makeNullObject(attributeTypes map[string]tfattr.Type) types.Object { + return types.ObjectNull(attributeTypes) +} + +func makeObjectsListNull(ctx context.Context, attributeTypes map[string]tfattr.Type) types.List { + return types.ListNull(types.ObjectNull(attributeTypes).Type(ctx)) +} + +func makeObjectsList(ctx context.Context, objects ...types.Object) (types.List, diag.Diagnostics) { + obj := objects[0] + + items := utils.Map(objects, func(item types.Object) tfattr.Value { + return tfattr.Value(item) + }) + + return types.ListValue(obj.Type(ctx), items) +} + +func makeSet(list []string) (types.Set, diag.Diagnostics) { + return types.SetValue(types.StringType, stringsToTerraformValue(list)) +} + +func stringsToTerraformValue(list []string) []tfattr.Value { + if len(list) == 0 { + return nil + } + + out := make([]tfattr.Value, 0, len(list)) + for _, item := range list { + out = append(out, types.StringValue(item)) + } + + return out +} + +func CaseInsensitiveDiff() planmodifier.String { + return caseInsensitiveDiffModifier{ + description: "Handles case insensitive strings", + } +} + +type caseInsensitiveDiffModifier struct { + description string +} + +func (m caseInsensitiveDiffModifier) Description(_ context.Context) string { + return m.description +} + +func (m caseInsensitiveDiffModifier) MarkdownDescription(_ context.Context) string { + return m.description +} + +func (m caseInsensitiveDiffModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Do not replace on resource creation. + if req.State.Raw.IsNull() { + return + } + + // Do not replace on resource destroy. + if req.Plan.Raw.IsNull() { + return + } + + if !req.PlanValue.IsUnknown() && req.StateValue.IsNull() { + return + } + + if strings.EqualFold(strings.ToLower(req.PlanValue.ValueString()), strings.ToLower(req.StateValue.ValueString())) { + resp.PlanValue = req.StateValue + } +} + +var cidrRgxp = regexp.MustCompile(`(\d{1,3}\.){3}\d{1,3}(/\d+)?`) + +func isWildcardAddress(address string) bool { + return strings.ContainsAny(address, "*?") || cidrRgxp.MatchString(address) +} + +func UseDefaultPolicyForUnknownModifier() planmodifier.String { + return useDefaultPolicyForUnknownModifier{} +} + +// useDefaultPolicyForUnknownModifier implements the plan modifier. +type useDefaultPolicyForUnknownModifier struct{} + +// Description returns a human-readable description of the plan modifier. +func (m useDefaultPolicyForUnknownModifier) Description(_ context.Context) string { + return "Once set, the value of this attribute will fallback to Default Policy on unset." +} + +// MarkdownDescription returns a markdown description of the plan modifier. +func (m useDefaultPolicyForUnknownModifier) MarkdownDescription(_ context.Context) string { + return "Once set, the value of this attribute will fallback to Default Policy on unset." +} + +// PlanModifyString implements the plan modification logic. +func (m useDefaultPolicyForUnknownModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if req.StateValue.IsNull() && req.ConfigValue.IsNull() { + resp.PlanValue = types.StringPointerValue(nil) + + return + } + + // Do nothing if there is no state value. + if req.StateValue.IsNull() { + return + } + + // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. + if req.ConfigValue.IsUnknown() { + return + } + + // Do nothing if there is a known planned value. + if req.ConfigValue.ValueString() != "" { + return + } + + if req.StateValue.ValueString() == "" && req.PlanValue.ValueString() == DefaultSecurityPolicyID { + resp.PlanValue = types.StringValue("") + } else if req.StateValue.ValueString() == DefaultSecurityPolicyID && req.PlanValue.ValueString() == "" { + resp.PlanValue = types.StringValue(DefaultSecurityPolicyID) + } } diff --git a/twingate/internal/test/acctests/datasource/connectors_test.go b/twingate/internal/test/acctests/datasource/connectors_test.go index 077fb7a3..0aab4815 100644 --- a/twingate/internal/test/acctests/datasource/connectors_test.go +++ b/twingate/internal/test/acctests/datasource/connectors_test.go @@ -13,6 +13,11 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" ) +var ( + connectorsLen = attr.Len(attr.Connectors) + connectorNamePath = attr.Path(attr.Connectors, attr.Name) +) + func TestAccDatasourceTwingateConnectors_basic(t *testing.T) { acctests.SetPageLimit(t, 1) @@ -197,3 +202,140 @@ func testCheckOutputNestedLen(name string, index int, attr string, length int) r return nil } } + +func TestAccDatasourceTwingateConnectorsFilterByName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + connectorName := test.RandomConnectorName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, "", connectorName), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func testDatasourceTwingateConnectorsFilter(resourceName, networkName, connectorName, filter, name string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s_network" { + name = "%[2]s" + } + resource "twingate_connector" "%[1]s_connector" { + remote_network_id = twingate_remote_network.%[1]s_network.id + name = "%[3]s" + } + + data "twingate_connectors" "%[1]s" { + name%[4]s = "%[5]s" + depends_on = [twingate_connector.%[1]s_connector] + } + `, resourceName, networkName, connectorName, filter, name) +} + +func TestAccDatasourceTwingateConnectorsFilterByPrefix(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + connectorName := test.RandomConnectorName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterByPrefix, prefix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateConnectorsFilterBySuffix(t *testing.T) { + t.Parallel() + + connectorName := test.RandomConnectorName() + prefix := test.Prefix() + suffix := connectorName[len(prefix):] + resourceName := test.RandomResourceName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterBySuffix, suffix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateConnectorsFilterByContains(t *testing.T) { + t.Parallel() + + connectorName := test.RandomConnectorName() + contains := connectorName[len(connectorName)/2 : len(connectorName)/2+5] + resourceName := test.RandomResourceName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterByContains, contains), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateConnectorsFilterByRegexp(t *testing.T) { + t.Parallel() + + connectorName := test.RandomConnectorName() + contains := connectorName[len(connectorName)/2 : len(connectorName)/2+5] + resourceName := test.RandomResourceName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterByRegexp, fmt.Sprintf(".*%s.*", contains)), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/groups_test.go b/twingate/internal/test/acctests/datasource/groups_test.go index 1a1bd26c..34d7e566 100644 --- a/twingate/internal/test/acctests/datasource/groups_test.go +++ b/twingate/internal/test/acctests/datasource/groups_test.go @@ -2,6 +2,7 @@ package datasource import ( "fmt" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "regexp" "testing" @@ -72,21 +73,20 @@ func testDatasourceTwingateGroups(name, securityPolicyID string) string { } func TestAccDatasourceTwingateGroups_emptyResult(t *testing.T) { - t.Run("Test Twingate Datasource : Acc Groups - empty result", func(t *testing.T) { - groupName := test.RandomName() - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctests.ProviderFactories, - PreCheck: func() { acctests.PreCheck(t) }, - Steps: []resource.TestStep{ - { - Config: testTwingateGroupsDoesNotExists(groupName), - Check: acctests.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.twingate_groups.out_dgs2", groupsLen, "0"), - ), - }, + t.Parallel() + groupName := test.RandomName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testTwingateGroupsDoesNotExists(groupName), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.twingate_groups.out_dgs2", groupsLen, "0"), + ), }, - }) + }, }) } @@ -104,20 +104,18 @@ func TestAccDatasourceTwingateGroupsWithFilters_basic(t *testing.T) { const theDatasource = "data.twingate_groups.out_dgs2" - t.Run("Test Twingate Datasource : Acc Groups with filters - basic", func(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctests.ProviderFactories, - PreCheck: func() { acctests.PreCheck(t) }, - Steps: []resource.TestStep{ - { - Config: testDatasourceTwingateGroupsWithFilters(groupName), - Check: acctests.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(theDatasource, groupsLen, "2"), - resource.TestCheckResourceAttr(theDatasource, groupNamePath, groupName), - ), - }, + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateGroupsWithFilters(groupName), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, groupsLen, "2"), + resource.TestCheckResourceAttr(theDatasource, groupNamePath, groupName), + ), }, - }) + }, }) } @@ -133,7 +131,7 @@ func testDatasourceTwingateGroupsWithFilters(name string) string { data "twingate_groups" "out_dgs2" { name = "%s" - type = "MANUAL" + types = ["MANUAL"] is_active = true depends_on = [twingate_group.test_dgs2_1, twingate_group.test_dgs2_2] @@ -142,26 +140,26 @@ func testDatasourceTwingateGroupsWithFilters(name string) string { } func TestAccDatasourceTwingateGroupsWithFilters_ErrorNotSupportedTypes(t *testing.T) { - t.Run("Test Twingate Datasource : Acc Groups with filters - error not supported types", func(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctests.ProviderFactories, - PreCheck: func() { - acctests.PreCheck(t) - }, - Steps: []resource.TestStep{ - { - Config: testTwingateGroupsWithFilterNotSupportedType(), - ExpectError: regexp.MustCompile("Attribute type value must be one of"), - }, + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { + acctests.PreCheck(t) + }, + Steps: []resource.TestStep{ + { + Config: testTwingateGroupsWithFilterNotSupportedType(), + ExpectError: regexp.MustCompile("Attribute types.* value must be one of"), }, - }) + }, }) } func testTwingateGroupsWithFilterNotSupportedType() string { return ` data "twingate_groups" "test" { - type = "OTHER" + types = ["OTHER"] } output "my_groups" { @@ -171,51 +169,60 @@ func testTwingateGroupsWithFilterNotSupportedType() string { } func TestAccDatasourceTwingateGroups_WithEmptyFilters(t *testing.T) { - t.Run("Test Twingate Datasource : Acc Groups - with empty filters", func(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctests.ProviderFactories, - PreCheck: func() { - acctests.PreCheck(t) - }, - Steps: []resource.TestStep{ - { - Config: testTwingateGroupsWithEmptyFilter(), - }, + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { + acctests.PreCheck(t) + }, + Steps: []resource.TestStep{ + { + Config: testTwingateGroupsWithEmptyFilter(test.RandomGroupName()), + ExpectNonEmptyPlan: true, + Check: acctests.ComposeTestCheckFunc( + testCheckResourceAttrNotEqual("data.twingate_groups.all", groupsLen, "0"), + ), }, - }) + }, }) } -func testTwingateGroupsWithEmptyFilter() string { - return ` +func testTwingateGroupsWithEmptyFilter(name string) string { + return fmt.Sprintf(` + resource "twingate_group" "test_group" { + name = "%s" + } + data "twingate_groups" "all" {} output "my_groups" { value = data.twingate_groups.all.groups + + depends_on = [twingate_group.test_group] } - ` + `, name) } func TestAccDatasourceTwingateGroups_withTwoDatasource(t *testing.T) { - t.Run("Test Twingate Datasource : Acc Groups with two datasource", func(t *testing.T) { - - groupName := test.RandomName() - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctests.ProviderFactories, - PreCheck: func() { acctests.PreCheck(t) }, - CheckDestroy: acctests.CheckTwingateGroupDestroy, - Steps: []resource.TestStep{ - { - Config: testDatasourceTwingateGroupsWithDatasource(groupName), - Check: acctests.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.twingate_groups.two_dgs3", groupNamePath, groupName), - resource.TestCheckResourceAttr("data.twingate_groups.one_dgs3", groupsLen, "1"), - resource.TestCheckResourceAttr("data.twingate_groups.two_dgs3", groupsLen, "2"), - ), - }, + t.Parallel() + + groupName := test.RandomName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateGroupsWithDatasource(groupName), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.twingate_groups.two_dgs3", groupNamePath, groupName), + resource.TestCheckResourceAttr("data.twingate_groups.one_dgs3", groupsLen, "1"), + resource.TestCheckResourceAttr("data.twingate_groups.two_dgs3", groupsLen, "2"), + ), }, - }) + }, }) } @@ -246,3 +253,138 @@ func testDatasourceTwingateGroupsWithDatasource(name string) string { } `, name, name, name, name, name) } + +func TestAccDatasourceTwingateGroupsWithFilterByPrefix(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + + theDatasource := "data.twingate_groups." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateGroupsWithFilter( + resourceName, + prefix+"_g1", + prefix+"_g2", + prefix, + attr.FilterByPrefix, + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, groupsLen, "2"), + ), + }, + }, + }) +} + +func testDatasourceTwingateGroupsWithFilter(resourceName, name1, name2, name, filter string) string { + return fmt.Sprintf(` + resource "twingate_group" "%[1]s_1" { + name = "%[2]s" + } + + resource "twingate_group" "%[1]s_2" { + name = "%[3]s" + } + + data "twingate_groups" "%[1]s" { + name%[4]s = "%[5]s" + types = ["MANUAL"] + is_active = true + + depends_on = [twingate_group.%[1]s_1, twingate_group.%[1]s_2] + } + `, resourceName, name1, name2, filter, name) +} + +func TestAccDatasourceTwingateGroupsWithFilterBySuffix(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + suffix := acctest.RandString(5) + + theDatasource := "data.twingate_groups." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateGroupsWithFilter( + resourceName, + fmt.Sprintf("%s_g1_%s", prefix, suffix), + fmt.Sprintf("%s_g2_%s", prefix, suffix), + suffix, + attr.FilterBySuffix, + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, groupsLen, "2"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateGroupsWithFilterByContains(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + contains := acctest.RandString(5) + + theDatasource := "data.twingate_groups." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateGroupsWithFilter( + resourceName, + fmt.Sprintf("%s_%s_g1", prefix, contains), + fmt.Sprintf("%s_%s_g2", prefix, contains), + contains, + attr.FilterByContains, + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, groupsLen, "2"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateGroupsWithFilterByRegexp(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + contains := acctest.RandString(5) + + theDatasource := "data.twingate_groups." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateGroupsWithFilter( + resourceName, + fmt.Sprintf("%s_%s_g1", prefix, contains), + fmt.Sprintf("%s_%s_g2", prefix, contains), + fmt.Sprintf(".*_%s_.*", contains), + attr.FilterByRegexp, + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, groupsLen, "2"), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/remote-network_test.go b/twingate/internal/test/acctests/datasource/remote-network_test.go index 8e5c1f6c..7fb29057 100644 --- a/twingate/internal/test/acctests/datasource/remote-network_test.go +++ b/twingate/internal/test/acctests/datasource/remote-network_test.go @@ -28,6 +28,7 @@ func TestAccDatasourceTwingateRemoteNetwork_basic(t *testing.T) { Check: acctests.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.twingate_remote_network.test_dn1_2", attr.Name, networkName), ), + ExpectNonEmptyPlan: true, }, }, }) diff --git a/twingate/internal/test/acctests/datasource/remote-networks_test.go b/twingate/internal/test/acctests/datasource/remote-networks_test.go index c6bc394d..92783156 100644 --- a/twingate/internal/test/acctests/datasource/remote-networks_test.go +++ b/twingate/internal/test/acctests/datasource/remote-networks_test.go @@ -1,14 +1,21 @@ package datasource import ( + "fmt" "testing" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +var ( + remoteNetworksLen = attr.Len(attr.RemoteNetworks) + remoteNetworkNamePath = attr.Path(attr.RemoteNetworks, attr.Name) +) + func TestAccDatasourceTwingateRemoteNetworks_read(t *testing.T) { acctests.SetPageLimit(t, 1) @@ -46,7 +53,7 @@ func testDatasourceTwingateRemoteNetworks2(networkName1, networkName2, prefix st } output "test_networks" { - value = [for n in [for net in data.twingate_remote_networks.all : net if can(net.*.name)][0] : n if length(regexall("${prefix}.*", n.name)) > 0] + value = [for n in [for net in data.twingate_remote_networks.all : net if can(net.*.name)][6] : n if length(regexall("${prefix}.*", n.name)) > 0] } `, map[string]any{ @@ -55,3 +62,136 @@ func testDatasourceTwingateRemoteNetworks2(networkName1, networkName2, prefix st "prefix": prefix, }) } + +func TestAccDatasourceTwingateRemoteNetworksFilterByName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + networkName := test.RandomName() + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, networkName, ""), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, name, filter string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[2]s" + } + + data "twingate_remote_networks" "%[1]s" { + name%[3]s = "%[4]s" + + depends_on = [twingate_remote_network.%[1]s] + } + `, resourceName, networkName, filter, name) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterByPrefix(t *testing.T) { + t.Parallel() + + prefix := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := prefix + "_" + test.RandomName() + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, prefix, attr.FilterByPrefix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterBySuffix(t *testing.T) { + t.Parallel() + + suffix := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := test.RandomName() + "_" + suffix + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, suffix, attr.FilterBySuffix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterByContains(t *testing.T) { + t.Parallel() + + randString := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := test.RandomName() + "_" + randString + "_" + acctest.RandString(5) + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, randString, attr.FilterByContains), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterByRegexp(t *testing.T) { + t.Parallel() + + randString := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := test.RandomName() + "_" + randString + "_" + acctest.RandString(5) + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, ".*_"+randString+"_.*", attr.FilterByRegexp), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/resource_test.go b/twingate/internal/test/acctests/datasource/resource_test.go index aa5cb9f1..e95ac0d1 100644 --- a/twingate/internal/test/acctests/datasource/resource_test.go +++ b/twingate/internal/test/acctests/datasource/resource_test.go @@ -44,13 +44,13 @@ func testDatasourceTwingateResource(networkName, resourceName string) string { name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.test_dr1.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "RESTRICTED" ports = ["80-83", "85"] } - udp { + udp = { policy = "ALLOW_ALL" ports = [] } diff --git a/twingate/internal/test/acctests/datasource/resources_test.go b/twingate/internal/test/acctests/datasource/resources_test.go index 3c0bcb50..c64a1d0a 100644 --- a/twingate/internal/test/acctests/datasource/resources_test.go +++ b/twingate/internal/test/acctests/datasource/resources_test.go @@ -2,11 +2,13 @@ package datasource import ( "fmt" + "regexp" "testing" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -47,13 +49,13 @@ func testDatasourceTwingateResources(networkName, resourceName string) string { name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.test_drs1.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "RESTRICTED" ports = ["80-83", "85"] } - udp { + udp = { policy = "ALLOW_ALL" ports = [] } @@ -64,13 +66,13 @@ func testDatasourceTwingateResources(networkName, resourceName string) string { name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.test_drs1.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "ALLOW_ALL" ports = [] } - udp { + udp = { policy = "ALLOW_ALL" ports = [] } @@ -121,3 +123,191 @@ func testTwingateResourcesDoesNotExists(name string) string { } `, name) } + +func TestAccDatasourceTwingateResourcesWithMultipleFilters(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceResourcesWithMultipleFilters(test.RandomResourceName()), + ExpectError: regexp.MustCompile("Only one of name.*"), + }, + }, + }) +} + +func testDatasourceResourcesWithMultipleFilters(name string) string { + return fmt.Sprintf(` + data "twingate_resources" "with-multiple-filters" { + name_regexp = "%[1]s" + name_contains = "%[1]s" + } + `, name) +} + +func TestAccDatasourceTwingateResourcesFilterByPrefix(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + networkName := test.RandomName() + theDatasource := "data.twingate_resources." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateResourcesFilter(resourceName, networkName, prefix+"_test_app", prefix+"_one", prefix+"_on", attr.FilterByPrefix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, resourcesLen, "1"), + resource.TestCheckResourceAttr(theDatasource, resourceNamePath, prefix+"_one"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateResourcesFilterBySuffix(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + networkName := test.RandomName() + theDatasource := "data.twingate_resources." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateResourcesFilter(resourceName, networkName, "test_app_"+prefix, "one_"+prefix, "e_"+prefix, attr.FilterBySuffix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, resourcesLen, "1"), + resource.TestCheckResourceAttr(theDatasource, resourceNamePath, "one_"+prefix), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateResourcesFilterByContains(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + networkName := test.RandomName() + theDatasource := "data.twingate_resources." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateResourcesFilter(resourceName, networkName, prefix+"_test_app", prefix+"_one", prefix+"_on", attr.FilterByContains), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, resourcesLen, "1"), + resource.TestCheckResourceAttr(theDatasource, resourceNamePath, prefix+"_one"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateResourcesFilterByRegexp(t *testing.T) { + t.Parallel() + + prefix := acctest.RandString(6) + resourceName := test.RandomResourceName() + networkName := test.RandomName() + theDatasource := "data.twingate_resources." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateResourcesFilter(resourceName, networkName, prefix+"_test_app", prefix+"_one", prefix+".*app", attr.FilterByRegexp), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, resourcesLen, "1"), + resource.TestCheckResourceAttr(theDatasource, resourceNamePath, prefix+"_test_app"), + ), + }, + }, + }) +} + +func testDatasourceTwingateResourcesFilter(resourceName, networkName, name1, name2, name, filter string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[2]s" { + name = "%[2]s" + } + + resource "twingate_resource" "%[1]s_1" { + name = "%[3]s" + address = "acc-test.com" + remote_network_id = twingate_remote_network.%[2]s.id + } + + resource "twingate_resource" "%[1]s_2" { + name = "%[4]s" + address = "acc-test.com" + remote_network_id = twingate_remote_network.%[2]s.id + } + + data "twingate_resources" "%[1]s" { + name%[6]s = "%[5]s" + + depends_on = [twingate_resource.%[1]s_1, twingate_resource.%[1]s_2] + } + `, resourceName, networkName, name1, name2, name, filter) +} + +func TestAccDatasourceTwingateResourcesWithoutFilters(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + networkName := test.RandomName() + theDatasource := "data.twingate_resources." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateResourcesAll(resourceName, networkName, prefix+"_test_app"), + Check: acctests.ComposeTestCheckFunc( + testCheckResourceAttrNotEqual(theDatasource, resourcesLen, "0"), + ), + }, + }, + }) +} + +func testDatasourceTwingateResourcesAll(resourceName, networkName, name string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[2]s" { + name = "%[2]s" + } + + resource "twingate_resource" "%[1]s" { + name = "%[3]s" + address = "acc-test.com" + remote_network_id = twingate_remote_network.%[2]s.id + } + + data "twingate_resources" "%[1]s" { + depends_on = [twingate_resource.%[1]s] + } + `, resourceName, networkName, name) +} diff --git a/twingate/internal/test/acctests/datasource/security-policies_test.go b/twingate/internal/test/acctests/datasource/security-policies_test.go index 2342cbac..177dd320 100644 --- a/twingate/internal/test/acctests/datasource/security-policies_test.go +++ b/twingate/internal/test/acctests/datasource/security-policies_test.go @@ -9,6 +9,8 @@ import ( sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +var securityPolicyNamePath = attr.Path(attr.SecurityPolicies, attr.Name) + func TestAccDatasourceTwingateSecurityPoliciesBasic(t *testing.T) { acctests.SetPageLimit(t, 1) @@ -36,3 +38,95 @@ func testDatasourceTwingateSecurityPolicies() string { data "twingate_security_policies" "all" {} ` } + +func testDatasourceTwingateSecurityPoliciesFilter(filter, name string) string { + return fmt.Sprintf(` + data "twingate_security_policies" "filtered" { + name%[1]s = "%[2]s" + } + `, filter, name) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterByPrefix(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByPrefix, "Def"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterBySuffix(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterBySuffix, "ault Policy"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterByContains(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByContains, "ault"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterByRegexp(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByRegexp, ".*ault .*"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/service-accounts_test.go b/twingate/internal/test/acctests/datasource/service-accounts_test.go index 092e8d0a..9b760015 100644 --- a/twingate/internal/test/acctests/datasource/service-accounts_test.go +++ b/twingate/internal/test/acctests/datasource/service-accounts_test.go @@ -2,6 +2,7 @@ package datasource import ( "fmt" + "regexp" "strings" "testing" @@ -16,6 +17,7 @@ import ( var ( serviceAccountsLen = attr.Len(attr.ServiceAccounts) keyIDsLen = attr.Len(attr.ServiceAccounts, attr.KeyIDs) + serviceAccountName = attr.Path(attr.ServiceAccounts, attr.Name) ) func TestAccDatasourceTwingateServicesFilterByName(t *testing.T) { @@ -88,6 +90,7 @@ func TestAccDatasourceTwingateServicesAll(t *testing.T) { Check: acctests.ComposeTestCheckFunc( resource.TestCheckResourceAttr(theDatasource, attr.ID, "all-services"), ), + ExpectNonEmptyPlan: true, }, { Config: filterDatasourceServices(prefix, config), @@ -311,3 +314,202 @@ func datasourceServicesConfig(prefix string) string { } `, map[string]any{"prefix": prefix}) } + +func TestAccDatasourceTwingateServicesWithMultipleFilters(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceServicesWithMultipleFilters(test.RandomName()), + ExpectError: regexp.MustCompile("Only one of name.*"), + }, + }, + }) +} + +func testDatasourceServicesWithMultipleFilters(name string) string { + return fmt.Sprintf(` + data "twingate_service_accounts" "with-multiple-filters" { + name_regexp = "%[1]s" + name_contains = "%[1]s" + } + `, name) +} + +func TestAccDatasourceTwingateServicesFilterByPrefix(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + prefix := test.Prefix("orange") + name := acctest.RandomWithPrefix(prefix) + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, prefix, attr.FilterByPrefix), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +} + +func datasourceServicesWithFilter(configs []terraformServiceConfig, name, filter string) string { + var dependsOn string + ids := getTerraformServiceKeys(configs) + + if ids != "" { + dependsOn = fmt.Sprintf("depends_on = [%s]", ids) + } + + return fmt.Sprintf(` + data "twingate_service_accounts" "out" { + name%s = "%s" + + %s + } + `, filter, name, dependsOn) +} + +func TestAccDatasourceTwingateServicesFilterBySuffix(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + name := test.Prefix("orange") + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, "orange", attr.FilterBySuffix), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateServicesFilterByContains(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + name := test.Prefix("orange") + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, "rang", attr.FilterByContains), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateServicesFilterByRegexp(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + name := test.Prefix("orange") + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, ".*ora.*", attr.FilterByRegexp), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/users_test.go b/twingate/internal/test/acctests/datasource/users_test.go index 6521810f..6383551b 100644 --- a/twingate/internal/test/acctests/datasource/users_test.go +++ b/twingate/internal/test/acctests/datasource/users_test.go @@ -1,17 +1,27 @@ package datasource import ( + "errors" "fmt" + "strings" "testing" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestAccDatasourceTwingateUsers_basic(t *testing.T) { acctests.SetPageLimit(t, 1) + + users, err := acctests.GetTestUsers() + if err != nil && !errors.Is(err, acctests.ErrResourceNotFound) { + t.Skip("can't run test:", err) + } + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, PreCheck: func() { acctests.PreCheck(t) }, @@ -19,7 +29,7 @@ func TestAccDatasourceTwingateUsers_basic(t *testing.T) { { Config: testDatasourceTwingateUsers(), Check: acctests.ComposeTestCheckFunc( - testCheckResourceAttrNotEqual("data.twingate_users.all", attr.Len(attr.Users), "0"), + resource.TestCheckResourceAttr("data.twingate_users.all", attr.Len(attr.Users), fmt.Sprintf("%d", len(users))), ), }, }, @@ -53,3 +63,646 @@ func testCheckResourceAttrNotEqual(name, key, value string) resource.TestCheckFu return nil } } + +func join(configs ...string) string { + return strings.Join(configs, "\n") +} + +func TestAccDatasourceTwingateUsers_filterByEmail(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("email") + email := prefix + "_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, "", email, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailPrefix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("email_prefix") + email := prefix + "_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterByPrefix, prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailSuffix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + const suffix = "suf" + email := test.RandomEmail() + "." + suffix + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterBySuffix, suffix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailContains(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + + val := acctest.RandString(6) + email := test.TerraformRandName(val) + "_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterByContains, val, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByEmailRegexp(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + + prefix := acctest.RandString(6) + email := test.TerraformRandName(prefix) + "_email_by_regexp_" + test.RandomEmail() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUser(resourceName, email), + terraformDatasourceUsersByEmail(datasourceName, attr.FilterByRegexp, prefix+".*_regexp_.*", resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + ), + }, + }, + }) +} + +func terraformResourceTwingateUser(terraformResourceName, email string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + send_invite = false + } + `, terraformResourceName, email) +} + +func terraformDatasourceUsersByEmail(datasourceName, filter, email, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "%[1]s" { + email%[2]s = "%[3]s" + + depends_on = [%[4]s] + } +`, datasourceName, filter, email, acctests.TerraformUser(resourceName)) +} + +func TestAccDatasourceTwingateUsers_filterByFirstName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("first_name") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, "", firstName, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNamePrefix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + prefix := test.TerraformRandName("first_name") + email := test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterByPrefix, prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNameSuffix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + suffix := acctest.RandString(5) + email := test.RandomEmail() + firstName := test.RandomName() + "_" + suffix + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterBySuffix, suffix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNameContains(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + email := test.RandomEmail() + firstName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterByContains, val, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByFirstNameRegexp(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + email := test.RandomEmail() + firstName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByFirstName(datasourceName, attr.FilterByRegexp, fmt.Sprintf(".*_%s_.*", val), resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func terraformDatasourceUsersByFirstName(datasourceName, filter, name, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "%[1]s" { + first_name%[2]s = "%[3]s" + + depends_on = [%[4]s] + } +`, datasourceName, filter, name, acctests.TerraformUser(resourceName)) +} + +func TestAccDatasourceTwingateUsers_filterByEmailAndFirstName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + prefix := test.TerraformRandName("orange") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + const theDatasource = "data.twingate_users.filter_by_email_and_first_name_prefix" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstName(resourceName, email, firstName), + terraformDatasourceUsersByEmailAndFirstNamePrefix(prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithFirstName(resourceName, email, firstName string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + first_name = "%s" + send_invite = false + } + `, resourceName, email, firstName) +} + +func terraformDatasourceUsersByEmailAndFirstNamePrefix(prefix, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "filter_by_email_and_first_name_prefix" { + email_prefix = "%[1]s" + first_name_prefix = "%[1]s" + + depends_on = [twingate_user.%[2]s] + } +`, prefix, resourceName) +} + +func TestAccDatasourceTwingateUsers_filterByLastName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + prefix := test.TerraformRandName("last_name") + lastName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, "", lastName, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNamePrefix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + prefix := test.TerraformRandName("last_name") + lastName := prefix + "_" + test.RandomName() + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterByPrefix, prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNameSuffix(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + suffix := acctest.RandString(5) + lastName := test.RandomName() + "_" + suffix + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterBySuffix, suffix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNameContains(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + lastName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterByContains, val, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateUsers_filterByLastNameRegexp(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + datasourceName := test.RandomName() + email := test.RandomEmail() + val := acctest.RandString(6) + suffix := acctest.RandString(5) + lastName := fmt.Sprintf("%s_%s_%s", test.RandomName(), val, suffix) + theDatasource := acctests.TerraformDatasourceUsers(datasourceName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithLastName(resourceName, email, lastName), + terraformDatasourceUsersByLastName(datasourceName, attr.FilterByRegexp, fmt.Sprintf(".*_%s_.*", val), resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithLastName(resourceName, email, lastName string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + last_name = "%s" + send_invite = false + } + `, resourceName, email, lastName) +} + +func terraformDatasourceUsersByLastName(datasourceName, filter, name, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "%[1]s" { + last_name%[2]s = "%[3]s" + + depends_on = [%[4]s] + } +`, datasourceName, filter, name, acctests.TerraformUser(resourceName)) +} + +func TestAccDatasourceTwingateUsers_filterByEmailFirstNameAndLastName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + prefix := test.TerraformRandName("yellow") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + lastName := prefix + "_" + test.RandomName() + const theDatasource = "data.twingate_users.filter_by_email_and_first_name_and_last_name_prefix" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstNameAndLastName(resourceName, email, firstName, lastName), + terraformDatasourceUsersByEmailAndFirstNameAndLastNamePrefix(prefix, resourceName), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithFirstNameAndLastName(resourceName, email, firstName, lastName string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + first_name = "%s" + last_name = "%s" + send_invite = false + } + `, resourceName, email, firstName, lastName) +} + +func terraformDatasourceUsersByEmailAndFirstNameAndLastNamePrefix(prefix, resourceName string) string { + return fmt.Sprintf(` + data "twingate_users" "filter_by_email_and_first_name_and_last_name_prefix" { + email_prefix = "%[1]s" + first_name_prefix = "%[1]s" + last_name_prefix = "%[1]s" + + depends_on = [twingate_user.%[2]s] + } +`, prefix, resourceName) +} + +func TestAccDatasourceTwingateUsers_filterByEmailFirstNameLastNameAndRole(t *testing.T) { + t.Parallel() + + resourceName := test.RandomName() + prefix := test.TerraformRandName("tree") + email := prefix + "_" + test.RandomEmail() + firstName := prefix + "_" + test.RandomName() + lastName := prefix + "_" + test.RandomName() + const theDatasource = "data.twingate_users.filter_by_email_first-name_last-name_prefix_and_role" + const role = "DEVOPS" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: join( + terraformResourceTwingateUserWithFirstNameLastNameAndRole(resourceName, email, firstName, lastName, role), + terraformDatasourceUsersByEmailAndFirstNameLastNamePrefixAndRole(prefix, resourceName, role), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, attr.Len(attr.Users), "1"), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Email), email), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.FirstName), firstName), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.LastName), lastName), + resource.TestCheckResourceAttr(theDatasource, attr.Path(attr.Users, attr.Role), role), + ), + }, + }, + }) +} + +func terraformResourceTwingateUserWithFirstNameLastNameAndRole(resourceName, email, firstName, lastName, role string) string { + return fmt.Sprintf(` + resource "twingate_user" "%s" { + email = "%s" + first_name = "%s" + last_name = "%s" + role = "%s" + send_invite = false + } + `, resourceName, email, firstName, lastName, role) +} + +func terraformDatasourceUsersByEmailAndFirstNameLastNamePrefixAndRole(prefix, resourceName, role string) string { + return fmt.Sprintf(` + data "twingate_users" "filter_by_email_first-name_last-name_prefix_and_role" { + email_prefix = "%[1]s" + first_name_prefix = "%[1]s" + last_name_prefix = "%[1]s" + roles = ["%[2]s"] + + depends_on = [twingate_user.%[3]s] + } +`, prefix, role, resourceName) +} diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index 799a63e7..adf9aaea 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -15,15 +15,15 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/datasource" "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" - twingateV2 "github.com/Twingate/terraform-provider-twingate/twingate/v2" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-mux/tf5to6server" - "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) var ( @@ -34,6 +34,7 @@ var ( ErrUnknownResourceType = errors.New("unknown resource type") ErrClientNotInited = errors.New("meta client not inited") ErrSecurityPoliciesNotFound = errors.New("security policies not found") + ErrInvalidPath = errors.New("invalid path: the path value cannot be asserted as string") ) func ErrServiceAccountsLenMismatch(expected, actual int) error { @@ -70,26 +71,7 @@ var providerClient = func() *client.Client { //nolint }() var ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ //nolint - "twingate": func() (tfprotov6.ProviderServer, error) { - upgradedSdkProvider, err := tf5to6server.UpgradeServer(context.Background(), twingate.Provider("test").GRPCProvider) - if err != nil { - log.Fatal(err) - } - - providers := []func() tfprotov6.ProviderServer{ - func() tfprotov6.ProviderServer { - return upgradedSdkProvider - }, - providerserver.NewProtocol6(twingateV2.New("test")()), - } - - provider, err := tf6muxserver.NewMuxServer(context.Background(), providers...) - if err != nil { - return nil, fmt.Errorf("failed to run mux server: %w", err) - } - - return provider, nil - }, + "twingate": providerserver.NewProtocol6WithError(twingate.New("test")()), } // SetPageLimit - changes page limit, can't be uses in parallel tests. @@ -206,6 +188,10 @@ func GetTwingateResourceID(resourceName string, resourceID **string) sdk.TestChe } } +func DatasourceName(resource, name string) string { + return fmt.Sprintf("data.%s.%s", resource, name) +} + func ResourceName(resource, name string) string { return fmt.Sprintf("%s.%s", resource, name) } @@ -242,6 +228,10 @@ func TerraformUser(name string) string { return ResourceName(resource.TwingateUser, name) } +func TerraformDatasourceUsers(name string) string { + return DatasourceName(datasource.TwingateUsers, name) +} + func DeleteTwingateResource(resourceName, resourceType string) sdk.TestCheckFunc { return func(s *terraform.State) error { resourceState, ok := s.RootModule().Resources[resourceName] @@ -357,6 +347,66 @@ func CheckTwingateResourceActiveState(resourceName string, expectedActiveState b } } +type checkResourceActiveState struct { + resourceAddress string + expectedActiveState bool +} + +// CheckPlan implements the plan check logic. +func (e checkResourceActiveState) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + var resourceID string + + for _, rc := range req.Plan.ResourceChanges { + if e.resourceAddress != rc.Address { + continue + } + + result, err := tfjsonpath.Traverse(rc.Change.Before, tfjsonpath.New(attr.ID)) + if err != nil { + resp.Error = err + + return + } + + resultID, ok := result.(string) + if !ok { + resp.Error = ErrInvalidPath + + return + } + + resourceID = resultID + + break + } + + if resourceID == "" { + resp.Error = fmt.Errorf("%s - Resource not found in plan ResourceChanges", e.resourceAddress) //nolint:goerr113 + + return + } + + res, err := providerClient.ReadResource(ctx, resourceID) + if err != nil { + resp.Error = fmt.Errorf("failed to read resource: %w", err) + + return + } + + if res.IsActive != e.expectedActiveState { + resp.Error = fmt.Errorf("expected active state %v, got %v", e.expectedActiveState, res.IsActive) //nolint:goerr113 + + return + } +} + +func CheckResourceActiveState(resourceAddress string, activeState bool) plancheck.PlanCheck { + return checkResourceActiveState{ + resourceAddress: resourceAddress, + expectedActiveState: activeState, + } +} + func CheckImportState(attributes map[string]string) func(data []*terraform.InstanceState) error { return func(data []*terraform.InstanceState) error { if len(data) != 1 { @@ -475,7 +525,7 @@ func ListSecurityPolicies() ([]*model.SecurityPolicy, error) { return nil, ErrClientNotInited } - securityPolicies, err := providerClient.ReadSecurityPolicies(context.Background()) + securityPolicies, err := providerClient.ReadSecurityPolicies(context.Background(), "", "") if err != nil { return nil, fmt.Errorf("failed to fetch all security policies: %w", err) } @@ -627,6 +677,49 @@ func CheckResourceServiceAccountsLen(resourceName string, expectedServiceAccount } } +func CheckResourceSecurityPolicy(resourceName string, expectedSecurityPolicyID string) sdk.TestCheckFunc { + return func(state *terraform.State) error { + resourceID, err := getResourceID(state, resourceName) + if err != nil { + return err + } + + resource, err := providerClient.ReadResource(context.Background(), resourceID) + if err != nil { + return fmt.Errorf("resource with ID %s failed to read: %w", resourceID, err) + } + + if resource.SecurityPolicyID != nil && *resource.SecurityPolicyID != expectedSecurityPolicyID { + return fmt.Errorf("expected security_policy_id %s, got %s", expectedSecurityPolicyID, *resource.SecurityPolicyID) //nolint + } + + return nil + } +} + +func UpdateResourceSecurityPolicy(resourceName, securityPolicyID string) sdk.TestCheckFunc { + return func(state *terraform.State) error { + resourceID, err := getResourceID(state, resourceName) + if err != nil { + return err + } + + resource, err := providerClient.ReadResource(context.Background(), resourceID) + if err != nil { + return fmt.Errorf("resource with ID %s failed to read: %w", resourceID, err) + } + + resource.SecurityPolicyID = &securityPolicyID + + _, err = providerClient.UpdateResource(context.Background(), resource) + if err != nil { + return fmt.Errorf("resource with ID %s failed to update security_policy: %w", resourceID, err) + } + + return nil + } +} + func AddGroupUser(groupResource, groupName, terraformUserID string) sdk.TestCheckFunc { return func(state *terraform.State) error { userID, err := getResourceID(state, getResourceNameFromID(terraformUserID)) @@ -702,7 +795,7 @@ func GetTestUsers() ([]*model.User, error) { return nil, ErrClientNotInited } - users, err := providerClient.ReadUsers(context.Background()) + users, err := providerClient.ReadUsers(context.Background(), nil) if err != nil { return nil, err //nolint } @@ -756,7 +849,7 @@ func GetTestUser() (*model.User, error) { return nil, ErrClientNotInited } - users, err := providerClient.ReadUsers(context.Background()) + users, err := providerClient.ReadUsers(context.Background(), nil) if err != nil { return nil, fmt.Errorf("failed to get test users: %w", err) } diff --git a/twingate/internal/test/acctests/resource/config-builder.go b/twingate/internal/test/acctests/resource/config-builder.go new file mode 100644 index 00000000..e0bb385f --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-builder.go @@ -0,0 +1,100 @@ +package resource + +import ( + "bytes" + "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" +) + +type TerraformResource interface { + TerraformResource() string +} + +func collectResourceIDs[T TerraformResource](resources ...T) []string { + ids := make([]string, 0, len(resources)) + + for _, res := range resources { + ids = append(ids, res.TerraformResource()+".id") + } + + return ids +} + +func optionalInt(val any) *int { + if val == nil { + return nil + } + + switch t := val.(type) { + case int: + return &t + case *int: + return t + default: + return nil + } +} + +func optionalString(val any) *string { + if val == nil { + return nil + } + + switch t := val.(type) { + case string: + return &t + case *string: + return t + default: + return nil + } +} + +func optionalBool(val any) *bool { + if val == nil { + return nil + } + + switch t := val.(type) { + case bool: + return &t + case *bool: + return t + default: + return nil + } +} + +type wrapper struct { + str string +} + +func (w *wrapper) String() string { + return w.str +} + +func wrap(str string) fmt.Stringer { + return &wrapper{str: str} +} + +func configBuilder(resources ...any) string { + var list []fmt.Stringer + + for _, r := range resources { + switch t := r.(type) { + case fmt.Stringer: + list = append(list, t) + case []*User: + list = append(list, utils.Map(t, func(item *User) fmt.Stringer { + return item + })...) + } + } + + buff := bytes.NewBufferString("") + for _, item := range list { + buff.WriteString(item.String() + "\n") + } + + return buff.String() +} diff --git a/twingate/internal/test/acctests/resource/config-connector-token.go b/twingate/internal/test/acctests/resource/config-connector-token.go new file mode 100644 index 00000000..6fe3264a --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-connector-token.go @@ -0,0 +1,48 @@ +package resource + +import ( + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" +) + +type ConnectorToken struct { + ResourceName string + ConnectorID string +} + +func NewConnectorToken(connectorID string) *ConnectorToken { + return &ConnectorToken{ + ResourceName: test.RandomResourceName(), + ConnectorID: connectorID, + } +} + +func (r *ConnectorToken) TerraformResource() string { + return acctests.TerraformConnectorTokens(r.ResourceName) +} + +func (r *ConnectorToken) String() string { + return acctests.Nprintf(` + resource "twingate_connector_tokens" "${terraform_resource}" { + connector_id = ${connector_id} + } + `, map[string]any{ + "terraform_resource": r.ResourceName, + "connector_id": r.ConnectorID, + }) +} + +func (r *ConnectorToken) Set(values ...any) *ConnectorToken { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.ConnectorID: + r.ConnectorID = val.(string) + } + } + + return r +} diff --git a/twingate/internal/test/acctests/resource/config-connector.go b/twingate/internal/test/acctests/resource/config-connector.go new file mode 100644 index 00000000..9edb9be2 --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-connector.go @@ -0,0 +1,77 @@ +package resource + +import ( + "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "strings" +) + +type Connector struct { + ResourceName string + RemoteNetworkID string + Name *string + StatusUpdatesEnabled *bool +} + +func NewConnector(remoteNetworkID string) *Connector { + return &Connector{ + ResourceName: test.RandomResourceName(), + RemoteNetworkID: remoteNetworkID, + } +} + +func (r *Connector) optionalAttributes() string { + var optional []string + + if r.Name != nil { + optional = append(optional, fmt.Sprintf(`name = "%s"`, *r.Name)) + } + + if r.StatusUpdatesEnabled != nil { + optional = append(optional, fmt.Sprintf(`status_updates_enabled = %v`, *r.StatusUpdatesEnabled)) + } + + return strings.Join(optional, "\n") +} + +func (r *Connector) TerraformResource() string { + return acctests.TerraformConnector(r.ResourceName) +} + +func (r *Connector) TerraformResourceID() string { + return r.TerraformResource() + ".id" +} + +func (r *Connector) String() string { + return acctests.Nprintf(` + resource "twingate_connector" "${terraform_resource}" { + remote_network_id = ${remote_network_id} + + ${optional_attributes} + } + `, map[string]any{ + "terraform_resource": r.ResourceName, + "remote_network_id": r.RemoteNetworkID, + "optional_attributes": r.optionalAttributes(), + }) +} + +func (r *Connector) Set(values ...any) *Connector { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.Name: + r.Name = optionalString(val) + case attr.RemoteNetworkID: + r.RemoteNetworkID = val.(string) + case attr.StatusUpdatesEnabled: + r.StatusUpdatesEnabled = optionalBool(val) + } + } + + return r +} diff --git a/twingate/internal/test/acctests/resource/config-group.go b/twingate/internal/test/acctests/resource/config-group.go new file mode 100644 index 00000000..5e4c6501 --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-group.go @@ -0,0 +1,95 @@ +package resource + +import ( + "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "strings" +) + +type Group struct { + ResourceName string + Name string + SecurityPolicyID *string + + UserIDs []string + userIDsEnabled bool + + IsAuthoritative *bool +} + +func NewGroup() *Group { + return &Group{ + ResourceName: test.RandomResourceName(), + Name: test.RandomGroupName(), + } +} + +func (g *Group) optionalAttributes() string { + var optional []string + + if g.SecurityPolicyID != nil { + optional = append(optional, fmt.Sprintf(`security_policy_id = "%s"`, *g.SecurityPolicyID)) + } + + if g.userIDsEnabled { + optional = append(optional, fmt.Sprintf(`user_ids = [%s]`, strings.Join(g.UserIDs, ", "))) + } + + if g.IsAuthoritative != nil { + optional = append(optional, fmt.Sprintf(`is_authoritative = %v`, *g.IsAuthoritative)) + } + + return strings.Join(optional, "\n") +} + +func (g *Group) TerraformResource() string { + return acctests.TerraformGroup(g.ResourceName) +} + +func (g *Group) String() string { + return acctests.Nprintf(` + resource "twingate_group" "${terraform_resource}" { + name = "${name}" + + ${optional_attributes} + } + `, map[string]any{ + "terraform_resource": g.ResourceName, + "name": g.Name, + "optional_attributes": g.optionalAttributes(), + }) +} + +func (g *Group) Set(values ...any) *Group { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.Name: + g.Name = val.(string) + case attr.SecurityPolicyID: + g.SecurityPolicyID = optionalString(val) + case attr.UserIDs: + g.UserIDs = val.([]string) + g.userIDsEnabled = len(g.UserIDs) > 0 + case attr.IsAuthoritative: + g.IsAuthoritative = optionalBool(val) + } + } + + return g +} + +func configGroup(groupResource, name string) string { + return acctests.Nprintf(` + resource "twingate_group" "${group_resource}" { + name = "${name}" + } + `, map[string]any{ + "group_resource": groupResource, + "name": name, + }) +} diff --git a/twingate/internal/test/acctests/resource/config-remote-network.go b/twingate/internal/test/acctests/resource/config-remote-network.go new file mode 100644 index 00000000..21c38bc5 --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-remote-network.go @@ -0,0 +1,71 @@ +package resource + +import ( + "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "strings" +) + +type RemoteNetwork struct { + ResourceName string + Name string + Location *string +} + +func NewRemoteNetwork() *RemoteNetwork { + return &RemoteNetwork{ + ResourceName: test.RandomResourceName(), + Name: test.RandomNetworkName(), + } +} + +func (r *RemoteNetwork) optionalAttributes() string { + var optional []string + + if r.Location != nil { + optional = append(optional, fmt.Sprintf(`location = "%s"`, *r.Location)) + } + + return strings.Join(optional, "\n") +} + +func (r *RemoteNetwork) TerraformResource() string { + return acctests.TerraformRemoteNetwork(r.ResourceName) +} + +func (r *RemoteNetwork) TerraformResourceID() string { + return r.TerraformResource() + ".id" +} + +func (r *RemoteNetwork) String() string { + return acctests.Nprintf(` + resource "twingate_remote_network" "${terraform_resource}" { + name = "${name}" + + + ${optional_attributes} + } + `, map[string]any{ + "terraform_resource": r.ResourceName, + "name": r.Name, + "optional_attributes": r.optionalAttributes(), + }) +} + +func (r *RemoteNetwork) Set(values ...any) *RemoteNetwork { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.Name: + r.Name = val.(string) + case attr.Location: + r.Location = optionalString(val) + } + } + + return r +} diff --git a/twingate/internal/test/acctests/resource/config-resource.go b/twingate/internal/test/acctests/resource/config-resource.go new file mode 100644 index 00000000..6a034f92 --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-resource.go @@ -0,0 +1,221 @@ +package resource + +import ( + "fmt" + "strings" + + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" +) + +type Resource struct { + ResourceName string + Name string + Address string + RemoteNetworkID string + Protocols *Protocols + Access []Access + IsActive *bool + IsVisible *bool + IsAuthoritative *bool + IsBrowserShortcutEnabled *bool + Alias *string + SecurityPolicyID *string +} + +func NewResource(remoteNetworkID string) *Resource { + return &Resource{ + ResourceName: test.RandomResourceName(), + Name: test.RandomName(), + Address: test.RandomName() + ".com", + RemoteNetworkID: remoteNetworkID, + } +} + +func (r *Resource) optionalAttributes() string { + var optional []string + + if r.Alias != nil { + optional = append(optional, fmt.Sprintf(`alias = "%s"`, *r.Alias)) + } + + if r.SecurityPolicyID != nil { + optional = append(optional, fmt.Sprintf(`security_policy_id = "%s"`, *r.SecurityPolicyID)) + } + + if r.IsAuthoritative != nil { + optional = append(optional, fmt.Sprintf(`is_authoritative = %v`, *r.IsAuthoritative)) + } + + if r.IsActive != nil { + optional = append(optional, fmt.Sprintf(`is_active = %v`, *r.IsActive)) + } + + if r.IsVisible != nil { + optional = append(optional, fmt.Sprintf(`is_visible = %v`, *r.IsVisible)) + } + + if r.IsBrowserShortcutEnabled != nil { + optional = append(optional, fmt.Sprintf(`is_browser_shortcut_enabled = %v`, *r.IsBrowserShortcutEnabled)) + } + + for _, access := range r.Access { + block := access.String() + if block != "" { + optional = append(optional, block) + } + } + + if r.Protocols != nil { + optional = append(optional, fmt.Sprintf(`protocols = %s`, r.Protocols.String())) + } + + return strings.Join(optional, "\n") +} + +func (r *Resource) TerraformResource() string { + return acctests.TerraformResource(r.ResourceName) +} + +func (r *Resource) String() string { + return acctests.Nprintf(` + resource "twingate_resource" "${terraform_resource}" { + name = "${name}" + address = "${address}" + remote_network_id = ${remote_network_id} + + ${optional_attributes} + } + `, map[string]any{ + "terraform_resource": r.ResourceName, + "name": r.Name, + "address": r.Address, + "remote_network_id": r.RemoteNetworkID, + "optional_attributes": r.optionalAttributes(), + }) +} + +func (r *Resource) Set(values ...any) *Resource { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.Name: + r.Name = val.(string) + case attr.Address: + r.Address = val.(string) + case attr.RemoteNetworkID: + r.RemoteNetworkID = val.(string) + + case attr.Alias: + r.Alias = optionalString(val) + case attr.SecurityPolicyID: + r.SecurityPolicyID = optionalString(val) + case attr.IsActive: + r.IsActive = optionalBool(val) + case attr.IsVisible: + r.IsVisible = optionalBool(val) + case attr.IsAuthoritative: + r.IsAuthoritative = optionalBool(val) + case attr.IsBrowserShortcutEnabled: + r.IsBrowserShortcutEnabled = optionalBool(val) + case attr.Protocols: + r.Protocols = val.(*Protocols) + case attr.Access: + r.Access = val.([]Access) + } + } + + return r +} + +type Protocols struct { + AllowIcmp bool + UDP Protocol + TCP Protocol +} + +func (p *Protocols) String() string { + return acctests.Nprintf(`{ + allow_icmp = ${allow_icmp} + tcp = ${tcp} + udp = ${udp} + } + `, map[string]any{ + "allow_icmp": p.AllowIcmp, + "tcp": p.TCP.String(), + "udp": p.UDP.String(), + }) +} + +type Protocol struct { + Policy string + Ports []string + ShowEmptyPorts bool +} + +func (p *Protocol) String() string { + return acctests.Nprintf(`{ + policy = "${policy}" + ${optional_attributes} + } + `, map[string]any{ + "policy": p.Policy, + "optional_attributes": p.optionalAttributes(), + }) +} + +func (p *Protocol) optionalAttributes() string { + if !p.ShowEmptyPorts && len(p.Ports) == 0 { + return "" + } + + return fmt.Sprintf(`ports = [%s]`, toStringList(p.Ports)) +} + +type Access struct { + GroupIDs []string + ServiceAccountIDs []string +} + +func (a *Access) String() string { + if len(a.GroupIDs) == 0 && len(a.ServiceAccountIDs) == 0 { + return "" + } + + return acctests.Nprintf(` + access { + ${optional_attributes} + } + `, map[string]any{ + "optional_attributes": a.optionalAttributes(), + }) +} + +func (a *Access) optionalAttributes() string { + var optional []string + + if len(a.GroupIDs) > 0 { + optional = append(optional, fmt.Sprintf(`group_ids = [%s]`, toList(a.GroupIDs))) + } + + if len(a.ServiceAccountIDs) > 0 { + optional = append(optional, fmt.Sprintf(`service_account_ids = [%s]`, toList(a.ServiceAccountIDs))) + } + + return strings.Join(optional, "\n") +} + +func toList(ids []string) string { + return strings.Join(ids, ", ") +} + +func toStringList(ids []string) string { + if len(ids) == 0 { + return "" + } + + return `"` + strings.Join(ids, `", "`) + `"` +} diff --git a/twingate/internal/test/acctests/resource/config-service-account.go b/twingate/internal/test/acctests/resource/config-service-account.go new file mode 100644 index 00000000..bcebc23a --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-service-account.go @@ -0,0 +1,52 @@ +package resource + +import ( + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" +) + +type ServiceAccount struct { + ResourceName string + Name string +} + +func NewServiceAccount() *ServiceAccount { + return &ServiceAccount{ + ResourceName: test.RandomResourceName(), + Name: test.RandomServiceAccountName(), + } +} + +func (r *ServiceAccount) TerraformResource() string { + return acctests.TerraformServiceAccount(r.ResourceName) +} + +func (r *ServiceAccount) TerraformResourceID() string { + return r.TerraformResource() + ".id" +} + +func (r *ServiceAccount) String() string { + return acctests.Nprintf(` + resource "twingate_service_account" "${terraform_resource}" { + name = "${name}" + } + `, map[string]any{ + "terraform_resource": r.ResourceName, + "name": r.Name, + }) +} + +func (r *ServiceAccount) Set(values ...any) *ServiceAccount { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.Name: + r.Name = val.(string) + } + } + + return r +} diff --git a/twingate/internal/test/acctests/resource/config-service-key.go b/twingate/internal/test/acctests/resource/config-service-key.go new file mode 100644 index 00000000..2d1afd1c --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-service-key.go @@ -0,0 +1,78 @@ +package resource + +import ( + "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "strings" +) + +type ServiceAccountKey struct { + ResourceName string + ServiceAccountID string + + ExpirationTime *int + Name *string +} + +func NewServiceAccountKey(serviceAccountID string) *ServiceAccountKey { + return &ServiceAccountKey{ + ResourceName: test.RandomResourceName(), + ServiceAccountID: serviceAccountID, + } +} + +func (r *ServiceAccountKey) TerraformResource() string { + return acctests.TerraformServiceKey(r.ResourceName) +} + +func (r *ServiceAccountKey) TerraformResourceID() string { + return r.TerraformResource() + ".id" +} + +func (r *ServiceAccountKey) String() string { + return acctests.Nprintf(` + resource "twingate_service_account_key" "${terraform_resource}" { + service_account_id = ${service_account_id} + + ${optional_attributes} + } + `, map[string]any{ + "terraform_resource": r.ResourceName, + "service_account_id": r.ServiceAccountID, + "optional_attributes": r.optionalAttributes(), + }) +} + +func (r *ServiceAccountKey) Set(values ...any) *ServiceAccountKey { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.Name: + r.Name = optionalString(val) + case attr.ExpirationTime: + r.ExpirationTime = optionalInt(val) + case attr.ServiceAccountID: + r.ServiceAccountID = val.(string) + } + } + + return r +} + +func (r *ServiceAccountKey) optionalAttributes() string { + var optional []string + + if r.Name != nil { + optional = append(optional, fmt.Sprintf(`name = "%s"`, *r.Name)) + } + + if r.ExpirationTime != nil { + optional = append(optional, fmt.Sprintf(`expiration_time = %v`, *r.ExpirationTime)) + } + + return strings.Join(optional, "\n") +} diff --git a/twingate/internal/test/acctests/resource/config-user.go b/twingate/internal/test/acctests/resource/config-user.go new file mode 100644 index 00000000..47ad3a5e --- /dev/null +++ b/twingate/internal/test/acctests/resource/config-user.go @@ -0,0 +1,112 @@ +package resource + +import ( + "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" + "strings" +) + +type User struct { + ResourceName string + Email string + FirstName *string + LastName *string + Role *string + SendInvite bool + IsActive bool +} + +func NewUser(terraformResourceName ...string) *User { + resourceName := test.RandomResourceName() + if len(terraformResourceName) > 0 { + resourceName = terraformResourceName[0] + } + + return &User{ + ResourceName: resourceName, + Email: test.RandomEmail(), + IsActive: true, // default value + SendInvite: false, // default value for tests + } +} + +func (u *User) optionalAttributes() string { + var optional []string + + if u.FirstName != nil { + optional = append(optional, fmt.Sprintf(`first_name = "%s"`, *u.FirstName)) + } + + if u.LastName != nil { + optional = append(optional, fmt.Sprintf(`last_name = "%s"`, *u.LastName)) + } + + if u.Role != nil { + optional = append(optional, fmt.Sprintf(`role = "%s"`, *u.Role)) + } + + return strings.Join(optional, "\n") +} + +func (u *User) TerraformResource() string { + return acctests.TerraformUser(u.ResourceName) +} + +func (u *User) String() string { + return acctests.Nprintf(` + resource "twingate_user" "${terraform_resource}" { + email = "${email}" + send_invite = ${send_invite} + is_active = ${is_active} + + ${optional_attributes} + } + `, map[string]any{ + "terraform_resource": u.ResourceName, + "email": u.Email, + "send_invite": u.SendInvite, + "is_active": u.IsActive, + "optional_attributes": u.optionalAttributes(), + }) +} + +func (u *User) Set(values ...any) *User { + for i := 0; i < len(values); i += 2 { + key := values[i].(string) + val := values[i+1] + + switch key { + case attr.Email: + u.Email = val.(string) + case attr.FirstName: + u.FirstName = optionalString(val) + case attr.LastName: + u.LastName = optionalString(val) + case attr.Role: + u.Role = optionalString(val) + case attr.SendInvite: + u.SendInvite = val.(bool) + case attr.IsActive: + u.IsActive = val.(bool) + } + } + + return u +} + +func genUsers(count int, resourcePrefix ...string) []*User { + users := make([]*User, 0, count) + + prefix := test.RandomUserName() + if len(resourcePrefix) > 0 { + prefix = resourcePrefix[0] + } + + for i := 0; i < count; i++ { + users = append(users, NewUser(fmt.Sprintf("%s_%d", prefix, i+1))) + } + + return users +} diff --git a/twingate/internal/test/acctests/resource/connector_test.go b/twingate/internal/test/acctests/resource/connector_test.go index 229e05aa..ed0647b7 100644 --- a/twingate/internal/test/acctests/resource/connector_test.go +++ b/twingate/internal/test/acctests/resource/connector_test.go @@ -16,8 +16,9 @@ import ( func TestAccRemoteConnectorCreate(t *testing.T) { t.Parallel() - connectorName := test.RandomConnectorName() - theResource := acctests.TerraformConnector(connectorName) + network := NewRemoteNetwork() + connector := NewConnector(network.TerraformResourceID()) + theResource := connector.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -25,9 +26,9 @@ func TestAccRemoteConnectorCreate(t *testing.T) { CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configConnector(connectorName, connectorName, test.RandomName()), + Config: configBuilder(network, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(connectorName)), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network.TerraformResource()), sdk.TestCheckResourceAttrSet(theResource, attr.Name), ), }, @@ -38,8 +39,9 @@ func TestAccRemoteConnectorCreate(t *testing.T) { func TestAccRemoteConnectorWithCustomName(t *testing.T) { t.Parallel() - connectorName := test.RandomConnectorName() - theResource := acctests.TerraformConnector(connectorName) + network := NewRemoteNetwork() + connector := NewConnector(network.TerraformResourceID()).Set(attr.Name, test.RandomConnectorName()) + theResource := connector.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -47,10 +49,10 @@ func TestAccRemoteConnectorWithCustomName(t *testing.T) { CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configConnectorWithName(connectorName, test.RandomName(), connectorName), + Config: configBuilder(network, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(connectorName)), - sdk.TestCheckResourceAttr(theResource, attr.Name, connectorName), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network.TerraformResource()), + sdk.TestCheckResourceAttr(theResource, attr.Name, *connector.Name), ), }, }, @@ -60,8 +62,9 @@ func TestAccRemoteConnectorWithCustomName(t *testing.T) { func TestAccRemoteConnectorImport(t *testing.T) { t.Parallel() - connectorName := test.RandomConnectorName() - theResource := acctests.TerraformConnector(connectorName) + network := NewRemoteNetwork() + connector := NewConnector(network.TerraformResourceID()).Set(attr.Name, test.RandomConnectorName()) + theResource := connector.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -69,10 +72,10 @@ func TestAccRemoteConnectorImport(t *testing.T) { CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configConnectorWithName(connectorName, test.RandomName(), connectorName), + Config: configBuilder(network, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(connectorName)), - sdk.TestCheckResourceAttr(theResource, attr.Name, connectorName), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network.TerraformResource()), + sdk.TestCheckResourceAttr(theResource, attr.Name, *connector.Name), ), }, { @@ -80,7 +83,7 @@ func TestAccRemoteConnectorImport(t *testing.T) { ImportStateVerify: true, ResourceName: theResource, ImportStateCheck: acctests.CheckImportState(map[string]string{ - attr.Name: connectorName, + attr.Name: *connector.Name, }), }, }, @@ -90,12 +93,10 @@ func TestAccRemoteConnectorImport(t *testing.T) { func TestAccRemoteConnectorNotAllowedToChangeRemoteNetworkId(t *testing.T) { t.Parallel() - terraformConnectorName := test.RandomConnectorName() - terraformRemoteNetworkName1 := test.RandomNetworkName() - terraformRemoteNetworkName2 := test.RandomNetworkName() - theResource := acctests.TerraformConnector(terraformConnectorName) - remoteNetworkName1 := test.RandomName() - remoteNetworkName2 := test.RandomName() + network1 := NewRemoteNetwork() + network2 := NewRemoteNetwork() + connector := NewConnector(network1.TerraformResourceID()) + theResource := connector.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -103,13 +104,13 @@ func TestAccRemoteConnectorNotAllowedToChangeRemoteNetworkId(t *testing.T) { CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configConnector(terraformRemoteNetworkName1, terraformConnectorName, remoteNetworkName1), + Config: configBuilder(network1, network2, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(terraformRemoteNetworkName1)), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network1.TerraformResource()), ), }, { - Config: configConnector(terraformRemoteNetworkName2, terraformConnectorName, remoteNetworkName2), + Config: configBuilder(network1, network2, connector.Set(attr.RemoteNetworkID, network2.TerraformResourceID())), ExpectError: regexp.MustCompile(resource.ErrNotAllowChangeRemoteNetworkID.Error()), }, }, @@ -119,9 +120,9 @@ func TestAccRemoteConnectorNotAllowedToChangeRemoteNetworkId(t *testing.T) { func TestAccTwingateConnectorReCreateAfterDeletion(t *testing.T) { t.Parallel() - connectorName := test.RandomConnectorName() - theResource := acctests.TerraformConnector(connectorName) - networkName := test.RandomName() + network := NewRemoteNetwork() + connector := NewConnector(network.TerraformResourceID()) + theResource := connector.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -129,17 +130,17 @@ func TestAccTwingateConnectorReCreateAfterDeletion(t *testing.T) { CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configConnector(connectorName, connectorName, networkName), + Config: configBuilder(network, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(connectorName)), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network.TerraformResource()), acctests.DeleteTwingateResource(theResource, resource.TwingateConnector), ), ExpectNonEmptyPlan: true, }, { - Config: configConnector(connectorName, connectorName, networkName), + Config: configBuilder(network, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(connectorName)), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network.TerraformResource()), ), }, }, @@ -161,23 +162,6 @@ func configConnector(networkTR, connectorTR, networkName string) string { }) } -func configConnectorWithName(terraformResource, networkName, connectorName string) string { - return acctests.Nprintf(` - ${remote_network} - - resource "twingate_connector" "${connector_resource}" { - remote_network_id = twingate_remote_network.${remote_network_resource}.id - name = "${connector_name}" - } - `, - map[string]any{ - "remote_network": configRemoteNetwork(terraformResource, networkName), - "connector_resource": terraformResource, - "remote_network_resource": terraformResource, - "connector_name": connectorName, - }) -} - func checkTwingateConnectorSetWithRemoteNetwork(connectorResource, remoteNetworkResource string) sdk.TestCheckFunc { return func(s *terraform.State) error { connector, ok := s.RootModule().Resources[connectorResource] @@ -206,9 +190,10 @@ func TestAccRemoteConnectorUpdateName(t *testing.T) { t.Parallel() connectorName := test.RandomConnectorName() - theResource := acctests.TerraformConnector(connectorName) - networkName := test.RandomName() - connectorNewName := test.RandomConnectorName() + + network := NewRemoteNetwork() + connector := NewConnector(network.TerraformResourceID()) + theResource := connector.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -216,16 +201,16 @@ func TestAccRemoteConnectorUpdateName(t *testing.T) { CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configConnector(connectorName, connectorName, networkName), + Config: configBuilder(network, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(connectorName)), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network.TerraformResource()), sdk.TestCheckResourceAttrSet(theResource, attr.Name), ), }, { - Config: configConnectorWithName(connectorName, networkName, connectorNewName), + Config: configBuilder(network, connector.Set(attr.Name, connectorName)), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckResourceAttr(theResource, attr.Name, connectorNewName), + sdk.TestCheckResourceAttr(theResource, attr.Name, connectorName), ), }, }, @@ -235,9 +220,9 @@ func TestAccRemoteConnectorUpdateName(t *testing.T) { func TestAccRemoteConnectorCreateWithNotificationStatus(t *testing.T) { t.Parallel() - connectorName := test.RandomConnectorName() - theResource := acctests.TerraformConnector(connectorName) - networkName := test.RandomName() + network := NewRemoteNetwork() + connector := NewConnector(network.TerraformResourceID()) + theResource := connector.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -245,22 +230,22 @@ func TestAccRemoteConnectorCreateWithNotificationStatus(t *testing.T) { CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configConnector(connectorName, connectorName, networkName), + Config: configBuilder(network, connector), Check: acctests.ComposeTestCheckFunc( - checkTwingateConnectorSetWithRemoteNetwork(theResource, acctests.TerraformRemoteNetwork(connectorName)), + checkTwingateConnectorSetWithRemoteNetwork(theResource, network.TerraformResource()), sdk.TestCheckResourceAttrSet(theResource, attr.Name), ), }, { // expecting no changes, as by default notifications enabled PlanOnly: true, - Config: configConnectorWithNotificationStatus(connectorName, connectorName, networkName, true), + Config: configBuilder(network, connector.Set(attr.StatusUpdatesEnabled, true)), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, attr.StatusUpdatesEnabled, "true"), ), }, { - Config: configConnectorWithNotificationStatus(connectorName, connectorName, networkName, false), + Config: configBuilder(network, connector.Set(attr.StatusUpdatesEnabled, false)), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, attr.StatusUpdatesEnabled, "false"), ), @@ -268,7 +253,7 @@ func TestAccRemoteConnectorCreateWithNotificationStatus(t *testing.T) { { // expecting no changes, when user removes `status_updates_enabled` field from terraform PlanOnly: true, - Config: configConnector(connectorName, connectorName, networkName), + Config: configBuilder(network, connector.Set(attr.StatusUpdatesEnabled, nil)), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, attr.StatusUpdatesEnabled, "false"), ), @@ -277,20 +262,24 @@ func TestAccRemoteConnectorCreateWithNotificationStatus(t *testing.T) { }) } -func configConnectorWithNotificationStatus(terraformRemoteNetworkName, terraformConnectorName, remoteNetworkName string, notificationStatus bool) string { - return acctests.Nprintf(` - ${remote_network} +func TestAccRemoteConnectorCreateWithNotificationStatusFalse(t *testing.T) { + t.Parallel() - resource "twingate_connector" "${connector_resource}" { - remote_network_id = twingate_remote_network.${remote_network_resource}.id - status_updates_enabled = ${notification_status} - } - `, - map[string]any{ - "remote_network": configRemoteNetwork(terraformRemoteNetworkName, remoteNetworkName), - "connector_resource": terraformConnectorName, - "remote_network_resource": terraformRemoteNetworkName, - "notification_status": notificationStatus, - }) + network := NewRemoteNetwork() + connector := NewConnector(network.TerraformResourceID()) + theResource := connector.TerraformResource() + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateConnectorAndRemoteNetworkDestroy, + Steps: []sdk.TestStep{ + { + Config: configBuilder(network, connector.Set(attr.StatusUpdatesEnabled, false)), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theResource, attr.StatusUpdatesEnabled, "false"), + ), + }, + }, + }) } diff --git a/twingate/internal/test/acctests/resource/group_test.go b/twingate/internal/test/acctests/resource/group_test.go index ab226b40..98e1b449 100644 --- a/twingate/internal/test/acctests/resource/group_test.go +++ b/twingate/internal/test/acctests/resource/group_test.go @@ -1,7 +1,6 @@ package resource import ( - "strings" "testing" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" @@ -9,6 +8,7 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" ) var userIdsLen = attr.Len(attr.UserIDs) @@ -16,25 +16,26 @@ var userIdsLen = attr.Len(attr.UserIDs) func TestAccTwingateGroupCreateUpdate(t *testing.T) { t.Parallel() - groupResource := test.RandomGroupName() - theResource := acctests.TerraformGroup(groupResource) name1 := test.RandomName() name2 := test.RandomName() + group := NewGroup() + theResource := group.TerraformResource() + sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, PreCheck: func() { acctests.PreCheck(t) }, CheckDestroy: acctests.CheckTwingateGroupDestroy, Steps: []sdk.TestStep{ { - Config: configGroup(groupResource, name1), + Config: configBuilder(group.Set(attr.Name, name1)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Name, name1), ), }, { - Config: configGroup(groupResource, name2), + Config: configBuilder(group.Set(attr.Name, name2)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Name, name2), @@ -44,22 +45,10 @@ func TestAccTwingateGroupCreateUpdate(t *testing.T) { }) } -func configGroup(groupResource, name string) string { - return acctests.Nprintf(` - resource "twingate_group" "${group_resource}" { - name = "${name}" - } - `, map[string]any{ - "group_resource": groupResource, - "name": name, - }) -} - func TestAccTwingateGroupDeleteNonExisting(t *testing.T) { t.Parallel() - groupResource := test.RandomGroupName() - theResource := acctests.TerraformGroup(groupResource) + group := NewGroup() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -67,11 +56,16 @@ func TestAccTwingateGroupDeleteNonExisting(t *testing.T) { CheckDestroy: acctests.CheckTwingateGroupDestroy, Steps: []sdk.TestStep{ { - Config: configGroup(groupResource, test.RandomName()), + Config: configBuilder(group), Destroy: true, - Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceDoesNotExists(theResource), - ), + }, + { + Config: configBuilder(group), + ConfigPlanChecks: sdk.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(group.TerraformResource(), plancheck.ResourceActionCreate), + }, + }, }, }, }) @@ -80,9 +74,8 @@ func TestAccTwingateGroupDeleteNonExisting(t *testing.T) { func TestAccTwingateGroupReCreateAfterDeletion(t *testing.T) { t.Parallel() - groupResource := test.RandomGroupName() - theResource := acctests.TerraformGroup(groupResource) - groupName := test.RandomName() + group := NewGroup() + theResource := group.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -90,7 +83,7 @@ func TestAccTwingateGroupReCreateAfterDeletion(t *testing.T) { CheckDestroy: acctests.CheckTwingateGroupDestroy, Steps: []sdk.TestStep{ { - Config: configGroup(groupResource, groupName), + Config: configBuilder(group), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), acctests.DeleteTwingateResource(theResource, resource.TwingateGroup), @@ -98,7 +91,7 @@ func TestAccTwingateGroupReCreateAfterDeletion(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: configGroup(groupResource, groupName), + Config: configBuilder(group), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), @@ -110,9 +103,8 @@ func TestAccTwingateGroupReCreateAfterDeletion(t *testing.T) { func TestAccTwingateGroupWithSecurityPolicy(t *testing.T) { t.Parallel() - groupResource := test.RandomGroupName() - theResource := acctests.TerraformGroup(groupResource) - name := test.RandomName() + group := NewGroup() + theResource := group.TerraformResource() securityPolicies, err := acctests.ListSecurityPolicies() if err != nil { @@ -127,55 +119,41 @@ func TestAccTwingateGroupWithSecurityPolicy(t *testing.T) { CheckDestroy: acctests.CheckTwingateGroupDestroy, Steps: []sdk.TestStep{ { - Config: configGroup(groupResource, name), + Config: configBuilder(group), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Name, name), + sdk.TestCheckResourceAttr(theResource, attr.Name, group.Name), ), }, { - Config: configGroupWithSecurityPolicy(groupResource, name, testPolicy.ID), + Config: configBuilder(group.Set(attr.SecurityPolicyID, testPolicy.ID)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Name, name), + sdk.TestCheckResourceAttr(theResource, attr.Name, group.Name), sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, testPolicy.ID), ), }, { // expecting no changes PlanOnly: true, - Config: configGroup(groupResource, name), + Config: configBuilder(group.Set(attr.SecurityPolicyID, nil)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Name, name), + sdk.TestCheckResourceAttr(theResource, attr.Name, group.Name), ), }, }, }) } -func configGroupWithSecurityPolicy(terraformResourceName, name, securityPolicyID string) string { - return acctests.Nprintf(` - resource "twingate_group" "${group_resource}" { - name = "${name}" - security_policy_id = "${security_policy_id}" - } - `, - map[string]any{ - "group_resource": terraformResourceName, - "name": name, - "security_policy_id": securityPolicyID, - }) -} - func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { t.Parallel() - groupResource := test.RandomGroupName() - theResource := acctests.TerraformGroup(groupResource) - groupName := test.RandomName() + users := genUsers(3) + userIDs := collectResourceIDs(users...) - users, userIDs := genNewUsers("u005", 3) + group := NewGroup() + theResource := group.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -183,17 +161,17 @@ func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { CheckDestroy: acctests.CheckTwingateGroupDestroy, Steps: []sdk.TestStep{ { - Config: configGroupWithUsers(groupResource, groupName, users, userIDs[:1]), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1])), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "1"), acctests.CheckGroupUsersLen(theResource, 1), ), }, { - Config: configGroupWithUsers(groupResource, groupName, users, userIDs[:1]), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1])), Check: acctests.ComposeTestCheckFunc( // added new user to the group though API - acctests.AddGroupUser(theResource, groupName, userIDs[1]), + acctests.AddGroupUser(theResource, group.Name, userIDs[1]), acctests.WaitTestFunc(), acctests.CheckGroupUsersLen(theResource, 2), ), @@ -201,7 +179,7 @@ func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: configGroupWithUsers(groupResource, groupName, users, userIDs[:1]), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1])), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "1"), acctests.CheckGroupUsersLen(theResource, 1), @@ -209,14 +187,14 @@ func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { }, { // added 2 new users to the group though terraform - Config: configGroupWithUsers(groupResource, groupName, users, userIDs[:3]), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:3])), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "3"), acctests.CheckGroupUsersLen(theResource, 3), ), }, { - Config: configGroupWithUsers(groupResource, groupName, users, userIDs[:3]), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:3])), Check: acctests.ComposeTestCheckFunc( // delete one user from the group though API acctests.DeleteGroupUser(theResource, userIDs[2]), @@ -228,7 +206,7 @@ func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: configGroupWithUsers(groupResource, groupName, users, userIDs[:3]), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:3])), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "3"), acctests.CheckGroupUsersLen(theResource, 3), @@ -236,7 +214,7 @@ func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { }, { // remove 2 users from the group though terraform - Config: configGroupWithUsers(groupResource, groupName, users, userIDs[:1]), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1])), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "1"), acctests.CheckGroupUsersLen(theResource, 1), @@ -244,11 +222,11 @@ func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { }, { // expecting no drift - Config: configGroupWithUsersAuthoritative(groupResource, groupName, users, userIDs[:1], true), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1], attr.IsAuthoritative, true)), PlanOnly: true, }, { - Config: configGroupWithUsersAuthoritative(groupResource, groupName, users, userIDs[:2], true), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:2], attr.IsAuthoritative, true)), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "2"), acctests.CheckGroupUsersLen(theResource, 2), @@ -258,50 +236,14 @@ func TestAccTwingateGroupUsersAuthoritativeByDefault(t *testing.T) { }) } -func configGroupWithUsers(terraformResourceName, name string, users, usersID []string) string { - return acctests.Nprintf(` - ${users} - - resource "twingate_group" "${group_resource}" { - name = "${name}" - user_ids = [${user_ids}] - } - `, - map[string]any{ - "users": strings.Join(users, "\n"), - "group_resource": terraformResourceName, - "name": name, - "user_ids": strings.Join(usersID, ", "), - }) -} - -func configGroupWithUsersAuthoritative(terraformResourceName, name string, users, usersID []string, authoritative bool) string { - return acctests.Nprintf(` - ${users} - - resource "twingate_group" "${group_resource}" { - name = "${name}" - user_ids = [${user_ids}] - is_authoritative = ${authoritative} - } - `, - map[string]any{ - "users": strings.Join(users, "\n"), - "group_resource": terraformResourceName, - "name": name, - "user_ids": strings.Join(usersID, ", "), - "authoritative": authoritative, - }) -} - func TestAccTwingateGroupUsersNotAuthoritative(t *testing.T) { t.Parallel() - groupResource := test.RandomGroupName() - theResource := acctests.TerraformGroup(groupResource) - groupName := test.RandomName() + users := genUsers(3) + userIDs := collectResourceIDs(users...) - users, userIDs := genNewUsers("u006", 3) + group := NewGroup() + theResource := group.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -309,24 +251,24 @@ func TestAccTwingateGroupUsersNotAuthoritative(t *testing.T) { CheckDestroy: acctests.CheckTwingateGroupDestroy, Steps: []sdk.TestStep{ { - Config: configGroupWithUsersAuthoritative(groupResource, groupName, users, userIDs[:1], false), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1], attr.IsAuthoritative, false)), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "1"), acctests.CheckGroupUsersLen(theResource, 1), ), }, { - Config: configGroupWithUsersAuthoritative(groupResource, groupName, users, userIDs[:1], false), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1], attr.IsAuthoritative, false)), Check: acctests.ComposeTestCheckFunc( // added new user to the group though API - acctests.AddGroupUser(theResource, groupName, userIDs[2]), + acctests.AddGroupUser(theResource, group.Name, userIDs[2]), acctests.WaitTestFunc(), acctests.CheckGroupUsersLen(theResource, 2), ), }, { // added new user to the group though terraform - Config: configGroupWithUsersAuthoritative(groupResource, groupName, users, userIDs[:2], false), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:2], attr.IsAuthoritative, false)), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "2"), acctests.CheckGroupUsersLen(theResource, 3), @@ -334,7 +276,7 @@ func TestAccTwingateGroupUsersNotAuthoritative(t *testing.T) { }, { // remove one user from the group though terraform - Config: configGroupWithUsersAuthoritative(groupResource, groupName, users, userIDs[:1], false), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1], attr.IsAuthoritative, false)), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, userIdsLen, "1"), acctests.CheckGroupUsersLen(theResource, 2), @@ -346,7 +288,7 @@ func TestAccTwingateGroupUsersNotAuthoritative(t *testing.T) { }, { // expecting no drift - empty plan - Config: configGroupWithUsersAuthoritative(groupResource, groupName, users, userIDs[:1], false), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs[:1], attr.IsAuthoritative, false)), PlanOnly: true, }, }, @@ -356,11 +298,11 @@ func TestAccTwingateGroupUsersNotAuthoritative(t *testing.T) { func TestAccTwingateGroupUsersCursor(t *testing.T) { acctests.SetPageLimit(t, 1) - groupResource := test.RandomGroupName() - theResource := acctests.TerraformGroup(groupResource) - groupName := test.RandomName() + users := genUsers(3) + userIDs := collectResourceIDs(users...) - users, userIDs := genNewUsers("u007", 3) + group := NewGroup() + theResource := group.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -368,13 +310,13 @@ func TestAccTwingateGroupUsersCursor(t *testing.T) { CheckDestroy: acctests.CheckTwingateGroupDestroy, Steps: []sdk.TestStep{ { - Config: configGroupWithUsers(groupResource, groupName, users, userIDs), + Config: configBuilder(users, group.Set(attr.UserIDs, userIDs)), Check: acctests.ComposeTestCheckFunc( acctests.CheckGroupUsersLen(theResource, len(users)), ), }, { - Config: configGroupWithUsers(groupResource, groupName, users[:2], userIDs[:2]), + Config: configBuilder(users[:2], group.Set(attr.UserIDs, userIDs[:2])), Check: acctests.ComposeTestCheckFunc( acctests.CheckGroupUsersLen(theResource, 2), ), diff --git a/twingate/internal/test/acctests/resource/remote-network_test.go b/twingate/internal/test/acctests/resource/remote-network_test.go index aa5e2a7c..e7b05512 100644 --- a/twingate/internal/test/acctests/resource/remote-network_test.go +++ b/twingate/internal/test/acctests/resource/remote-network_test.go @@ -9,15 +9,26 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" ) +func configRemoteNetwork(networkResource, name string) string { + return acctests.Nprintf(` + resource "twingate_remote_network" "${network_resource}" { + name = "${name}" + } + `, + map[string]any{ + "network_resource": networkResource, + "name": name, + }) +} + func TestAccTwingateRemoteNetworkCreate(t *testing.T) { t.Parallel() - networkResource := test.RandomNetworkName() - theResource := acctests.TerraformRemoteNetwork(networkResource) - networkName := test.RandomName() - networkLocation := model.LocationAzure + network := NewRemoteNetwork() + theResource := network.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -25,45 +36,33 @@ func TestAccTwingateRemoteNetworkCreate(t *testing.T) { CheckDestroy: acctests.CheckTwingateRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configRemoteNetworkWithLocation(networkResource, networkName, networkLocation), + Config: configBuilder(network.Set(attr.Location, model.LocationAzure)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Name, networkName), - sdk.TestCheckResourceAttr(theResource, attr.Location, networkLocation), + sdk.TestCheckResourceAttr(theResource, attr.Name, network.Name), + sdk.TestCheckResourceAttr(theResource, attr.Location, model.LocationAzure), ), }, }, }) } -func configRemoteNetworkWithLocation(networkResource, name, location string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "${name}" - location = "${location}" - } - `, map[string]any{ - "network_resource": networkResource, - "name": name, - "location": location, - }) -} - func TestAccTwingateRemoteNetworkUpdate(t *testing.T) { t.Parallel() - networkResource := test.RandomNetworkName() - theResource := acctests.TerraformRemoteNetwork(networkResource) name1 := test.RandomName() name2 := test.RandomName() + network := NewRemoteNetwork() + theResource := network.TerraformResource() + sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, PreCheck: func() { acctests.PreCheck(t) }, CheckDestroy: acctests.CheckTwingateRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configRemoteNetwork(networkResource, name1), + Config: configBuilder(network.Set(attr.Name, name1)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Name, name1), @@ -71,7 +70,7 @@ func TestAccTwingateRemoteNetworkUpdate(t *testing.T) { ), }, { - Config: configRemoteNetworkWithLocation(networkResource, name2, model.LocationAWS), + Config: configBuilder(network.Set(attr.Name, name2, attr.Location, model.LocationAWS)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Name, name2), @@ -82,23 +81,11 @@ func TestAccTwingateRemoteNetworkUpdate(t *testing.T) { }) } -func configRemoteNetwork(networkResource, name string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "${name}" - } - `, - map[string]any{ - "network_resource": networkResource, - "name": name, - }) -} - func TestAccTwingateRemoteNetworkDeleteNonExisting(t *testing.T) { t.Parallel() - networkResource := test.RandomNetworkName() - theResource := acctests.TerraformRemoteNetwork(networkResource) + network := NewRemoteNetwork() + theResource := network.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -106,11 +93,16 @@ func TestAccTwingateRemoteNetworkDeleteNonExisting(t *testing.T) { CheckDestroy: acctests.CheckTwingateRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configRemoteNetwork(networkResource, test.RandomName()), + Config: configBuilder(network), Destroy: true, - Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceDoesNotExists(theResource), - ), + }, + { + Config: configBuilder(network), + ConfigPlanChecks: sdk.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(theResource, plancheck.ResourceActionCreate), + }, + }, }, }, }) @@ -119,9 +111,8 @@ func TestAccTwingateRemoteNetworkDeleteNonExisting(t *testing.T) { func TestAccTwingateRemoteNetworkReCreateAfterDeletion(t *testing.T) { t.Parallel() - networkResource := test.RandomNetworkName() - theResource := acctests.TerraformRemoteNetwork(networkResource) - networkName := test.RandomName() + network := NewRemoteNetwork() + theResource := network.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -129,7 +120,7 @@ func TestAccTwingateRemoteNetworkReCreateAfterDeletion(t *testing.T) { CheckDestroy: acctests.CheckTwingateRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configRemoteNetwork(networkResource, networkName), + Config: configBuilder(network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), acctests.DeleteTwingateResource(theResource, resource.TwingateRemoteNetwork), @@ -137,7 +128,7 @@ func TestAccTwingateRemoteNetworkReCreateAfterDeletion(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: configRemoteNetwork(networkResource, networkName), + Config: configBuilder(network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), @@ -149,9 +140,8 @@ func TestAccTwingateRemoteNetworkReCreateAfterDeletion(t *testing.T) { func TestAccTwingateRemoteNetworkUpdateWithTheSameName(t *testing.T) { t.Parallel() - networkResource := test.RandomNetworkName() - theResource := acctests.TerraformRemoteNetwork(networkResource) - name := test.RandomName() + network := NewRemoteNetwork() + theResource := network.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -159,18 +149,18 @@ func TestAccTwingateRemoteNetworkUpdateWithTheSameName(t *testing.T) { CheckDestroy: acctests.CheckTwingateRemoteNetworkDestroy, Steps: []sdk.TestStep{ { - Config: configRemoteNetwork(networkResource, name), + Config: configBuilder(network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Name, name), + sdk.TestCheckResourceAttr(theResource, attr.Name, network.Name), sdk.TestCheckResourceAttr(theResource, attr.Location, model.LocationOther), ), }, { - Config: configRemoteNetworkWithLocation(networkResource, name, model.LocationAWS), + Config: configBuilder(network.Set(attr.Location, model.LocationAWS)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Name, name), + sdk.TestCheckResourceAttr(theResource, attr.Name, network.Name), sdk.TestCheckResourceAttr(theResource, attr.Location, model.LocationAWS), ), }, diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 17758c70..cef00df6 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -12,16 +12,17 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/stretchr/testify/assert" ) var ( - tcpPolicy = attr.Path(attr.Protocols, attr.TCP, attr.Policy) - udpPolicy = attr.Path(attr.Protocols, attr.UDP, attr.Policy) - firstTCPPort = attr.First(attr.Protocols, attr.TCP, attr.Ports) - firstUDPPort = attr.First(attr.Protocols, attr.UDP, attr.Ports) - tcpPortsLen = attr.Len(attr.Protocols, attr.TCP, attr.Ports) - udpPortsLen = attr.Len(attr.Protocols, attr.UDP, attr.Ports) + tcpPolicy = attr.PathAttr(attr.Protocols, attr.TCP, attr.Policy) + udpPolicy = attr.PathAttr(attr.Protocols, attr.UDP, attr.Policy) + firstTCPPort = attr.FirstAttr(attr.Protocols, attr.TCP, attr.Ports) + firstUDPPort = attr.FirstAttr(attr.Protocols, attr.UDP, attr.Ports) + tcpPortsLen = attr.LenAttr(attr.Protocols, attr.TCP, attr.Ports) + udpPortsLen = attr.LenAttr(attr.Protocols, attr.UDP, attr.Ports) accessGroupIdsLen = attr.Len(attr.Access, attr.GroupIDs) accessServiceAccountIdsLen = attr.Len(attr.Access, attr.ServiceAccountIDs) ) @@ -29,10 +30,9 @@ var ( func TestAccTwingateResourceCreate(t *testing.T) { t.Parallel() - resourceName := test.RandomResourceName() - theResource := acctests.TerraformResource(resourceName) - remoteNetworkName := test.RandomName() - name := test.RandomResourceName() + remoteNetwork := NewRemoteNetwork() + resource := NewResource(remoteNetwork.TerraformResourceID()) + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -40,13 +40,13 @@ func TestAccTwingateResourceCreate(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceBasic(resourceName, remoteNetworkName, name), + Config: configBuilder(resource, remoteNetwork), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckNoResourceAttr(theResource, accessGroupIdsLen), - sdk.TestCheckResourceAttr(acctests.TerraformRemoteNetwork(resourceName), attr.Name, remoteNetworkName), - sdk.TestCheckResourceAttr(theResource, attr.Name, name), - sdk.TestCheckResourceAttr(theResource, attr.Address, "acc-test.com"), + sdk.TestCheckResourceAttr(remoteNetwork.TerraformResource(), attr.Name, remoteNetwork.Name), + sdk.TestCheckResourceAttr(theResource, attr.Name, resource.Name), + sdk.TestCheckResourceAttr(theResource, attr.Address, resource.Address), ), }, }, @@ -56,10 +56,16 @@ func TestAccTwingateResourceCreate(t *testing.T) { func TestAccTwingateResourceUpdateProtocols(t *testing.T) { t.Parallel() - resourceName := test.RandomResourceName() - theResource := acctests.TerraformResource(resourceName) - remoteNetworkName := test.RandomName() - name := test.RandomResourceName() + remoteNetwork := NewRemoteNetwork() + resource := NewResource(remoteNetwork.TerraformResourceID()) + theResource := resource.TerraformResource() + + var nullProtocols *Protocols + protocols := &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyDenyAll}, + UDP: Protocol{Policy: model.PolicyDenyAll}, + } sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -67,19 +73,19 @@ func TestAccTwingateResourceUpdateProtocols(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceBasic(resourceName, remoteNetworkName, name), + Config: configBuilder(resource, remoteNetwork), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), }, { - Config: configResourceWithSimpleProtocols(resourceName, remoteNetworkName, name), + Config: configBuilder(resource.Set(attr.Protocols, protocols), remoteNetwork), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), }, { - Config: configResourceBasic(resourceName, remoteNetworkName, name), + Config: configBuilder(resource.Set(attr.Protocols, nullProtocols), remoteNetwork), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), @@ -107,44 +113,27 @@ func configResourceBasic(terraformResource, networkName, name string) string { }) } -func configResourceWithSimpleProtocols(terraformResource, networkName, name string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "${network_name}" - } - resource "twingate_resource" "${resource_resource}" { - name = "${resource_name}" - address = "acc-test.com" - remote_network_id = twingate_remote_network.${network_resource}.id - - protocols { - allow_icmp = true - tcp { - policy = "DENY_ALL" - } - udp { - policy = "DENY_ALL" - } - } - } - `, - map[string]any{ - "network_resource": terraformResource, - "network_name": networkName, - "resource_resource": terraformResource, - "resource_name": name, - }) -} - func TestAccTwingateResourceCreateWithProtocolsAndGroups(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - networkName := test.RandomName() - group1 := test.RandomGroupName() - group2 := test.RandomGroupName() - name := test.RandomResourceName() + group1 := NewGroup() + group2 := NewGroup() + + remoteNetwork := NewRemoteNetwork() + protocols := &Protocols{ + AllowIcmp: true, + TCP: Protocol{ + Ports: []string{"80", "82-83"}, + Policy: model.PolicyRestricted, + }, + UDP: Protocol{Policy: model.PolicyAllowAll}, + } + resource := NewResource(remoteNetwork.TerraformResourceID()).Set( + attr.Protocols, protocols, + attr.Access, []Access{{GroupIDs: collectResourceIDs(group1, group2)}}, + ) + + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -152,10 +141,10 @@ func TestAccTwingateResourceCreateWithProtocolsAndGroups(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithProtocolsAndGroups(terraformResource, networkName, group1, group2, name), + Config: configBuilder(resource, remoteNetwork, group1, group2), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Address, "new-acc-test.com"), + sdk.TestCheckResourceAttr(theResource, attr.Address, resource.Address), sdk.TestCheckResourceAttr(theResource, accessGroupIdsLen, "2"), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyRestricted), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "80"), @@ -165,61 +154,26 @@ func TestAccTwingateResourceCreateWithProtocolsAndGroups(t *testing.T) { }) } -func configResourceWithProtocolsAndGroups(terraformResource, networkName, groupName1, groupName2, resourceName string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "${network_name}" - } - - resource "twingate_group" "g21" { - name = "${group_1}" - } - - resource "twingate_group" "g22" { - name = "${group_2}" - } - - resource "twingate_resource" "${resource_resource}" { - name = "${resource_name}" - address = "new-acc-test.com" - remote_network_id = twingate_remote_network.${network_resource}.id - - protocols { - allow_icmp = true - tcp { - policy = "${tcp_policy}" - ports = ["80", "82-83"] - } - udp { - policy = "${udp_policy}" - } - } - - access { - group_ids = [twingate_group.g21.id, twingate_group.g22.id] - } - } - `, - map[string]any{ - "network_resource": terraformResource, - "network_name": networkName, - "group_1": groupName1, - "group_2": groupName2, - "resource_resource": terraformResource, - "resource_name": resourceName, - "tcp_policy": model.PolicyRestricted, - "udp_policy": model.PolicyAllowAll, - }) -} - func TestAccTwingateResourceFullCreationFlow(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - networkName := test.RandomName() - groupName := test.RandomGroupName() - name := test.RandomResourceName() + group := NewGroup() + network := NewRemoteNetwork() + protocols := &Protocols{ + AllowIcmp: true, + TCP: Protocol{ + Ports: []string{"80"}, + Policy: model.PolicyRestricted, + }, + UDP: Protocol{Policy: model.PolicyAllowAll}, + } + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, protocols, + attr.Access, []Access{{GroupIDs: collectResourceIDs(group)}}, + ) + + connector := NewConnector(network.TerraformResourceID()) + connectorToken := NewConnectorToken(connector.TerraformResourceID()) sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -227,127 +181,49 @@ func TestAccTwingateResourceFullCreationFlow(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configCompleteResource(terraformResource, networkName, groupName, name), + Config: configBuilder(resource, network, group, connector, connectorToken), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckResourceAttr(acctests.TerraformRemoteNetwork(terraformResource), attr.Name, networkName), - sdk.TestCheckResourceAttr(theResource, attr.Name, name), - sdk.TestMatchResourceAttr(acctests.TerraformConnectorTokens(terraformResource), attr.AccessToken, regexp.MustCompile(".+")), + sdk.TestCheckResourceAttr(network.TerraformResource(), attr.Name, network.Name), + sdk.TestCheckResourceAttr(resource.TerraformResource(), attr.Name, resource.Name), + sdk.TestMatchResourceAttr(connectorToken.TerraformResource(), attr.AccessToken, regexp.MustCompile(".+")), ), }, }, }) } -func configCompleteResource(terraformResource, networkName, groupName, resourceName string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "${network_name}" - } - - resource "twingate_connector" "${connector_resource}" { - remote_network_id = twingate_remote_network.${network_resource}.id - } - - resource "twingate_connector_tokens" "${connector_tokens_resource}" { - connector_id = twingate_connector.${connector_resource}.id - } - - resource "twingate_connector" "${connector_resource}-2" { - remote_network_id = twingate_remote_network.${network_resource}.id - } - - resource "twingate_connector_tokens" "${connector_tokens_resource}-2" { - connector_id = twingate_connector.${connector_resource}-2.id - } - - resource "twingate_group" "${group_resource}" { - name = "${group_name}" - } - - resource "twingate_resource" "${resource_resource}" { - name = "${resource_name}" - address = "acc-test.com" - remote_network_id = twingate_remote_network.${network_resource}.id - - protocols { - allow_icmp = true - tcp { - policy = "${tcp_policy}" - ports = ["3306"] - } - udp { - policy = "${udp_policy}" - } - } - - access { - group_ids = [twingate_group.${group_resource}.id] - } - } - `, - map[string]any{ - "network_resource": terraformResource, - "network_name": networkName, - "connector_resource": terraformResource, - "connector_tokens_resource": terraformResource, - "group_resource": terraformResource, - "group_name": groupName, - "resource_resource": terraformResource, - "resource_name": resourceName, - "tcp_policy": model.PolicyRestricted, - "udp_policy": model.PolicyAllowAll, - }) -} - func TestAccTwingateResourceWithInvalidGroupId(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - networkName := test.RandomResourceName() - name := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Access, []Access{{GroupIDs: []string{`"foo"`, `"bar"`}}}, + ) sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, PreCheck: func() { acctests.PreCheck(t) }, Steps: []sdk.TestStep{ { - Config: configResourceWithInvalidGroupId(terraformResource, networkName, name), + Config: configBuilder(resource, network), ExpectError: regexp.MustCompile("failed to create resource: Field 'groupIds' Unable to parse global ID"), }, }, }) } -func configResourceWithInvalidGroupId(terraformResource, networkName, resourceName string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "%s" - } - - resource "twingate_resource" "${resource_resource}" { - name = "${resource_name}" - address = "acc-test.com" - access { - group_ids = ["foo", "bar"] - } - remote_network_id = twingate_remote_network.${network_resource}.id - } - `, - map[string]any{ - "network_resource": terraformResource, - "network_name": networkName, - "resource_resource": terraformResource, - "resource_name": resourceName, - }) -} - func TestAccTwingateResourceWithTcpDenyAllPolicy(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - resourceName := test.RandomResourceName() - networkName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyDenyAll}, + UDP: Protocol{Policy: model.PolicyAllowAll}, + }, + ) + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -355,7 +231,7 @@ func TestAccTwingateResourceWithTcpDenyAllPolicy(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPolicy(terraformResource, networkName, resourceName, model.PolicyDenyAll, model.PolicyAllowAll), + Config: configBuilder(resource, network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyDenyAll), @@ -363,7 +239,7 @@ func TestAccTwingateResourceWithTcpDenyAllPolicy(t *testing.T) { }, // expecting no changes - empty plan { - Config: configResourceWithPolicy(terraformResource, networkName, resourceName, model.PolicyDenyAll, model.PolicyAllowAll), + Config: configBuilder(resource, network), PlanOnly: true, }, }, @@ -380,13 +256,13 @@ func configResourceWithPolicy(terraformResource, networkName, resourceName, tcpP name = "${resource_name}" address = "new-acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - - protocols { + + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" } - udp { + udp = { policy = "${udp_policy}" } } @@ -406,10 +282,15 @@ func configResourceWithPolicy(terraformResource, networkName, resourceName, tcpP func TestAccTwingateResourceWithUdpDenyAllPolicy(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyAllowAll}, + UDP: Protocol{Policy: model.PolicyDenyAll}, + }, + ) + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -417,7 +298,7 @@ func TestAccTwingateResourceWithUdpDenyAllPolicy(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPolicy(terraformResource, remoteNetworkName, resourceName, model.PolicyAllowAll, model.PolicyDenyAll), + Config: configBuilder(resource, network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, udpPolicy, model.PolicyDenyAll), @@ -425,7 +306,7 @@ func TestAccTwingateResourceWithUdpDenyAllPolicy(t *testing.T) { }, // expecting no changes - empty plan { - Config: configResourceWithPolicy(terraformResource, remoteNetworkName, resourceName, model.PolicyAllowAll, model.PolicyDenyAll), + Config: configBuilder(resource, network), PlanOnly: true, }, }, @@ -435,11 +316,17 @@ func TestAccTwingateResourceWithUdpDenyAllPolicy(t *testing.T) { func TestAccTwingateResourceWithDenyAllPolicyAndEmptyPortsList(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - groupName := test.RandomGroupName() - resourceName := test.RandomResourceName() + group := NewGroup() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyDenyAll, ShowEmptyPorts: true}, + UDP: Protocol{Policy: model.PolicyDenyAll}, + }, + attr.Access, []Access{{GroupIDs: collectResourceIDs(group)}}, + ) + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -447,9 +334,9 @@ func TestAccTwingateResourceWithDenyAllPolicyAndEmptyPortsList(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPolicyAndEmptyTCPPortsList(terraformResource, remoteNetworkName, groupName, resourceName, model.PolicyDenyAll, model.PolicyDenyAll), + Config: configBuilder(resource, network, group), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckResourceAttr(theResource, attr.Name, resourceName), + sdk.TestCheckResourceAttr(theResource, attr.Name, resource.Name), sdk.TestCheckResourceAttr(theResource, tcpPolicy, model.PolicyDenyAll), sdk.TestCheckNoResourceAttr(theResource, tcpPortsLen), sdk.TestCheckResourceAttr(theResource, udpPolicy, model.PolicyDenyAll), @@ -460,57 +347,25 @@ func TestAccTwingateResourceWithDenyAllPolicyAndEmptyPortsList(t *testing.T) { }) } -func configResourceWithPolicyAndEmptyTCPPortsList(terraformResource, networkName, groupName, resourceName, tcpPolicy, udpPolicy string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "${network_name}" - } - - resource "twingate_group" "${group_resource}" { - name = "${group_name}" - } - - resource "twingate_resource" "${resource_resource}" { - name = "${resource_name}" - address = "new-acc-test.com" - remote_network_id = twingate_remote_network.${network_resource}.id - access { - group_ids = [twingate_group.${group_resource}.id] - } - protocols { - allow_icmp = true - tcp { - policy = "${tcp_policy}" - ports = [] - } - udp { - policy = "${udp_policy}" - } - } - } - `, - map[string]any{ - "network_resource": terraformResource, - "network_name": networkName, - "group_resource": terraformResource, - "group_name": groupName, - "resource_resource": terraformResource, - "resource_name": resourceName, - "tcp_policy": tcpPolicy, - "udp_policy": udpPolicy, - }) -} - func TestAccTwingateResourceWithInvalidPortRange(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() expectedError := regexp.MustCompile("failed to parse protocols port range") + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted}, + UDP: Protocol{Policy: model.PolicyRestricted}, + }, + ) + genConfig := func(portRange string) string { - return configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, portRange) + resource.Protocols.TCP.Ports = []string{portRange} + resource.Protocols.UDP.Ports = []string{portRange} + + return configBuilder(resource, network) } sdk.Test(t, sdk.TestCase{ @@ -518,35 +373,35 @@ func TestAccTwingateResourceWithInvalidPortRange(t *testing.T) { PreCheck: func() { acctests.PreCheck(t) }, Steps: []sdk.TestStep{ { - Config: genConfig(`""`), + Config: genConfig(""), ExpectError: expectedError, }, { - Config: genConfig(`" "`), + Config: genConfig(" "), ExpectError: expectedError, }, { - Config: genConfig(`"foo"`), + Config: genConfig("foo"), ExpectError: expectedError, }, { - Config: genConfig(`"80-"`), + Config: genConfig("80-"), ExpectError: expectedError, }, { - Config: genConfig(`"-80"`), + Config: genConfig("-80"), ExpectError: expectedError, }, { - Config: genConfig(`"80-90-100"`), + Config: genConfig("80-90-100"), ExpectError: expectedError, }, { - Config: genConfig(`"80-70"`), + Config: genConfig("80-70"), ExpectError: expectedError, }, { - Config: genConfig(`"0-65536"`), + Config: genConfig("0-65536"), ExpectError: expectedError, }, }, @@ -556,10 +411,22 @@ func TestAccTwingateResourceWithInvalidPortRange(t *testing.T) { func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted}, + UDP: Protocol{Policy: model.PolicyRestricted}, + }, + ) + theResource := resource.TerraformResource() + + genConfig := func(portRange ...string) string { + resource.Protocols.TCP.Ports = portRange + resource.Protocols.UDP.Ports = portRange + + return configBuilder(resource, network) + } sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -567,7 +434,7 @@ func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"80", "82-83"`), + Config: genConfig("80", "82-83"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "80"), @@ -576,17 +443,17 @@ func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) { }, // no changes { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"82-83", "80"`), + Config: genConfig("82-83", "80"), PlanOnly: true, }, // no changes { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"82", "83", "80"`), + Config: genConfig("82", "83", "80"), PlanOnly: true, }, // new changes applied { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"70", "82-83"`), + Config: genConfig("70", "82-83"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "70"), @@ -607,13 +474,13 @@ func configResourceWithPortRange(terraformResource, networkName, resourceName, p name = "${resource_name}" address = "new-acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = [${port_range}] } - udp { + udp = { policy = "${udp_policy}" ports = [${port_range}] } @@ -634,10 +501,22 @@ func configResourceWithPortRange(terraformResource, networkName, resourceName, p func TestAccTwingateResourcePortsRepresentationChanged(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted}, + UDP: Protocol{Policy: model.PolicyRestricted}, + }, + ) + theResource := resource.TerraformResource() + + genConfig := func(portRange ...string) string { + resource.Protocols.TCP.Ports = portRange + resource.Protocols.UDP.Ports = portRange + + return configBuilder(resource, network) + } sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -645,7 +524,7 @@ func TestAccTwingateResourcePortsRepresentationChanged(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"82", "83", "80"`), + Config: genConfig("82", "83", "80"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPortsLen, "3"), @@ -658,10 +537,22 @@ func TestAccTwingateResourcePortsRepresentationChanged(t *testing.T) { func TestAccTwingateResourcePortsNotChanged(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted}, + UDP: Protocol{Policy: model.PolicyRestricted}, + }, + ) + theResource := resource.TerraformResource() + + genConfig := func(portRange ...string) string { + resource.Protocols.TCP.Ports = portRange + resource.Protocols.UDP.Ports = portRange + + return configBuilder(resource, network) + } sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -669,7 +560,7 @@ func TestAccTwingateResourcePortsNotChanged(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"82", "83", "80"`), + Config: genConfig("82", "83", "80"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPortsLen, "3"), @@ -677,7 +568,7 @@ func TestAccTwingateResourcePortsNotChanged(t *testing.T) { }, { PlanOnly: true, - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"80", "82-83"`), + Config: genConfig("80", "82-83"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, tcpPortsLen, "2"), @@ -690,10 +581,22 @@ func TestAccTwingateResourcePortsNotChanged(t *testing.T) { func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted}, + UDP: Protocol{Policy: model.PolicyRestricted}, + }, + ) + theResource := resource.TerraformResource() + + genConfig := func(portRange ...string) string { + resource.Protocols.TCP.Ports = portRange + resource.Protocols.UDP.Ports = portRange + + return configBuilder(resource, network) + } sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -701,21 +604,21 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"82", "83", "80"`), + Config: genConfig("82", "83", "80"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, firstTCPPort, "82"), - sdk.TestCheckResourceAttr(theResource, firstUDPPort, "82"), + sdk.TestCheckResourceAttr(theResource, firstTCPPort, "80"), + sdk.TestCheckResourceAttr(theResource, firstUDPPort, "80"), ), }, // no changes { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"82-83", "80"`), + Config: genConfig("82-83", "80"), PlanOnly: true, }, // no changes { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"82-83", "80"`), + Config: genConfig("82-83", "80"), PlanOnly: true, Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, udpPortsLen, "2"), @@ -723,7 +626,7 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { }, // new changes applied { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"70", "82-83"`), + Config: genConfig("70", "82-83"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "70"), @@ -737,10 +640,9 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { func TestAccTwingateResourceSetActiveStateOnUpdate(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()) + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -748,16 +650,18 @@ func TestAccTwingateResourceSetActiveStateOnUpdate(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), + Config: configBuilder(resource, network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), acctests.DeactivateTwingateResource(theResource), acctests.WaitTestFunc(), acctests.CheckTwingateResourceActiveState(theResource, false), ), + // provider noticed drift and tried to change it to true + ExpectNonEmptyPlan: true, }, { - Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), + Config: configBuilder(resource, network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceActiveState(theResource, true), ), @@ -769,10 +673,9 @@ func TestAccTwingateResourceSetActiveStateOnUpdate(t *testing.T) { func TestAccTwingateResourceReCreateAfterDeletion(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + res := NewResource(network.TerraformResourceID()) + theResource := res.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -780,7 +683,7 @@ func TestAccTwingateResourceReCreateAfterDeletion(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), + Config: configBuilder(res, network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), acctests.DeleteTwingateResource(theResource, resource.TwingateResource), @@ -788,7 +691,7 @@ func TestAccTwingateResourceReCreateAfterDeletion(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), + Config: configBuilder(res, network), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), @@ -800,10 +703,22 @@ func TestAccTwingateResourceReCreateAfterDeletion(t *testing.T) { func TestAccTwingateResourceImport(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - remoteNetworkName := test.RandomName() - resourceName := test.RandomResourceName() + network := NewRemoteNetwork() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted}, + UDP: Protocol{Policy: model.PolicyRestricted}, + }, + ) + theResource := resource.TerraformResource() + + genConfig := func(portRange ...string) string { + resource.Protocols.TCP.Ports = portRange + resource.Protocols.UDP.Ports = portRange + + return configBuilder(resource, network) + } sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -811,7 +726,7 @@ func TestAccTwingateResourceImport(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"80", "82-83"`), + Config: genConfig("80", "82-83"), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), @@ -820,10 +735,11 @@ func TestAccTwingateResourceImport(t *testing.T) { ImportState: true, ResourceName: theResource, ImportStateCheck: acctests.CheckImportState(map[string]string{ - attr.Address: "new-acc-test.com", + attr.Address: resource.Address, tcpPolicy: model.PolicyRestricted, tcpPortsLen: "2", firstTCPPort: "80", + udpPolicy: model.PolicyRestricted, }), }, }, @@ -868,10 +784,17 @@ func genNewServiceAccounts(resourcePrefix string, count int) ([]string, []string func TestAccTwingateResourceAddAccessServiceAccounts(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - serviceAccountConfig := configServiceAccount(terraformResource, test.RandomName()) - serviceAccountID := acctests.TerraformServiceAccount(terraformResource) + ".id" + network := NewRemoteNetwork() + serviceAccount := NewServiceAccount() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted, Ports: []string{"80", "82-83"}}, + UDP: Protocol{Policy: model.PolicyAllowAll}, + }, + attr.Access, []Access{{ServiceAccountIDs: collectResourceIDs(serviceAccount)}}, + ) + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -879,7 +802,7 @@ func TestAccTwingateResourceAddAccessServiceAccounts(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithServiceAccount(terraformResource, test.RandomName(), test.RandomResourceName(), serviceAccountConfig, serviceAccountID), + Config: configBuilder(resource, network, serviceAccount), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, accessServiceAccountIdsLen, "1"), @@ -901,13 +824,13 @@ func configResourceWithServiceAccount(terraformResource, networkName, resourceNa name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = ["80", "82-83"] } - udp { + udp = { policy = "${udp_policy}" } } @@ -932,12 +855,23 @@ func configResourceWithServiceAccount(terraformResource, networkName, resourceNa func TestAccTwingateResourceAddAccessGroupsAndServiceAccounts(t *testing.T) { t.Parallel() - terraformResource := test.RandomResourceName() - theResource := acctests.TerraformResource(terraformResource) - - groups, groupsID := genNewGroups(terraformResource, 1) - serviceAccountConfig := []string{configServiceAccount(terraformResource, test.RandomName())} - serviceAccountID := []string{acctests.TerraformServiceAccount(terraformResource) + ".id"} + network := NewRemoteNetwork() + group := NewGroup() + serviceAccount := NewServiceAccount() + resource := NewResource(network.TerraformResourceID()).Set( + attr.Protocols, &Protocols{ + AllowIcmp: true, + TCP: Protocol{Policy: model.PolicyRestricted, Ports: []string{"80", "82-83"}}, + UDP: Protocol{Policy: model.PolicyAllowAll}, + }, + attr.Access, []Access{ + { + GroupIDs: collectResourceIDs(group), + ServiceAccountIDs: collectResourceIDs(serviceAccount), + }, + }, + ) + theResource := resource.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -945,21 +879,37 @@ func TestAccTwingateResourceAddAccessGroupsAndServiceAccounts(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: configResourceWithGroupsAndServiceAccounts(terraformResource, test.RandomName(), test.RandomResourceName(), groups, groupsID, serviceAccountConfig, serviceAccountID), + Config: configBuilder(resource, network, group, serviceAccount), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, accessGroupIdsLen, "1"), sdk.TestCheckResourceAttr(theResource, accessServiceAccountIdsLen, "1"), ), }, - }, - }) -} - -func configResourceWithGroupsAndServiceAccounts(terraformResource, networkName, resourceName string, groups, groupIDs, serviceAccounts, serviceAccountIDs []string) string { - return acctests.Nprintf(` - resource "twingate_remote_network" "${network_resource}" { - name = "${network_name}" + { + Config: configBuilder(network, group, serviceAccount, resource.Set(attr.Access, []Access{{GroupIDs: collectResourceIDs(group)}})), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, accessGroupIdsLen, "1"), + sdk.TestCheckResourceAttr(theResource, accessServiceAccountIdsLen, "0"), + ), + }, + { + Config: configBuilder(network, group, serviceAccount, resource.Set(attr.Access, []Access{{ServiceAccountIDs: collectResourceIDs(serviceAccount)}})), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, accessGroupIdsLen, "0"), + sdk.TestCheckResourceAttr(theResource, accessServiceAccountIdsLen, "1"), + ), + }, + }, + }) +} + +func configResourceWithGroupsAndServiceAccounts(terraformResource, networkName, resourceName string, groups, groupIDs, serviceAccounts, serviceAccountIDs []string) string { + return acctests.Nprintf(` + resource "twingate_remote_network" "${network_resource}" { + name = "${network_name}" } ${group} @@ -970,13 +920,13 @@ func configResourceWithGroupsAndServiceAccounts(terraformResource, networkName, name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = ["80", "82-83"] } - udp { + udp = { policy = "${udp_policy}" } } @@ -1095,13 +1045,13 @@ func configResourceWithServiceAccountsAndAuthoritativeFlag(terraformResource, ne name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = ["80", "82-83"] } - udp { + udp = { policy = "${udp_policy}" } } @@ -1204,6 +1154,39 @@ func TestAccTwingateResourceAccessServiceAccountsAuthoritative(t *testing.T) { }) } +func createResource13(networkName, resourceName string, serviceAccounts, serviceAccountIDs []string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "test13" { + name = "%s" + } + + %s + + resource "twingate_resource" "test13" { + name = "%s" + address = "acc-test.com.13" + remote_network_id = twingate_remote_network.test13.id + + protocols = { + allow_icmp = true + tcp = { + policy = "%s" + ports = ["80", "82-83"] + } + udp = { + policy = "%s" + } + } + + is_authoritative = true + access { + service_account_ids = [%s] + } + + } + `, networkName, strings.Join(serviceAccounts, "\n"), resourceName, model.PolicyRestricted, model.PolicyAllowAll, strings.Join(serviceAccountIDs, ", ")) +} + func TestAccTwingateResourceAccessWithEmptyGroups(t *testing.T) { t.Parallel() @@ -1218,7 +1201,7 @@ func TestAccTwingateResourceAccessWithEmptyGroups(t *testing.T) { Steps: []sdk.TestStep{ { Config: configResourceWithGroups(terraformResource, remoteNetworkName, resourceName, nil, nil), - ExpectError: regexp.MustCompile("Error: Not enough list items"), + ExpectError: regexp.MustCompile("Error: Invalid Attribute Value"), }, }, }) @@ -1236,13 +1219,13 @@ func configResourceWithGroups(terraformResource, networkName, resourceName strin name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = ["80", "82-83"] } - udp { + udp = { policy = "${udp_policy}" } } @@ -1278,12 +1261,42 @@ func TestAccTwingateResourceAccessWithEmptyServiceAccounts(t *testing.T) { Steps: []sdk.TestStep{ { Config: configResourceWithServiceAccount(terraformResource, remoteNetworkName, resourceName, "", ""), - ExpectError: regexp.MustCompile("Error: Not enough list items"), + ExpectError: regexp.MustCompile("Error: Invalid Attribute Value"), }, }, }) } +func createResource19(networkName, resourceName string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "test19" { + name = "%s" + } + + resource "twingate_resource" "test19" { + name = "%s" + address = "acc-test.com.19" + remote_network_id = twingate_remote_network.test19.id + + protocols = { + allow_icmp = true + tcp = { + policy = "%s" + ports = ["80", "82-83"] + } + udp = { + policy = "%s" + } + } + + access { + service_account_ids = [] + } + + } + `, networkName, resourceName, model.PolicyRestricted, model.PolicyAllowAll) +} + func TestAccTwingateResourceAccessWithEmptyBlock(t *testing.T) { t.Parallel() @@ -1298,7 +1311,7 @@ func TestAccTwingateResourceAccessWithEmptyBlock(t *testing.T) { Steps: []sdk.TestStep{ { Config: configResourceWithEmptyAccessBlock(terraformResource, remoteNetworkName, resourceName), - ExpectError: regexp.MustCompile("Missing required argument"), + ExpectError: regexp.MustCompile("invalid attribute combination"), }, }, }) @@ -1314,13 +1327,13 @@ func configResourceWithEmptyAccessBlock(terraformResource, networkName, resource name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = ["80", "82-83"] } - udp { + udp = { policy = "${udp_policy}" } } @@ -1433,13 +1446,13 @@ func configResourceWithGroupsAndAuthoritativeFlag(terraformResource, networkName name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = ["80", "82-83"] } - udp { + udp = { policy = "${udp_policy}" } } @@ -1542,6 +1555,39 @@ func TestAccTwingateResourceAccessGroupsAuthoritative(t *testing.T) { }) } +func createResource23(networkName, resourceName string, groups, groupsID []string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "test23" { + name = "%s" + } + + %s + + resource "twingate_resource" "test23" { + name = "%s" + address = "acc-test.com.23" + remote_network_id = twingate_remote_network.test23.id + + protocols = { + allow_icmp = true + tcp = { + policy = "%s" + ports = ["80", "82-83"] + } + udp = { + policy = "%s" + } + } + + is_authoritative = true + access { + group_ids = [%s] + } + + } + `, networkName, strings.Join(groups, "\n"), resourceName, model.PolicyRestricted, model.PolicyAllowAll, strings.Join(groupsID, ", ")) +} + func TestGetResourceNameFromID(t *testing.T) { cases := []struct { input string @@ -1584,7 +1630,7 @@ func TestAccTwingateCreateResourceWithFlagIsVisible(t *testing.T) { Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckNoResourceAttr(theResource, attr.IsVisible), + sdk.TestCheckResourceAttr(theResource, attr.IsVisible, "true"), ), }, { @@ -1610,11 +1656,9 @@ func TestAccTwingateCreateResourceWithFlagIsVisible(t *testing.T) { ), }, { - // expecting no changes - flag not set - PlanOnly: true, - Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), + Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckNoResourceAttr(theResource, attr.IsVisible), + sdk.TestCheckResourceAttr(theResource, attr.IsVisible, "true"), ), }, }, @@ -1672,37 +1716,35 @@ func TestAccTwingateCreateResourceWithFlagIsBrowserShortcutEnabled(t *testing.T) Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckNoResourceAttr(theResource, attr.IsBrowserShortcutEnabled), + sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "false"), ), }, { - // expecting no changes - default value on the backend side is `true` - PlanOnly: true, - Config: configResourceWithBrowserShortcutEnabledFlag(terraformResource, remoteNetworkName, resourceName, true), + // expecting no changes - default value is `false` + Config: configResourceWithBrowserShortcutEnabledFlag(terraformResource, remoteNetworkName, resourceName, false), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "true"), + sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "false"), ), + PlanOnly: true, }, { - Config: configResourceWithBrowserShortcutEnabledFlag(terraformResource, remoteNetworkName, resourceName, false), + Config: configResourceWithBrowserShortcutEnabledFlag(terraformResource, remoteNetworkName, resourceName, true), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "false"), + sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "true"), ), }, { // expecting no changes - no drift after re-applying changes PlanOnly: true, - Config: configResourceWithBrowserShortcutEnabledFlag(terraformResource, remoteNetworkName, resourceName, false), + Config: configResourceWithBrowserShortcutEnabledFlag(terraformResource, remoteNetworkName, resourceName, true), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "false"), + sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "true"), ), }, { - // expecting no changes - flag not set - PlanOnly: true, - Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), + Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckNoResourceAttr(theResource, attr.IsBrowserShortcutEnabled), + sdk.TestCheckResourceAttr(theResource, attr.IsBrowserShortcutEnabled, "false"), ), }, }, @@ -1809,6 +1851,38 @@ func TestAccTwingateResourceGroupsAuthoritativeByDefault(t *testing.T) { }) } +func createResource26(networkName, resourceName string, groups, groupsID []string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "test26" { + name = "%s" + } + + %s + + resource "twingate_resource" "test26" { + name = "%s" + address = "acc-test.com.26" + remote_network_id = twingate_remote_network.test26.id + + protocols = { + allow_icmp = true + tcp = { + policy = "%s" + ports = ["80", "82-83"] + } + udp = { + policy = "%s" + } + } + + access { + group_ids = [%s] + } + + } + `, networkName, strings.Join(groups, "\n"), resourceName, model.PolicyRestricted, model.PolicyAllowAll, strings.Join(groupsID, ", ")) +} + func TestAccTwingateResourceDoesNotSupportOldGroups(t *testing.T) { t.Parallel() @@ -1842,13 +1916,13 @@ func configResourceWithOldGroups(terraformResource, networkName, resourceName st name = "${resource_name}" address = "acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${tcp_policy}" ports = ["80", "82-83"] } - udp { + udp = { policy = "${udp_policy}" } } @@ -1891,14 +1965,13 @@ func TestAccTwingateResourceCreateWithAlias(t *testing.T) { ), }, { - // alias attr commented out, means state keeps the same value without changes Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckResourceAttr(theResource, attr.Alias, aliasName), + sdk.TestCheckNoResourceAttr(theResource, attr.Alias), ), }, { - // alias attr set with emtpy string + // alias attr set with empty string Config: configResourceWithAlias(terraformResource, remoteNetworkName, resourceName, ""), Check: acctests.ComposeTestCheckFunc( sdk.TestCheckResourceAttr(theResource, attr.Alias, ""), @@ -1968,6 +2041,41 @@ func TestAccTwingateResourceGroupsCursor(t *testing.T) { }) } +func createResourceWithGroupsAndServiceAccounts(name, networkName, resourceName string, groups, groupsID, serviceAccounts, serviceAccountIDs []string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%s" { + name = "%s" + } + + %s + + %s + + resource "twingate_resource" "%s" { + name = "%s" + address = "acc-test.com.26" + remote_network_id = twingate_remote_network.%s.id + + protocols = { + allow_icmp = true + tcp = { + policy = "%s" + ports = ["80", "82-83"] + } + udp = { + policy = "%s" + } + } + + access { + group_ids = [%s] + service_account_ids = [%s] + } + + } + `, name, networkName, strings.Join(groups, "\n"), strings.Join(serviceAccounts, "\n"), name, resourceName, name, model.PolicyRestricted, model.PolicyAllowAll, strings.Join(groupsID, ", "), strings.Join(serviceAccountIDs, ", ")) +} + func TestAccTwingateResourceCreateWithPort(t *testing.T) { t.Parallel() @@ -2004,6 +2112,29 @@ func TestAccTwingateResourceCreateWithPort(t *testing.T) { }) } +func createResourceWithPort(networkName, resourceName, port string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "test30" { + name = "%s" + } + resource "twingate_resource" "test30" { + name = "%s" + address = "new-acc-test.com" + remote_network_id = twingate_remote_network.test30.id + protocols = { + allow_icmp = true + tcp = { + policy = "%s" + ports = ["%s"] + } + udp = { + policy = "%s" + } + } + } + `, networkName, resourceName, model.PolicyRestricted, port, model.PolicyAllowAll) +} + func TestAccTwingateResourceUpdateWithPort(t *testing.T) { t.Parallel() @@ -2074,13 +2205,13 @@ func configResourceWithPolicyAndPortRange(terraformResource, networkName, resour address = "new-acc-test.com" remote_network_id = twingate_remote_network.${network_resource}.id - protocols { + protocols = { allow_icmp = true - tcp { + tcp = { policy = "${policy}" ports = [${port_range}] } - udp { + udp = { policy = "${policy}" ports = [${port_range}] } @@ -2134,6 +2265,29 @@ func TestAccTwingateResourceWithoutPortsOkForAllowAllAndDenyAllPolicy(t *testing }) } +func createResourceWithoutPorts(name, networkName, resourceName, policy string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[2]s" + } + resource "twingate_resource" "%[1]s" { + name = "%[3]s" + address = "acc-test-%[1]s.com" + remote_network_id = twingate_remote_network.%[1]s.id + + protocols = { + allow_icmp = true + tcp = { + policy = "%[4]s" + } + udp = { + policy = "%[5]s" + } + } + } + `, name, networkName, resourceName, policy, model.PolicyAllowAll) +} + func TestAccTwingateResourceWithRestrictedPolicy(t *testing.T) { t.Parallel() @@ -2402,6 +2556,38 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToDenyAll(t *testing.T) { }) } +func TestAccTwingateResourceTestCaseInsensitiveAlias(t *testing.T) { + t.Parallel() + + terraformResource := test.RandomResourceName() + theResource := acctests.TerraformResource(terraformResource) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + const aliasName = "test.com" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: configResourceWithAlias(terraformResource, remoteNetworkName, resourceName, aliasName), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theResource, attr.Alias, aliasName), + ), + }, + { + // expecting no changes + PlanOnly: true, + Config: configResourceWithAlias(terraformResource, remoteNetworkName, resourceName, strings.ToUpper(aliasName)), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theResource, attr.Alias, aliasName), + ), + }, + }, + }) +} + func TestAccTwingateResourceWithBrowserOption(t *testing.T) { t.Parallel() @@ -2430,7 +2616,7 @@ func TestAccTwingateResourceWithBrowserOption(t *testing.T) { }, { Config: configResourceWithAddressAndBrowserOption(terraformResource, remoteNetworkName, resourceName, wildcardAddress, true), - ExpectError: regexp.MustCompile(resource.ErrWildcardAddressWithEnabledShortcut.Error()), + ExpectError: regexp.MustCompile("Resources with a CIDR range or wildcard"), }, }, }) @@ -2465,7 +2651,7 @@ func TestAccTwingateResourceWithBrowserOptionFailOnUpdate(t *testing.T) { }, { Config: configResourceWithAddressAndBrowserOption(terraformResource, remoteNetworkName, resourceName, wildcardAddress, true), - ExpectError: regexp.MustCompile(resource.ErrWildcardAddressWithEnabledShortcut.Error()), + ExpectError: regexp.MustCompile("Resources with a CIDR range or wildcard"), }, }, }) @@ -2543,3 +2729,445 @@ func configResourceWithAddressAndBrowserOption(terraformResource, networkName, n "browser_flag": browserFlag, }) } + +func createResourceWithSecurityPolicy(remoteNetwork, resource, policyID string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + security_policy_id = "%[3]s" + } + `, remoteNetwork, resource, policyID) +} + +func createResourceWithoutSecurityPolicy(remoteNetwork, resource string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + } + `, remoteNetwork, resource) +} + +func TestAccTwingateResourceUpdateWithDefaultProtocols(t *testing.T) { + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithProtocols(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithoutProtocols(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + }, + }) +} + +func createResourceWithProtocols(remoteNetwork, resource string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + protocols = { + allow_icmp = true + tcp = { + policy = "RESTRICTED" + ports = ["80-83"] + } + udp = { + policy = "RESTRICTED" + ports = ["80"] + } + } + } + `, remoteNetwork, resource) +} + +func createResourceWithoutProtocols(remoteNetwork, resource string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + } + `, remoteNetwork, resource) +} + +func TestAccTwingateResourceUpdatePortsFromEmptyListToNull(t *testing.T) { + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithEmptyArrayPorts(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + // expect no changes + PlanOnly: true, + Config: createResourceWithDefaultPorts(remoteNetworkName, resourceName), + }, + }, + }) +} + +func TestAccTwingateResourceUpdatePortsFromNullToEmptyList(t *testing.T) { + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithDefaultPorts(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + // expect no changes + PlanOnly: true, + Config: createResourceWithEmptyArrayPorts(remoteNetworkName, resourceName), + }, + }, + }) +} + +func createResourceWithDefaultPorts(remoteNetwork, resource string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + protocols = { + allow_icmp = true + tcp = { + policy = "ALLOW_ALL" + } + udp = { + policy = "ALLOW_ALL" + } + } + } + `, remoteNetwork, resource) +} + +func createResourceWithEmptyArrayPorts(remoteNetwork, resource string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test-address.com" + remote_network_id = twingate_remote_network.%[1]s.id + protocols = { + allow_icmp = true + tcp = { + policy = "ALLOW_ALL" + ports = [] + } + udp = { + policy = "ALLOW_ALL" + ports = [] + } + } + } + `, remoteNetwork, resource) +} + +func TestAccTwingateResourceUpdateSecurityPolicy(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + defaultPolicy, testPolicy := preparePolicies(t) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, defaultPolicy), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, defaultPolicy), + ), + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, testPolicy), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, testPolicy), + ), + }, + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, defaultPolicy), + ), + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, ""), + // no changes + PlanOnly: true, + }, + }, + }) +} + +func preparePolicies(t *testing.T) (string, string) { + policies, err := acctests.ListSecurityPolicies() + if err != nil { + t.Skipf("failed to retrieve security policies: %v", err) + } + + if len(policies) < 2 { + t.Skip("requires at least 2 security policy for the test") + } + + var defaultPolicy, testPolicy string + if policies[0].Name == resource.DefaultSecurityPolicyName { + defaultPolicy = policies[0].ID + testPolicy = policies[1].ID + } else { + testPolicy = policies[0].ID + defaultPolicy = policies[1].ID + } + + return defaultPolicy, testPolicy +} + +func TestAccTwingateResourceSetDefaultSecurityPolicyByDefault(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + defaultPolicy, testPolicy := preparePolicies(t) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, testPolicy), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, testPolicy), + ), + }, + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, defaultPolicy), + acctests.CheckResourceSecurityPolicy(theResource, defaultPolicy), + // set new policy via API + acctests.UpdateResourceSecurityPolicy(theResource, testPolicy), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, ""), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckResourceSecurityPolicy(theResource, defaultPolicy), + ), + }, + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + // no changes + PlanOnly: true, + }, + }, + }) +} + +func TestAccTwingateResourceSecurityPolicy(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + _, testPolicy := preparePolicies(t) + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithoutSecurityPolicy(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckNoResourceAttr(theResource, attr.SecurityPolicyID), + ), + }, + { + Config: createResourceWithSecurityPolicy(remoteNetworkName, resourceName, testPolicy), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttr(theResource, attr.SecurityPolicyID, testPolicy), + ), + }, + }, + }) +} + +func TestAccTwingateResourceCreateInactive(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithIsActiveFlag(remoteNetworkName, resourceName, false), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theResource, attr.IsActive, "false"), + acctests.CheckTwingateResourceActiveState(theResource, false), + ), + }, + }, + }) +} + +func createResourceWithIsActiveFlag(networkName, resourceName string, isActive bool) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test.com" + remote_network_id = twingate_remote_network.%[1]s.id + is_active = %[3]v + } + `, networkName, resourceName, isActive) +} + +func TestAccTwingateResourceTestInactiveFlag(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithIsActiveFlag(remoteNetworkName, resourceName, true), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theResource, attr.IsActive, "true"), + ), + }, + { + Config: createResourceWithIsActiveFlag(remoteNetworkName, resourceName, false), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theResource, attr.IsActive, "false"), + acctests.CheckTwingateResourceActiveState(theResource, false), + ), + }, + }, + }) +} + +func TestAccTwingateResourceTestPlanOnDisabledResource(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + theResource := acctests.TerraformResource(resourceName) + remoteNetworkName := test.RandomName() + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResource(remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceActiveState(theResource, true), + acctests.DeactivateTwingateResource(theResource), + acctests.CheckTwingateResourceActiveState(theResource, false), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: sdk.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectNonEmptyPlan(), + plancheck.ExpectResourceAction(theResource, plancheck.ResourceActionUpdate), + acctests.CheckResourceActiveState(theResource, false), + }, + }, + }, + }, + }) +} + +func createResource(networkName, resourceName string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[1]s" + } + resource "twingate_resource" "%[2]s" { + name = "%[2]s" + address = "acc-test.com" + remote_network_id = twingate_remote_network.%[1]s.id + } + `, networkName, resourceName) +} diff --git a/twingate/internal/test/acctests/resource/service-account_test.go b/twingate/internal/test/acctests/resource/service-account_test.go index f4c65528..07d9e133 100644 --- a/twingate/internal/test/acctests/resource/service-account_test.go +++ b/twingate/internal/test/acctests/resource/service-account_test.go @@ -8,6 +8,7 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" ) func configServiceAccount(resourceName, serviceAccountName string) string { @@ -25,8 +26,8 @@ func configServiceAccount(resourceName, serviceAccountName string) string { func TestAccTwingateServiceAccountCreateUpdate(t *testing.T) { t.Parallel() - resourceName := test.RandomServiceAccountName() - theResource := acctests.TerraformServiceAccount(resourceName) + serviceAccount := NewServiceAccount() + theResource := serviceAccount.TerraformResource() name1 := test.RandomName() name2 := test.RandomName() @@ -36,14 +37,14 @@ func TestAccTwingateServiceAccountCreateUpdate(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceAccount(resourceName, name1), + Config: configBuilder(serviceAccount.Set(attr.Name, name1)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Name, name1), ), }, { - Config: configServiceAccount(resourceName, name2), + Config: configBuilder(serviceAccount.Set(attr.Name, name2)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Name, name2), @@ -53,12 +54,11 @@ func TestAccTwingateServiceAccountCreateUpdate(t *testing.T) { }) } -func TestAccTwingateServiceAccountDeleteNonExisting(t *testing.T) { +func TestAccTwingateServiceAccountDelete(t *testing.T) { t.Parallel() - resourceName := test.RandomServiceAccountName() - theResource := acctests.TerraformServiceAccount(resourceName) - name := test.RandomName() + serviceAccount := NewServiceAccount() + theResource := serviceAccount.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -66,11 +66,16 @@ func TestAccTwingateServiceAccountDeleteNonExisting(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceAccount(resourceName, name), + Config: configBuilder(serviceAccount), Destroy: true, - Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceDoesNotExists(theResource), - ), + }, + { + Config: configBuilder(serviceAccount), + ConfigPlanChecks: sdk.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(theResource, plancheck.ResourceActionCreate), + }, + }, }, }, }) @@ -79,9 +84,8 @@ func TestAccTwingateServiceAccountDeleteNonExisting(t *testing.T) { func TestAccTwingateServiceAccountReCreateAfterDeletion(t *testing.T) { t.Parallel() - resourceName := test.RandomServiceAccountName() - theResource := acctests.TerraformServiceAccount(resourceName) - name := test.RandomName() + serviceAccount := NewServiceAccount() + theResource := serviceAccount.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -89,7 +93,7 @@ func TestAccTwingateServiceAccountReCreateAfterDeletion(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceAccount(resourceName, name), + Config: configBuilder(serviceAccount), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), acctests.DeleteTwingateResource(theResource, resource.TwingateServiceAccount), @@ -98,7 +102,7 @@ func TestAccTwingateServiceAccountReCreateAfterDeletion(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: configServiceAccount(resourceName, name), + Config: configBuilder(serviceAccount), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), diff --git a/twingate/internal/test/acctests/resource/service-key_test.go b/twingate/internal/test/acctests/resource/service-key_test.go index d9b1cef4..2a098783 100644 --- a/twingate/internal/test/acctests/resource/service-key_test.go +++ b/twingate/internal/test/acctests/resource/service-key_test.go @@ -11,59 +11,11 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" ) var ErrEmptyValue = errors.New("empty value") -func configServiceKey(resourceName, serviceAccountName string) string { - return acctests.Nprintf(` - ${service_account} - - resource "twingate_service_account_key" "${service_account_key_resource}" { - service_account_id = twingate_service_account.${service_account_resource}.id - } - `, - map[string]any{ - "service_account": configServiceAccount(resourceName, serviceAccountName), - "service_account_key_resource": resourceName, - "service_account_resource": resourceName, - }) -} - -func configServiceKeyWithName(resourceName, serviceAccountName, serviceKeyName string) string { - return acctests.Nprintf(` - ${service_account} - - resource "twingate_service_account_key" "${service_account_key_resource}" { - service_account_id = twingate_service_account.${service_account_resource}.id - name = "${name}" - } - `, - map[string]any{ - "service_account": configServiceAccount(resourceName, serviceAccountName), - "service_account_key_resource": resourceName, - "service_account_resource": resourceName, - "name": serviceKeyName, - }) -} - -func configServiceKeyWithExpiration(resourceName, serviceAccountName string, expirationTime int) string { - return acctests.Nprintf(` - ${service_account} - - resource "twingate_service_account_key" "${service_account_key_resource}" { - service_account_id = twingate_service_account.${service_account_resource}.id - expiration_time = ${expiration_time} - } - `, - map[string]any{ - "service_account": configServiceAccount(resourceName, serviceAccountName), - "service_account_key_resource": resourceName, - "service_account_resource": resourceName, - "expiration_time": expirationTime, - }) -} - func nonEmptyValue(value string) error { if value != "" { return nil @@ -75,10 +27,8 @@ func nonEmptyValue(value string) error { func TestAccTwingateServiceKeyCreateUpdate(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") - serviceAccount := acctests.TerraformServiceAccount(terraformResourceName) - serviceKey := acctests.TerraformServiceKey(terraformResourceName) + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -86,21 +36,17 @@ func TestAccTwingateServiceKeyCreateUpdate(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKey(terraformResourceName, serviceAccountName), + Config: configBuilder(serviceAccount, serviceKey), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceAccount), - sdk.TestCheckResourceAttr(serviceAccount, attr.Name, serviceAccountName), - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + sdk.TestCheckResourceAttr(serviceAccount.TerraformResource(), attr.Name, serviceAccount.Name), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), ), }, { - Config: configServiceKey(terraformResourceName, serviceAccountName), + Config: configBuilder(serviceAccount, serviceKey), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceAccount), - sdk.TestCheckResourceAttr(serviceAccount, attr.Name, serviceAccountName), - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + sdk.TestCheckResourceAttr(serviceAccount.TerraformResource(), attr.Name, serviceAccount.Name), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), ), }, }, @@ -110,10 +56,9 @@ func TestAccTwingateServiceKeyCreateUpdate(t *testing.T) { func TestAccTwingateServiceKeyCreateUpdateWithName(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") - serviceAccount := acctests.TerraformServiceAccount(terraformResourceName) - serviceKey := acctests.TerraformServiceKey(terraformResourceName) + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) + name1 := test.RandomName() name2 := test.RandomName() @@ -123,23 +68,19 @@ func TestAccTwingateServiceKeyCreateUpdateWithName(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKeyWithName(terraformResourceName, serviceAccountName, name1), + Config: configBuilder(serviceKey.Set(attr.Name, name1), serviceAccount), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceAccount), - sdk.TestCheckResourceAttr(serviceAccount, attr.Name, serviceAccountName), - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttr(serviceKey, attr.Name, name1), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + sdk.TestCheckResourceAttr(serviceAccount.TerraformResource(), attr.Name, serviceAccount.Name), + sdk.TestCheckResourceAttr(serviceKey.TerraformResource(), attr.Name, name1), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), ), }, { - Config: configServiceKeyWithName(terraformResourceName, serviceAccountName, name2), + Config: configBuilder(serviceKey.Set(attr.Name, name2), serviceAccount), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceAccount), - sdk.TestCheckResourceAttr(serviceAccount, attr.Name, serviceAccountName), - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttr(serviceKey, attr.Name, name2), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + sdk.TestCheckResourceAttr(serviceAccount.TerraformResource(), attr.Name, serviceAccount.Name), + sdk.TestCheckResourceAttr(serviceKey.TerraformResource(), attr.Name, name2), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), acctests.WaitTestFunc(), ), }, @@ -150,9 +91,8 @@ func TestAccTwingateServiceKeyCreateUpdateWithName(t *testing.T) { func TestAccTwingateServiceKeyWontReCreateAfterInactive(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") - serviceKey := acctests.TerraformServiceKey(terraformResourceName) + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) resourceID := new(string) @@ -162,23 +102,23 @@ func TestAccTwingateServiceKeyWontReCreateAfterInactive(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKey(terraformResourceName, serviceAccountName), + Config: configBuilder(serviceKey, serviceAccount), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceKey), - acctests.GetTwingateResourceID(serviceKey, &resourceID), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), - acctests.RevokeTwingateServiceKey(serviceKey), + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + acctests.GetTwingateResourceID(serviceKey.TerraformResource(), &resourceID), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), + acctests.RevokeTwingateServiceKey(serviceKey.TerraformResource()), acctests.WaitTestFunc(), - acctests.CheckTwingateServiceKeyStatus(serviceKey, model.StatusRevoked), + acctests.CheckTwingateServiceKeyStatus(serviceKey.TerraformResource(), model.StatusRevoked), ), }, { - Config: configServiceKey(terraformResourceName, serviceAccountName), + Config: configBuilder(serviceKey, serviceAccount), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttr(serviceKey, attr.IsActive, "false"), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), - sdk.TestCheckResourceAttrWith(serviceKey, attr.ID, func(value string) error { + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + sdk.TestCheckResourceAttr(serviceKey.TerraformResource(), attr.IsActive, "false"), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.ID, func(value string) error { if *resourceID == "" { return errors.New("failed to fetch resource id") } @@ -198,9 +138,8 @@ func TestAccTwingateServiceKeyWontReCreateAfterInactive(t *testing.T) { func TestAccTwingateServiceKeyDelete(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") - serviceKey := acctests.TerraformServiceKey(terraformResourceName) + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -208,11 +147,16 @@ func TestAccTwingateServiceKeyDelete(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKey(terraformResourceName, serviceAccountName), + Config: configBuilder(serviceKey, serviceAccount), Destroy: true, - Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceDoesNotExists(serviceKey), - ), + }, + { + Config: configBuilder(serviceKey, serviceAccount), + ConfigPlanChecks: sdk.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(serviceKey.TerraformResource(), plancheck.ResourceActionCreate), + }, + }, }, }, }) @@ -221,9 +165,8 @@ func TestAccTwingateServiceKeyDelete(t *testing.T) { func TestAccTwingateServiceKeyReCreateAfterDeletion(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") - serviceKey := acctests.TerraformServiceKey(terraformResourceName) + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -231,19 +174,19 @@ func TestAccTwingateServiceKeyReCreateAfterDeletion(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKey(terraformResourceName, serviceAccountName), + Config: configBuilder(serviceKey, serviceAccount), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceKey), - acctests.RevokeTwingateServiceKey(serviceKey), - acctests.DeleteTwingateResource(serviceKey, resource.TwingateServiceAccountKey), + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + acctests.RevokeTwingateServiceKey(serviceKey.TerraformResource()), + acctests.DeleteTwingateResource(serviceKey.TerraformResource(), resource.TwingateServiceAccountKey), ), ExpectNonEmptyPlan: true, }, { - Config: configServiceKey(terraformResourceName, serviceAccountName), + Config: configBuilder(serviceKey, serviceAccount), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), ), }, }, @@ -253,8 +196,8 @@ func TestAccTwingateServiceKeyReCreateAfterDeletion(t *testing.T) { func TestAccTwingateServiceKeyCreateWithInvalidExpiration(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -262,11 +205,11 @@ func TestAccTwingateServiceKeyCreateWithInvalidExpiration(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKeyWithExpiration(terraformResourceName, serviceAccountName, -1), + Config: configBuilder(serviceAccount, serviceKey.Set(attr.ExpirationTime, -1)), ExpectError: regexp.MustCompile(resource.ErrInvalidExpirationTime.Error()), }, { - Config: configServiceKeyWithExpiration(terraformResourceName, serviceAccountName, 366), + Config: configBuilder(serviceAccount, serviceKey.Set(attr.ExpirationTime, 366)), ExpectError: regexp.MustCompile(resource.ErrInvalidExpirationTime.Error()), }, }, @@ -276,10 +219,8 @@ func TestAccTwingateServiceKeyCreateWithInvalidExpiration(t *testing.T) { func TestAccTwingateServiceKeyCreateWithExpiration(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") - serviceAccount := acctests.TerraformServiceAccount(terraformResourceName) - serviceKey := acctests.TerraformServiceKey(terraformResourceName) + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -287,13 +228,13 @@ func TestAccTwingateServiceKeyCreateWithExpiration(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKeyWithExpiration(terraformResourceName, serviceAccountName, 365), + Config: configBuilder(serviceAccount, serviceKey.Set(attr.ExpirationTime, 365)), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceAccount), - sdk.TestCheckResourceAttr(serviceAccount, attr.Name, serviceAccountName), - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttr(serviceKey, attr.IsActive, "true"), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + acctests.CheckTwingateResourceExists(serviceAccount.TerraformResource()), + sdk.TestCheckResourceAttr(serviceAccount.TerraformResource(), attr.Name, serviceAccount.Name), + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + sdk.TestCheckResourceAttr(serviceKey.TerraformResource(), attr.IsActive, "true"), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), ), }, }, @@ -303,9 +244,8 @@ func TestAccTwingateServiceKeyCreateWithExpiration(t *testing.T) { func TestAccTwingateServiceKeyReCreateAfterChangingExpirationTime(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - terraformResourceName := test.TerraformRandName("test_key") - serviceKey := acctests.TerraformServiceKey(terraformResourceName) + serviceAccount := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount.TerraformResourceID()) resourceID := new(string) @@ -315,18 +255,18 @@ func TestAccTwingateServiceKeyReCreateAfterChangingExpirationTime(t *testing.T) CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configServiceKeyWithExpiration(terraformResourceName, serviceAccountName, 1), + Config: configBuilder(serviceAccount, serviceKey.Set(attr.ExpirationTime, 1)), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceKey), - acctests.GetTwingateResourceID(serviceKey, &resourceID), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + acctests.GetTwingateResourceID(serviceKey.TerraformResource(), &resourceID), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), ), }, { - Config: configServiceKeyWithExpiration(terraformResourceName, serviceAccountName, 2), + Config: configBuilder(serviceAccount, serviceKey.Set(attr.ExpirationTime, 2)), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttrWith(serviceKey, attr.ID, func(value string) error { + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.ID, func(value string) error { if *resourceID == "" { return errors.New("failed to fetch resource id") } @@ -346,14 +286,9 @@ func TestAccTwingateServiceKeyReCreateAfterChangingExpirationTime(t *testing.T) func TestAccTwingateServiceKeyAndServiceAccountLifecycle(t *testing.T) { t.Parallel() - serviceAccountName := test.RandomName() - serviceAccountNameV2 := test.RandomName() - terraformServiceAccountName := test.TerraformRandName("test_acc") - terraformServiceAccountNameV2 := test.TerraformRandName("test_acc_v2") - terraformServiceAccountKeyName := test.TerraformRandName("test_key") - serviceAccount := acctests.TerraformServiceAccount(terraformServiceAccountName) - serviceAccountV2 := acctests.TerraformServiceAccount(terraformServiceAccountNameV2) - serviceKey := acctests.TerraformServiceKey(terraformServiceAccountKeyName) + serviceAccount1 := NewServiceAccount() + serviceAccount2 := NewServiceAccount() + serviceKey := NewServiceAccountKey(serviceAccount1.TerraformResourceID()) serviceKeyResourceID := new(string) serviceAccountResourceID := new(string) @@ -364,26 +299,26 @@ func TestAccTwingateServiceKeyAndServiceAccountLifecycle(t *testing.T) { CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, Steps: []sdk.TestStep{ { - Config: configTwoServiceAccounts(terraformServiceAccountName, serviceAccountName, terraformServiceAccountNameV2, serviceAccountNameV2, terraformServiceAccountKeyName, terraformServiceAccountName), + Config: configBuilder(serviceKey, serviceAccount1, serviceAccount2), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceAccount), - sdk.TestCheckResourceAttr(serviceAccount, attr.Name, serviceAccountName), - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), - acctests.GetTwingateResourceID(serviceKey, &serviceKeyResourceID), - acctests.GetTwingateResourceID(serviceKey, &serviceAccountResourceID), + acctests.CheckTwingateResourceExists(serviceAccount1.TerraformResource()), + sdk.TestCheckResourceAttr(serviceAccount1.TerraformResource(), attr.Name, serviceAccount1.Name), + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), + acctests.GetTwingateResourceID(serviceKey.TerraformResource(), &serviceKeyResourceID), + acctests.GetTwingateResourceID(serviceKey.TerraformResource(), &serviceAccountResourceID), ), }, { - Config: configTwoServiceAccounts(terraformServiceAccountName, serviceAccountName, terraformServiceAccountNameV2, serviceAccountNameV2, terraformServiceAccountKeyName, terraformServiceAccountNameV2), + Config: configBuilder(serviceKey.Set(attr.ServiceAccountID, serviceAccount2.TerraformResourceID()), serviceAccount1, serviceAccount2), Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceExists(serviceAccountV2), - sdk.TestCheckResourceAttr(serviceAccountV2, attr.Name, serviceAccountNameV2), - acctests.CheckTwingateResourceExists(serviceKey), - sdk.TestCheckResourceAttrWith(serviceKey, attr.Token, nonEmptyValue), + acctests.CheckTwingateResourceExists(serviceAccount2.TerraformResource()), + sdk.TestCheckResourceAttr(serviceAccount2.TerraformResource(), attr.Name, serviceAccount2.Name), + acctests.CheckTwingateResourceExists(serviceKey.TerraformResource()), + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.Token, nonEmptyValue), // test resources were re-created - sdk.TestCheckResourceAttrWith(serviceKey, attr.ID, func(value string) error { + sdk.TestCheckResourceAttrWith(serviceKey.TerraformResource(), attr.ID, func(value string) error { if *serviceKeyResourceID == "" { return errors.New("failed to fetch service_key resource id") } @@ -395,7 +330,7 @@ func TestAccTwingateServiceKeyAndServiceAccountLifecycle(t *testing.T) { return nil }), - sdk.TestCheckResourceAttrWith(serviceAccountV2, attr.ID, func(value string) error { + sdk.TestCheckResourceAttrWith(serviceAccount2.TerraformResource(), attr.ID, func(value string) error { if *serviceAccountResourceID == "" { return errors.New("failed to fetch service_account resource id") } @@ -411,27 +346,3 @@ func TestAccTwingateServiceKeyAndServiceAccountLifecycle(t *testing.T) { }, }) } - -func configTwoServiceAccounts(terraformServiceAccountName, serviceAccountName, terraformServiceAccountNameV2, serviceAccountNameV2, terraformServiceAccountKeyName, serviceAccount string) string { - return acctests.Nprintf(` - resource "twingate_service_account" "${service_account_resource_1}" { - name = "${name_1}" - } - - resource "twingate_service_account" "${service_account_resource_2}" { - name = "${name_2}" - } - - resource "twingate_service_account_key" "${service_account_key_resource}" { - service_account_id = twingate_service_account.${service_account_resource}.id - } - `, - map[string]any{ - "service_account_resource_1": terraformServiceAccountName, - "name_1": serviceAccountName, - "service_account_resource_2": terraformServiceAccountNameV2, - "name_2": serviceAccountNameV2, - "service_account_key_resource": terraformServiceAccountKeyName, - "service_account_resource": serviceAccount, - }) -} diff --git a/twingate/internal/test/acctests/resource/user_test.go b/twingate/internal/test/acctests/resource/user_test.go index 5bf33f8c..2dc32f4b 100644 --- a/twingate/internal/test/acctests/resource/user_test.go +++ b/twingate/internal/test/acctests/resource/user_test.go @@ -1,7 +1,6 @@ package resource import ( - "fmt" "regexp" "testing" @@ -11,52 +10,53 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" ) func TestAccTwingateUserCreateUpdate(t *testing.T) { t.Parallel() - userResource := test.RandomUserName() - theResource := acctests.TerraformUser(userResource) - email := test.RandomEmail() firstName := test.RandomName() lastName := test.RandomName() role := model.UserRoleSupport + user := NewUser() + theResource := user.TerraformResource() + sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, PreCheck: func() { acctests.PreCheck(t) }, CheckDestroy: acctests.CheckTwingateUserDestroy, Steps: []sdk.TestStep{ { - Config: configUser(userResource, email), + Config: configBuilder(user), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Email, email), + sdk.TestCheckResourceAttr(theResource, attr.Email, user.Email), ), }, { - Config: configUserWithFirstName(userResource, email, firstName), + Config: configBuilder(user.Set(attr.FirstName, firstName)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Email, email), + sdk.TestCheckResourceAttr(theResource, attr.Email, user.Email), sdk.TestCheckResourceAttr(theResource, attr.FirstName, firstName), ), }, { - Config: configUserWithLastName(userResource, email, lastName), + Config: configBuilder(user.Set(attr.LastName, lastName)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Email, email), + sdk.TestCheckResourceAttr(theResource, attr.Email, user.Email), sdk.TestCheckResourceAttr(theResource, attr.FirstName, firstName), sdk.TestCheckResourceAttr(theResource, attr.LastName, lastName), ), }, { - Config: configUserWithRole(userResource, email, role), + Config: configBuilder(user.Set(attr.Role, role)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Email, email), + sdk.TestCheckResourceAttr(theResource, attr.Email, user.Email), sdk.TestCheckResourceAttr(theResource, attr.FirstName, firstName), sdk.TestCheckResourceAttr(theResource, attr.LastName, lastName), sdk.TestCheckResourceAttr(theResource, attr.Role, role), @@ -66,69 +66,15 @@ func TestAccTwingateUserCreateUpdate(t *testing.T) { }) } -func configUser(userResource, email string) string { - return acctests.Nprintf(` - resource "twingate_user" "${user_resource}" { - email = "${email}" - send_invite = false - } - `, map[string]any{ - "user_resource": userResource, - "email": email, - }) -} - -func configUserWithFirstName(userResource, email, firstName string) string { - return acctests.Nprintf(` - resource "twingate_user" "${user_resource}" { - email = "${email}" - first_name = "${first_name}" - send_invite = false - } - `, map[string]any{ - "user_resource": userResource, - "email": email, - "first_name": firstName, - }) -} - -func configUserWithLastName(userResource, email, lastName string) string { - return acctests.Nprintf(` - resource "twingate_user" "${user_resource}" { - email = "${email}" - last_name = "${last_name}" - send_invite = false - } - `, map[string]any{ - "user_resource": userResource, - "email": email, - "last_name": lastName, - }) -} - -func configUserWithRole(userResource, email, role string) string { - return acctests.Nprintf(` - resource "twingate_user" "${user_resource}" { - email = "${email}" - role = "${role}" - send_invite = false - } - `, map[string]any{ - "user_resource": userResource, - "email": email, - "role": role, - }) -} - func TestAccTwingateUserFullCreate(t *testing.T) { t.Parallel() - userResource := test.RandomUserName() - theResource := acctests.TerraformUser(userResource) - email := test.RandomEmail() - firstName := test.RandomName() - lastName := test.RandomName() - role := test.RandomUserRole() + user := NewUser().Set( + attr.FirstName, test.RandomName(), + attr.LastName, test.RandomName(), + attr.Role, test.RandomUserRole(), + ) + theResource := user.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -136,59 +82,42 @@ func TestAccTwingateUserFullCreate(t *testing.T) { CheckDestroy: acctests.CheckTwingateUserDestroy, Steps: []sdk.TestStep{ { - Config: configUserFull(userResource, email, firstName, lastName, role), + Config: configBuilder(user), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Email, email), - sdk.TestCheckResourceAttr(theResource, attr.FirstName, firstName), - sdk.TestCheckResourceAttr(theResource, attr.LastName, lastName), - sdk.TestCheckResourceAttr(theResource, attr.Role, role), + sdk.TestCheckResourceAttr(theResource, attr.Email, user.Email), + sdk.TestCheckResourceAttr(theResource, attr.FirstName, *user.FirstName), + sdk.TestCheckResourceAttr(theResource, attr.LastName, *user.LastName), + sdk.TestCheckResourceAttr(theResource, attr.Role, *user.Role), ), }, }, }) } -func configUserFull(userResource, email, firstName, lastName, role string) string { - return acctests.Nprintf(` - resource "twingate_user" "${user_resource}" { - email = "${email}" - first_name = "${first_name}" - last_name = "${last_name}" - role = "${role}" - send_invite = false - } - `, map[string]any{ - "user_resource": userResource, - "email": email, - "first_name": firstName, - "last_name": lastName, - "role": role, - }) -} - func TestAccTwingateUserReCreation(t *testing.T) { t.Parallel() - userResource := test.RandomUserName() - theResource := acctests.TerraformUser(userResource) email1 := test.RandomEmail() email2 := test.RandomEmail() + user := NewUser().Set(attr.Email, email1) + theResource := user.TerraformResource() + sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, PreCheck: func() { acctests.PreCheck(t) }, CheckDestroy: acctests.CheckTwingateUserDestroy, Steps: []sdk.TestStep{ { - Config: configUser(userResource, email1), + Config: configBuilder(user), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Email, email1), ), }, { - Config: configUser(userResource, email2), + Config: configBuilder(user.Set(attr.Email, email2)), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, attr.Email, email2), @@ -201,9 +130,8 @@ func TestAccTwingateUserReCreation(t *testing.T) { func TestAccTwingateUserUpdateState(t *testing.T) { t.Parallel() - userResource := test.RandomUserName() - theResource := acctests.TerraformUser(userResource) - email := test.RandomEmail() + user := NewUser() + theResource := user.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -211,38 +139,24 @@ func TestAccTwingateUserUpdateState(t *testing.T) { CheckDestroy: acctests.CheckTwingateUserDestroy, Steps: []sdk.TestStep{ { - Config: configUser(userResource, email), + Config: configBuilder(user), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, attr.Email, email), + sdk.TestCheckResourceAttr(theResource, attr.Email, user.Email), ), }, { - Config: configUserDisabled(userResource, email), + Config: configBuilder(user.Set(attr.IsActive, false)), ExpectError: regexp.MustCompile(`User in PENDING state`), }, }, }) } -func configUserDisabled(userResource, email string) string { - return acctests.Nprintf(` - resource "twingate_user" "${user_resource}" { - email = "${email}" - send_invite = false - is_active = false - } - `, map[string]any{ - "user_resource": userResource, - "email": email, - }) -} - func TestAccTwingateUserDelete(t *testing.T) { t.Parallel() - userResource := test.RandomUserName() - theResource := acctests.TerraformUser(userResource) + user := NewUser() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -250,11 +164,16 @@ func TestAccTwingateUserDelete(t *testing.T) { CheckDestroy: acctests.CheckTwingateUserDestroy, Steps: []sdk.TestStep{ { - Config: configUser(userResource, test.RandomEmail()), + Config: configBuilder(user), Destroy: true, - Check: acctests.ComposeTestCheckFunc( - acctests.CheckTwingateResourceDoesNotExists(theResource), - ), + }, + { + Config: configBuilder(user), + ConfigPlanChecks: sdk.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(user.TerraformResource(), plancheck.ResourceActionCreate), + }, + }, }, }, }) @@ -263,9 +182,8 @@ func TestAccTwingateUserDelete(t *testing.T) { func TestAccTwingateUserReCreateAfterDeletion(t *testing.T) { t.Parallel() - userResource := test.RandomUserName() - theResource := acctests.TerraformUser(userResource) - email := test.RandomEmail() + user := NewUser() + theResource := user.TerraformResource() sdk.Test(t, sdk.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, @@ -273,7 +191,7 @@ func TestAccTwingateUserReCreateAfterDeletion(t *testing.T) { CheckDestroy: acctests.CheckTwingateUserDestroy, Steps: []sdk.TestStep{ { - Config: configUser(userResource, email), + Config: configBuilder(user), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), acctests.DeleteTwingateResource(theResource, resource.TwingateUser), @@ -281,7 +199,7 @@ func TestAccTwingateUserReCreateAfterDeletion(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: configUser(userResource, email), + Config: configBuilder(user), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), ), @@ -299,7 +217,7 @@ func TestAccTwingateUserCreateWithUnknownRole(t *testing.T) { CheckDestroy: acctests.CheckTwingateUserDestroy, Steps: []sdk.TestStep{ { - Config: configUserWithRole(test.RandomUserName(), test.RandomEmail(), "UnknownRole"), + Config: configBuilder(NewUser().Set(attr.Role, "UnknownRole")), ExpectError: regexp.MustCompile(`Attribute role value must be one of`), }, }, @@ -322,25 +240,12 @@ func TestAccTwingateUserCreateWithoutEmail(t *testing.T) { }) } -func configUserWithoutEmail(userResource string) string { +func configUserWithoutEmail(terraformResource string) string { return acctests.Nprintf(` - resource "twingate_user" "${user_resource}" { + resource "twingate_user" "${terraform_resource}" { send_invite = false } `, map[string]any{ - "user_resource": userResource, + "terraform_resource": terraformResource, }) } - -func genNewUsers(resourcePrefix string, count int) ([]string, []string) { - users := make([]string, 0, count) - userIDs := make([]string, 0, count) - - for i := 0; i < count; i++ { - resourceName := fmt.Sprintf("%s_%d", resourcePrefix, i+1) - users = append(users, configUser(resourceName, test.RandomEmail())) - userIDs = append(userIDs, fmt.Sprintf("twingate_user.%s.id", resourceName)) - } - - return users, userIDs -} diff --git a/twingate/internal/test/client/connector_test.go b/twingate/internal/test/client/connector_test.go index 6136ed8b..565db6af 100644 --- a/twingate/internal/test/client/connector_test.go +++ b/twingate/internal/test/client/connector_test.go @@ -510,7 +510,7 @@ func TestClientConnectorReadEmptyError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, emptyResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Empty(t, connectors) assert.EqualError(t, err, "failed to read connector with id All: query result is empty") @@ -603,7 +603,7 @@ func TestClientConnectorReadAllOk(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, connectors) }) @@ -769,7 +769,7 @@ func TestClientReadConnectorsWithRemoteNetworkOk(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, connectors) @@ -789,7 +789,7 @@ func TestClientReadConnectorsWithRemoteNetworkError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, "failed to read connector with id All: query result is empty") @@ -803,7 +803,7 @@ func TestClientReadConnectorsWithRemoteNetworkRequestError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, graphqlErr(client, "failed to read connector with id All", errBadRequest)) @@ -883,7 +883,7 @@ func TestClientReadConnectorsAllPagesOk(t *testing.T) { }), ) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, connectors) }) @@ -933,7 +933,7 @@ func TestClientReadConnectorsAllPagesEmptyResultOnFetching(t *testing.T) { ), ) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, `failed to read connector with id All: query result is empty`) }) @@ -972,7 +972,7 @@ func TestClientReadConnectorsAllPagesRequestErrorOnFetching(t *testing.T) { ), ) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, graphqlErr(client, "failed to read connector with id All", errBadRequest)) }) diff --git a/twingate/internal/test/client/remote-network_test.go b/twingate/internal/test/client/remote-network_test.go index 5f43666f..37df276f 100644 --- a/twingate/internal/test/client/remote-network_test.go +++ b/twingate/internal/test/client/remote-network_test.go @@ -497,7 +497,7 @@ func TestClientNetworkReadAllOk(t *testing.T) { ), ) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.NoError(t, err) assert.EqualValues(t, expected, networks) @@ -511,7 +511,7 @@ func TestClientNetworkReadAllRequestError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.Nil(t, networks) assert.EqualError(t, err, graphqlErr(client, "failed to read remote network with id All", errBadRequest)) @@ -552,7 +552,7 @@ func TestClientNetworkReadAllEmptyResponse(t *testing.T) { ), ) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.Nil(t, networks) assert.EqualError(t, err, `failed to read remote network: query result is empty`) @@ -582,7 +582,7 @@ func TestClientNetworkReadAllRequestErrorOnPageFetch(t *testing.T) { ), ) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.Nil(t, networks) assert.EqualError(t, err, graphqlErr(client, "failed to read remote network", errBadRequest)) diff --git a/twingate/internal/test/client/resource_test.go b/twingate/internal/test/client/resource_test.go index 0b533749..e1e597cd 100644 --- a/twingate/internal/test/client/resource_test.go +++ b/twingate/internal/test/client/resource_test.go @@ -1184,7 +1184,7 @@ func TestClientResourcesReadByNameOk(t *testing.T) { }), ) - resources, err := client.ReadResourcesByName(context.Background(), "resource-test") + resources, err := client.ReadResourcesByName(context.Background(), "resource-test", "") assert.Nil(t, err) assert.Equal(t, expected, resources) @@ -1204,7 +1204,7 @@ func TestClientResourcesReadByNameEmptyResult(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - resources, err := client.ReadResourcesByName(context.Background(), "resource-name") + resources, err := client.ReadResourcesByName(context.Background(), "resource-name", "") assert.Nil(t, resources) assert.EqualError(t, err, "failed to read resource with id All: query result is empty") @@ -1218,7 +1218,7 @@ func TestClientResourcesReadByNameRequestError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - groups, err := client.ReadResourcesByName(context.Background(), "resource-name") + groups, err := client.ReadResourcesByName(context.Background(), "resource-name", "") assert.Nil(t, groups) assert.EqualError(t, err, graphqlErr(client, "failed to read resource with id All", errBadRequest)) @@ -1238,7 +1238,7 @@ func TestClientResourcesReadByNameErrorEmptyName(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - groups, err := client.ReadResourcesByName(context.Background(), "") + groups, err := client.ReadResourcesByName(context.Background(), "", "") assert.Nil(t, groups) assert.EqualError(t, err, "failed to read resource with id All: query result is empty") @@ -1274,7 +1274,7 @@ func TestClientResourcesReadByNameAllEmptyResult(t *testing.T) { ), ) - resources, err := client.ReadResourcesByName(context.Background(), "resource-name") + resources, err := client.ReadResourcesByName(context.Background(), "resource-name", "") assert.Nil(t, resources) assert.EqualError(t, err, "failed to read resource with id All: query result is empty") @@ -1304,7 +1304,7 @@ func TestClientResourcesReadByNameAllRequestError(t *testing.T) { ), ) - resources, err := client.ReadResourcesByName(context.Background(), "resource-name") + resources, err := client.ReadResourcesByName(context.Background(), "resource-name", "") assert.Nil(t, resources) assert.EqualError(t, err, graphqlErr(client, "failed to read resource with id All", errBadRequest)) diff --git a/twingate/internal/test/client/security-policy_test.go b/twingate/internal/test/client/security-policy_test.go index 4dbae951..8c647f93 100644 --- a/twingate/internal/test/client/security-policy_test.go +++ b/twingate/internal/test/client/security-policy_test.go @@ -187,7 +187,7 @@ func TestClientSecurityPoliciesReadOk(t *testing.T) { ), ) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, securityPolicies) @@ -201,7 +201,7 @@ func TestClientSecurityPoliciesReadRequestError(t *testing.T) { httpmock.RegisterResponder("POST", c.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") assert.Nil(t, securityPolicies) assert.EqualError(t, err, graphqlErr(c, "failed to read security policy", errBadRequest)) @@ -240,7 +240,7 @@ func TestClientSecurityPoliciesReadEmptyResponse(t *testing.T) { httpmock.RegisterResponder("POST", c.GraphqlServerURL, httpmock.NewStringResponder(http.StatusOK, resp)) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") httpmock.Reset() @@ -281,7 +281,7 @@ func TestClientSecurityPoliciesReadRequestErrorOnFetching(t *testing.T) { ), ) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") assert.Nil(t, securityPolicies) assert.EqualError(t, err, graphqlErr(c, "failed to read security policy", errBadRequest)) @@ -343,7 +343,7 @@ func TestClientSecurityPoliciesReadEmptyResultOnFetching(t *testing.T) { ), ) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") httpmock.Reset() diff --git a/twingate/internal/test/client/user_test.go b/twingate/internal/test/client/user_test.go index 12534dd4..f7837859 100644 --- a/twingate/internal/test/client/user_test.go +++ b/twingate/internal/test/client/user_test.go @@ -12,11 +12,10 @@ import ( func TestClientUserReadOk(t *testing.T) { testData := []struct { - role string - isAdmin bool + role string }{ - {role: "ADMIN", isAdmin: true}, - {role: "DEVOPS", isAdmin: false}, + {role: "ADMIN"}, + {role: "DEVOPS"}, } for _, td := range testData { @@ -49,7 +48,6 @@ func TestClientUserReadOk(t *testing.T) { assert.EqualValues(t, userID, user.ID) assert.EqualValues(t, email, user.Email) assert.EqualValues(t, td.role, user.Role) - assert.EqualValues(t, td.isAdmin, user.IsAdmin()) }) } } diff --git a/twingate/internal/test/client/users_test.go b/twingate/internal/test/client/users_test.go index ab6bb09f..0d2adc0c 100644 --- a/twingate/internal/test/client/users_test.go +++ b/twingate/internal/test/client/users_test.go @@ -79,7 +79,7 @@ func TestClientUsersReadOk(t *testing.T) { }), ) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, err) assert.Equal(t, expected, users) @@ -99,7 +99,7 @@ func TestClientUsersReadEmptyResult(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, err) assert.Nil(t, users) @@ -114,7 +114,7 @@ func TestClientUsersReadRequestError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, users) assert.EqualError(t, err, graphqlErr(client, "failed to read user with id All", errBadRequest)) @@ -169,7 +169,7 @@ func TestClientUsersReadNextPageEmptyResponse(t *testing.T) { }), ) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, users) assert.EqualError(t, err, "failed to read user with id All: query result is empty") @@ -219,7 +219,7 @@ func TestClientReadUsersAfterRequestError(t *testing.T) { ), ) - users, err := client.ReadUsers(context.Background()) + users, err := client.ReadUsers(context.Background(), nil) assert.Nil(t, users) assert.EqualError(t, err, graphqlErr(client, "failed to read user with id All", errBadRequest)) diff --git a/twingate/internal/test/models/user_test.go b/twingate/internal/test/models/user_test.go index 4cd96cec..d1b72681 100644 --- a/twingate/internal/test/models/user_test.go +++ b/twingate/internal/test/models/user_test.go @@ -21,7 +21,6 @@ func TestUserModel(t *testing.T) { attr.FirstName: "", attr.LastName: "", attr.Email: "", - attr.IsAdmin: false, attr.Role: "", attr.Type: "", }, @@ -40,7 +39,6 @@ func TestUserModel(t *testing.T) { attr.FirstName: "John", attr.LastName: "White", attr.Email: "john@white.com", - attr.IsAdmin: true, attr.Role: "ADMIN", attr.Type: "MANUAL", }, @@ -59,7 +57,6 @@ func TestUserModel(t *testing.T) { attr.FirstName: "Hue", attr.LastName: "Black", attr.Email: "hue@black.com", - attr.IsAdmin: false, attr.Role: "USER", attr.Type: "SYNCED", }, diff --git a/twingate/internal/test/sweepers/connector_test.go b/twingate/internal/test/sweepers/connector_test.go index 3fd63f94..4c0c1da7 100644 --- a/twingate/internal/test/sweepers/connector_test.go +++ b/twingate/internal/test/sweepers/connector_test.go @@ -3,6 +3,8 @@ package sweepers import ( "context" "errors" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -15,7 +17,7 @@ func init() { Name: resourceConnector, F: newTestSweeper(resourceConnector, func(c *client.Client, ctx context.Context) ([]Resource, error) { - resources, err := c.ReadConnectors(ctx) + resources, err := c.ReadConnectors(ctx, test.Prefix(), attr.FilterByPrefix) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { return nil, err } diff --git a/twingate/internal/test/sweepers/remote-network_test.go b/twingate/internal/test/sweepers/remote-network_test.go index be43eda3..5254291e 100644 --- a/twingate/internal/test/sweepers/remote-network_test.go +++ b/twingate/internal/test/sweepers/remote-network_test.go @@ -14,7 +14,7 @@ func init() { Name: resourceRemoteNetwork, F: newTestSweeper(resourceRemoteNetwork, func(client *client.Client, ctx context.Context) ([]Resource, error) { - resources, err := client.ReadRemoteNetworks(ctx) + resources, err := client.ReadRemoteNetworks(ctx, "", "") if err != nil { return nil, err } diff --git a/twingate/internal/test/sweepers/sweeper_test.go b/twingate/internal/test/sweepers/sweeper_test.go index 58357976..711e4768 100644 --- a/twingate/internal/test/sweepers/sweeper_test.go +++ b/twingate/internal/test/sweepers/sweeper_test.go @@ -3,6 +3,7 @@ package sweepers import ( "context" "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "log" "os" "strings" @@ -11,7 +12,6 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) diff --git a/twingate/internal/test/sweepers/user_test.go b/twingate/internal/test/sweepers/user_test.go index 292d7211..9a49515a 100644 --- a/twingate/internal/test/sweepers/user_test.go +++ b/twingate/internal/test/sweepers/user_test.go @@ -14,7 +14,7 @@ func init() { Name: name, F: newTestSweeper(name, func(client *client.Client, ctx context.Context) ([]Resource, error) { - resources, err := client.ReadUsers(ctx) + resources, err := client.ReadUsers(ctx, nil) if err != nil { return nil, err } diff --git a/twingate/provider.go b/twingate/provider.go index a21e4c78..6e63c770 100644 --- a/twingate/provider.go +++ b/twingate/provider.go @@ -9,9 +9,14 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + twingateDatasource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/datasource" + twingateResource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" ) const ( @@ -27,92 +32,143 @@ const ( EnvHTTPMaxRetry = "TWINGATE_HTTP_MAX_RETRY" ) -func Provider(version string) *schema.Provider { - provider := &schema.Provider{ - Schema: providerOptions(), - ResourcesMap: map[string]*schema.Resource{ - resource.TwingateResource: resource.Resource(), - }, - DataSourcesMap: map[string]*schema.Resource{}, +var _ provider.Provider = &Twingate{} + +type Twingate struct { + version string +} + +type twingateProviderModel struct { + APIToken types.String `tfsdk:"api_token"` + Network types.String `tfsdk:"network"` + URL types.String `tfsdk:"url"` + HTTPTimeout types.Int64 `tfsdk:"http_timeout"` + HTTPMaxRetry types.Int64 `tfsdk:"http_max_retry"` +} + +func New(version string) func() provider.Provider { + return func() provider.Provider { + return &Twingate{ + version: version, + } } - provider.ConfigureContextFunc = configure(version, provider) +} - return provider +func (t Twingate) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) { + response.TypeName = "twingate" + response.Version = t.version } -func providerOptions() map[string]*schema.Schema { - return map[string]*schema.Schema{ - attr.APIToken: { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: fmt.Sprintf("The access key for API operations. You can retrieve this\n"+ - "from the Twingate Admin Console ([documentation](https://docs.twingate.com/docs/api-overview)).\n"+ - "Alternatively, this can be specified using the %s environment variable.", EnvAPIToken), - }, - attr.Network: { - Type: schema.TypeString, - Optional: true, - Sensitive: false, - Description: fmt.Sprintf("Your Twingate network ID for API operations.\n"+ - "You can find it in the Admin Console URL, for example:\n"+ - "`autoco.twingate.com`, where `autoco` is your network ID\n"+ - "Alternatively, this can be specified using the %s environment variable.", EnvNetwork), - }, - attr.URL: { - Type: schema.TypeString, - Optional: true, - Sensitive: false, - Description: fmt.Sprintf("The default is '%s'\n"+ - "This is optional and shouldn't be changed under normal circumstances.", DefaultURL), - }, - attr.HTTPTimeout: { - Type: schema.TypeInt, - Optional: true, - Description: fmt.Sprintf("Specifies a time limit in seconds for the http requests made. The default value is %s seconds.\n"+ - "Alternatively, this can be specified using the %s environment variable", DefaultHTTPTimeout, EnvHTTPTimeout), - }, - attr.HTTPMaxRetry: { - Type: schema.TypeInt, - Optional: true, - Description: fmt.Sprintf("Specifies a retry limit for the http requests made. The default value is %s.\n"+ - "Alternatively, this can be specified using the %s environment variable", DefaultHTTPMaxRetry, EnvHTTPMaxRetry), +func (t Twingate) Schema(ctx context.Context, request provider.SchemaRequest, response *provider.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + attr.APIToken: schema.StringAttribute{ + Optional: true, + Sensitive: true, + Description: fmt.Sprintf("The access key for API operations. You can retrieve this\n"+ + "from the Twingate Admin Console ([documentation](https://docs.twingate.com/docs/api-overview)).\n"+ + "Alternatively, this can be specified using the %s environment variable.", EnvAPIToken), + }, + attr.Network: schema.StringAttribute{ + Optional: true, + Description: fmt.Sprintf("Your Twingate network ID for API operations.\n"+ + "You can find it in the Admin Console URL, for example:\n"+ + "`autoco.twingate.com`, where `autoco` is your network ID\n"+ + "Alternatively, this can be specified using the %s environment variable.", EnvNetwork), + }, + attr.URL: schema.StringAttribute{ + Optional: true, + Description: fmt.Sprintf("The default is '%s'\n"+ + "This is optional and shouldn't be changed under normal circumstances.", DefaultURL), + }, + attr.HTTPTimeout: schema.Int64Attribute{ + Optional: true, + Description: fmt.Sprintf("Specifies a time limit in seconds for the http requests made. The default value is %s seconds.\n"+ + "Alternatively, this can be specified using the %s environment variable", DefaultHTTPTimeout, EnvHTTPTimeout), + }, + attr.HTTPMaxRetry: schema.Int64Attribute{ + Optional: true, + Description: fmt.Sprintf("Specifies a retry limit for the http requests made. The default value is %s.\n"+ + "Alternatively, this can be specified using the %s environment variable", DefaultHTTPMaxRetry, EnvHTTPMaxRetry), + }, }, } } -func configure(version string, _ *schema.Provider) func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) { - return func(ctx context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) { - apiToken := os.Getenv(EnvAPIToken) - network := os.Getenv(EnvNetwork) - url := withDefault(os.Getenv(EnvURL), DefaultURL) - httpTimeout := mustGetInt(withDefault(os.Getenv(EnvHTTPTimeout), DefaultHTTPTimeout)) - httpMaxRetry := mustGetInt(withDefault(os.Getenv(EnvHTTPMaxRetry), DefaultHTTPMaxRetry)) - - apiToken = withDefault(data.Get(attr.APIToken).(string), apiToken) - network = withDefault(data.Get(attr.Network).(string), network) - url = withDefault(data.Get(attr.URL).(string), url) - httpTimeout = withDefault(data.Get(attr.HTTPTimeout).(int), httpTimeout) - httpMaxRetry = withDefault(data.Get(attr.HTTPMaxRetry).(int), httpMaxRetry) - - if network != "" { - return client.NewClient(url, - apiToken, - network, - time.Duration(httpTimeout)*time.Second, - httpMaxRetry, - version), - nil - } +func (t Twingate) Configure(ctx context.Context, request provider.ConfigureRequest, response *provider.ConfigureResponse) { + var config twingateProviderModel - return nil, diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Error, - Summary: "Unable to create Twingate client", - Detail: "Unable to create anonymous Twingate client, network has to be provided", - }, - } + response.Diagnostics.Append(request.Config.Get(ctx, &config)...) + + if response.Diagnostics.HasError() { + return + } + + // Default values to environment variables, but override + // with Terraform configuration value if set. + + apiToken := os.Getenv(EnvAPIToken) + network := os.Getenv(EnvNetwork) + url := withDefault(os.Getenv(EnvURL), DefaultURL) + httpTimeout := mustGetInt(withDefault(os.Getenv(EnvHTTPTimeout), DefaultHTTPTimeout)) + httpMaxRetry := mustGetInt(withDefault(os.Getenv(EnvHTTPMaxRetry), DefaultHTTPMaxRetry)) + + apiToken = overrideStrWithConfig(config.APIToken, apiToken) + network = overrideStrWithConfig(config.Network, network) + url = overrideStrWithConfig(config.URL, url) + httpTimeout = overrideIntWithConfig(config.HTTPTimeout, httpTimeout) + httpMaxRetry = overrideIntWithConfig(config.HTTPMaxRetry, httpMaxRetry) + + if network == "" { + response.Diagnostics.AddAttributeError( + path.Root(attr.Network), + "Missing Twingate "+attr.Network, + fmt.Sprintf("The provider cannot create the Twingate API client as there is a missing or empty value for the Twingate %s. "+ + "Set the %s value in the configuration or use the %s environment variable. "+ + "If either is already set, ensure the value is not empty.", attr.Network, attr.Network, EnvNetwork), + ) + + return } + + client := client.NewClient(url, + apiToken, + network, + time.Duration(httpTimeout)*time.Second, + httpMaxRetry, + t.version) + + response.DataSourceData = client + response.ResourceData = client + + policy, _ := client.ReadSecurityPolicy(ctx, "", twingateResource.DefaultSecurityPolicyName) + if policy != nil { + twingateResource.DefaultSecurityPolicyID = policy.ID + } +} + +func mustGetInt(str string) int { + if val, err := strconv.Atoi(str); err == nil { + return val + } + + return 0 +} + +func overrideStrWithConfig(cfg types.String, defaultValue string) string { + if !cfg.IsNull() { + return cfg.ValueString() + } + + return defaultValue +} + +func overrideIntWithConfig(cfg types.Int64, defaultValue int) int { + if !cfg.IsNull() { + return int(cfg.ValueInt64()) + } + + return defaultValue } func withDefault[T comparable](val, defaultVal T) T { @@ -124,10 +180,33 @@ func withDefault[T comparable](val, defaultVal T) T { return val } -func mustGetInt(str string) int { - if val, err := strconv.Atoi(str); err == nil { - return val +func (t Twingate) DataSources(ctx context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + twingateDatasource.NewConnectorDatasource, + twingateDatasource.NewConnectorsDatasource, + twingateDatasource.NewGroupDatasource, + twingateDatasource.NewGroupsDatasource, + twingateDatasource.NewRemoteNetworkDatasource, + twingateDatasource.NewRemoteNetworksDatasource, + twingateDatasource.NewServiceAccountsDatasource, + twingateDatasource.NewUserDatasource, + twingateDatasource.NewUsersDatasource, + twingateDatasource.NewSecurityPolicyDatasource, + twingateDatasource.NewSecurityPoliciesDatasource, + twingateDatasource.NewResourceDatasource, + twingateDatasource.NewResourcesDatasource, } +} - return 0 +func (t Twingate) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + twingateResource.NewConnectorTokensResource, + twingateResource.NewConnectorResource, + twingateResource.NewGroupResource, + twingateResource.NewRemoteNetworkResource, + twingateResource.NewServiceAccountResource, + twingateResource.NewServiceKeyResource, + twingateResource.NewUserResource, + twingateResource.NewResourceResource, + } } diff --git a/twingate/v2/provider.go b/twingate/v2/provider.go deleted file mode 100644 index ed9e9546..00000000 --- a/twingate/v2/provider.go +++ /dev/null @@ -1,206 +0,0 @@ -package v2 - -import ( - "context" - "fmt" - "os" - "strconv" - "time" - - "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" - twingateDatasource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/datasource" - twingateResource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-framework/provider/schema" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -const ( - DefaultHTTPTimeout = "35" - DefaultHTTPMaxRetry = "10" - DefaultURL = "twingate.com" - - // EnvAPIToken env var for Token. - EnvAPIToken = "TWINGATE_API_TOKEN" // #nosec G101 - EnvNetwork = "TWINGATE_NETWORK" - EnvURL = "TWINGATE_URL" - EnvHTTPTimeout = "TWINGATE_HTTP_TIMEOUT" - EnvHTTPMaxRetry = "TWINGATE_HTTP_MAX_RETRY" -) - -var _ provider.Provider = &Twingate{} - -type Twingate struct { - version string -} - -type twingateProviderModel struct { - APIToken types.String `tfsdk:"api_token"` - Network types.String `tfsdk:"network"` - URL types.String `tfsdk:"url"` - HTTPTimeout types.Int64 `tfsdk:"http_timeout"` - HTTPMaxRetry types.Int64 `tfsdk:"http_max_retry"` -} - -func New(version string) func() provider.Provider { - return func() provider.Provider { - return &Twingate{ - version: version, - } - } -} - -func (t Twingate) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) { - response.TypeName = "twingate" - response.Version = t.version -} - -func (t Twingate) Schema(ctx context.Context, request provider.SchemaRequest, response *provider.SchemaResponse) { - response.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - attr.APIToken: schema.StringAttribute{ - Optional: true, - Sensitive: true, - Description: fmt.Sprintf("The access key for API operations. You can retrieve this\n"+ - "from the Twingate Admin Console ([documentation](https://docs.twingate.com/docs/api-overview)).\n"+ - "Alternatively, this can be specified using the %s environment variable.", EnvAPIToken), - }, - attr.Network: schema.StringAttribute{ - Optional: true, - Description: fmt.Sprintf("Your Twingate network ID for API operations.\n"+ - "You can find it in the Admin Console URL, for example:\n"+ - "`autoco.twingate.com`, where `autoco` is your network ID\n"+ - "Alternatively, this can be specified using the %s environment variable.", EnvNetwork), - }, - attr.URL: schema.StringAttribute{ - Optional: true, - Description: fmt.Sprintf("The default is '%s'\n"+ - "This is optional and shouldn't be changed under normal circumstances.", DefaultURL), - }, - attr.HTTPTimeout: schema.Int64Attribute{ - Optional: true, - Description: fmt.Sprintf("Specifies a time limit in seconds for the http requests made. The default value is %s seconds.\n"+ - "Alternatively, this can be specified using the %s environment variable", DefaultHTTPTimeout, EnvHTTPTimeout), - }, - attr.HTTPMaxRetry: schema.Int64Attribute{ - Optional: true, - Description: fmt.Sprintf("Specifies a retry limit for the http requests made. The default value is %s.\n"+ - "Alternatively, this can be specified using the %s environment variable", DefaultHTTPMaxRetry, EnvHTTPMaxRetry), - }, - }, - } -} - -func (t Twingate) Configure(ctx context.Context, request provider.ConfigureRequest, response *provider.ConfigureResponse) { - var config twingateProviderModel - - response.Diagnostics.Append(request.Config.Get(ctx, &config)...) - - if response.Diagnostics.HasError() { - return - } - - // Default values to environment variables, but override - // with Terraform configuration value if set. - - apiToken := os.Getenv(EnvAPIToken) - network := os.Getenv(EnvNetwork) - url := withDefault(os.Getenv(EnvURL), DefaultURL) - httpTimeout := mustGetInt(withDefault(os.Getenv(EnvHTTPTimeout), DefaultHTTPTimeout)) - httpMaxRetry := mustGetInt(withDefault(os.Getenv(EnvHTTPMaxRetry), DefaultHTTPMaxRetry)) - - apiToken = overrideStrWithConfig(config.APIToken, apiToken) - network = overrideStrWithConfig(config.Network, network) - url = overrideStrWithConfig(config.URL, url) - httpTimeout = overrideIntWithConfig(config.HTTPTimeout, httpTimeout) - httpMaxRetry = overrideIntWithConfig(config.HTTPMaxRetry, httpMaxRetry) - - if network == "" { - response.Diagnostics.AddAttributeError( - path.Root(attr.Network), - fmt.Sprintf("Missing Twingate %s", attr.Network), - fmt.Sprintf("The provider cannot create the Twingate API client as there is a missing or empty value for the Twingate %s. "+ - "Set the %s value in the configuration or use the %s environment variable. "+ - "If either is already set, ensure the value is not empty.", attr.Network, attr.Network, EnvNetwork), - ) - - return - } - - client := client.NewClient(url, - apiToken, - network, - time.Duration(httpTimeout)*time.Second, - httpMaxRetry, - t.version) - - response.DataSourceData = client - response.ResourceData = client -} - -func mustGetInt(str string) int { - if val, err := strconv.Atoi(str); err == nil { - return val - } - - return 0 -} - -func overrideStrWithConfig(cfg types.String, defaultValue string) string { - if !cfg.IsNull() { - return cfg.ValueString() - } - - return defaultValue -} - -func overrideIntWithConfig(cfg types.Int64, defaultValue int) int { - if !cfg.IsNull() { - return int(cfg.ValueInt64()) - } - - return defaultValue -} - -func withDefault[T comparable](val, defaultVal T) T { - var zeroValue T - if val == zeroValue { - return defaultVal - } - - return val -} - -func (t Twingate) DataSources(ctx context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - twingateDatasource.NewConnectorDatasource, - twingateDatasource.NewConnectorsDatasource, - twingateDatasource.NewGroupDatasource, - twingateDatasource.NewGroupsDatasource, - twingateDatasource.NewRemoteNetworkDatasource, - twingateDatasource.NewRemoteNetworksDatasource, - twingateDatasource.NewServiceAccountsDatasource, - twingateDatasource.NewUserDatasource, - twingateDatasource.NewUsersDatasource, - twingateDatasource.NewSecurityPolicyDatasource, - twingateDatasource.NewSecurityPoliciesDatasource, - twingateDatasource.NewResourceDatasource, - twingateDatasource.NewResourcesDatasource, - } -} - -func (t Twingate) Resources(ctx context.Context) []func() resource.Resource { - return []func() resource.Resource{ - twingateResource.NewConnectorTokensResource, - twingateResource.NewConnectorResource, - twingateResource.NewGroupResource, - twingateResource.NewRemoteNetworkResource, - twingateResource.NewServiceAccountResource, - twingateResource.NewServiceKeyResource, - twingateResource.NewUserResource, - } -}