diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bbea8fa3..4942a294 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -66,12 +66,20 @@ jobs: include: - terraform: '1.0.*' domain: 'dnsimple-1-0-terraform.bio' + registrant_contact_id: 10854 + registrant_change_domain: 'peoa1hvrl5s7q7os1bqadhd29uar81nnc4m0oyaloxex9kapsn20u6nr8z6l5h.eu' - terraform: '1.1.*' domain: 'dnsimple-1-1-terraform.bio' + registrant_contact_id: 10169 + registrant_change_domain: '9qy9lpesl2f2o5ya45zyujrggori1mh8sl6k2oz37usv48lhn3ziistg3u5kgv.eu' - terraform: '1.2.*' domain: 'dnsimple-1-2-terraform.bio' + registrant_contact_id: 10854 + registrant_change_domain: 'lqyivkga231hkiqihu0k7bjic2ixd01xs5vex8rmn2iaw0l7gxvhcbicigpfm3.eu' - terraform: '1.4.*' domain: 'dnsimple-1-4-terraform.bio' + registrant_contact_id: 10169 + registrant_change_domain: 'z0u2w48bo5fzgdsh1g7zjpflbpt0tiyl6tmc75ltzzm6dbphghrgepbaxs6zrm.eu' steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 @@ -90,8 +98,18 @@ jobs: DNSIMPLE_TOKEN: ${{ secrets.DNSIMPLE_TOKEN }} DNSIMPLE_DOMAIN: ${{ matrix.domain }} DNSIMPLE_CONTACT_ID: ${{ secrets.DNSIMPLE_CONTACT_ID }} + DNSIMPLE_REGISTRANT_CHANGE_DOMAIN: ${{ matrix.registrant_change_domain }} + DNSIMPLE_REGISTRANT_CHANGE_CONTACT_ID: ${{ matrix.registrant_contact_id }} run: go test -v -cover ./internal/... -timeout 15m timeout-minutes: 10 + - run: make sweep + if: always() + env: + DNSIMPLE_SANDBOX: true + DNSIMPLE_ACCOUNT: ${{ secrets.DNSIMPLE_ACCOUNT }} + DNSIMPLE_TOKEN: ${{ secrets.DNSIMPLE_TOKEN }} + DNSIMPLE_DOMAIN: ${{ matrix.domain }} + DNSIMPLE_REGISTRANT_CHANGE_DOMAIN: ${{ matrix.registrant_change_domain }} slack-workflow-status: diff --git a/CHANGELOG.md b/CHANGELOG.md index 251d7f6c..a453d663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## main +## 1.3.0 (Unreleased) + +FEATURES: + +- **New Data Source:** `dnsimple_registrant_change_check` (dnsimple/terraform-provider-dnsimple#155) +- **Updated Resource:** `dnsimple_registered_domain` now supports the change of `contact_id` which results in domain contact change at the registry (dnsimple/terraform-provider-dnsimple#155) + +NOTES: + +The `contact_id` attribute previously only supported configuring at resource creation. Now the attribute can be changed and it will result in registrant change being initiated at the registrar. In addition the `extended_attributes` attribute can now also be updated after a domain has been registered and the values in the extended attributes will be passed along the registrant change as some TLDs require extended attributes to be passed along the registrant change. +The registrant change can happen asynchronously for certain contact changes and TLDs. The resource supports this by attempting to sync the state of the registrant change in a similar way to how domain registration works with the `dnsimple_registered_domain` resource. + ## 1.2.1 ENHANCEMENTS: diff --git a/docs/data-sources/registrant_change_check.md b/docs/data-sources/registrant_change_check.md new file mode 100644 index 00000000..9bb497b1 --- /dev/null +++ b/docs/data-sources/registrant_change_check.md @@ -0,0 +1,58 @@ +--- +page_title: "DNSimple: dnsimple_registrant_change_check" +--- + +# dnsimple\_registrant_change_check + +Get information on the requirements of a registrant change. + +-> **Note:** The registrant change API is currently in developer preview and is subject to change. + +# Example Usage + +Get registrant change requirements for the `dnsimple.com` domain and the contact with ID `1234`: + +```hcl +data "dnsimple_registrant_change_check" "example" { + domain_id = "dnsimple.com" + contact_id = "1234" +} +``` + +# Argument Reference + +The following arguments are supported: + +* `domain_id` - (Required) The name or ID of the domain. +* `contact_id` - (Required) The ID of the contact you are planning to change to. + +# Attributes Reference + +The following additional attributes are exported: + +* `contact_id` - The ID of the contact you are planning to change to. +* `domain_id` - The name or ID of the domain. +* `extended_attributes` - (List) A list of extended attributes that are required for the registrant change. (see [below for nested schema](#nestedblock--extended_attributes)) +* `registry_owner_change` - (Boolean) Whether the registrant change is going to result in an owner change at the registry. + + + +### Nested Schema for `extended_attributes` + +Attributes Reference: + +- `name` (String) - The name of the extended attribute. e.g. `x-au-registrant-id-type` +- `description` (String) - The description of the extended attribute. +- `required` (Boolean) - Whether the extended attribute is required. +- `options` (List) - A list of options for the extended attribute. (see [below for nested schema](#nestedblock--options)) + + + +### Nested Schema for `extended_attributes.options` + +Attributes Reference: + +- `title` (String) - The human readable title of the option. e.g. `Australian Company Number (ACN)` +- `value` (String) - The value of the option. +- `description` (String) - The description of the option. + diff --git a/docs/resources/registered_domain.md b/docs/resources/registered_domain.md index cbd75a43..8e21a103 100644 --- a/docs/resources/registered_domain.md +++ b/docs/resources/registered_domain.md @@ -46,13 +46,13 @@ resource "dnsimple_registered_domain" "appleseed_bio" { The following argument(s) are supported: * `name` - (Required) The domain name to be registered -* `contact_id` - (Required) The ID of the contact to be used for the domain registration +* `contact_id` - (Required) The ID of the contact to be used for the domain registration. The contact ID can be changed after the domain has been registered. The change will result in a new registrant change this may result in a [60-day lock](https://support.dnsimple.com/articles/icann-60-day-lock-registrant-change/). * `auto_renew_enabled` - (Optional) Whether the domain should be set to auto-renew (default: `false`) * `whois_privacy_enabled` - (Optional) Whether the domain should have WhoIs privacy enabled (default: `false`) * `dnssec_enabled` - (Optional) Whether the domain should have DNSSEC enabled (default: `false`) * `transfer_lock_enabled` - (Optional) Whether the domain transfer lock protection is enabled (default: `true`) * `premium_price` - (Optional) The premium price for the domain registration. This is only required if the domain is a premium domain. You can use our [Check domain API](https://developer.dnsimple.com/v2/registrar/#checkDomain) to check if a domain is premium. And [Retrieve domain prices API](https://developer.dnsimple.com/v2/registrar/#getDomainPrices) to retrieve the premium price for a domain. -* `extended_attributes` - (Optional) A map of extended attributes to be set for the domain registration. To see if there are any required extended attributes for any TLD use our [Lists the TLD Extended Attributes API](https://developer.dnsimple.com/v2/tlds/#getTldExtendedAttributes). +* `extended_attributes` - (Optional) A map of extended attributes to be set for the domain registration. To see if there are any required extended attributes for any TLD use our [Lists the TLD Extended Attributes API](https://developer.dnsimple.com/v2/tlds/#getTldExtendedAttributes). The values provided in the `extended_attributes` will also be sent when a registrant change is initiated as part of changing the `contact_id`. * `timeouts` - (Optional) (see [below for nested schema](#nestedblock--timeouts)) # Attributes Reference diff --git a/go.mod b/go.mod index 9e4bcd1f..422a8bcc 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/terraform-providers/terraform-provider-dnsimple require ( github.com/dnsimple/dnsimple-go v1.4.0 github.com/hashicorp/terraform-plugin-docs v0.16.0 - github.com/hashicorp/terraform-plugin-framework v1.2.0 - github.com/hashicorp/terraform-plugin-go v0.15.0 + github.com/hashicorp/terraform-plugin-framework v1.4.0 + github.com/hashicorp/terraform-plugin-go v0.19.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-mux v0.10.0 - github.com/hashicorp/terraform-plugin-testing v1.2.0 + github.com/hashicorp/terraform-plugin-mux v0.12.0 + github.com/hashicorp/terraform-plugin-testing v1.5.1 github.com/stretchr/testify v1.8.4 golang.org/x/oauth2 v0.12.0 ) @@ -16,9 +16,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/cloudflare/circl v1.3.3 // indirect @@ -34,20 +34,20 @@ 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.9 // indirect + github.com/hashicorp/go-plugin v1.5.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // 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.16.2 // 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-sdk/v2 v2.26.1 // indirect - github.com/hashicorp/terraform-registry-address v0.2.0 // indirect - github.com/hashicorp/terraform-svchost v0.0.1 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 // 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/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mitchellh/cli v1.1.5 // indirect @@ -65,16 +65,16 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.13.2 // 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-20230626212559-97b1e661b5df // indirect - golang.org/x/mod v0.11.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.15.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect - google.golang.org/grpc v1.54.0 // 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.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e198e919..927fac44 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,22 +6,22 @@ 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= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 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= @@ -33,10 +34,11 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -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-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +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.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -68,51 +70,51 @@ 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.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU= -github.com/hashicorp/go-plugin v1.4.9/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +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-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0= -github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= -github.com/hashicorp/hcl/v2 v2.16.2 h1:mpkHZh/Tv+xet3sy3F9Ld4FyI2tUpWe9x3XtPx9f1a0= -github.com/hashicorp/hcl/v2 v2.16.2/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng= +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.2.0 h1:MZjFFfULnFq8fh04FqrKPcJ/nGpHOvX4buIygT3MSNY= -github.com/hashicorp/terraform-plugin-framework v1.2.0/go.mod h1:nToI62JylqXDq84weLJ/U3umUsBhZAaTmU0HXIVUOcw= -github.com/hashicorp/terraform-plugin-go v0.15.0 h1:1BJNSUFs09DS8h/XNyJNJaeusQuWc/T9V99ylU9Zwp0= -github.com/hashicorp/terraform-plugin-go v0.15.0/go.mod h1:tk9E3/Zx4RlF/9FdGAhwxHExqIHHldqiQGt20G6g+nQ= +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-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-mux v0.10.0 h1:VejY1BffxGy2iYOaa8DDHavY4k9jbvAE8F3lhruspKY= -github.com/hashicorp/terraform-plugin-mux v0.10.0/go.mod h1:9sdnpmY20xIsl4ItsfODZYE+MgpSy/osXpSf+RwaZCY= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 h1:G9WAfb8LHeCxu7Ae8nc1agZlQOSCUWsb610iAogBhCs= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1/go.mod h1:xcOSYlRVdPLmDUoqPhO9fiO/YCN/l6MGYeTzGt5jgkQ= -github.com/hashicorp/terraform-plugin-testing v1.2.0 h1:pASRAe6BOZFO4xSGQr9WzitXit0nrQAYDk8ziuRfn9E= -github.com/hashicorp/terraform-plugin-testing v1.2.0/go.mod h1:+8bp3O7xUb1UtBcdknrGdVRIuTw4b62TYSIgXHqlyew= -github.com/hashicorp/terraform-registry-address v0.2.0 h1:92LUg03NhfgZv44zpNTLBGIbiyTokQCDcdH5BhVHT3s= -github.com/hashicorp/terraform-registry-address v0.2.0/go.mod h1:478wuzJPzdmqT6OGbB/iH82EDcI8VFM4yujknh/1nIs= -github.com/hashicorp/terraform-svchost v0.0.1 h1:Zj6fR5wnpOHnJUmLyWozjMeDaVuE+cstMPj41/eKmSQ= -github.com/hashicorp/terraform-svchost v0.0.1/go.mod h1:ut8JaH0vumgdCfJaihdcZULqkAwHdQNwNH7taIDdsZM= +github.com/hashicorp/terraform-plugin-mux v0.12.0 h1:TJlmeslQ11WlQtIFAfth0vXx+gSNgvMEng2Rn9z3WZY= +github.com/hashicorp/terraform-plugin-mux v0.12.0/go.mod h1:8MR0AgmV+Q03DIjyrAKxXyYlq2EUnYBQP8gxAAA0zeM= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 h1:wcOKYwPI9IorAJEBLzgclh3xVolO7ZorYd6U1vnok14= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0/go.mod h1:qH/34G25Ugdj5FcM95cSoXzUgIbgfhVLXCcEcYaMwq8= +github.com/hashicorp/terraform-plugin-testing v1.5.1 h1:T4aQh9JAhmWo4+t1A7x+rnxAJHCDIYW9kXyo4sVO92c= +github.com/hashicorp/terraform-plugin-testing v1.5.1/go.mod h1:dg8clO6K59rZ8w9EshBmDp1CxTIPu3yA4iaDpX1h5u0= +github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= +github.com/hashicorp/terraform-registry-address v0.2.2/go.mod h1:LtwNbCihUoUZ3RYriyS2wF/lGPB6gF9ICLRtuDk7hSo= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/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/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/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -159,7 +161,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= @@ -179,26 +181,39 @@ github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= -github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc= +github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/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.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-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -207,26 +222,44 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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.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/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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +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= @@ -238,6 +271,5 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= diff --git a/internal/consts/provider.go b/internal/consts/provider.go index cfe064e4..cd7e9027 100644 --- a/internal/consts/provider.go +++ b/internal/consts/provider.go @@ -8,4 +8,11 @@ const ( DomainStateFailed = "failed" DomainStateCancelling = "cancelling" DomainStateCancelled = "cancelled" + + // Domain Registrant Change (Contact change) states + RegistrantChangeStateNew = "new" + RegistrantChangeStatePending = "pending" + RegistrantChangeStateCompleted = "completed" + RegistrantChangeStateCancelling = "cancelling" + RegistrantChangeStateCancelled = "cancelled" ) diff --git a/internal/framework/common/domain_registration.go b/internal/framework/common/domain_registration.go index a48e4a9f..bdc65e78 100644 --- a/internal/framework/common/domain_registration.go +++ b/internal/framework/common/domain_registration.go @@ -16,3 +16,27 @@ var DomainRegistrationAttrType = map[string]attr.Type{ "state": types.StringType, "id": types.Int64Type, } + +type RegistrantChange struct { + Id types.Int64 `tfsdk:"id"` + AccountId types.Int64 `tfsdk:"account_id"` + ContactId types.Int64 `tfsdk:"contact_id"` + DomainId types.String `tfsdk:"domain_id"` + State types.String `tfsdk:"state"` + ExtendedAttributes types.Map `tfsdk:"extended_attributes"` + RegistryOwnerChange types.Bool `tfsdk:"registry_owner_change"` + IrtLockLiftedBy types.String `tfsdk:"irt_lock_lifted_by"` +} + +var RegistrantChangeAttrType = map[string]attr.Type{ + "id": types.Int64Type, + "account_id": types.Int64Type, + "contact_id": types.Int64Type, + "domain_id": types.StringType, + "state": types.StringType, + "extended_attributes": types.MapType{ + ElemType: types.StringType, + }, + "registry_owner_change": types.BoolType, + "irt_lock_lifted_by": types.StringType, +} diff --git a/internal/framework/datasources/registrant_change_check_data_source.go b/internal/framework/datasources/registrant_change_check_data_source.go new file mode 100644 index 00000000..7362fe83 --- /dev/null +++ b/internal/framework/datasources/registrant_change_check_data_source.go @@ -0,0 +1,159 @@ +package datasources + +import ( + "context" + "fmt" + + "github.com/dnsimple/dnsimple-go/dnsimple" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &RegistrantChangeCheckDataSource{} + +func NewRegistrantChangeCheckDataSource() datasource.DataSource { + return &RegistrantChangeCheckDataSource{} +} + +// RegistrantChangeCheckDataSource defines the data source implementation. +type RegistrantChangeCheckDataSource struct { + config *common.DnsimpleProviderConfig +} + +// RegistrantChangeCheckDataSourceModel describes the data source data model. +type RegistrantChangeCheckDataSourceModel struct { + Id types.String `tfsdk:"id"` + ContactId types.String `tfsdk:"contact_id"` + DomainId types.String `tfsdk:"domain_id"` + ExtendedAttributes []ExtendedAttribute `tfsdk:"extended_attributes"` + RegistryOwnerChange types.Bool `tfsdk:"registry_owner_change"` +} + +type ExtendedAttribute struct { + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Required types.Bool `tfsdk:"required"` + Options []ExtendedAttributeOption `tfsdk:"options"` +} + +type ExtendedAttributeOption struct { + Title types.String `tfsdk:"title"` + Value types.String `tfsdk:"value"` + Description types.String `tfsdk:"description"` +} + +func (d *RegistrantChangeCheckDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_registrant_change_check" +} + +func (d *RegistrantChangeCheckDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "DNSimple registrant change check data source", + + Attributes: map[string]schema.Attribute{ + "id": common.IDStringAttribute(), + "contact_id": schema.StringAttribute{ + MarkdownDescription: "DNSimple contact ID for which the registrant change check is being performed", + Required: true, + }, + "domain_id": schema.StringAttribute{ + MarkdownDescription: "DNSimple domain ID for which the registrant change check is being performed", + Required: true, + }, + "extended_attributes": schema.ListAttribute{ + MarkdownDescription: "Extended attributes for the registrant change", + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "description": types.StringType, + "required": types.BoolType, + "options": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "title": types.StringType, + "value": types.StringType, + "description": types.StringType, + }, + }, + }, + }, + }, + Computed: true, + }, + "registry_owner_change": schema.BoolAttribute{ + MarkdownDescription: "True if the registrant change will result in a registry owner change", + Computed: true, + }, + }, + } +} + +func (d *RegistrantChangeCheckDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *provider.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.config = config +} + +func (d *RegistrantChangeCheckDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data RegistrantChangeCheckDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + requestInput := dnsimple.CheckRegistrantChangeInput{ + ContactId: data.ContactId.ValueString(), + DomainId: data.DomainId.ValueString(), + } + + response, err := d.config.Client.Registrar.CheckRegistrantChange(ctx, d.config.AccountID, &requestInput) + if err != nil { + resp.Diagnostics.AddError( + "failed to check registrant change", + err.Error(), + ) + return + } + + contactIdString := fmt.Sprintf("%d", response.Data.ContactId) + data.Id = types.StringValue(data.DomainId.ValueString() + ":" + contactIdString) + data.ContactId = types.StringValue(contactIdString) + data.ExtendedAttributes = make([]ExtendedAttribute, len(response.Data.ExtendedAttributes)) + for i, extendedAttribute := range response.Data.ExtendedAttributes { + data.ExtendedAttributes[i].Name = types.StringValue(extendedAttribute.Name) + data.ExtendedAttributes[i].Description = types.StringValue(extendedAttribute.Description) + data.ExtendedAttributes[i].Required = types.BoolValue(extendedAttribute.Required) + data.ExtendedAttributes[i].Options = make([]ExtendedAttributeOption, len(extendedAttribute.Options)) + for j, option := range extendedAttribute.Options { + data.ExtendedAttributes[i].Options[j].Title = types.StringValue(option.Title) + data.ExtendedAttributes[i].Options[j].Value = types.StringValue(option.Value) + data.ExtendedAttributes[i].Options[j].Description = types.StringValue(option.Description) + } + } + data.RegistryOwnerChange = types.BoolValue(response.Data.RegistryOwnerChange) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/framework/datasources/registrant_change_check_data_source_test.go b/internal/framework/datasources/registrant_change_check_data_source_test.go new file mode 100644 index 00000000..9dcafd69 --- /dev/null +++ b/internal/framework/datasources/registrant_change_check_data_source_test.go @@ -0,0 +1,44 @@ +package datasources_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" + "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" +) + +func TestAccRegistrantChangeCheckDataSource(t *testing.T) { + // Get convert the contact id to int + contactId := os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_CONTACT_ID") + domainName := os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_DOMAIN") + resourceName := "data.dnsimple_registrant_change_check.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { test_utils.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: provider.NewProto6ProviderFactory(), + Steps: []resource.TestStep{ + // Read testing + { + Config: testAccRegistrantChangeCheckDataSourceConfig(contactId, domainName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "contact_id", contactId), + resource.TestCheckResourceAttr(resourceName, "domain_id", domainName), + resource.TestCheckResourceAttrSet(resourceName, "extended_attributes.#"), + resource.TestCheckResourceAttrSet(resourceName, "registry_owner_change"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + }, + }) +} + +func testAccRegistrantChangeCheckDataSourceConfig(contactId, domainName string) string { + return fmt.Sprintf(` +data "dnsimple_registrant_change_check" "test" { + contact_id = %[1]q + domain_id = %[2]q +}`, contactId, domainName) +} diff --git a/internal/framework/provider/provider.go b/internal/framework/provider/provider.go index 719ecf4d..72cb2b2a 100644 --- a/internal/framework/provider/provider.go +++ b/internal/framework/provider/provider.go @@ -183,6 +183,7 @@ func (p *DnsimpleProvider) Resources(ctx context.Context) []func() resource.Reso func (p *DnsimpleProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ datasources.NewCertificateDataSource, + datasources.NewRegistrantChangeCheckDataSource, datasources.NewZoneDataSource, } } diff --git a/internal/framework/resources/registered_domain/common.go b/internal/framework/resources/registered_domain/common.go index 64b5012e..6c1f19a6 100644 --- a/internal/framework/resources/registered_domain/common.go +++ b/internal/framework/resources/registered_domain/common.go @@ -2,11 +2,14 @@ package registered_domain import ( "context" + "errors" "fmt" "time" "github.com/dnsimple/dnsimple-go/dnsimple" "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/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -19,6 +22,10 @@ const ( RegistrationConverged = "registration_converged" RegistrationConvergenceTimeout = "registration_converged_timeout" RegistrationFailed = "registration_failed" + + RegistrantChangeConverged = "registrant_change_converged" + RegistrantChangeConvergenceTimeout = "registrant_change_converged_timeout" + RegistrantChangeFailed = "registrant_change_failed" ) func (r *RegisteredDomainResource) setAutoRenewal(ctx context.Context, data *RegisteredDomainResourceModel) diag.Diagnostics { @@ -149,7 +156,6 @@ func (r *RegisteredDomainResource) updateModelFromAPIResponse(ctx context.Contex data.State = types.StringValue(domain.State) data.UnicodeName = types.StringValue(domain.UnicodeName) data.AccountId = types.Int64Value(domain.AccountID) - data.ContactId = types.Int64Value(domain.RegistrantID) data.ExpiresAt = types.StringValue(domain.ExpiresAt) data.Name = types.StringValue(domain.Name) } @@ -188,7 +194,6 @@ func (r *RegisteredDomainResource) updateModelFromAPIResponsePartialCreate(ctx c data.State = types.StringValue(domain.State) data.UnicodeName = types.StringValue(domain.UnicodeName) data.AccountId = types.Int64Value(domain.AccountID) - data.ContactId = types.Int64Value(domain.RegistrantID) data.ExpiresAt = types.StringValue(domain.ExpiresAt) data.Name = types.StringValue(domain.Name) } @@ -209,6 +214,26 @@ func (r *RegisteredDomainResource) domainRegistrationAPIResponseToObject(ctx con return types.ObjectValueFrom(ctx, common.DomainRegistrationAttrType, domainRegistrationData) } +func (r *RegisteredDomainResource) registrantChangeAPIResponseToObject(ctx context.Context, registrantChange *dnsimple.RegistrantChange) (basetypes.ObjectValue, diag.Diagnostics) { + elements, diags := types.MapValueFrom(ctx, types.StringType, registrantChange.ExtendedAttributes) + if diags.HasError() { + return basetypes.ObjectValue{}, diags + } + + registrantChangeData := common.RegistrantChange{ + Id: types.Int64Value(int64(registrantChange.Id)), + AccountId: types.Int64Value(int64(registrantChange.AccountId)), + ContactId: types.Int64Value(int64(registrantChange.ContactId)), + DomainId: types.StringValue(fmt.Sprintf("%d", registrantChange.DomainId)), + State: types.StringValue(registrantChange.State), + ExtendedAttributes: elements, + RegistryOwnerChange: types.BoolValue(registrantChange.RegistryOwnerChange), + IrtLockLiftedBy: types.StringValue(registrantChange.IrtLockLiftedBy), + } + + return types.ObjectValueFrom(ctx, common.RegistrantChangeAttrType, registrantChangeData) +} + func getTimeouts(ctx context.Context, model *RegisteredDomainResourceModel) (*common.Timeouts, diag.Diagnostics) { timeouts := &common.Timeouts{} diags := model.Timeouts.As(ctx, timeouts, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true}) @@ -223,6 +248,13 @@ func getDomainRegistration(ctx context.Context, data *RegisteredDomainResourceMo return domainRegistration, diags } +func getRegistrantChange(ctx context.Context, data *RegisteredDomainResourceModel) (*common.RegistrantChange, diag.Diagnostics) { + registrantChange := &common.RegistrantChange{} + diags := data.RegistrantChange.As(ctx, registrantChange, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true}) + + return registrantChange, diags +} + func tryToConvergeRegistration(ctx context.Context, data *RegisteredDomainResourceModel, diagnostics *diag.Diagnostics, r *RegisteredDomainResource, registrationID string) (string, error) { timeouts, diags := getTimeouts(ctx, data) diagnostics.Append(diags...) @@ -274,3 +306,137 @@ func tryToConvergeRegistration(ctx context.Context, data *RegisteredDomainResour return RegistrationConverged, nil } + +func tryToConvergeRegistrantChange(ctx context.Context, data *RegisteredDomainResourceModel, diagnostics *diag.Diagnostics, r *RegisteredDomainResource, registrantChangeId int) (string, error) { + timeouts, diags := getTimeouts(ctx, data) + diagnostics.Append(diags...) + if diagnostics.HasError() { + return RegistrantChangeFailed, nil + } + + err := utils.RetryWithTimeout(ctx, func() (error, bool) { + registrantChangeResponse, err := r.config.Client.Registrar.GetRegistrantChange(ctx, r.config.AccountID, registrantChangeId) + + if err != nil { + return err, false + } + + if registrantChangeResponse.Data.State == consts.RegistrantChangeStateCancelled || registrantChangeResponse.Data.State == consts.RegistrantChangeStateCancelling { + diagnostics.AddError( + fmt.Sprintf("failed to change registrant for DNSimple Domain: %s, registrant change id: %d", data.Name.ValueString(), registrantChangeId), + "Registrant change was cancelled, please investigate why this happened. You can refer to our support article https://support.dnsimple.com/articles/changing-domain-contact/ to get started and if you need assistance, please contact support at support@dnsimple.com.", + ) + return nil, true + } + + if registrantChangeResponse.Data.State != consts.RegistrantChangeStateCompleted { + tflog.Info(ctx, fmt.Sprintf("[RETRYING] Registrant change is not complete, current state: %s", registrantChangeResponse.Data.State)) + + return fmt.Errorf("registrant change is not complete, current state: %s. You can try to run terraform again to try and converge the registrant change", registrantChangeResponse.Data.State), false + } + + return nil, false + }, timeouts.CreateDuration(), 20*time.Second) + + if diagnostics.HasError() { + // If we have diagnostic errors, we suspended the retry loop because the domain is in a bad state, and cannot converge. + return RegistrantChangeFailed, nil + } + + if err != nil { + // If we have an error, it means the retry loop timed out, and we cannot converge during this run. + return RegistrantChangeConvergenceTimeout, err + } + + return RegistrantChangeConverged, nil +} + +func createRegistrantChange(ctx context.Context, data *RegisteredDomainResourceModel, r *RegisteredDomainResource, resp *resource.UpdateResponse) { + registrantChangeAttributes := dnsimple.CreateRegistrantChangeInput{ + DomainId: fmt.Sprintf("%d", data.Id.ValueInt64()), + ContactId: fmt.Sprintf("%d", data.ContactId.ValueInt64()), + } + + if !data.ExtendedAttributes.IsNull() { + extendedAttrs := make(map[string]string) + diags := data.ExtendedAttributes.ElementsAs(ctx, &extendedAttrs, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + registrantChangeAttributes.ExtendedAttributes = extendedAttrs + } + + registrantChangeResponse, err := r.config.Client.Registrar.CreateRegistrantChange(ctx, r.config.AccountID, ®istrantChangeAttributes) + + if err != nil { + var errorResponse *dnsimple.ErrorResponse + if errors.As(err, &errorResponse) { + resp.Diagnostics.Append(utils.AttributeErrorsToDiagnostics(errorResponse)...) + return + } + + resp.Diagnostics.AddError( + "failed to create DNSimple registrant change", + err.Error(), + ) + return + } + + if registrantChangeResponse.Data.State != consts.RegistrantChangeStateCompleted { + if registrantChangeResponse.Data.State == consts.RegistrantChangeStateCancelled || registrantChangeResponse.Data.State == consts.RegistrantChangeStateCancelling { + resp.Diagnostics.AddError( + fmt.Sprintf("failed to create DNSimple registrant change current state: %s", registrantChangeResponse.Data.State), + "", + ) + return + } + + // Registrant change has been created, but is not yet completed + if registrantChangeResponse.Data.State == consts.RegistrantChangeStateNew || registrantChangeResponse.Data.State == consts.RegistrantChangeStatePending { + convergenceState, err := tryToConvergeRegistrantChange(ctx, data, &resp.Diagnostics, r, registrantChangeResponse.Data.Id) + if convergenceState == RegistrantChangeFailed { + // Response is already populated with the error we can safely return + return + } + + if convergenceState == RegistrantChangeConvergenceTimeout { + // We attempted to converge on the registrant change, but the registrant change was not ready + // user needs to run terraform again to try and converge the registrant change + + registrantChangeObject, diags := r.registrantChangeAPIResponseToObject(ctx, registrantChangeResponse.Data) + resp.Diagnostics.Append(diags...) + + // Update the data with the current registrant change + data.RegistrantChange = registrantChangeObject + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("registrant_change"), registrantChangeObject)...) + + // Exit with warning to prevent the state from being tainted + resp.Diagnostics.AddWarning( + "failed to converge on registrant change", + err.Error(), + ) + return + } + + registrantChangeResponse, err = r.config.Client.Registrar.GetRegistrantChange(ctx, r.config.AccountID, int(data.Id.ValueInt64())) + + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("failed to read DNSimple Registrant Change for: %s, %d", data.Name.ValueString(), data.Id.ValueInt64()), + err.Error(), + ) + return + } + } + } + + // Registrant change was successful, we can now update the resource model + registrantChangeObject, diags := r.registrantChangeAPIResponseToObject(ctx, registrantChangeResponse.Data) + resp.Diagnostics.Append(diags...) + + // Update the data with the current registrant change + data.RegistrantChange = registrantChangeObject +} diff --git a/internal/framework/resources/registered_domain/create.go b/internal/framework/resources/registered_domain/create.go index f21fb466..31ee066c 100644 --- a/internal/framework/resources/registered_domain/create.go +++ b/internal/framework/resources/registered_domain/create.go @@ -8,7 +8,9 @@ import ( "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" + "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" ) @@ -160,6 +162,8 @@ func (r *RegisteredDomainResource) Create(ctx context.Context, req resource.Crea return } + data.RegistrantChange = types.ObjectNull(common.RegistrantChangeAttrType) + // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/framework/resources/registered_domain/import.go b/internal/framework/resources/registered_domain/import.go index c360c581..320c14e7 100644 --- a/internal/framework/resources/registered_domain/import.go +++ b/internal/framework/resources/registered_domain/import.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" @@ -55,4 +56,6 @@ func (r *RegisteredDomainResource) ImportState(ctx context.Context, req resource resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), domainResponse.Data.ID)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), domainResponse.Data.Name)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("contact_id"), domainResponse.Data.RegistrantID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("registrant_change"), types.ObjectNull(common.RegistrantChangeAttrType))...) } diff --git a/internal/framework/resources/registered_domain/modifiers.go b/internal/framework/resources/registered_domain/modifiers.go index a4b2c833..ef77b881 100644 --- a/internal/framework/resources/registered_domain/modifiers.go +++ b/internal/framework/resources/registered_domain/modifiers.go @@ -73,3 +73,40 @@ func (m domainRegistrationState) PlanModifyObject(ctx context.Context, req planm resp.PlanValue = obj } + +type registrantChangeState struct { +} + +// RegistrantChangeState return a object plan modifier that sets the specified value if the planned value is Null. +func RegistrantChangeState() planmodifier.String { + return registrantChangeState{} +} + +func (m registrantChangeState) Description(context.Context) string { + return "If the registrant change state is not completed, set it to completed. Unless the state is a cancelled or cancelling state" +} + +func (m registrantChangeState) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (m registrantChangeState) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if !req.ConfigValue.IsNull() || req.PlanValue.IsUnknown() || req.PlanValue.IsNull() { + return + } + + state := req.PlanValue.ValueString() + + // If the registrant change state is a cancelled state, do not attempt to update it + if state == consts.RegistrantChangeStateCancelling || state == consts.RegistrantChangeStateCancelled { + return + } + + // If the registrant change state is not completed, set it to completed + // this will trigger a plan change and result in an update so we can attempt to sync + if state == consts.RegistrantChangeStateCompleted { + return + } + + resp.PlanValue = types.StringValue(consts.RegistrantChangeStateCompleted) +} diff --git a/internal/framework/resources/registered_domain/modifiers_test.go b/internal/framework/resources/registered_domain/modifiers_test.go index 57f7d2b7..2f1dba80 100644 --- a/internal/framework/resources/registered_domain/modifiers_test.go +++ b/internal/framework/resources/registered_domain/modifiers_test.go @@ -139,3 +139,90 @@ func TestDomainRegistrationStateModifier(t *testing.T) { }) } } + +func TestRegistrantChangeStateModifier(t *testing.T) { + t.Parallel() + + type testCase struct { + skipDomainRegistration bool + plannedValue types.String + currentValue types.String + expectedValue types.String + expectError bool + } + tests := map[string]testCase{ + "imported has no plan and state and is unkown": { + plannedValue: types.StringUnknown(), + currentValue: types.StringNull(), + expectedValue: types.StringNull(), + }, + "imported has no plan and is null": { + plannedValue: types.StringNull(), + currentValue: types.StringNull(), + expectedValue: types.StringNull(), + }, + "not completed has plan and state": { + plannedValue: types.StringValue(consts.RegistrantChangeStateNew), + currentValue: types.StringValue(consts.RegistrantChangeStateNew), + expectedValue: types.StringValue(consts.RegistrantChangeStateCompleted), + }, + "state null but plan is known": { + plannedValue: types.StringValue(consts.RegistrantChangeStateNew), + currentValue: types.StringNull(), + expectedValue: types.StringValue(consts.RegistrantChangeStateCompleted), + }, + "null planned": { + plannedValue: types.StringNull(), + currentValue: types.StringValue(consts.RegistrantChangeStateCancelled), + expectedValue: types.StringNull(), + }, + "state cancelled": { + plannedValue: types.StringValue(consts.RegistrantChangeStateCancelled), + currentValue: types.StringValue(consts.RegistrantChangeStateCancelled), + expectedValue: types.StringValue(consts.RegistrantChangeStateCancelled), + }, + "state cancelling": { + plannedValue: types.StringValue(consts.RegistrantChangeStateCancelling), + currentValue: types.StringValue(consts.RegistrantChangeStateCancelling), + expectedValue: types.StringValue(consts.RegistrantChangeStateCancelling), + }, + "on create": { + plannedValue: types.StringNull(), + currentValue: types.StringNull(), + expectedValue: types.StringNull(), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + request := planmodifier.StringRequest{ + Path: path.Root("test"), + PlanValue: test.plannedValue, + StateValue: test.currentValue, + } + + if test.skipDomainRegistration { + request.Private.SetKey(ctx, "skipDomainRegistration", []byte("true")) + } + + response := planmodifier.StringResponse{ + PlanValue: request.PlanValue, + } + registered_domain.RegistrantChangeState().PlanModifyString(ctx, request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + + assert.Equal(t, test.expectedValue.ValueString(), response.PlanValue.ValueString()) + }) + } +} diff --git a/internal/framework/resources/registered_domain/read.go b/internal/framework/resources/registered_domain/read.go index 8425d7f1..f9f80d2e 100644 --- a/internal/framework/resources/registered_domain/read.go +++ b/internal/framework/resources/registered_domain/read.go @@ -40,6 +40,32 @@ func (r *RegisteredDomainResource) Read(ctx context.Context, req resource.ReadRe } } + registrantChange, diags := getRegistrantChange(ctx, data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + var registrantChangeResponse *dnsimple.RegistrantChangeResponse + if !registrantChange.Id.IsNull() { + registrantChangeResponse, err = r.config.Client.Registrar.GetRegistrantChange(ctx, r.config.AccountID, int(registrantChange.Id.ValueInt64())) + + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("failed to read DNSimple Registrant Change Id: %d", registrantChange.Id.ValueInt64()), + err.Error(), + ) + return + } + + registrantChangeObject, diags := r.registrantChangeAPIResponseToObject(ctx, registrantChangeResponse.Data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + data.RegistrantChange = registrantChangeObject + } + domainResponse, err := r.config.Client.Domains.GetDomain(ctx, r.config.AccountID, data.Name.ValueString()) if err != nil { diff --git a/internal/framework/resources/registered_domain/resource_test.go b/internal/framework/resources/registered_domain/resource_test.go index 8648c639..f0097c6a 100644 --- a/internal/framework/resources/registered_domain/resource_test.go +++ b/internal/framework/resources/registered_domain/resource_test.go @@ -11,6 +11,7 @@ import ( "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" ) @@ -106,6 +107,52 @@ func TestAccRegisteredDomainResource_WithExtendedAttrs(t *testing.T) { }) } +func TestAccRegisteredDomainResource_RegistrantChange_WithExtendedAttrs(t *testing.T) { + contactID, err := strconv.Atoi(os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_CONTACT_ID")) + if err != nil { + t.Fatal(err) + } + domainName := os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_DOMAIN") + resourceName := "dnsimple_registered_domain.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckRegistrantChange(t) }, + ProtoV6ProviderFactories: test_utils.TestAccProtoV6ProviderFactories(), + CheckDestroy: testAccCheckRegisteredDomainRegistrantChangeDestroy, + Steps: []resource.TestStep{ + { + ResourceName: resourceName, + Config: testAccRegisteredDomainResourceConfig(domainName, 1234), + ImportStateId: domainName, + ImportState: true, + ImportStateVerify: false, + ImportStatePersist: true, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", domainName), + resource.TestCheckResourceAttr(resourceName, "state", "registered"), + resource.TestCheckResourceAttr(resourceName, "contact_id", "1234"), + resource.TestCheckNoResourceAttr(resourceName, "domain_registration"), + ), + }, + { + Config: testAccRegisteredDomainResourceConfig_WithExtendedAttrs(domainName, contactID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "registrant_change.id"), + resource.TestCheckResourceAttrSet(resourceName, "registrant_change.state"), + resource.TestCheckResourceAttrSet(resourceName, "registrant_change.registry_owner_change"), + resource.TestCheckResourceAttrSet(resourceName, "registrant_change.domain_id"), + resource.TestCheckResourceAttr(resourceName, "registrant_change.contact_id", fmt.Sprintf("%d", contactID)), + resource.TestCheckResourceAttr(resourceName, "registrant_change.extended_attributes.x-eu-registrant-citizenship", "bg"), + ), + // We expect the plan to be non-empty because we are creating a registrant change that will not be completed + // and we will attempt to converge it by setting the state to completed + ExpectNonEmptyPlan: true, + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + func TestAccRegisteredDomainResource_WithOptions(t *testing.T) { // Get convert the contact id to int contactID, err := strconv.Atoi(os.Getenv("DNSIMPLE_CONTACT_ID")) @@ -188,6 +235,16 @@ func testAccPreCheckRegisteredDomain(t *testing.T) { } } +func testAccPreCheckRegistrantChange(t *testing.T) { + test_utils.TestAccPreCheck(t) + if os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_CONTACT_ID") == "" { + t.Fatal("DNSIMPLE_REGISTRANT_CHANGE_CONTACT_ID must be set for acceptance tests") + } + if os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_DOMAIN") == "" { + t.Fatal("DNSIMPLE_REGISTRANT_CHANGE_DOMAIN must be set for acceptance tests") + } +} + func testAccRegisteredDomainImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[resourceName] @@ -223,6 +280,32 @@ func testAccCheckRegisteredDomainResourceDestroy(state *terraform.State) error { return nil } +func testAccCheckRegisteredDomainRegistrantChangeDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "dnsimple_registered_domain_contact" { + continue + } + + id, err := strconv.Atoi(rs.Primary.Attributes["id"]) + if err != nil { + return err + } + + if rs.Primary.Attributes["state"] == consts.RegistrantChangeStateCompleted || + rs.Primary.Attributes["state"] == consts.RegistrantChangeStateCancelled || + rs.Primary.Attributes["state"] == consts.RegistrantChangeStateCancelling { + continue + } + + _, err = dnsimpleClient.Registrar.DeleteRegistrantChange(context.Background(), testAccAccount, id) + + if err != nil { + return err + } + } + return nil +} + func testAccRegisteredDomainResourceConfig(domainName string, contactId int) string { return fmt.Sprintf(` resource "dnsimple_registered_domain" "test" { diff --git a/internal/framework/resources/registered_domain/schema.go b/internal/framework/resources/registered_domain/schema.go index b1db1af6..9f60f483 100644 --- a/internal/framework/resources/registered_domain/schema.go +++ b/internal/framework/resources/registered_domain/schema.go @@ -5,6 +5,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" @@ -31,6 +33,18 @@ type RegisteredDomainResource struct { config *common.DnsimpleProviderConfig } +// RegistrantChangeResourceModel describes the resource data model. +type RegistrantChangeResourceModel struct { + Id types.Int64 `tfsdk:"id"` + AccountId types.Int64 `tfsdk:"account_id"` + ContactId types.Int64 `tfsdk:"contact_id"` + DomainId types.String `tfsdk:"domain_id"` + State types.String `tfsdk:"state"` + ExtendedAttributes types.Map `tfsdk:"extended_attributes"` + RegistryOwnerChange types.Bool `tfsdk:"registry_owner_change"` + IrtLockLiftedBy types.String `tfsdk:"irt_lock_lifted_by"` +} + // DomainResourceModel describes the resource data model. type RegisteredDomainResourceModel struct { Name types.String `tfsdk:"name"` @@ -46,6 +60,7 @@ type RegisteredDomainResourceModel struct { ExtendedAttributes types.Map `tfsdk:"extended_attributes"` PremiumPrice types.String `tfsdk:"premium_price"` DomainRegistration types.Object `tfsdk:"domain_registration"` + RegistrantChange types.Object `tfsdk:"registrant_change"` Timeouts types.Object `tfsdk:"timeouts"` Id types.Int64 `tfsdk:"id"` } @@ -123,6 +138,55 @@ func (r *RegisteredDomainResource) Schema(_ context.Context, _ resource.SchemaRe DomainRegistrationState(), }, }, + "registrant_change": schema.SingleNestedAttribute{ + Description: "The registrant change details.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "id": common.IDInt64Attribute(), + "account_id": schema.Int64Attribute{ + MarkdownDescription: "DNSimple Account ID to which the registrant change belongs to", + Computed: true, + }, + "contact_id": schema.Int64Attribute{ + MarkdownDescription: "DNSimple contact ID for which the registrant change is being performed", + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "domain_id": schema.StringAttribute{ + MarkdownDescription: "DNSimple domain ID for which the registrant change is being performed", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "state": schema.StringAttribute{ + MarkdownDescription: "State of the registrant change", + PlanModifiers: []planmodifier.String{ + RegistrantChangeState(), + }, + Computed: true, + Optional: true, + }, + "extended_attributes": schema.MapAttribute{ + MarkdownDescription: "Extended attributes for the registrant change", + ElementType: types.StringType, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "registry_owner_change": schema.BoolAttribute{ + MarkdownDescription: "True if the registrant change will result in a registry owner change", + Computed: true, + }, + "irt_lock_lifted_by": schema.StringAttribute{ + MarkdownDescription: "Date when the registrant change lock was lifted for the domain", + Computed: true, + }, + }, + }, "timeouts": schema.SingleNestedAttribute{ MarkdownDescription: "Timeouts for operations, given as a parsable string as in `10m` or `30s`.", Optional: true, diff --git a/internal/framework/resources/registered_domain/update.go b/internal/framework/resources/registered_domain/update.go index b296add7..14ca696d 100644 --- a/internal/framework/resources/registered_domain/update.go +++ b/internal/framework/resources/registered_domain/update.go @@ -7,6 +7,7 @@ import ( "strconv" "github.com/dnsimple/dnsimple-go/dnsimple" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" @@ -35,19 +36,23 @@ func (r *RegisteredDomainResource) Update(ctx context.Context, req resource.Upda } if planData.ContactId.ValueInt64() != stateData.ContactId.ValueInt64() { - resp.Diagnostics.AddError( - fmt.Sprintf("contact_id change not supported: %s, %d", planData.Name.ValueString(), planData.Id.ValueInt64()), - "contact_id change not supported by the DNSimple API", - ) - return + if stateData.State.ValueString() != consts.DomainStateRegistered { + resp.Diagnostics.AddError( + fmt.Sprintf("contact_id change not supported: %s, %d", planData.Name.ValueString(), planData.Id.ValueInt64()), + "contact_id change not supported for domains that are not in registered state", + ) + return + } } if !planData.ExtendedAttributes.Equal(stateData.ExtendedAttributes) { - resp.Diagnostics.AddError( - fmt.Sprintf("extended_attributes change not supported: %s, %d", planData.Name.ValueString(), planData.Id.ValueInt64()), - "extended_attributes change not supported by the DNSimple API", - ) - return + if stateData.State.ValueString() != consts.DomainStateRegistered { + resp.Diagnostics.AddError( + fmt.Sprintf("extended_attributes change not supported: %s, %d", planData.Name.ValueString(), planData.Id.ValueInt64()), + "extended_attributes change not supported for domains that are not in registered state", + ) + return + } } domainRegistration, diags := getDomainRegistration(ctx, stateData) @@ -106,7 +111,104 @@ func (r *RegisteredDomainResource) Update(ctx context.Context, req resource.Upda } } - if planData.AutoRenewEnabled.ValueBool() != stateData.AutoRenewEnabled.ValueBool() { + registrantChange, diags := getRegistrantChange(ctx, stateData) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + var registrantChangeResponse *dnsimple.RegistrantChangeResponse + if planData.ContactId.ValueInt64() != stateData.ContactId.ValueInt64() { + if !registrantChange.Id.IsNull() { + convergenceState, _ := tryToConvergeRegistrantChange(ctx, planData, &resp.Diagnostics, r, int(registrantChange.Id.ValueInt64())) + if convergenceState == RegistrantChangeFailed { + // Response is already populated with the error we can safely return + return + } + + if convergenceState == RegistrantChangeConvergenceTimeout { + // We attempted to converge on the registrant change, but the registrant change was not ready + // user needs to run terraform again to try and converge the registrant change + + // Update the data with the current registrant change + registrantChangeResponse, err = r.config.Client.Registrar.GetRegistrantChange(ctx, r.config.AccountID, int(registrantChange.Id.ValueInt64())) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("failed to read DNSimple Registrant Change Id: %d", registrantChange.Id.ValueInt64()), + err.Error(), + ) + return + } + + registrantChangeObject, diags := r.registrantChangeAPIResponseToObject(ctx, registrantChangeResponse.Data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + planData.RegistrantChange = registrantChangeObject + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("registrant_change"), registrantChangeObject)...) + + // Exit with warning to prevent the state from being tainted + resp.Diagnostics.AddWarning( + "failed to converge on registrant change", + err.Error(), + ) + return + } + + } else { + // Create a new registrant change and handle any errors + createRegistrantChange(ctx, planData, r, resp) + } + } else if !registrantChange.Id.IsNull() && registrantChange.State.ValueString() != consts.RegistrantChangeStateCompleted { + registrantChangeResponse, err = r.config.Client.Registrar.GetRegistrantChange(ctx, r.config.AccountID, int(registrantChange.Id.ValueInt64())) + + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("failed to read DNSimple Registrant Change Id: %d", registrantChange.Id.ValueInt64()), + err.Error(), + ) + return + } + + if registrantChangeResponse.Data.State != consts.RegistrantChangeStateCompleted { + convergenceState, err := tryToConvergeRegistrantChange(ctx, planData, &resp.Diagnostics, r, int(registrantChange.Id.ValueInt64())) + if convergenceState == RegistrantChangeFailed { + // Response is already populated with the error we can safely return + return + } + + if convergenceState == RegistrantChangeConvergenceTimeout { + // We attempted to converge on the registrant change, but the registrant change was not ready + // user needs to run terraform again to try and converge the registrant change + + // Update the data with the current registrant change + registrantChangeObject, diags := r.registrantChangeAPIResponseToObject(ctx, registrantChangeResponse.Data) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + planData.RegistrantChange = registrantChangeObject + + // Exit with warning to prevent the state from being tainted + resp.Diagnostics.AddError( + "failed to converge on registrant change", + err.Error(), + ) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &planData)...) + return + } + } + } else { + // Use state data for registrant change if no changes are detected + planData.RegistrantChange = stateData.RegistrantChange + } + + if !(planData.AutoRenewEnabled.IsUnknown() || planData.AutoRenewEnabled.IsNull()) && planData.AutoRenewEnabled.ValueBool() != stateData.AutoRenewEnabled.ValueBool() { diags := r.setAutoRenewal(ctx, planData) if diags.HasError() { @@ -115,7 +217,7 @@ func (r *RegisteredDomainResource) Update(ctx context.Context, req resource.Upda } } - if planData.WhoisPrivacyEnabled.ValueBool() != stateData.WhoisPrivacyEnabled.ValueBool() { + if !(planData.WhoisPrivacyEnabled.IsUnknown() || planData.WhoisPrivacyEnabled.IsNull()) && planData.WhoisPrivacyEnabled.ValueBool() != stateData.WhoisPrivacyEnabled.ValueBool() { diags := r.setWhoisPrivacy(ctx, planData) if diags.HasError() { @@ -124,7 +226,7 @@ func (r *RegisteredDomainResource) Update(ctx context.Context, req resource.Upda } } - if planData.DNSSECEnabled.ValueBool() != stateData.DNSSECEnabled.ValueBool() { + if !(planData.DNSSECEnabled.IsUnknown() || planData.DNSSECEnabled.IsNull()) && planData.DNSSECEnabled.ValueBool() != stateData.DNSSECEnabled.ValueBool() { diags := r.setDNSSEC(ctx, planData) if diags.HasError() { @@ -133,7 +235,7 @@ func (r *RegisteredDomainResource) Update(ctx context.Context, req resource.Upda } } - if planData.TransferLockEnabled.ValueBool() != stateData.TransferLockEnabled.ValueBool() { + if !(planData.TransferLockEnabled.IsUnknown() || planData.TransferLockEnabled.IsNull()) && planData.TransferLockEnabled.ValueBool() != stateData.TransferLockEnabled.ValueBool() { diags := r.setTransferLock(ctx, planData) if diags.HasError() { diff --git a/tools/sweep/main.go b/tools/sweep/main.go index 46d63f8b..b6458fce 100644 --- a/tools/sweep/main.go +++ b/tools/sweep/main.go @@ -2,7 +2,9 @@ package main import ( "context" + "fmt" "os" + "strings" "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" @@ -11,6 +13,11 @@ import ( func main() { token := os.Getenv("DNSIMPLE_TOKEN") account := os.Getenv("DNSIMPLE_ACCOUNT") + sandbox := os.Getenv("DNSIMPLE_SANDBOX") + + if sandbox != "true" { + panic("DNSIMPLE_SANDBOX must be set to true") + } dnsimpleClient := dnsimple.NewClient(dnsimple.StaticTokenHTTPClient(context.Background(), token)) dnsimpleClient.UserAgent = "terraform-provider-dnsimple/test" @@ -35,4 +42,170 @@ func main() { } } } + + cancelAllContactChanges(context.Background(), dnsimpleClient, account) + cleanupDomains(context.Background(), dnsimpleClient, account) +} + +var ( + // RegistrantChangeCancelStates is a list of states that can be cancelled + RegistrantChangeCancelStates = []string{ + consts.RegistrantChangeStateNew, + consts.RegistrantChangeStatePending, + } +) + +func cancelAllContactChanges(ctx context.Context, dnsimpleClient *dnsimple.Client, account string) { + domainName := os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_DOMAIN") + + if domainName == "" { + fmt.Println("Skipping registrant change cleanup as DNSIMPLE_REGISTRANT_CHANGE_DOMAIN is not set") + return + } + + // Get the domain ID + domainResponse, err := dnsimpleClient.Domains.GetDomain(ctx, account, domainName) + if err != nil { + panic(err) + } + + listOptions := &dnsimple.RegistrantChangeListOptions{ + State: dnsimple.String(consts.RegistrantChangeStateNew), + ListOptions: dnsimple.ListOptions{ + PerPage: dnsimple.Int(100), + }, + } + + contactChanges, err := dnsimpleClient.Registrar.ListRegistrantChange(ctx, account, listOptions) + if err != nil { + panic(err) + } + + for _, contactChange := range contactChanges.Data { + if !contains(RegistrantChangeCancelStates, contactChange.State) { + continue + } + + if contactChange.DomainId != int(domainResponse.Data.ID) { + continue + } + + fmt.Printf("Cancelling registrant change for %s id=%d state=%s\n", domainName, contactChange.Id, contactChange.State) + _, err := dnsimpleClient.Registrar.DeleteRegistrantChange(ctx, account, contactChange.Id) + if err != nil { + panic(err) + } + } + + if contactChanges.Pagination.TotalPages > 1 { + for page := 2; page <= contactChanges.Pagination.TotalPages; page++ { + listOptions := &dnsimple.RegistrantChangeListOptions{ + State: dnsimple.String(consts.RegistrantChangeStateNew), + ListOptions: dnsimple.ListOptions{ + Page: dnsimple.Int(page), + PerPage: dnsimple.Int(100), + }, + } + + contactChanges, err := dnsimpleClient.Registrar.ListRegistrantChange(ctx, account, listOptions) + if err != nil { + panic(err) + } + + for _, contactChange := range contactChanges.Data { + if !contains(RegistrantChangeCancelStates, contactChange.State) { + continue + } + + if contactChange.DomainId != int(domainResponse.Data.ID) { + continue + } + + fmt.Printf("Cancelling registrant change for %s id=%d state=%s\n", domainName, contactChange.Id, contactChange.State) + _, err := dnsimpleClient.Registrar.DeleteRegistrantChange(ctx, account, contactChange.Id) + if err != nil { + panic(err) + } + } + } + } +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + + return false +} + +func cleanupDomains(ctx context.Context, dnsimpleClient *dnsimple.Client, account string) { + cleanupEnabled := os.Getenv("DNSIMPLE_CLEANUP_DOMAINS") + + if cleanupEnabled != "true" { + fmt.Println("Skipping domain cleanup as DNSIMPLE_CLEANUP_DOMAIN is not set") + return + } + + domainsToKeep := os.Getenv("DNSIMPLE_DOMAINS_TO_KEEP") + var ( + domainsToKeepList []string + ) + if domainsToKeep != "" { + // Split the comma separated list of domains to keep + domainsToKeepList = strings.Split(domainsToKeep, ",") + } + + listOptions := &dnsimple.DomainListOptions{ + ListOptions: dnsimple.ListOptions{ + PerPage: dnsimple.Int(100), + }, + } + + domains, err := dnsimpleClient.Domains.ListDomains(ctx, account, listOptions) + if err != nil { + panic(err) + } + + for _, domain := range domains.Data { + if contains(domainsToKeepList, domain.Name) { + continue + } + + fmt.Printf("Deleting domain %s\n", domain.Name) + _, err := dnsimpleClient.Domains.DeleteDomain(ctx, account, domain.Name) + if err != nil { + panic(err) + } + } + + if domains.Pagination.TotalPages > 1 { + for page := 2; page <= domains.Pagination.TotalPages; page++ { + listOptions := &dnsimple.DomainListOptions{ + ListOptions: dnsimple.ListOptions{ + Page: dnsimple.Int(page), + PerPage: dnsimple.Int(100), + }, + } + + domains, err := dnsimpleClient.Domains.ListDomains(ctx, account, listOptions) + if err != nil { + panic(err) + } + + for _, domain := range domains.Data { + if contains(domainsToKeepList, domain.Name) { + continue + } + + fmt.Printf("Deleting domain %s\n", domain.Name) + _, err := dnsimpleClient.Domains.DeleteDomain(ctx, account, domain.Name) + if err != nil { + panic(err) + } + } + } + } }