diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bff49ac4..2c4684f2 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,7 @@ 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 +126,8 @@ jobs:
fail-fast: false
matrix:
terraform:
- - '1.3.*'
+ - '1.4.*'
+ - '1.5.*'
- 'latest'
steps:
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..16c07f10 100644
--- a/docs/data-sources/users.md
+++ b/docs/data-sources/users.md
@@ -32,7 +32,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/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/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..9e8a442d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,16 +5,14 @@ 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.17.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.20.0
+ github.com/hashicorp/terraform-plugin-testing v1.6.0
+ github.com/hasura/go-graphql-client v0.10.2
github.com/iancoleman/strcase v0.3.0
github.com/jarcoal/httpmock v1.3.1
github.com/mattn/goveralls v0.0.12
@@ -24,48 +22,50 @@ require (
)
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/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.5.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.20.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.17.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/mod v0.14.0 // indirect
+ golang.org/x/net v0.19.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/sys v0.16.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ golang.org/x/text v0.14.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/appengine v1.6.8 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect
+ google.golang.org/grpc v1.60.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
- gopkg.in/yaml.v2 v2.4.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..7d0ea59f 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=
@@ -32,8 +30,11 @@ github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV
github.com/ccojocar/zxcvbn-go v1.0.1/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-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.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-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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.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.20.0 h1:cJcvn4gIOTi0SD7pIy+xiofV1zFA3hza+6K+fo52IX8=
+github.com/hashicorp/terraform-json v0.20.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
+github.com/hashicorp/terraform-plugin-docs v0.17.0 h1:H1Yc+bgB//Geau5g7YKkhG5v9tghI3vplfIYTOl85Uw=
+github.com/hashicorp/terraform-plugin-docs v0.17.0/go.mod h1:cKC8GSLE+0a0bi7LtlpXgrqnlRDCGoGDn15PTEA+Ang=
+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.20.0 h1:oqvoUlL+2EUbKNsJbIt3zqqZ7wi6lzn4ufkn/UA51xQ=
+github.com/hashicorp/terraform-plugin-go v0.20.0/go.mod h1:Rr8LBdMlY53a3Z/HpP+ZU3/xCDqtKNCkeI9qOyT10QE=
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.10.2 h1:+/v5/gWYgWr0cpd0aCi4GNtiy8uAIxnkADHTKDM+boY=
+github.com/hasura/go-graphql-client v0.10.2/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,10 +201,6 @@ 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=
@@ -246,11 +211,11 @@ 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=
@@ -260,30 +225,24 @@ github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX
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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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,8 +288,8 @@ 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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
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=
@@ -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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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=
@@ -397,12 +358,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
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-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
+google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
+google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
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=
@@ -413,16 +374,13 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
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/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..53975ca7 100644
--- a/twingate/internal/attr/user.go
+++ b/twingate/internal/attr/user.go
@@ -4,7 +4,6 @@ const (
FirstName = "first_name"
LastName = "last_name"
Email = "email"
- IsAdmin = "is_admin"
Role = "role"
Users = "users"
SendInvite = "send_invite"
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/resource.go b/twingate/internal/client/resource.go
index 8518c30e..f2f074c4 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
}
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/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/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/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..3b9aa08d 100644
--- a/twingate/internal/provider/datasource/users.go
+++ b/twingate/internal/provider/datasource/users.go
@@ -81,11 +81,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),
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..5c65140e 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, types.SetNull(types.StringType), types.SetNull(types.StringType))
+
+ 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,721 @@ 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
+ }
+ }
+
+ if !state.Access.IsNull() {
+ access, diags := convertAccessBlockToTerraform(ctx, resource,
+ state.Access.Elements()[0].(types.Object).Attributes()[attr.GroupIDs],
+ state.Access.Elements()[0].(types.Object).Attributes()[attr.ServiceAccountIDs])
+
+ 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, stateGroupIDs, stateServiceAccounts tfattr.Value) (types.List, diag.Diagnostics) {
+ var diagnostics, diags diag.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: stateGroupIDs,
+ attr.ServiceAccountIDs: stateServiceAccounts,
+ }
+
+ if !groupIDs.IsNull() {
+ attributes[attr.GroupIDs] = groupIDs
+ }
+
+ if !serviceAccountIDs.IsNull() {
+ attributes[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/groups_test.go b/twingate/internal/test/acctests/datasource/groups_test.go
index 1a1bd26c..8b18e59a 100644
--- a/twingate/internal/test/acctests/datasource/groups_test.go
+++ b/twingate/internal/test/acctests/datasource/groups_test.go
@@ -179,21 +179,31 @@ func TestAccDatasourceTwingateGroups_WithEmptyFilters(t *testing.T) {
},
Steps: []resource.TestStep{
{
- Config: testTwingateGroupsWithEmptyFilter(),
+ 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) {
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/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..461e7d14 100644
--- a/twingate/internal/test/acctests/datasource/resources_test.go
+++ b/twingate/internal/test/acctests/datasource/resources_test.go
@@ -47,13 +47,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 +64,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 = []
}
diff --git a/twingate/internal/test/acctests/datasource/service-accounts_test.go b/twingate/internal/test/acctests/datasource/service-accounts_test.go
index 092e8d0a..8d9d2bb0 100644
--- a/twingate/internal/test/acctests/datasource/service-accounts_test.go
+++ b/twingate/internal/test/acctests/datasource/service-accounts_test.go
@@ -88,6 +88,7 @@ func TestAccDatasourceTwingateServicesAll(t *testing.T) {
Check: acctests.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(theDatasource, attr.ID, "all-services"),
),
+ ExpectNonEmptyPlan: true,
},
{
Config: filterDatasourceServices(prefix, config),
diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go
index 799a63e7..451ee51d 100644
--- a/twingate/internal/test/acctests/helper.go
+++ b/twingate/internal/test/acctests/helper.go
@@ -17,13 +17,12 @@ import (
"github.com/Twingate/terraform-provider-twingate/twingate/internal/model"
"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 (
@@ -70,26 +69,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.
@@ -357,6 +337,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 = fmt.Errorf("invalid path: the path value cannot be asserted as string") //nolint:goerr113
+
+ 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 {
@@ -627,6 +667,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))
diff --git a/twingate/internal/test/acctests/resource/group_test.go b/twingate/internal/test/acctests/resource/group_test.go
index 398dcea7..6642b601 100644
--- a/twingate/internal/test/acctests/resource/group_test.go
+++ b/twingate/internal/test/acctests/resource/group_test.go
@@ -12,6 +12,7 @@ import (
"github.com/Twingate/terraform-provider-twingate/twingate/internal/test/acctests"
"github.com/Twingate/terraform-provider-twingate/twingate/internal/utils"
sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/plancheck"
)
const attrTerraformResource = "terraform_resource"
@@ -227,9 +228,14 @@ func TestAccTwingateGroupDeleteNonExisting(t *testing.T) {
{
Config: configBuilder(group),
Destroy: true,
- Check: acctests.ComposeTestCheckFunc(
- acctests.CheckTwingateResourceDoesNotExists(group.TerraformResource()),
- ),
+ },
+ {
+ Config: configBuilder(group),
+ ConfigPlanChecks: sdk.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction(group.TerraformResource(), plancheck.ResourceActionCreate),
+ },
+ },
},
},
})
diff --git a/twingate/internal/test/acctests/resource/remote-network_test.go b/twingate/internal/test/acctests/resource/remote-network_test.go
index aa5e2a7c..f6270ffa 100644
--- a/twingate/internal/test/acctests/resource/remote-network_test.go
+++ b/twingate/internal/test/acctests/resource/remote-network_test.go
@@ -9,6 +9,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 TestAccTwingateRemoteNetworkCreate(t *testing.T) {
@@ -108,9 +109,14 @@ func TestAccTwingateRemoteNetworkDeleteNonExisting(t *testing.T) {
{
Config: configRemoteNetwork(networkResource, test.RandomName()),
Destroy: true,
- Check: acctests.ComposeTestCheckFunc(
- acctests.CheckTwingateResourceDoesNotExists(theResource),
- ),
+ },
+ {
+ Config: configRemoteNetwork(networkResource, test.RandomName()),
+ ConfigPlanChecks: sdk.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction(theResource, plancheck.ResourceActionCreate),
+ },
+ },
},
},
})
diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go
index 17758c70..9a529c93 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)
)
@@ -117,15 +118,15 @@ func configResourceWithSimpleProtocols(terraformResource, networkName, name stri
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"
- }
- }
+ protocols = {
+ allow_icmp = true
+ tcp = {
+ policy = "DENY_ALL"
+ }
+ udp = {
+ policy = "DENY_ALL"
+ }
+ }
}
`,
map[string]any{
@@ -184,13 +185,13 @@ func configResourceWithProtocolsAndGroups(terraformResource, networkName, groupN
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 = ["80", "82-83"]
}
- udp {
+ udp = {
policy = "${udp_policy}"
}
}
@@ -269,13 +270,13 @@ func configCompleteResource(terraformResource, networkName, groupName, resourceN
address = "acc-test.com"
remote_network_id = twingate_remote_network.${network_resource}.id
- protocols {
+ protocols = {
allow_icmp = true
- tcp {
+ tcp = {
policy = "${tcp_policy}"
ports = ["3306"]
}
- udp {
+ udp = {
policy = "${udp_policy}"
}
}
@@ -380,13 +381,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}"
}
}
@@ -432,6 +433,36 @@ func TestAccTwingateResourceWithUdpDenyAllPolicy(t *testing.T) {
})
}
+func createResourceWithUdpDenyAllPolicy(networkName, groupName, resourceName string) string {
+ return fmt.Sprintf(`
+ resource "twingate_remote_network" "test6" {
+ name = "%s"
+ }
+
+ resource "twingate_group" "g6" {
+ name = "%s"
+ }
+
+ resource "twingate_resource" "test6" {
+ name = "%s"
+ address = "acc-test.com"
+ remote_network_id = twingate_remote_network.test6.id
+ access {
+ group_ids = [twingate_group.g6.id]
+ }
+ protocols = {
+ allow_icmp = true
+ tcp = {
+ policy = "%s"
+ }
+ udp = {
+ policy = "%s"
+ }
+ }
+ }
+ `, networkName, groupName, resourceName, model.PolicyAllowAll, model.PolicyDenyAll)
+}
+
func TestAccTwingateResourceWithDenyAllPolicyAndEmptyPortsList(t *testing.T) {
t.Parallel()
@@ -477,13 +508,13 @@ func configResourceWithPolicyAndEmptyTCPPortsList(terraformResource, networkName
access {
group_ids = [twingate_group.${group_resource}.id]
}
- protocols {
+ protocols = {
allow_icmp = true
- tcp {
+ tcp = {
policy = "${tcp_policy}"
ports = []
}
- udp {
+ udp = {
policy = "${udp_policy}"
}
}
@@ -553,6 +584,30 @@ func TestAccTwingateResourceWithInvalidPortRange(t *testing.T) {
})
}
+func createResourceWithRestrictedPolicyAndPortRange(networkName, resourceName, portRange string) string {
+ return fmt.Sprintf(`
+ resource "twingate_remote_network" "test8" {
+ name = "%s"
+ }
+
+ resource "twingate_resource" "test8" {
+ name = "%s"
+ address = "new-acc-test.com"
+ remote_network_id = twingate_remote_network.test8.id
+ protocols = {
+ allow_icmp = true
+ tcp = {
+ policy = "%s"
+ ports = [%s]
+ }
+ udp = {
+ policy = "%s"
+ }
+ }
+ }
+ `, networkName, resourceName, model.PolicyRestricted, portRange, model.PolicyAllowAll)
+}
+
func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) {
t.Parallel()
@@ -607,13 +662,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}]
}
@@ -704,8 +759,8 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) {
Config: configResourceWithPortRange(terraformResource, remoteNetworkName, resourceName, `"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
@@ -755,6 +810,8 @@ func TestAccTwingateResourceSetActiveStateOnUpdate(t *testing.T) {
acctests.WaitTestFunc(),
acctests.CheckTwingateResourceActiveState(theResource, false),
),
+ // provider noticed drift and tried to change it to true
+ ExpectNonEmptyPlan: true,
},
{
Config: configResourceBasic(terraformResource, remoteNetworkName, resourceName),
@@ -824,12 +881,48 @@ func TestAccTwingateResourceImport(t *testing.T) {
tcpPolicy: model.PolicyRestricted,
tcpPortsLen: "2",
firstTCPPort: "80",
+ udpPolicy: model.PolicyRestricted,
}),
},
},
})
}
+func createResource12(networkName, groupName1, groupName2, resourceName string) string {
+ return fmt.Sprintf(`
+ resource "twingate_remote_network" "test12" {
+ name = "%s"
+ }
+
+ resource "twingate_group" "g121" {
+ name = "%s"
+ }
+
+ resource "twingate_group" "g122" {
+ name = "%s"
+ }
+
+ resource "twingate_resource" "test12" {
+ name = "%s"
+ address = "acc-test.com.12"
+ remote_network_id = twingate_remote_network.test12.id
+ access {
+ group_ids = [twingate_group.g121.id, twingate_group.g122.id]
+ }
+ protocols = {
+ allow_icmp = true
+ tcp = {
+ policy = "%s"
+ ports = ["80", "82-83"]
+ }
+ udp = {
+ policy = "%s"
+ }
+ }
+ }
+ `, networkName, groupName1, groupName2, resourceName, model.PolicyRestricted, model.PolicyAllowAll)
+}
+
func genNewGroups(resourcePrefix string, count int) ([]string, []string) {
groups := make([]string, 0, count)
groupsID := make([]string, 0, count)
@@ -901,13 +994,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}"
}
}
@@ -970,13 +1063,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 +1188,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 +1297,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 +1344,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 +1362,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 +1404,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 +1454,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 +1470,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 +1589,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 +1698,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 +1773,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 +1799,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 +1859,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 +1994,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 +2059,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 +2108,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 +2184,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 +2255,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 +2348,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 +2408,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 +2699,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 +2759,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 +2794,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 +2872,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..5b126566 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 {
@@ -53,7 +54,7 @@ func TestAccTwingateServiceAccountCreateUpdate(t *testing.T) {
})
}
-func TestAccTwingateServiceAccountDeleteNonExisting(t *testing.T) {
+func TestAccTwingateServiceAccountDelete(t *testing.T) {
t.Parallel()
resourceName := test.RandomServiceAccountName()
@@ -68,9 +69,14 @@ func TestAccTwingateServiceAccountDeleteNonExisting(t *testing.T) {
{
Config: configServiceAccount(resourceName, name),
Destroy: true,
- Check: acctests.ComposeTestCheckFunc(
- acctests.CheckTwingateResourceDoesNotExists(theResource),
- ),
+ },
+ {
+ Config: configServiceAccount(resourceName, name),
+ ConfigPlanChecks: sdk.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction(theResource, plancheck.ResourceActionCreate),
+ },
+ },
},
},
})
diff --git a/twingate/internal/test/acctests/resource/service-key_test.go b/twingate/internal/test/acctests/resource/service-key_test.go
index d9b1cef4..f8d58aee 100644
--- a/twingate/internal/test/acctests/resource/service-key_test.go
+++ b/twingate/internal/test/acctests/resource/service-key_test.go
@@ -11,6 +11,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 ErrEmptyValue = errors.New("empty value")
@@ -210,9 +211,14 @@ func TestAccTwingateServiceKeyDelete(t *testing.T) {
{
Config: configServiceKey(terraformResourceName, serviceAccountName),
Destroy: true,
- Check: acctests.ComposeTestCheckFunc(
- acctests.CheckTwingateResourceDoesNotExists(serviceKey),
- ),
+ },
+ {
+ Config: configServiceKey(terraformResourceName, serviceAccountName),
+ ConfigPlanChecks: sdk.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction(serviceKey, plancheck.ResourceActionCreate),
+ },
+ },
},
},
})
diff --git a/twingate/internal/test/acctests/resource/user_test.go b/twingate/internal/test/acctests/resource/user_test.go
index 3ed5d949..472dfaaa 100644
--- a/twingate/internal/test/acctests/resource/user_test.go
+++ b/twingate/internal/test/acctests/resource/user_test.go
@@ -12,6 +12,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"
)
type User struct {
@@ -270,9 +271,14 @@ func TestAccTwingateUserDelete(t *testing.T) {
{
Config: configBuilder(user),
Destroy: true,
- Check: acctests.ComposeTestCheckFunc(
- acctests.CheckTwingateResourceDoesNotExists(user.TerraformResource()),
- ),
+ },
+ {
+ Config: configBuilder(user),
+ ConfigPlanChecks: sdk.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction(user.TerraformResource(), plancheck.ResourceActionCreate),
+ },
+ },
},
},
})
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/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/provider.go b/twingate/provider.go
index a21e4c78..b8478ce3 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),
+ 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
+
+ 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,
- }
-}