diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 6d6a32e4..7c5a03e1 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -64,12 +64,12 @@ resource "twingate_resource" "resource" { ### Optional -- `access` (Block List) Restrict access to certain groups or service accounts (see [below for nested schema](#nestedblock--access)) +- `access` (Block List, Max: 1) 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_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) 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)) +- `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)) ### Read-Only @@ -87,11 +87,14 @@ Optional: ### 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` (Block List) (see [below for nested schema](#nestedblock--protocols--tcp)) -- `udp` (Block List) (see [below for nested schema](#nestedblock--protocols--udp)) ### Nested Schema for `protocols.tcp` @@ -102,7 +105,7 @@ Required: Optional: -- `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 +- `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 @@ -114,7 +117,7 @@ Required: Optional: -- `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 +- `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 ## Import diff --git a/go.mod b/go.mod index 1f852d10..15f95c6b 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,11 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.4 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/terraform-plugin-docs v0.16.0 - github.com/hashicorp/terraform-plugin-framework v1.3.5 + github.com/hashicorp/terraform-plugin-framework v1.4.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 - github.com/hashicorp/terraform-plugin-go v0.18.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/iancoleman/strcase v0.3.0 @@ -25,9 +27,9 @@ require ( 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-20230217124315-7d5c6f04bbb8 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/agext/levenshtein v1.2.2 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // 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 @@ -47,20 +49,19 @@ require ( 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.4.10 // indirect + github.com/hashicorp/go-plugin v1.5.1 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hc-install v0.5.2 // indirect - github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/hashicorp/hc-install v0.6.0 // indirect + github.com/hashicorp/hcl/v2 v2.18.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.18.1 // indirect + github.com/hashicorp/terraform-exec v0.19.0 // indirect github.com/hashicorp/terraform-json v0.17.1 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.1 // indirect + github.com/hashicorp/terraform-registry-address v0.2.2 // 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/imdario/mergo v0.3.13 // 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 @@ -80,19 +81,19 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.5 // 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.13.3 // indirect - golang.org/x/crypto v0.12.0 // indirect + github.com/zclconf/go-cty v1.14.0 // indirect + golang.org/x/crypto v0.13.0 // indirect golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/term v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect + google.golang.org/grpc v1.57.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index fd9adf22..00c155d7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 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= @@ -5,15 +6,15 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 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/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +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/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/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +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= @@ -21,12 +22,12 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitfield/gotestdox v0.2.1 h1:Zj8IMLAO5/oiAKoMmtN96eyFiPZraJRTH2p0zDgtxc0= github.com/bitfield/gotestdox v0.2.1/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4= github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -46,9 +47,9 @@ 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.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= +github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 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= @@ -65,6 +66,7 @@ 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/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= @@ -107,8 +109,8 @@ 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.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= -github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= +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-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -116,32 +118,34 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C 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.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0= -github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= -github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +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/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.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= -github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980= +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.3.5 h1:FJ6s3CVWVAxlhiF/jhy6hzs4AnPHiflsp9KgzTGl1wo= -github.com/hashicorp/terraform-plugin-framework v1.3.5/go.mod h1:2gGDpWiTI0irr9NSTLFAKlTi6KwGti3AoU19rFqU30o= +github.com/hashicorp/terraform-plugin-framework v1.4.0 h1:WKbtCRtNrjsh10eA7NZvC/Qyr7zp77j+D21aDO5th9c= +github.com/hashicorp/terraform-plugin-framework v1.4.0/go.mod h1:XC0hPcQbBvlbxwmjxuV/8sn8SbZRg4XwGMs22f+kqV0= 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.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE= -github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= +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-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-sdk/v2 v2.28.0 h1:gY4SG34ANc6ZSeWEKC9hDTChY0ZiN+Myon17fSA0Xgc= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0/go.mod h1:deXEw/iJXtJxNV9d1c/OVJrvL7Zh0a++v7rzokW6wVY= +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.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= -github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= +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-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= @@ -154,12 +158,12 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq 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= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 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= @@ -227,7 +231,7 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 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.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= 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= @@ -257,17 +261,19 @@ 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.13.3 h1:m+b9q3YDbg6Bec5rr+KGy1MzEVzY/jC2X+YX4yqKtHI= -github.com/zclconf/go-cty v1.13.3/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +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= 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.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.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 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= @@ -283,7 +289,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL 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= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +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= @@ -306,36 +314,41 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +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.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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.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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.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/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= @@ -353,10 +366,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T 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 v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= -google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +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.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= 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= @@ -371,7 +384,6 @@ 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.0/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= diff --git a/main.go b/main.go index e2805d77..561a0aaa 100644 --- a/main.go +++ b/main.go @@ -6,27 +6,52 @@ 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() - err := providerserver.Serve(context.Background(), twingate.New(version), - providerserver.ServeOpts{ - Debug: debug, - Address: registry, - ProtocolVersion: 6, + 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 }, + 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/twingate/internal/provider/resource/converter_test.go b/twingate/internal/provider/resource/converter_test.go index 188b0145..1bc533de 100644 --- a/twingate/internal/provider/resource/converter_test.go +++ b/twingate/internal/provider/resource/converter_test.go @@ -1,39 +1,43 @@ package resource import ( - "context" "errors" "fmt" "testing" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" - 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 types.List + input []interface{} expected *model.Protocol expectedErr error }{ {}, { - input: makeObjectsListMust(types.ObjectValueMust(protocolAttributeTypes(), map[string]tfattr.Value{ - attr.Policy: types.StringValue(model.PolicyAllowAll), - attr.Ports: makeTestSet("-"), - })), + input: []interface{}{ + map[string]interface{}{ + attr.Policy: model.PolicyAllowAll, + attr.Ports: []interface{}{ + "-", + }, + }, + }, expectedErr: errors.New("failed to parse protocols port range"), }, { - input: makeObjectsListMust(types.ObjectValueMust(protocolAttributeTypes(), map[string]tfattr.Value{ - attr.Policy: types.StringValue(model.PolicyRestricted), - attr.Ports: makeTestSet("80-88"), - })), + input: []interface{}{ + map[string]interface{}{ + attr.Policy: model.PolicyRestricted, + attr.Ports: []interface{}{ + "80-88", + }, + }, + }, expected: &model.Protocol{ Policy: model.PolicyRestricted, Ports: []*model.PortRange{ @@ -58,17 +62,6 @@ func TestConvertProtocol(t *testing.T) { } -func makeObjectsListMust(objects ...types.Object) types.List { - obj := objects[0] - - items := utils.Map(objects, func(item types.Object) tfattr.Value { - return tfattr.Value(item) - }) - - return types.ListValueMust(obj.Type(context.Background()), items) - -} - func TestConvertPortsRangeToMap(t *testing.T) { cases := []struct { portsRange []*model.PortRange @@ -132,49 +125,40 @@ 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 types.Set - inputB types.Set + inputA []interface{} + inputB []interface{} expected bool }{ { - inputA: makeTestSet(""), - inputB: makeTestSet(""), + inputA: []interface{}{""}, + inputB: []interface{}{""}, expected: false, }, { - inputA: makeTestSet("80"), - inputB: makeTestSet(""), + inputA: []interface{}{"80"}, + inputB: []interface{}{""}, expected: false, }, { - inputA: makeTestSet("80"), - inputB: makeTestSet("90"), + inputA: []interface{}{"80"}, + inputB: []interface{}{"90"}, expected: false, }, { - inputA: makeTestSet("80"), - inputB: makeTestSet("80"), + inputA: []interface{}{"80"}, + inputB: []interface{}{"80"}, expected: true, }, { - inputA: makeTestSet("80-81"), - inputB: makeTestSet("80", "81"), + inputA: []interface{}{"80-81"}, + inputB: []interface{}{"80", "81"}, expected: true, }, { - inputA: makeTestSet("80-81", "70"), - inputB: makeTestSet("70", "80", "81"), + inputA: []interface{}{"80-81", "70"}, + inputB: []interface{}{"70", "80", "81"}, expected: true, }, } diff --git a/twingate/internal/provider/resource/helper.go b/twingate/internal/provider/resource/helper.go index 3a18a2e1..feb14fe5 100644 --- a/twingate/internal/provider/resource/helper.go +++ b/twingate/internal/provider/resource/helper.go @@ -5,8 +5,27 @@ import ( "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" ) +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}. diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 798ad35b..7555031d 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -4,608 +4,272 @@ import ( "context" "errors" "fmt" + "log" "reflect" + "strings" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/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/planmodifier" - "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" + "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" ) var ( 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") ) -// 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.List `tfsdk:"protocols"` - Access types.List `tfsdk:"access"` - IsVisible types.Bool `tfsdk:"is_visible"` - IsBrowserShortcutEnabled types.Bool `tfsdk:"is_browser_shortcut_enabled"` - Alias types.String `tfsdk:"alias"` -} - -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 +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, + }, + }, } - 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 + 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, + }, + }, } - if res.Protocols != nil { - protocols, diags := convertProtocolsToTerraformOnImport(ctx, res.Protocols) - resp.Diagnostics.Append(diags...) - - if resp.Diagnostics.HasError() { - return - } - - resp.State.SetAttribute(ctx, path.Root(attr.Protocols), protocols) + 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 len(res.Groups) > 0 || len(res.ServiceAccounts) > 0 { - access, diags := convertAccessBlockToTerraformOnImport(ctx, res) - - resp.Diagnostics.Append(diags...) + 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 resp.Diagnostics.HasError() { - return - } - - resp.State.SetAttribute(ctx, path.Root(attr.Access), access) - } -} - -func (r *twingateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Resources in Twingate represent servers on the private network that clients can connect to. Resources can be defined by IP, CIDR range, FQDN, or DNS zone. For more information, see the Twingate [documentation](https://docs.twingate.com/docs/resources-and-access-nodes).", - Attributes: map[string]schema.Attribute{ - attr.Name: schema.StringAttribute{ + Schema: map[string]*schema.Schema{ + // required + attr.Name: { + Type: schema.TypeString, Required: true, Description: "The name of the Resource", }, - attr.Address: schema.StringAttribute{ + attr.Address: { + Type: schema.TypeString, Required: true, Description: "The Resource's IP/CIDR or FQDN/DNS zone", }, - attr.RemoteNetworkID: schema.StringAttribute{ + attr.RemoteNetworkID: { + Type: schema.TypeString, Required: true, Description: "Remote Network ID where the Resource lives", }, // optional - attr.IsAuthoritative: schema.BoolAttribute{ + attr.IsAuthoritative: { + Type: schema.TypeBool, 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{ - UseStateForUnknownBool(), - }, }, - - attr.Alias: schema.StringAttribute{ + 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.Access: { + Type: schema.TypeList, Optional: true, - Description: "Set a DNS alias address for the Resource. Must be a DNS-valid name string.", + MaxItems: 1, + Description: "Restrict access to certain groups or service accounts", + Elem: accessSchema, }, - // computed - 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.", - PlanModifiers: []planmodifier.Bool{UseStateForUnknownBool()}, - }, - 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.`, - PlanModifiers: []planmodifier.Bool{UseStateForUnknownBool()}, - }, - - attr.ID: schema.StringAttribute{ + attr.IsVisible: { + Type: schema.TypeBool, + Optional: true, Computed: true, - Description: "Autogenerated ID of the Resource, encoded in base64", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, + Description: "Controls whether this Resource will be visible in the main Resource list in the Twingate Client.", }, - }, - - Blocks: map[string]schema.Block{ - attr.Access: accessBlock(), - attr.Protocols: protocolsBlock(), - }, - } -} - -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, - ElementType: types.StringType, - Description: "List of Group IDs that will have permission to access the Resource.", - Validators: []validator.Set{ - setvalidator.SizeAtLeast(1), - }, - }, - attr.ServiceAccountIDs: schema.SetAttribute{ - Optional: true, - ElementType: types.StringType, - Description: "List of Service Account IDs that will have permission to access the Resource.", - Validators: []validator.Set{ - setvalidator.SizeAtLeast(1), - }, - }, + attr.IsBrowserShortcutEnabled: { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: `Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client.`, }, - }, - } -} - -func protocolsBlock() schema.ListNestedBlock { - return schema.ListNestedBlock{ - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - attr.AllowIcmp: schema.BoolAttribute{ - Computed: true, - Optional: true, - Description: "Whether to allow ICMP (ping) traffic", - }, + 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, }, - Blocks: map[string]schema.Block{ - attr.TCP: protocolSchema(), - attr.UDP: protocolSchema(), + attr.ID: { + Type: schema.TypeString, + Computed: true, + Description: "Autogenerated ID of the Resource, encoded in base64", }, }, - 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.", - } -} - -func protocolSchema() schema.ListNestedBlock { - return schema.ListNestedBlock{ - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - attr.Policy: schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(model.Policies...), - }, - 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(), - }, - }, - }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, }, } } -func (r *twingateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan resourceModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - - if resp.Diagnostics.HasError() { - return - } +func resourceCreate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*client.Client) - input, err := convertResource(&plan) + resource, err := convertResource(resourceData) if err != nil { - addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) - - return + return diag.FromErr(err) } - resource, err := r.client.CreateResource(ctx, input) + resource, err = client.CreateResource(ctx, resource) if err != nil { - addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) - - return - } - - if err = r.client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { - addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) - - return - } - - r.helper(ctx, resource, &plan, &plan, &resp.State, &resp.Diagnostics, err, operationCreate) -} - -func getAccessAttribute(list types.List, attribute string) []string { - if list.IsNull() || list.IsUnknown() || len(list.Elements()) == 0 { - return nil + return diag.FromErr(err) } - obj := list.Elements()[0].(types.Object) - if obj.IsNull() || obj.IsUnknown() { - return nil + if err = client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { + return diag.FromErr(err) } - val := obj.Attributes()[attribute] - if val == nil || val.IsNull() || val.IsUnknown() { - return nil - } + log.Printf("[INFO] Created resource %s", resource.Name) - return convertIDs(val.(types.Set)) + return resourceResourceReadHelper(ctx, client, resourceData, resource, nil) } -func convertResource(plan *resourceModel) (*model.Resource, error) { - protocols, err := convertProtocols(&plan.Protocols) - if err != nil { - return nil, err - } - - groupIDs := getAccessAttribute(plan.Access, attr.GroupIDs) - serviceAccountIDs := getAccessAttribute(plan.Access, attr.ServiceAccountIDs) - - if !plan.Access.IsNull() && groupIDs == nil && serviceAccountIDs == nil { - return nil, ErrInvalidAttributeCombination - } - - var isVisible, isBrowserShortcutEnabled *bool - if !plan.IsVisible.IsUnknown() { - isVisible = plan.IsVisible.ValueBoolPointer() - } - - if !plan.IsBrowserShortcutEnabled.IsUnknown() { - isBrowserShortcutEnabled = plan.IsBrowserShortcutEnabled.ValueBoolPointer() - } - - var alias *string - if !plan.Alias.IsUnknown() && !plan.Alias.IsNull() { - alias = plan.Alias.ValueStringPointer() - } +func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*client.Client) - return &model.Resource{ - Name: plan.Name.ValueString(), - RemoteNetworkID: plan.RemoteNetworkID.ValueString(), - Address: plan.Address.ValueString(), - Protocols: protocols, - Groups: groupIDs, - ServiceAccounts: serviceAccountIDs, - IsAuthoritative: convertAuthoritativeFlag(plan.IsAuthoritative), - Alias: alias, - IsVisible: isVisible, - IsBrowserShortcutEnabled: isBrowserShortcutEnabled, - }, nil -} - -func convertIDs(list types.Set) []string { - return utils.Map(list.Elements(), func(item tfattr.Value) string { - return item.(types.String).ValueString() - }) -} - -func convertProtocols(protocols *types.List) (*model.Protocols, error) { - if protocols == nil || protocols.IsNull() || len(protocols.Elements()) == 0 { - return model.DefaultProtocols(), nil - } - - udp, err := convertProtocol(protocols.Elements()[0].(types.Object).Attributes()[attr.UDP]) + resource, err := convertResource(resourceData) if err != nil { - return nil, err - } - - tcp, err := convertProtocol(protocols.Elements()[0].(types.Object).Attributes()[attr.TCP]) - if err != nil { - return nil, err - } - - return &model.Protocols{ - AllowIcmp: protocols.Elements()[0].(types.Object).Attributes()[attr.AllowIcmp].(types.Bool).ValueBool(), - UDP: udp, - TCP: tcp, - }, nil -} - -func convertProtocol(listVal tfattr.Value) (*model.Protocol, error) { - obj := convertProtocolObj(listVal) - if obj.IsNull() { - return nil, nil //nolint:nilnil - } - - ports, err := decodePorts(obj) - if err != nil { - return nil, err - } - - policy := obj.Attributes()[attr.Policy].(types.String).ValueString() - if err := isValidPolicy(policy, ports); err != nil { - return nil, err - } - - if policy == model.PolicyDenyAll { - policy = model.PolicyRestricted - } - - return model.NewProtocol(policy, ports), nil -} - -func convertProtocolObj(listVal tfattr.Value) types.Object { - if listVal == nil || listVal.IsNull() { - return types.ObjectNull(nil) - } - - list := listVal.(types.List) - - if list.IsNull() || len(list.Elements()) == 0 { - return types.ObjectNull(nil) - } - - protocol := list.Elements()[0] - - 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 isValidPolicy(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 diag.FromErr(err) } - return nil -} + resource.ID = resourceData.Id() -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 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 resourceData.HasChange(attr.Access) { + idsToDelete, idsToAdd, err := getChangedAccessIDs(ctx, resourceData, resource, client) if err != nil { - return nil, err //nolint:wrapcheck + return diag.FromErr(err) } - ports = append(ports, portRange) - } - - return ports, 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) - } - - 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 - } - - 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) + if err := client.RemoveResourceAccess(ctx, resource.ID, idsToDelete); err != nil { + return diag.FromErr(err) + } - return + if err = client.AddResourceAccess(ctx, resource.ID, idsToAdd); err != nil { + return diag.FromErr(err) } } - var resource *model.Resource - - if isResourceChanged(&plan, &state) { - resource, err = r.client.UpdateResource(ctx, input) + 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 = r.client.ReadResource(ctx, input.ID) + resource, err = client.ReadResource(ctx, resource.ID) } if resource != nil { - resource.IsAuthoritative = input.IsAuthoritative + resource.IsAuthoritative = convertAuthoritativeFlagLegacy(resourceData) } - r.helper(ctx, resource, &state, &plan, &resp.State, &resp.Diagnostics, err, operationUpdate) -} + log.Printf("[INFO] Updated resource %s", resource.Name) -func isResourceChanged(plan, state *resourceModel) bool { - return !plan.RemoteNetworkID.Equal(state.RemoteNetworkID) || - !plan.Name.Equal(state.Name) || - !plan.Address.Equal(state.Address) || - !plan.Protocols.Equal(state.Protocols) || - !plan.IsVisible.Equal(state.IsVisible) || - !plan.IsBrowserShortcutEnabled.Equal(state.IsBrowserShortcutEnabled) || - !plan.Alias.Equal(state.Alias) + return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } -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) - } +func resourceRead(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*client.Client) - 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) + resource, err := client.ReadResource(ctx, resourceData.Id()) + if resource != nil { + resource.IsAuthoritative = convertAuthoritativeFlagLegacy(resourceData) } - return nil + return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } -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) - } +func resourceDelete(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + c := meta.(*client.Client) + resourceID := resourceData.Id() - 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) + err := c.DeleteResource(ctx, resourceID) + if err != nil { + return diag.FromErr(err) } - // 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) - } + log.Printf("[INFO] Deleted resource id %s", resourceData.Id()) 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) { +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 - respState.RemoveResource(ctx) + resourceData.SetId("") - return + return nil } - addErr(diagnostics, err, operation, TwingateResource) - - return + return diag.FromErr(err) } if resource.Protocols == nil { @@ -614,494 +278,346 @@ func (r *twingateResource) helper(ctx context.Context, resource *model.Resource, if !resource.IsActive { // fix set active state for the resource on `terraform apply` - err = r.client.UpdateResourceActiveState(ctx, &model.Resource{ + err = resourceClient.UpdateResourceActiveState(ctx, &model.Resource{ ID: resource.ID, IsActive: true, }) if err != nil { - addErr(diagnostics, err, operationUpdate, TwingateResource) - - return + return diag.FromErr(err) } } if !resource.IsAuthoritative { - resource.Groups = setIntersection(getAccessAttribute(reference.Access, attr.GroupIDs), resource.Groups) - resource.ServiceAccounts = setIntersection(getAccessAttribute(reference.Access, attr.ServiceAccountIDs), resource.ServiceAccounts) + groups, serviceAccounts := convertAccess(resourceData) + resource.ServiceAccounts = setIntersection(serviceAccounts, resource.ServiceAccounts) + resource.Groups = setIntersection(groups, resource.Groups) } - setState(ctx, state, reference, resource, diagnostics) - - if diagnostics.HasError() { - return - } + resourceData.SetId(resource.ID) - // Set refreshed state - diagnostics.Append(respState.Set(ctx, state)...) + return readDiagnostics(resourceData, resource) } -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.IsAuthoritative = types.BoolValue(resource.IsAuthoritative) - - if !state.IsVisible.IsNull() || !reference.IsVisible.IsUnknown() { - state.IsVisible = types.BoolPointerValue(resource.IsVisible) +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) } - if !state.IsBrowserShortcutEnabled.IsNull() || !reference.IsBrowserShortcutEnabled.IsUnknown() { - state.IsBrowserShortcutEnabled = types.BoolPointerValue(resource.IsBrowserShortcutEnabled) + if err := resourceData.Set(attr.RemoteNetworkID, resource.RemoteNetworkID); err != nil { + return ErrAttributeSet(err, attr.RemoteNetworkID) } - if !state.Alias.IsNull() || !reference.Alias.IsUnknown() { - state.Alias = reference.Alias + if err := resourceData.Set(attr.Address, resource.Address); err != nil { + return ErrAttributeSet(err, attr.Address) } - if !state.Protocols.IsNull() { - protocols, diags := convertProtocolsToTerraform(ctx, resource.Protocols, reference.Protocols) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return - } - - state.Protocols = protocols + if err := resourceData.Set(attr.IsAuthoritative, resource.IsAuthoritative); err != nil { + return ErrAttributeSet(err, attr.IsAuthoritative) } - 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 + if err := resourceData.Set(attr.Access, resource.AccessToTerraform()); err != nil { + return ErrAttributeSet(err, attr.Access) } -} - -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) + protocols, _ := convertProtocols(resourceData) - if len(resource.Groups) > 0 { - groupIDs, diags = makeSet(resource.Groups) - diagnostics.Append(diags...) + if portRangeEqual(protocols.TCP.Ports, resource.Protocols.TCP.Ports) { + resource.Protocols.TCP.Ports = protocols.TCP.Ports } - if len(resource.ServiceAccounts) > 0 { - serviceAccountIDs, diags = makeSet(resource.ServiceAccounts) - diagnostics.Append(diags...) + if portRangeEqual(protocols.UDP.Ports, resource.Protocols.UDP.Ports) { + resource.Protocols.UDP.Ports = protocols.UDP.Ports } - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + if err := resourceData.Set(attr.Protocols, resource.Protocols.ToTerraform()); err != nil { + return ErrAttributeSet(err, attr.Protocols) } - attributes := map[string]tfattr.Value{ - attr.GroupIDs: stateGroupIDs, - attr.ServiceAccountIDs: stateServiceAccounts, + if resource.IsVisible != nil { + if err := resourceData.Set(attr.IsVisible, *resource.IsVisible); err != nil { + return ErrAttributeSet(err, attr.IsVisible) + } } - if !groupIDs.IsNull() { - attributes[attr.GroupIDs] = groupIDs + if resource.IsBrowserShortcutEnabled != nil { + if err := resourceData.Set(attr.IsBrowserShortcutEnabled, *resource.IsBrowserShortcutEnabled); err != nil { + return ErrAttributeSet(err, attr.IsBrowserShortcutEnabled) + } } - if !serviceAccountIDs.IsNull() { - attributes[attr.ServiceAccountIDs] = serviceAccountIDs + var alias interface{} + if resource.Alias != nil { + alias = *resource.Alias } - obj, diags := types.ObjectValue(accessAttributeTypes(), attributes) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + if err := resourceData.Set(attr.Alias, alias); err != nil { + return ErrAttributeSet(err, attr.Alias) } - return makeObjectsList(ctx, obj) + return nil } -func convertAccessBlockToTerraformOnImport(ctx context.Context, resource *model.Resource) (types.List, diag.Diagnostics) { - var diagnostics, diags diag.Diagnostics - - groupIDs, serviceAccountIDs := types.SetNull(types.StringType), types.SetNull(types.StringType) +func aliasDiff(key, _, _ string, resourceData *schema.ResourceData) bool { + oldVal, newVal := castToStrings(resourceData.GetChange(key)) - 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...) - } + return oldVal == newVal +} - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics - } +func equalPorts(a, b interface{}) bool { + oldPorts, newPorts := a.([]interface{}), b.([]interface{}) - attributes := map[string]tfattr.Value{ - attr.GroupIDs: groupIDs, - attr.ServiceAccountIDs: serviceAccountIDs, + oldPortsRange, err := convertPorts(oldPorts) + if err != nil { + return false } - obj, diags := types.ObjectValue(accessAttributeTypes(), attributes) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + newPortsRange, err := convertPorts(newPorts) + if err != nil { + return false } - return makeObjectsList(ctx, obj) + return portRangeEqual(oldPortsRange, newPortsRange) } -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 portRangeEqual(portsA, portsB []*model.PortRange) bool { + mapA := convertPortsRangeToMap(portsA) + mapB := convertPortsRangeToMap(portsB) -func makeObjectsListNull(ctx context.Context, attributeTypes map[string]tfattr.Type) types.List { - return types.ListNull(types.ObjectNull(attributeTypes).Type(ctx)) + return reflect.DeepEqual(mapA, mapB) } -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 convertPortsRangeToMap(portsRange []*model.PortRange) map[int]struct{} { + out := make(map[int]struct{}) -func makeSet(list []string) (types.Set, diag.Diagnostics) { - return types.SetValue(types.StringType, stringsToTerraformValue(list)) -} + for _, port := range portsRange { + if port.Start == port.End { + out[port.Start] = struct{}{} -func stringsToTerraformValue(list []string) []tfattr.Value { - if len(list) == 0 { - return nil - } + continue + } - out := make([]tfattr.Value, 0, len(list)) - for _, item := range list { - out = append(out, types.StringValue(item)) + for i := port.Start; i <= port.End; i++ { + out[i] = struct{}{} + } } return out } -func convertProtocolsToTerraform(ctx context.Context, protocols *model.Protocols, reference types.List) (types.List, diag.Diagnostics) { - if protocols == nil { - return defaultProtocolsModelToTerraform(ctx) - } - - var diagnostics diag.Diagnostics - - tcp, diags := convertProtocolModelToTerraform(protocols.TCP, reference.Elements()[0].(types.Object).Attributes()[attr.TCP]) - diagnostics.Append(diags...) - - udp, diags := convertProtocolModelToTerraform(protocols.UDP, reference.Elements()[0].(types.Object).Attributes()[attr.UDP]) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return types.ListNull(types.ObjectNull(protocolsAttributeTypes()).Type(ctx)), diagnostics - } - - attributes := map[string]tfattr.Value{ - attr.AllowIcmp: types.BoolValue(protocols.AllowIcmp), - attr.TCP: types.ListValueMust(tcp.Type(ctx), []tfattr.Value{tcp}), - attr.UDP: types.ListValueMust(udp.Type(ctx), []tfattr.Value{udp}), - } - - obj := types.ObjectValueMust(protocolsAttributeTypes(), attributes) - list := []tfattr.Value{obj} - - return types.ListValue(obj.Type(ctx), list) -} - -func convertProtocolsToTerraformOnImport(ctx context.Context, protocols *model.Protocols) (types.List, diag.Diagnostics) { - if protocols == nil { - return defaultProtocolsModelToTerraform(ctx) +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), } - var diagnostics diag.Diagnostics - - tcp, diags := convertProtocolModelToTerraformOnImport(protocols.TCP) - diagnostics.Append(diags...) - - udp, diags := convertProtocolModelToTerraformOnImport(protocols.UDP) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return types.ListNull(types.ObjectNull(protocolsAttributeTypes()).Type(ctx)), diagnostics + if strings.HasSuffix(attribute, "#") && newValue == "0" { + return newValue == oldValue } - attributes := map[string]tfattr.Value{ - attr.AllowIcmp: types.BoolValue(protocols.AllowIcmp), - attr.TCP: types.ListValueMust(tcp.Type(ctx), []tfattr.Value{tcp}), - attr.UDP: types.ListValueMust(udp.Type(ctx), []tfattr.Value{udp}), + for _, key := range keys { + if strings.HasPrefix(attribute, key) { + return equalPorts(data.GetChange(key)) + } } - obj := types.ObjectValueMust(protocolsAttributeTypes(), attributes) - list := []tfattr.Value{obj} - - return types.ListValue(obj.Type(ctx), list) + return false } -func protocolsAttributeTypes() map[string]tfattr.Type { - return map[string]tfattr.Type{ - attr.AllowIcmp: types.BoolType, - attr.TCP: types.ListType{ - ElemType: types.ObjectType{ - AttrTypes: protocolAttributeTypes(), - }, - }, - attr.UDP: types.ListType{ - ElemType: types.ObjectType{ - AttrTypes: protocolAttributeTypes(), - }, - }, +// 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 == "" } -} -func protocolAttributeTypes() map[string]tfattr.Type { - return map[string]tfattr.Type{ - attr.Policy: types.StringType, - attr.Ports: types.SetType{ - ElemType: types.StringType, - }, - } + return false } -func defaultProtocolsModelToTerraform(ctx context.Context) (types.List, diag.Diagnostics) { - attributeTypes := protocolsAttributeTypes() - - var diagnostics diag.Diagnostics - - defaultPorts, diags := defaultProtocolModelToTerraform() - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return makeObjectsListNull(ctx, attributeTypes), diagnostics +func getChangedAccessIDs(ctx context.Context, resourceData *schema.ResourceData, resource *model.Resource, client *client.Client) ([]string, []string, error) { + remote, err := client.ReadResource(ctx, resource.ID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get changedIDs: %w", err) } - tcp, diags := makeObjectsList(ctx, defaultPorts) - diagnostics.Append(diags...) - - udp, diags := makeObjectsList(ctx, defaultPorts) - diagnostics.Append(diags...) - - attributes := map[string]tfattr.Value{ - attr.AllowIcmp: types.BoolValue(true), - attr.TCP: tcp, - attr.UDP: udp, + var oldGroups, oldServiceAccounts []string + if resource.IsAuthoritative { + oldGroups, oldServiceAccounts = remote.Groups, remote.ServiceAccounts + } else { + oldGroups = getOldIDsNonAuthoritative(resourceData, attr.GroupIDs) + oldServiceAccounts = getOldIDsNonAuthoritative(resourceData, attr.ServiceAccountIDs) } - obj, diags := types.ObjectValue(attributeTypes, attributes) - diagnostics.Append(diags...) + // ids to delete + groupsToDelete := setDifference(oldGroups, resource.Groups) + serviceAccountsToDelete := setDifference(oldServiceAccounts, resource.ServiceAccounts) - if diagnostics.HasError() { - return makeObjectsListNull(ctx, attributeTypes), diagnostics - } + // ids to add + groupsToAdd := setDifference(resource.Groups, remote.Groups) + serviceAccountsToAdd := setDifference(resource.ServiceAccounts, remote.ServiceAccounts) - return makeObjectsList(ctx, obj) + return append(groupsToDelete, serviceAccountsToDelete...), append(groupsToAdd, serviceAccountsToAdd...), nil } -func defaultProtocolModelToTerraform() (basetypes.ObjectValue, diag.Diagnostics) { - attributes := map[string]tfattr.Value{ - attr.Policy: types.StringValue(model.PolicyAllowAll), - attr.Ports: types.ListNull(types.StringType), +func getOldIDsNonAuthoritative(resourceData *schema.ResourceData, attribute string) []string { + if resourceData.HasChange(attr.Path(attr.Access, attribute)) { + old, _ := resourceData.GetChange(attr.Path(attr.Access, attribute)) + + return convertIDs(old) } - return types.ObjectValue(protocolAttributeTypes(), attributes) + return nil } -func convertProtocolModelToTerraformOnImport(protocol *model.Protocol) (types.Object, diag.Diagnostics) { - if protocol == nil { - return types.ObjectNull(protocolAttributeTypes()), nil +func convertResource(data *schema.ResourceData) (*model.Resource, error) { + protocols, err := convertProtocols(data) + if err != nil { + return nil, err } - var statePorts = types.Set{} - - ports := convertPortsToTerraform(protocol.Ports) - if equalPorts(ports, statePorts) && !statePorts.IsNull() { - ports = statePorts + 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), } - policy := protocol.Policy - if policy == model.PolicyRestricted && len(ports.Elements()) == 0 { - policy = model.PolicyDenyAll + isVisible, ok := data.GetOkExists(attr.IsVisible) //nolint + if val := isVisible.(bool); ok { + res.IsVisible = &val } - attributes := map[string]tfattr.Value{ - attr.Policy: types.StringValue(policy), - attr.Ports: ports, + isBrowserShortcutEnabled, ok := data.GetOkExists(attr.IsBrowserShortcutEnabled) //nolint:staticcheck + if val := isBrowserShortcutEnabled.(bool); ok { + res.IsBrowserShortcutEnabled = &val } - return types.ObjectValue(protocolAttributeTypes(), attributes) + return res, nil } -func convertProtocolModelToTerraform(protocol *model.Protocol, reference tfattr.Value) (types.Object, diag.Diagnostics) { - if protocol == nil { - return types.ObjectNull(protocolAttributeTypes()), nil - } - - var statePorts = types.Set{} - - statePortsVal := reference.(types.List).Elements()[0].(types.Object).Attributes()[attr.Ports] - - if statePortsVal != nil && !statePortsVal.IsUnknown() { - statePortsSet, ok := statePortsVal.(types.Set) - if ok { - statePorts = statePortsSet - } - } - - ports := convertPortsToTerraform(protocol.Ports) - if equalPorts(ports, statePorts) && !statePorts.IsNull() { - ports = statePorts - } +func getOptionalString(data *schema.ResourceData, attr string) *string { + var result *string - policy := protocol.Policy - if policy == model.PolicyRestricted && len(ports.Elements()) == 0 { - policy = model.PolicyDenyAll - } + cfg := data.GetRawConfig() + val := cfg.GetAttr(attr) - attributes := map[string]tfattr.Value{ - attr.Policy: types.StringValue(policy), - attr.Ports: ports, + if !val.IsNull() { + str := val.AsString() + result = &str } - return types.ObjectValue(protocolAttributeTypes(), attributes) + return result } -func convertPortsToTerraform(ports []*model.PortRange) types.Set { - elements := make([]tfattr.Value, 0, len(ports)) - for _, port := range ports { - elements = append(elements, types.StringValue(port.String())) +func convertAccess(data *schema.ResourceData) ([]string, []string) { + rawList := data.Get(attr.Access).([]interface{}) + if len(rawList) == 0 || rawList[0] == nil { + return nil, nil } - return types.SetValueMust(types.StringType, elements) -} + rawMap := rawList[0].(map[string]interface{}) -func PortsDiff() planmodifier.Set { - return portsDiff{} + return convertIDs(rawMap[attr.GroupIDs]), convertIDs(rawMap[attr.ServiceAccountIDs]) } -type portsDiff struct{} +func convertAuthoritativeFlagLegacy(data *schema.ResourceData) bool { + flag, hasFlag := data.GetOkExists(attr.IsAuthoritative) //nolint:staticcheck -// Description returns a human-readable description of the plan modifier. -func (m portsDiff) Description(_ context.Context) string { - return "Handles ports difference." -} + if hasFlag { + return flag.(bool) + } -// MarkdownDescription returns a markdown description of the plan modifier. -func (m portsDiff) MarkdownDescription(_ context.Context) string { - return "Handles ports difference." + // default value + return true } -// PlanModifySet implements the plan modification logic. -func (m portsDiff) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { - return +func convertProtocols(data *schema.ResourceData) (*model.Protocols, error) { + rawList := data.Get(attr.Protocols).([]interface{}) + if len(rawList) == 0 { + return model.DefaultProtocols(), nil } - if equalPorts(req.StateValue, req.PlanValue) { - resp.PlanValue = req.StateValue - } -} + rawMap := rawList[0].(map[string]interface{}) -func equalPorts(one, another types.Set) bool { - oldPortsRange, err := convertPorts(one) + udp, err := convertProtocol(rawMap[attr.UDP].([]interface{})) if err != nil { - return false + return nil, err } - newPortsRange, err := convertPorts(another) + tcp, err := convertProtocol(rawMap[attr.TCP].([]interface{})) if err != nil { - return false + return nil, err } - oldPortsMap := convertPortsRangeToMap(oldPortsRange) - newPortsMap := convertPortsRangeToMap(newPortsRange) - - return reflect.DeepEqual(oldPortsMap, newPortsMap) + return &model.Protocols{ + UDP: udp, + TCP: tcp, + AllowIcmp: rawMap[attr.AllowIcmp].(bool), + }, nil } -func convertPortsRangeToMap(portsRange []*model.PortRange) map[int]struct{} { - out := make(map[int]struct{}) +func convertProtocol(rawList []interface{}) (*model.Protocol, error) { + if len(rawList) == 0 { + return nil, nil //nolint:nilnil + } - for _, port := range portsRange { - if port.Start == port.End { - out[port.Start] = struct{}{} + rawMap := rawList[0].(map[string]interface{}) + policy := rawMap[attr.Policy].(string) - continue - } + ports, err := convertPorts(rawMap[attr.Ports].([]interface{})) + if err != nil { + return nil, err + } - for i := port.Start; i <= port.End; i++ { - out[i] = struct{}{} + switch policy { + case model.PolicyAllowAll: + if len(ports) > 0 { + return nil, ErrPortsWithPolicyAllowAll } - } - return out -} + case model.PolicyDenyAll: + if len(ports) > 0 { + return nil, ErrPortsWithPolicyDenyAll + } -// UseStateForUnknownBool returns a plan modifier that copies a known prior state -// value into the planned value. Use this when it is known that an unconfigured -// value will remain the same after a resource update. -// -// To prevent Terraform errors, the framework automatically sets unconfigured -// and Computed attributes to an unknown value "(known after apply)" on update. -// Using this plan modifier will instead display the prior state value in the -// plan, unless a prior plan modifier adjusts the value. -func UseStateForUnknownBool() planmodifier.Bool { - return useStateForUnknownBoolModifier{} -} + case model.PolicyRestricted: + if len(ports) == 0 { + return nil, ErrPolicyRestrictedWithoutPorts + } + } -// useStateForUnknownModifier implements the plan modifier. -type useStateForUnknownBoolModifier struct{} + if policy == model.PolicyDenyAll { + policy = model.PolicyRestricted + } -// Description returns a human-readable description of the plan modifier. -func (m useStateForUnknownBoolModifier) Description(_ context.Context) string { - return "Once set, the value of this attribute in state will not change." + return model.NewProtocol(policy, ports), nil } -// MarkdownDescription returns a markdown description of the plan modifier. -func (m useStateForUnknownBoolModifier) MarkdownDescription(_ context.Context) string { - return "Once set, the value of this attribute in state will not change." -} +func convertPorts(rawList []interface{}) ([]*model.PortRange, error) { + var ports = make([]*model.PortRange, 0, len(rawList)) -// PlanModifyBool implements the plan modification logic. -func (m useStateForUnknownBoolModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { - return - } + for _, port := range rawList { + var str string + if port != nil { + str = port.(string) + } - // Do nothing if there is a known planned value. - if !req.PlanValue.IsUnknown() { - return - } + portRange, err := model.NewPortRange(str) + if err != nil { + return nil, err //nolint:wrapcheck + } - // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. - if req.ConfigValue.IsUnknown() { - return + ports = append(ports, portRange) } - resp.PlanValue = req.StateValue + return ports, nil } diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index 8b24bf18..59afc139 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -16,8 +16,11 @@ 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/terraform" ) @@ -54,7 +57,26 @@ var providerClient = func() *client.Client { //nolint }() var ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ //nolint - "twingate": providerserver.NewProtocol6WithError(twingate.New("test")()), + "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 + }, } func SetPageLimit(limit int) { diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 1a9d9363..74c2fd4d 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -486,7 +486,7 @@ func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82-83", "80"`), + Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"80", "82-83"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "80"), @@ -505,7 +505,7 @@ func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) { }, // new changes applied { - Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82-83", "70"`), + Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"70", "82-83"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "70"), @@ -605,8 +605,8 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82", "83", "80"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, firstTCPPort, "80"), - sdk.TestCheckResourceAttr(theResource, firstUDPPort, "80"), + sdk.TestCheckResourceAttr(theResource, firstTCPPort, "82"), + sdk.TestCheckResourceAttr(theResource, firstUDPPort, "82"), ), }, // no changes @@ -624,7 +624,7 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { }, // new changes applied { - Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82-83", "70"`), + Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"70", "82-83"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "70"), @@ -1147,7 +1147,7 @@ func TestAccTwingateResourceAccessWithEmptyGroups(t *testing.T) { Steps: []sdk.TestStep{ { Config: createResource18(remoteNetworkName, resourceName), - ExpectError: regexp.MustCompile("Error: Invalid Attribute Value"), + ExpectError: regexp.MustCompile("Error: Not enough list items"), }, }, }) @@ -1194,7 +1194,7 @@ func TestAccTwingateResourceAccessWithEmptyServiceAccounts(t *testing.T) { Steps: []sdk.TestStep{ { Config: createResource19(remoteNetworkName, resourceName), - ExpectError: regexp.MustCompile("Error: Invalid Attribute Value"), + ExpectError: regexp.MustCompile("Error: Not enough list items"), }, }, }) @@ -1241,7 +1241,7 @@ func TestAccTwingateResourceAccessWithEmptyBlock(t *testing.T) { Steps: []sdk.TestStep{ { Config: createResource20(remoteNetworkName, resourceName), - ExpectError: regexp.MustCompile("invalid attribute combination"), + ExpectError: regexp.MustCompile("Missing required argument"), }, }, }) @@ -1839,10 +1839,10 @@ func TestAccTwingateResourceCreateWithAlias(t *testing.T) { ), }, { - // alias attr commented out, means it has nil state + // alias attr commented out, means state keeps the same value without changes Config: createResource29WithoutAlias(terraformResourceName, remoteNetworkName, resourceName), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckNoResourceAttr(theResource, attr.Alias), + sdk.TestCheckResourceAttr(theResource, attr.Alias, aliasName), ), }, { diff --git a/twingate/provider.go b/twingate/provider.go index 79f23386..08e51024 100644 --- a/twingate/provider.go +++ b/twingate/provider.go @@ -9,14 +9,9 @@ import ( "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" + "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" ) const ( @@ -32,138 +27,92 @@ const ( 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 Provider(version string) *schema.Provider { + provider := &schema.Provider{ + Schema: providerOptions(), + ResourcesMap: map[string]*schema.Resource{ + resource.TwingateResource: resource.Resource(), }, + DataSourcesMap: map[string]*schema.Resource{}, } -} + provider.ConfigureContextFunc = configure(version, provider) -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 + return provider } -func mustGetInt(str string) int { - if val, err := strconv.Atoi(str); err == nil { - return val +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), + }, } - - return 0 } -func overrideStrWithConfig(cfg types.String, defaultValue string) string { - if !cfg.IsNull() { - return cfg.ValueString() - } - - return defaultValue -} +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 overrideIntWithConfig(cfg types.Int64, defaultValue int) int { - if !cfg.IsNull() { - return int(cfg.ValueInt64()) + 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", + }, + } } - - return defaultValue } func withDefault[T comparable](val, defaultVal T) T { @@ -175,33 +124,10 @@ func withDefault[T comparable](val, defaultVal T) T { 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 mustGetInt(str string) int { + if val, err := strconv.Atoi(str); err == nil { + return val } -} -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, - } + return 0 } diff --git a/twingate/v2/provider.go b/twingate/v2/provider.go new file mode 100644 index 00000000..09509f2d --- /dev/null +++ b/twingate/v2/provider.go @@ -0,0 +1,206 @@ +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 = "10" + 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, + } +}