├── example ├── secret.auto.tfvars.example └── main.tf ├── terraform-registry-manifest.json ├── .github ├── CODE_OF_CONDUCT.md ├── dependabot.yml ├── workflows │ ├── release.yml │ └── test.yml └── ISSUE_TEMPLATE │ └── generic-issue.md ├── .gitignore ├── tools ├── tools.go └── sweep │ └── main.go ├── internal ├── framework │ ├── common │ │ ├── provider.go │ │ ├── schema.go │ │ ├── timeouts.go │ │ ├── zone_record_cache_test.go │ │ ├── domain_registration.go │ │ └── zone_record_cache.go │ ├── resources │ │ ├── registered_domain │ │ │ ├── delete.go │ │ │ ├── configure.go │ │ │ ├── import.go │ │ │ ├── read.go │ │ │ ├── modifiers.go │ │ │ ├── create.go │ │ │ └── schema.go │ │ ├── provider_helpers_test.go │ │ ├── domain_resource_test.go │ │ ├── email_forward_resource_test.go │ │ ├── zone_resource_test.go │ │ ├── domain_ds_record_resource_test.go │ │ ├── domain_delegation_resource_test.go │ │ ├── contact_resource_test.go │ │ ├── lets_encrypt_certificate_resource_test.go │ │ ├── domain_resource.go │ │ ├── domain_delegation_resource.go │ │ ├── email_forward_resource.go │ │ └── domain_ds_record_resource.go │ ├── utils │ │ ├── utils_test.go │ │ └── utils.go │ ├── datasources │ │ ├── zone_data_source_test.go │ │ ├── registrant_change_check_data_source_test.go │ │ ├── certificate_data_source_test.go │ │ ├── zone_data_source.go │ │ └── registrant_change_check_data_source.go │ ├── modifiers │ │ ├── int64planmodifiers.go │ │ ├── stringplanmodifiers.go │ │ ├── setplanmodifier.go │ │ ├── int64planmodifiers_test.go │ │ ├── stringplanmodifiers_test.go │ │ └── setplanmodifier_test.go │ ├── validators │ │ ├── duration.go │ │ ├── record_type.go │ │ ├── domain_name.go │ │ └── record_type_test.go │ ├── provider │ │ └── provider_test.go │ └── test_utils │ │ └── utils.go └── consts │ └── provider.go ├── scripts ├── gogetcookie.sh ├── errcheck.sh └── changelog-links.sh ├── docs ├── data-sources │ ├── zone.md │ ├── certificate.md │ └── registrant_change_check.md ├── resources │ ├── domain_delegation.md │ ├── domain.md │ ├── email_forward.md │ ├── zone.md │ ├── ds_record.md │ ├── lets_encrypt_certificate.md │ ├── zone_record.md │ ├── contact.md │ └── registered_domain.md ├── index.md └── guides │ └── resource-migration.md ├── GNUmakefile ├── main.go ├── RELEASING.md ├── .goreleaser.yml ├── CONTRIBUTING.md ├── README.md └── go.mod /example/secret.auto.tfvars.example: -------------------------------------------------------------------------------- 1 | dnsimple_account = "123" 2 | dnsimple_token = "123CDF567" 3 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local setup 2 | .tool-versions 3 | 4 | # Examples 5 | example/**/.terraform* 6 | example/**/*.tfstate 7 | example/**/*.tfvars 8 | 9 | # Test exclusions 10 | !command/test-fixtures/**/*.tfstate 11 | !command/test-fixtures/**/.terraform/ 12 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build tools 5 | // +build tools 6 | 7 | package tools 8 | 9 | import ( 10 | // document generation 11 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/framework/common/provider.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 5 | ) 6 | 7 | type DnsimpleProviderConfig struct { 8 | Client *dnsimple.Client 9 | AccountID string 10 | Prefetch bool 11 | ZoneRecordCache ZoneRecordCache 12 | } 13 | -------------------------------------------------------------------------------- /scripts/gogetcookie.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | touch ~/.gitcookies 4 | chmod 0600 ~/.gitcookies 5 | 6 | git config --global http.cookiefile ~/.gitcookies 7 | 8 | tr , \\t <<\__END__ >>~/.gitcookies 9 | .googlesource.com,TRUE,/,TRUE,2147483647,o,git-paul.hashicorp.com=1/z7s05EYPudQ9qoe6dMVfmAVwgZopEkZBb1a2mA5QtHE 10 | __END__ 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | open-pull-requests-limit: 10 9 | labels: 10 | - task 11 | - dependencies 12 | 13 | - package-ecosystem: github-actions 14 | directory: / 15 | schedule: 16 | interval: weekly 17 | labels: 18 | - task 19 | - dependencies 20 | -------------------------------------------------------------------------------- /internal/framework/resources/registered_domain/delete.go: -------------------------------------------------------------------------------- 1 | package registered_domain 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource" 8 | "github.com/hashicorp/terraform-plugin-log/tflog" 9 | ) 10 | 11 | func (r *RegisteredDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 12 | var data *RegisteredDomainResourceModel 13 | 14 | // Read Terraform prior state data into the model 15 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 16 | 17 | if resp.Diagnostics.HasError() { 18 | return 19 | } 20 | 21 | tflog.Warn(ctx, fmt.Sprintf("Removing DNSimple Registered Domain from Terraform state only: %s, %s", data.Name, data.Id)) 22 | } 23 | -------------------------------------------------------------------------------- /scripts/errcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check gofmt 4 | echo "==> Checking for unchecked errors..." 5 | 6 | if ! which errcheck > /dev/null; then 7 | echo "==> Installing errcheck..." 8 | go get -u github.com/kisielk/errcheck 9 | fi 10 | 11 | err_files=$(errcheck -ignoretests \ 12 | -ignore 'github.com/hashicorp/terraform/helper/schema:Set' \ 13 | -ignore 'bytes:.*' \ 14 | -ignore 'io:Close|Write' \ 15 | $(go list ./...| grep -v /vendor/)) 16 | 17 | if [[ -n ${err_files} ]]; then 18 | echo 'Unchecked errors found in the following places:' 19 | echo "${err_files}" 20 | echo "Please handle returned errors. You can check directly with \`make errcheck\`" 21 | exit 1 22 | fi 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /internal/framework/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" 8 | ) 9 | 10 | func TestHasUnicodeChars(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | s string 14 | want bool 15 | }{ 16 | { 17 | name: "empty string", 18 | s: "", 19 | want: false, 20 | }, 21 | { 22 | name: "ascii string", 23 | s: "hello-world", 24 | want: false, 25 | }, 26 | { 27 | name: "unicode string", 28 | s: "hello-世界", 29 | want: true, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | assert.Equal(t, tt.want, utils.HasUnicodeChars(tt.s)) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.12" 3 | 4 | required_providers { 5 | dnsimple = { 6 | source = "dnsimple/dnsimple" 7 | version = ">= 1.9.0" 8 | } 9 | } 10 | } 11 | 12 | provider "dnsimple" { 13 | token = var.dnsimple_token 14 | account = var.dnsimple_account 15 | sandbox = true 16 | } 17 | 18 | 19 | variable "dnsimple_token" { 20 | description = "DNSimple API Token" 21 | type = string 22 | sensitive = true 23 | } 24 | 25 | variable "dnsimple_account" { 26 | description = "DNSimple Account ID" 27 | type = string 28 | } 29 | 30 | 31 | # Create a record. 32 | resource "dnsimple_zone_record" "record_1755513796" { 33 | zone_name = "example.com" 34 | name = "tf" 35 | value = "Hello Terraform!" 36 | type = "TXT" 37 | } 38 | -------------------------------------------------------------------------------- /internal/framework/common/schema.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 5 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 8 | ) 9 | 10 | func IDStringAttribute() schema.StringAttribute { 11 | return schema.StringAttribute{ 12 | Computed: true, 13 | PlanModifiers: []planmodifier.String{ 14 | stringplanmodifier.UseStateForUnknown(), 15 | }, 16 | } 17 | } 18 | 19 | func IDInt64Attribute() schema.Int64Attribute { 20 | return schema.Int64Attribute{ 21 | Computed: true, 22 | PlanModifiers: []planmodifier.Int64{ 23 | int64planmodifier.UseStateForUnknown(), 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/consts/provider.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | BaseURLSandbox = "https://api.sandbox.dnsimple.com" 5 | 6 | // Certificate states 7 | CertificateStateCancelled = "cancelled" 8 | CertificateStateFailed = "failed" 9 | CertificateStateIssued = "issued" 10 | CertificateStateRefunded = "refunded" 11 | 12 | // Domain states 13 | DomainStateRegistered = "registered" 14 | DomainStateHosted = "hosted" 15 | DomainStateNew = "new" 16 | DomainStateFailed = "failed" 17 | DomainStateCancelling = "cancelling" 18 | DomainStateCancelled = "cancelled" 19 | 20 | // Domain Registrant Change (Contact change) states 21 | RegistrantChangeStateNew = "new" 22 | RegistrantChangeStatePending = "pending" 23 | RegistrantChangeStateCompleted = "completed" 24 | RegistrantChangeStateCancelling = "cancelling" 25 | RegistrantChangeStateCancelled = "cancelled" 26 | ) 27 | -------------------------------------------------------------------------------- /internal/framework/resources/registered_domain/configure.go: -------------------------------------------------------------------------------- 1 | package registered_domain 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource" 8 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 9 | ) 10 | 11 | func (r *RegisteredDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 12 | // Prevent panic if the provider has not been configured. 13 | if req.ProviderData == nil { 14 | return 15 | } 16 | 17 | config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) 18 | 19 | if !ok { 20 | resp.Diagnostics.AddError( 21 | "Unexpected Resource Configure Type", 22 | fmt.Sprintf("Expected *common.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), 23 | ) 24 | 25 | return 26 | } 27 | 28 | r.config = config 29 | } 30 | -------------------------------------------------------------------------------- /docs/data-sources/zone.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_zone" 3 | --- 4 | 5 | # dnsimple\_zone 6 | 7 | Get information about a DNSimple zone. 8 | 9 | It is generally preferable to use the `dnsimple_zone` resource, but you may wish to only retrieve and link the zone information when the resource exists in multiple Terraform projects. 10 | 11 | ## Example Usage 12 | 13 | ```hcl 14 | data "dnsimple_zone" "example" { 15 | name = "example.com" 16 | } 17 | ``` 18 | 19 | ## Argument Reference 20 | 21 | The following arguments are supported: 22 | 23 | - `name` - (Required) The name of the zone. 24 | 25 | ## Attributes Reference 26 | 27 | The following attributes are exported: 28 | 29 | - `id` - The zone ID. 30 | - `account_id` - The account ID. 31 | - `reverse` - Whether the zone is a reverse zone (`true`) or forward zone (`false`). 32 | - `secondary` - Whether the zone is a secondary zone. 33 | - `active` - Whether the zone is active. 34 | - `last_transferred_at` - The last time the zone was transferred (only applicable for secondary zones). 35 | -------------------------------------------------------------------------------- /scripts/changelog-links.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to 4 | # be Markdown links to the given github issues. 5 | # 6 | # This is run during releases so that the issue references in all of the 7 | # released items are presented as clickable links, but we can just use the 8 | # easy [GH-nnnn] shorthand for quickly adding items to the "Unrelease" section 9 | # while merging things between releases. 10 | 11 | set -e 12 | 13 | if [[ ! -f CHANGELOG.md ]]; then 14 | echo "ERROR: CHANGELOG.md not found in pwd." 15 | echo "Please run this from the root of the terraform provider repository" 16 | exit 1 17 | fi 18 | 19 | if [[ `uname` == "Darwin" ]]; then 20 | echo "Using BSD sed" 21 | SED="sed -i.bak -E -e" 22 | else 23 | echo "Using GNU sed" 24 | SED="sed -i.bak -r -e" 25 | fi 26 | 27 | PROVIDER_URL="https:\/\/github.com\/terraform-providers\/terraform-provider-dnsimple\/issues" 28 | 29 | $SED "s/GH-([0-9]+)/\[#\1\]\($PROVIDER_URL\/\1\)/g" -e 's/\[\[#(.+)([0-9])\)]$/(\[#\1\2))/g' CHANGELOG.md 30 | 31 | rm CHANGELOG.md.bak 32 | -------------------------------------------------------------------------------- /internal/framework/common/timeouts.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/types" 7 | ) 8 | 9 | type Timeouts struct { 10 | Create types.String `tfsdk:"create"` 11 | Update types.String `tfsdk:"update"` 12 | Delete types.String `tfsdk:"delete"` 13 | } 14 | 15 | func (t Timeouts) CreateDuration() time.Duration { 16 | if !t.Create.IsUnknown() && !t.Create.IsNull() { 17 | d, _ := time.ParseDuration(t.Create.ValueString()) 18 | return d 19 | } 20 | 21 | d, _ := time.ParseDuration("30s") 22 | return d 23 | } 24 | 25 | func (t Timeouts) UpdateDuration() time.Duration { 26 | if !t.Update.IsUnknown() && !t.Update.IsNull() { 27 | d, _ := time.ParseDuration(t.Update.ValueString()) 28 | return d 29 | } 30 | 31 | d, _ := time.ParseDuration("30s") 32 | return d 33 | } 34 | 35 | func (t Timeouts) DeleteDuration() time.Duration { 36 | if !t.Delete.IsUnknown() && !t.Delete.IsNull() { 37 | d, _ := time.ParseDuration(t.Delete.ValueString()) 38 | return d 39 | } 40 | 41 | d, _ := time.ParseDuration("30s") 42 | return d 43 | } 44 | -------------------------------------------------------------------------------- /docs/data-sources/certificate.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_certificate" 3 | --- 4 | 5 | # dnsimple\_certificate 6 | 7 | Get information about a DNSimple SSL certificate. 8 | 9 | ## Example Usage 10 | 11 | ```hcl 12 | data "dnsimple_certificate" "example" { 13 | domain = "example.com" 14 | certificate_id = 1234 15 | } 16 | ``` 17 | 18 | ## Argument Reference 19 | 20 | The following arguments are supported: 21 | 22 | - `domain` - (Required) The domain name of the SSL certificate. 23 | - `certificate_id` - (Required) The ID of the SSL certificate. 24 | - `timeouts` - (Block, Optional) (see [below for nested schema](#nested-schema-for-timeouts)) 25 | 26 | ## Attributes Reference 27 | 28 | The following attributes are exported: 29 | 30 | - `id` - The certificate ID. 31 | - `server_certificate` - The SSL certificate. 32 | - `root_certificate` - The root certificate of the issuing CA. 33 | - `certificate_chain` - A list of certificates that make up the certificate chain. 34 | - `private_key` - The corresponding private key for the SSL certificate. 35 | 36 | ### Nested Schema for `timeouts` 37 | 38 | Optional: 39 | 40 | - `read` (String) - The timeout for the read operation, e.g., `5m`. 41 | -------------------------------------------------------------------------------- /internal/framework/datasources/zone_data_source_test.go: -------------------------------------------------------------------------------- 1 | package datasources_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" 10 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 11 | ) 12 | 13 | func TestAccZoneDataSource(t *testing.T) { 14 | resource.Test(t, resource.TestCase{ 15 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 16 | ProtoV6ProviderFactories: provider.NewProto6ProviderFactory(), 17 | Steps: []resource.TestStep{ 18 | // Read testing 19 | { 20 | Config: testAccZoneDataSourceConfig(os.Getenv("DNSIMPLE_DOMAIN")), 21 | Check: resource.ComposeAggregateTestCheckFunc( 22 | resource.TestCheckResourceAttr("data.dnsimple_zone.test", "reverse", "false"), 23 | resource.TestCheckResourceAttrSet("data.dnsimple_zone.test", "id"), 24 | ), 25 | }, 26 | }, 27 | }) 28 | } 29 | 30 | func testAccZoneDataSourceConfig(domainName string) string { 31 | return fmt.Sprintf(` 32 | data "dnsimple_zone" "test" { 33 | name = %[1]q 34 | }`, domainName) 35 | } 36 | -------------------------------------------------------------------------------- /docs/resources/domain_delegation.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_domain_delegation" 3 | --- 4 | 5 | # dnsimple\_domain\_delegation 6 | 7 | Provides a DNSimple domain delegation resource. 8 | 9 | This resource allows you to control the delegation records (name servers) for a domain. 10 | 11 | ~> **Warning:** This resource currently only supports the management of domains that are registered with DNSimple. 12 | 13 | -> **Note:** When this resource is destroyed, only the Terraform state is removed; the domain delegation is left intact and unmanaged by Terraform. 14 | 15 | ## Example Usage 16 | 17 | ```hcl 18 | resource "dnsimple_domain_delegation" "example" { 19 | domain = "example.com" 20 | name_servers = ["ns1.example.org", "ns2.example.com"] 21 | } 22 | ``` 23 | 24 | ## Argument Reference 25 | 26 | The following arguments are supported: 27 | 28 | - `domain` - (Required) The domain name. 29 | - `name_servers` - (Required) List of name servers to delegate to. 30 | 31 | ## Attributes Reference 32 | 33 | - `id` - The domain name. 34 | 35 | ## Import 36 | 37 | DNSimple domain delegations can be imported using the domain name. 38 | 39 | ```bash 40 | terraform import dnsimple_domain_delegation.example example.com 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/resources/domain.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_domain" 3 | --- 4 | 5 | # dnsimple\_domain 6 | 7 | Provides a DNSimple domain resource. 8 | 9 | ## Example Usage 10 | 11 | ```hcl 12 | resource "dnsimple_domain" "example" { 13 | name = "example.com" 14 | } 15 | ``` 16 | 17 | ## Argument Reference 18 | 19 | The following arguments are supported: 20 | 21 | - `name` - (Required) The domain name to be created. 22 | 23 | ## Attributes Reference 24 | 25 | - `id` - The ID of this resource. 26 | - `account_id` - The account ID for the domain. 27 | - `auto_renew` - Whether the domain is set to auto-renew. 28 | - `private_whois` - Whether the domain has WhoIs privacy enabled. 29 | - `registrant_id` - The ID of the registrant (contact) for the domain. 30 | - `state` - The state of the domain. 31 | - `unicode_name` - The domain name in Unicode format. 32 | 33 | ## Import 34 | 35 | DNSimple domains can be imported using the domain name. 36 | 37 | ```bash 38 | terraform import dnsimple_domain.example example.com 39 | ``` 40 | 41 | The domain name can be found within the [DNSimple Domains API](https://developer.dnsimple.com/v2/domains/#listDomains). Check out [Authentication](https://developer.dnsimple.com/v2/#authentication) in API Overview for available options. 42 | -------------------------------------------------------------------------------- /internal/framework/modifiers/int64planmodifiers.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/types" 9 | ) 10 | 11 | type int64DefaultValue struct { 12 | val int64 13 | } 14 | 15 | // DefaultValue return a bool plan modifier that sets the specified value if the planned value is Null. 16 | func Int64DefaultValue(i int64) planmodifier.Int64 { 17 | return int64DefaultValue{ 18 | val: i, 19 | } 20 | } 21 | 22 | func (m int64DefaultValue) Description(context.Context) string { 23 | return fmt.Sprintf("If value is not configured, defaults to %d", m.val) 24 | } 25 | 26 | func (m int64DefaultValue) MarkdownDescription(ctx context.Context) string { 27 | return m.Description(ctx) 28 | } 29 | 30 | func (m int64DefaultValue) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { 31 | if !req.ConfigValue.IsNull() { 32 | return 33 | } 34 | 35 | // If the attribute plan is "known" and "not null", then a previous plan modifier in the sequence 36 | // has already been applied, and we don't want to interfere. 37 | if !req.PlanValue.IsUnknown() && !req.PlanValue.IsNull() { 38 | return 39 | } 40 | 41 | resp.PlanValue = types.Int64Value(m.val) 42 | } 43 | -------------------------------------------------------------------------------- /internal/framework/modifiers/stringplanmodifiers.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/types" 9 | ) 10 | 11 | type stringDefaultValue struct { 12 | val string 13 | } 14 | 15 | // StringDefaultValue return a string plan modifier that sets the specified value if the planned value is Null. 16 | func StringDefaultValue(s string) planmodifier.String { 17 | return stringDefaultValue{ 18 | val: s, 19 | } 20 | } 21 | 22 | func (m stringDefaultValue) Description(context.Context) string { 23 | return fmt.Sprintf("If value is not configured, defaults to %s", m.val) 24 | } 25 | 26 | func (m stringDefaultValue) MarkdownDescription(ctx context.Context) string { 27 | return m.Description(ctx) 28 | } 29 | 30 | func (m stringDefaultValue) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { 31 | if !req.ConfigValue.IsNull() { 32 | return 33 | } 34 | 35 | // If the attribute plan is "known" and "not null", then a previous plan modifier in the sequence 36 | // has already been applied, and we don't want to interfere. 37 | if !req.PlanValue.IsUnknown() && !req.PlanValue.IsNull() { 38 | return 39 | } 40 | 41 | resp.PlanValue = types.StringValue(m.val) 42 | } 43 | -------------------------------------------------------------------------------- /internal/framework/modifiers/setplanmodifier.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 8 | "github.com/hashicorp/terraform-plugin-framework/types" 9 | ) 10 | 11 | type setTrimSuffix struct{} 12 | 13 | func SetTrimSuffixValue() planmodifier.Set { 14 | return setTrimSuffix{} 15 | } 16 | 17 | func (m setTrimSuffix) Description(context.Context) string { 18 | return "Trim suffix from configuration value when comparing with state" 19 | } 20 | 21 | func (m setTrimSuffix) MarkdownDescription(ctx context.Context) string { 22 | return m.Description(ctx) 23 | } 24 | 25 | func (m setTrimSuffix) PlanModifySet(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { 26 | if req.ConfigValue.IsNull() || req.PlanValue.IsUnknown() || req.PlanValue.IsNull() { 27 | return 28 | } 29 | 30 | var planValue []string 31 | resp.Diagnostics.Append(req.PlanValue.ElementsAs(ctx, &planValue, false)...) 32 | if resp.Diagnostics.HasError() { 33 | return 34 | } 35 | 36 | for i, element := range planValue { 37 | planValue[i] = strings.TrimSuffix(element, ".") 38 | } 39 | 40 | serializedPlanValue, diags := types.SetValueFrom(ctx, types.StringType, planValue) 41 | resp.Diagnostics.Append(diags...) 42 | 43 | resp.PlanValue = serializedPlanValue 44 | } 45 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | TEST ?= ./internal/... 2 | WEBSITE_REPO = github.com/hashicorp/terraform-website 3 | PKG_NAME = dnsimple 4 | HOSTNAME = registry.terraform.io 5 | NAMESPACE = dnsimple 6 | BINARY = terraform-provider-${PKG_NAME} 7 | VERSION = $(shell git describe --tags --always | cut -c 2-) 8 | OS_ARCH := $(shell echo "$$(uname -s)_$$(go env GOARCH)" | tr A-Z a-z) 9 | 10 | .PHONY: default 11 | default: build 12 | 13 | .PHONY: build 14 | build: fmtcheck 15 | go install 16 | 17 | .PHONY: install 18 | install: build 19 | mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${PKG_NAME}/${VERSION}/${OS_ARCH} 20 | mv ${GOPATH}/bin/${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${PKG_NAME}/${VERSION}/${OS_ARCH} 21 | 22 | .PHONY: test 23 | test: fmtcheck 24 | go test $(TEST) $(TESTARGS) 25 | 26 | .PHONY: testacc 27 | testacc: fmtcheck 28 | TF_ACC=1 go test $(TEST) $(TESTARGS) -timeout 10m $(ARGS) 29 | 30 | .PHONY: sweep 31 | sweep: 32 | go run $(CURDIR)/tools/sweep/main.go 33 | 34 | .PHONY: fmt 35 | fmt: 36 | gofumpt -l -w . 37 | 38 | .PHONY: fmtcheck 39 | fmtcheck: 40 | @test -z "$$(gofumpt -d -e . | tee /dev/stderr)" 41 | 42 | .PHONY: errcheck 43 | errcheck: 44 | @sh -c "'$(CURDIR)/scripts/errcheck.sh'" 45 | 46 | .PHONY: website 47 | website: 48 | @echo "Use this site to preview markdown rendering: https://registry.terraform.io/tools/doc-preview" 49 | -------------------------------------------------------------------------------- /docs/resources/email_forward.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_email_forward" 3 | --- 4 | 5 | # dnsimple\_email\_forward 6 | 7 | Provides a DNSimple email forward resource. 8 | 9 | ## Example Usage 10 | 11 | ```hcl 12 | resource "dnsimple_email_forward" "example" { 13 | domain = "example.com" 14 | alias_name = "sales" 15 | destination_email = "alice@example.com" 16 | } 17 | ``` 18 | 19 | ## Argument Reference 20 | 21 | The following arguments are supported: 22 | 23 | - `domain` - (Required) The domain name to add the email forwarding rule to. 24 | - `alias_name` - (Required) The name part (the part before the @) of the source email address on the domain. 25 | - `destination_email` - (Required) The destination email address. 26 | 27 | ## Attributes Reference 28 | 29 | The following attributes are exported: 30 | 31 | - `id` - The email forward ID. 32 | - `alias_email` - The source email address on the domain, in full form. This is a computed attribute. 33 | 34 | ## Import 35 | 36 | DNSimple email forwards can be imported using the domain name and numeric email forward ID in the format `domain_name_email_forward_id`. 37 | 38 | ```bash 39 | terraform import dnsimple_email_forward.example example.com_1234 40 | ``` 41 | 42 | The email forward ID can be found via the [DNSimple Email Forwards API](https://developer.dnsimple.com/v2/email-forwards/#listEmailForwards). 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 9 | 10 | framework "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" 11 | ) 12 | 13 | // Run "go generate" to format example terraform files and generate the docs for the registry/website 14 | 15 | // If you do not have terraform installed, you can remove the formatting command, but its suggested to 16 | // ensure the documentation is formatted properly. 17 | //go:generate terraform fmt -recursive ./example/ 18 | 19 | // Run the docs generation tool, check its repository for more information on how it works and how docs 20 | // can be customized. 21 | //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs 22 | 23 | // version is the version of the provider. 24 | var version = "dev" 25 | 26 | func main() { 27 | var debugMode bool 28 | 29 | flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve") 30 | flag.Parse() 31 | 32 | ctx := context.Background() 33 | serveOpts := providerserver.ServeOpts{ 34 | Address: "registry.terraform.io/dnsimple/dnsimple", 35 | Debug: debugMode, 36 | } 37 | 38 | err := providerserver.Serve(ctx, framework.New(version), serveOpts) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | This document describes the steps to release a new version of the Terraform DNSimple Provider. 4 | 5 | ## Prerequisites 6 | 7 | - You have commit access to the repository 8 | - You have push access to the repository 9 | - You have a GPG key configured for signing tags 10 | 11 | ## Release process 12 | 13 | The following instructions use `$VERSION` as a placeholder, where `$VERSION` is a `MAJOR.MINOR.BUGFIX` release such as `1.2.0`. 14 | 15 | 1. **Run the test suite** and ensure all the tests pass 16 | 17 | ```shell 18 | make test 19 | ``` 20 | 21 | 2. **Finalize the changelog** with the new version 22 | 23 | Edit `CHANGELOG.md` and finalize the `## main` section, assigning the version. 24 | 25 | 3. **Commit and push the changes** 26 | 27 | ```shell 28 | git commit -a -m "Release $VERSION" 29 | git push origin main 30 | ``` 31 | 32 | 4. **Wait for CI to complete** 33 | 34 | Ensure the CI build passes on the main branch before proceeding. 35 | 36 | 5. **Create a signed tag** 37 | 38 | ```shell 39 | git tag -a v$VERSION -s -m "Release $VERSION" 40 | git push origin --tags 41 | ``` 42 | 43 | 6. **CI and goreleaser will handle the rest** 44 | 45 | The CI workflow will automatically build and publish the release using goreleaser. 46 | 47 | ## Post-release 48 | 49 | - Verify the new version appears on the [Terraform Registry](https://registry.terraform.io/providers/dnsimple/dnsimple) 50 | -------------------------------------------------------------------------------- /internal/framework/resources/provider_helpers_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 5 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 6 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 7 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" 8 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 9 | ) 10 | 11 | var ( 12 | // testAccProtoV6ProviderFactories are used to instantiate a provider during 13 | // acceptance testing. The factory function will be invoked for every Terraform 14 | // CLI command executed to create a provider server to which the CLI can 15 | // reattach. 16 | testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ 17 | "dnsimple": providerserver.NewProtocol6WithError(provider.New("test")()), 18 | } 19 | 20 | // dnsimpleClient is the DNSimple client used to make API calls during 21 | // acceptance testing. 22 | dnsimpleClient *dnsimple.Client 23 | // testAccAccount is the DNSimple account used to make API calls during 24 | // acceptance testing. 25 | testAccAccount string 26 | ) 27 | 28 | func init() { 29 | // If we are running acceptance tests TC_ACC then we initialize the DNSimple client 30 | // with the credentials provided in the environment variables. 31 | dnsimpleClient, testAccAccount = test_utils.LoadDNSimpleTestClient() 32 | } 33 | -------------------------------------------------------------------------------- /internal/framework/common/zone_record_cache_test.go: -------------------------------------------------------------------------------- 1 | package common_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 9 | ) 10 | 11 | func TestZoneRecordCache(t *testing.T) { 12 | t.Parallel() 13 | 14 | // Set up a test cache. 15 | cache := common.NewZoneRecordCache() 16 | 17 | // Prepare dnsimple.ZoneRecord set to add to cache. 18 | records := []dnsimple.ZoneRecord{ 19 | { 20 | ID: 1, 21 | Name: "a", 22 | Content: "1.2.3.4", 23 | Type: "A", 24 | }, 25 | { 26 | ID: 2, 27 | Name: "a", 28 | Content: "1.2.3.5", 29 | Type: "A", 30 | }, 31 | { 32 | ID: 3, 33 | Name: "a", 34 | Content: "mail.example.com", 35 | Type: "MX", 36 | }, 37 | { 38 | ID: 4, 39 | Name: "b", 40 | Content: "1.2.3.4", 41 | Type: "A", 42 | }, 43 | } 44 | 45 | zoneName := "example.com" 46 | 47 | // Add zone to cache 48 | cache.Set(zoneName, records) 49 | 50 | // Get zone from cache 51 | records, ok := cache.Get(zoneName) 52 | assert.True(t, ok) 53 | 54 | // Find record in zone 55 | record, ok := cache.Find(zoneName, "a", "A", "1.2.3.4") 56 | assert.True(t, ok) 57 | assert.Equal(t, records[0], record) 58 | 59 | // Find does not find record in zone 60 | _, ok = cache.Find(zoneName, "b", "A", "1.2.3.5") 61 | assert.False(t, ok) 62 | } 63 | -------------------------------------------------------------------------------- /internal/framework/common/domain_registration.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-framework/attr" 5 | "github.com/hashicorp/terraform-plugin-framework/types" 6 | ) 7 | 8 | type DomainRegistration struct { 9 | Period types.Int64 `tfsdk:"period"` 10 | State types.String `tfsdk:"state"` 11 | Id types.Int64 `tfsdk:"id"` 12 | } 13 | 14 | var DomainRegistrationAttrType = map[string]attr.Type{ 15 | "period": types.Int64Type, 16 | "state": types.StringType, 17 | "id": types.Int64Type, 18 | } 19 | 20 | type RegistrantChange struct { 21 | Id types.Int64 `tfsdk:"id"` 22 | AccountId types.Int64 `tfsdk:"account_id"` 23 | ContactId types.Int64 `tfsdk:"contact_id"` 24 | DomainId types.String `tfsdk:"domain_id"` 25 | State types.String `tfsdk:"state"` 26 | ExtendedAttributes types.Map `tfsdk:"extended_attributes"` 27 | RegistryOwnerChange types.Bool `tfsdk:"registry_owner_change"` 28 | IrtLockLiftedBy types.String `tfsdk:"irt_lock_lifted_by"` 29 | } 30 | 31 | var RegistrantChangeAttrType = map[string]attr.Type{ 32 | "id": types.Int64Type, 33 | "account_id": types.Int64Type, 34 | "contact_id": types.Int64Type, 35 | "domain_id": types.StringType, 36 | "state": types.StringType, 37 | "extended_attributes": types.MapType{ 38 | ElemType: types.StringType, 39 | }, 40 | "registry_owner_change": types.BoolType, 41 | "irt_lock_lifted_by": types.StringType, 42 | } 43 | -------------------------------------------------------------------------------- /internal/framework/validators/duration.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 8 | "github.com/hashicorp/terraform-plugin-framework/tfsdk" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | ) 11 | 12 | var _ validator.String = Duration{} 13 | 14 | type Duration struct{} 15 | 16 | func (v Duration) Description(ctx context.Context) string { 17 | return "a duration given as a parsable string as in 60m or 2h" 18 | } 19 | 20 | // MarkdownDescription returns a markdown formatted description of the 21 | // validator's behavior, suitable for a practitioner to understand its impact. 22 | func (v Duration) MarkdownDescription(ctx context.Context) string { 23 | return v.Description(ctx) 24 | } 25 | 26 | // Validate runs the main validation logic of the validator, reading 27 | // configuration data out of `req` and updating `resp` with diagnostics. 28 | func (v Duration) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { 29 | var duration types.String 30 | resp.Diagnostics.Append(tfsdk.ValueAs(ctx, req.ConfigValue, &duration)...) 31 | if resp.Diagnostics.HasError() { 32 | return 33 | } 34 | 35 | if duration.IsUnknown() || duration.IsNull() { 36 | return 37 | } 38 | 39 | _, err := time.ParseDuration(duration.ValueString()) 40 | if err != nil { 41 | resp.Diagnostics.AddAttributeError( 42 | req.Path, 43 | "Invalid duration", 44 | err.Error(), 45 | ) 46 | return 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/resources/zone.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_zone" 3 | --- 4 | 5 | # dnsimple\_zone 6 | 7 | Provides a DNSimple zone resource. 8 | 9 | ~> **Note:** Currently the resource creation acts as an import, so the zone must already exist in DNSimple. The only attribute that will be modified during resource creation is the `active` state of the zone. This is because our API does not allow for the creation of zones. Creation of zones happens through the purchase or creation of domains. We expect this behavior to change in the future. 10 | 11 | ## Example Usage 12 | 13 | ```hcl 14 | resource "dnsimple_zone" "example" { 15 | name = "example.com" 16 | } 17 | ``` 18 | 19 | ## Argument Reference 20 | 21 | The following arguments are supported: 22 | 23 | - `name` - (Required) The zone name. 24 | 25 | ## Attributes Reference 26 | 27 | - `id` - The ID of this resource. 28 | - `account_id` - The account ID for the zone. 29 | - `reverse` - Whether the zone is a reverse zone. 30 | - `secondary` - Whether the zone is a secondary zone. 31 | - `active` - Whether the zone is active. 32 | - `last_transferred_at` - The last time the zone was transferred only applicable for **secondary** zones. 33 | 34 | ## Import 35 | 36 | DNSimple zones can be imported using the zone name. 37 | 38 | ```bash 39 | terraform import dnsimple_zone.example example.com 40 | ``` 41 | 42 | The zone name can be found within the [DNSimple Zones API](https://developer.dnsimple.com/v2/zones/#getZone). Check out [Authentication](https://developer.dnsimple.com/v2/#authentication) in API Overview for available options. 43 | -------------------------------------------------------------------------------- /internal/framework/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 8 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 9 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" 10 | ) 11 | 12 | const ( 13 | providerConfig = ` 14 | provider "dnsimple" { 15 | # token = "" 16 | # account = "" 17 | 18 | sandbox = true 19 | } 20 | ` 21 | ) 22 | 23 | // testAccProtoV6ProviderFactories are used to instantiate a provider during 24 | // acceptance testing. The factory function will be invoked for every Terraform 25 | // CLI command executed to create a provider server to which the CLI can 26 | // reattach. 27 | var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ 28 | "dnsimple": providerserver.NewProtocol6WithError(provider.New("test")()), 29 | } 30 | 31 | func testAccPreCheck(t *testing.T) { 32 | // You can add code here to run prior to any test case execution, for example assertions 33 | // about the appropriate environment variables being set are common to see in a pre-check 34 | // function. 35 | if v := os.Getenv("DNSIMPLE_TOKEN"); v == "" { 36 | t.Fatal("DNSIMPLE_TOKEN must be set for acceptance tests") 37 | } 38 | 39 | if v := os.Getenv("DNSIMPLE_ACCOUNT"); v == "" { 40 | t.Fatal("DNSIMPLE_ACCOUNT must be set for acceptance tests") 41 | } 42 | 43 | if v := os.Getenv("DNSIMPLE_DOMAIN"); v == "" { 44 | t.Fatal("DNSIMPLE_DOMAIN must be set for acceptance tests. The domain is used to create and destroy record against.") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub action can publish assets for release when a tag is created. 2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). 3 | # 4 | # This uses an action (crazy-max/ghaction-import-gpg) that assumes you set your 5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` 6 | # secret. If you would rather own your own GPG handling, please fork this action 7 | # or use an alternative one for key handling. 8 | # 9 | # You will need to pass the `--batch` flag to `gpg` in your signing step 10 | # in `goreleaser` to indicate this is being used in a non-interactive mode. 11 | # 12 | --- 13 | name: Release 14 | 15 | on: 16 | push: 17 | tags: 18 | - 'v*' 19 | 20 | jobs: 21 | goreleaser: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v6.0.1 26 | - name: Unshallow 27 | run: git fetch --prune --unshallow 28 | - uses: actions/setup-go@v6.1.0 29 | with: 30 | go-version-file: "go.mod" 31 | cache: true 32 | - name: Import GPG key 33 | id: import_gpg 34 | uses: crazy-max/ghaction-import-gpg@v6.3.0 35 | with: 36 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 37 | passphrase: ${{ secrets.PASSPHRASE }} 38 | - name: Run GoReleaser 39 | uses: goreleaser/goreleaser-action@v6.4.0 40 | with: 41 | version: latest 42 | args: release --clean 43 | env: 44 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /internal/framework/validators/record_type.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 9 | "github.com/hashicorp/terraform-plugin-framework/tfsdk" 10 | "github.com/hashicorp/terraform-plugin-framework/types" 11 | ) 12 | 13 | var _ validator.String = RecordType{} 14 | 15 | type RecordType struct{} 16 | 17 | func (v RecordType) Description(ctx context.Context) string { 18 | return "record type must be specified in UPPERCASE" 19 | } 20 | 21 | // MarkdownDescription returns a markdown formatted description of the 22 | // validator's behavior, suitable for a practitioner to understand its impact. 23 | func (v RecordType) MarkdownDescription(ctx context.Context) string { 24 | return v.Description(ctx) 25 | } 26 | 27 | // Validate runs the main validation logic of the validator, reading 28 | // configuration data out of `req` and updating `resp` with diagnostics. 29 | func (v RecordType) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { 30 | var recordType types.String 31 | resp.Diagnostics.Append(tfsdk.ValueAs(ctx, req.ConfigValue, &recordType)...) 32 | if resp.Diagnostics.HasError() { 33 | return 34 | } 35 | 36 | if recordType.IsUnknown() || recordType.IsNull() { 37 | return 38 | } 39 | 40 | recordTypeValue := recordType.ValueString() 41 | recordTypeUpper := strings.ToUpper(recordTypeValue) 42 | if recordTypeUpper != recordTypeValue { 43 | resp.Diagnostics.AddAttributeError( 44 | req.Path, 45 | "Record type must be uppercase", 46 | fmt.Sprintf("Record type must be specified in UPPERCASE, but got %q. Use %q instead.", recordTypeValue, recordTypeUpper), 47 | ) 48 | return 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/framework/datasources/registrant_change_check_data_source_test.go: -------------------------------------------------------------------------------- 1 | package datasources_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" 10 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 11 | ) 12 | 13 | func TestAccRegistrantChangeCheckDataSource(t *testing.T) { 14 | // Get convert the contact id to int 15 | contactId := os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_CONTACT_ID") 16 | domainName := os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_DOMAIN") 17 | resourceName := "data.dnsimple_registrant_change_check.test" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 21 | ProtoV6ProviderFactories: provider.NewProto6ProviderFactory(), 22 | Steps: []resource.TestStep{ 23 | // Read testing 24 | { 25 | Config: testAccRegistrantChangeCheckDataSourceConfig(contactId, domainName), 26 | Check: resource.ComposeAggregateTestCheckFunc( 27 | resource.TestCheckResourceAttr(resourceName, "contact_id", contactId), 28 | resource.TestCheckResourceAttr(resourceName, "domain_id", domainName), 29 | resource.TestCheckResourceAttrSet(resourceName, "extended_attributes.#"), 30 | resource.TestCheckResourceAttrSet(resourceName, "registry_owner_change"), 31 | resource.TestCheckResourceAttrSet(resourceName, "id"), 32 | ), 33 | }, 34 | }, 35 | }) 36 | } 37 | 38 | func testAccRegistrantChangeCheckDataSourceConfig(contactId, domainName string) string { 39 | return fmt.Sprintf(` 40 | data "dnsimple_registrant_change_check" "test" { 41 | contact_id = %[1]q 42 | domain_id = %[2]q 43 | }`, contactId, domainName) 44 | } 45 | -------------------------------------------------------------------------------- /docs/resources/ds_record.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_ds_record" 3 | --- 4 | 5 | # dnsimple\_ds\_record 6 | 7 | Provides a DNSimple domain delegation signer record resource. 8 | 9 | ## Example Usage 10 | 11 | ```hcl 12 | resource "dnsimple_ds_record" "example" { 13 | domain = "example.com" 14 | algorithm = "8" 15 | digest = "6CEEA0117A02480216EBF745A7B690F938860074E4AD11AF2AC573007205682B" 16 | digest_type = "2" 17 | key_tag = "12345" 18 | } 19 | ``` 20 | 21 | ## Argument Reference 22 | 23 | The following arguments are supported: 24 | 25 | - `domain` - (Required) The domain name or numeric ID to create the delegation signer record for. 26 | - `algorithm` - (Required) DNSSEC algorithm number as a string. 27 | - `digest` - (Optional) The hexadecimal representation of the digest of the corresponding DNSKEY record. 28 | - `digest_type` - (Optional) DNSSEC digest type number as a string. 29 | - `key_tag` - (Optional) A key tag that references the corresponding DNSKEY record. 30 | - `public_key` - (Optional) A public key that references the corresponding DNSKEY record. 31 | 32 | ## Attributes Reference 33 | 34 | - `id` - The ID of this resource. 35 | - `created_at` - The timestamp when the DS record was created. 36 | - `updated_at` - The timestamp when the DS record was last updated. 37 | 38 | ## Import 39 | 40 | DNSimple DS records can be imported using the domain name and numeric record ID in the format `domain_name_record_id`. 41 | 42 | ```bash 43 | terraform import dnsimple_ds_record.example example.com_5678 44 | ``` 45 | 46 | The record ID can be found within the [DNSimple DNSSEC API](https://developer.dnsimple.com/v2/domains/dnssec/#listDomainDelegationSignerRecords). Check out [Authentication](https://developer.dnsimple.com/v2/#authentication) in API Overview for available options. 47 | -------------------------------------------------------------------------------- /internal/framework/test_utils/utils.go: -------------------------------------------------------------------------------- 1 | package test_utils 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 9 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 10 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 11 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" 12 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" 13 | ) 14 | 15 | func TestAccProtoV6ProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) { 16 | return map[string]func() (tfprotov6.ProviderServer, error){ 17 | "dnsimple": providerserver.NewProtocol6WithError(provider.New("test")()), 18 | } 19 | } 20 | 21 | func TestAccPreCheck(t *testing.T) { 22 | // You can add code here to run prior to any test case execution, for example assertions 23 | // about the appropriate environment variables being set are common to see in a pre-check 24 | // function. 25 | if v := os.Getenv("DNSIMPLE_TOKEN"); v == "" { 26 | t.Fatal("DNSIMPLE_TOKEN must be set for acceptance tests") 27 | } 28 | 29 | if v := os.Getenv("DNSIMPLE_ACCOUNT"); v == "" { 30 | t.Fatal("DNSIMPLE_ACCOUNT must be set for acceptance tests") 31 | } 32 | 33 | if v := os.Getenv("DNSIMPLE_DOMAIN"); v == "" { 34 | t.Fatal("DNSIMPLE_DOMAIN must be set for acceptance tests. The domain is used to create and destroy record against.") 35 | } 36 | } 37 | 38 | func LoadDNSimpleTestClient() (*dnsimple.Client, string) { 39 | if os.Getenv("TF_ACC") != "1" { 40 | return nil, "" 41 | } 42 | 43 | token := os.Getenv("DNSIMPLE_TOKEN") 44 | account := os.Getenv("DNSIMPLE_ACCOUNT") 45 | 46 | dnsimpleClient := dnsimple.NewClient(dnsimple.StaticTokenHTTPClient(context.Background(), token)) 47 | dnsimpleClient.UserAgent = "terraform-provider-dnsimple/test" 48 | dnsimpleClient.BaseURL = consts.BaseURLSandbox 49 | 50 | return dnsimpleClient, account 51 | } 52 | -------------------------------------------------------------------------------- /internal/framework/datasources/certificate_data_source_test.go: -------------------------------------------------------------------------------- 1 | package datasources_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 9 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/provider" 10 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 11 | ) 12 | 13 | func TestAccCertificateDataSource(t *testing.T) { 14 | if os.Getenv("DNSIMPLE_SANDBOX") != "false" { 15 | t.Skip("DNSIMPLE_SANDBOX is not set to `false` (read in CONTRIBUTING.md how to run this test)") 16 | return 17 | } 18 | domain := os.Getenv("DNSIMPLE_DOMAIN") 19 | certificateId := os.Getenv("DNSIMPLE_CERTIFICATE_ID") 20 | resource.Test(t, resource.TestCase{ 21 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 22 | ProtoV6ProviderFactories: provider.NewProto6ProviderFactory(), 23 | Steps: []resource.TestStep{ 24 | { 25 | Config: testAccCertificateDataSourceConfig(domain, certificateId), 26 | Check: resource.ComposeAggregateTestCheckFunc( 27 | resource.TestCheckResourceAttr("data.dnsimple_certificate.test", "domain", domain), 28 | resource.TestCheckResourceAttr("data.dnsimple_certificate.test", "certificate_id", certificateId), 29 | ), 30 | }, 31 | { 32 | Config: testAccCertificateDataSourceConfig(domain, certificateId), 33 | Check: resource.ComposeAggregateTestCheckFunc( 34 | resource.TestCheckResourceAttr("data.dnsimple_certificate.test", "domain", domain), 35 | resource.TestCheckResourceAttr("data.dnsimple_certificate.test", "certificate_id", certificateId), 36 | ), 37 | ExpectNonEmptyPlan: false, 38 | }, 39 | }, 40 | }) 41 | } 42 | 43 | func testAccCertificateDataSourceConfig(domainName string, certificateId string) string { 44 | return fmt.Sprintf(` 45 | data "dnsimple_certificate" "test" { 46 | domain = %[1]q 47 | certificate_id = %[2]q 48 | }`, domainName, certificateId) 49 | } 50 | -------------------------------------------------------------------------------- /internal/framework/modifiers/int64planmodifiers_test.go: -------------------------------------------------------------------------------- 1 | package modifiers_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/path" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/modifiers" 12 | ) 13 | 14 | func TestInt64DefaultValue(t *testing.T) { 15 | t.Parallel() 16 | 17 | type testCase struct { 18 | plannedValue types.Int64 19 | currentValue types.Int64 20 | defaultValue int64 21 | expectedValue types.Int64 22 | expectError bool 23 | } 24 | tests := map[string]testCase{ 25 | "default int64": { 26 | plannedValue: types.Int64Null(), 27 | currentValue: types.Int64Value(1), 28 | defaultValue: 1, 29 | expectedValue: types.Int64Value(1), 30 | }, 31 | "default int64 on create": { 32 | plannedValue: types.Int64Null(), 33 | currentValue: types.Int64Null(), 34 | defaultValue: 1, 35 | expectedValue: types.Int64Value(1), 36 | }, 37 | } 38 | 39 | for name, test := range tests { 40 | name, test := name, test 41 | t.Run(name, func(t *testing.T) { 42 | t.Parallel() 43 | 44 | ctx := context.Background() 45 | request := planmodifier.Int64Request{ 46 | Path: path.Root("test"), 47 | PlanValue: test.plannedValue, 48 | StateValue: test.currentValue, 49 | } 50 | response := planmodifier.Int64Response{ 51 | PlanValue: request.PlanValue, 52 | } 53 | modifiers.Int64DefaultValue(test.defaultValue).PlanModifyInt64(ctx, request, &response) 54 | 55 | if !response.Diagnostics.HasError() && test.expectError { 56 | t.Fatal("expected error, got no error") 57 | } 58 | 59 | if response.Diagnostics.HasError() && !test.expectError { 60 | t.Fatalf("got unexpected error: %s", response.Diagnostics) 61 | } 62 | 63 | assert.Equal(t, test.expectedValue, response.PlanValue) 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/framework/validators/domain_name.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 9 | "github.com/hashicorp/terraform-plugin-framework/tfsdk" 10 | "github.com/hashicorp/terraform-plugin-framework/types" 11 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" 12 | ) 13 | 14 | var _ validator.String = DomainName{} 15 | 16 | type DomainName struct{} 17 | 18 | func (v DomainName) Description(ctx context.Context) string { 19 | return "a domain name should always be lower case" 20 | } 21 | 22 | // MarkdownDescription returns a markdown formatted description of the 23 | // validator's behavior, suitable for a practitioner to understand its impact. 24 | func (v DomainName) MarkdownDescription(ctx context.Context) string { 25 | return v.Description(ctx) 26 | } 27 | 28 | // Validate runs the main validation logic of the validator, reading 29 | // configuration data out of `req` and updating `resp` with diagnostics. 30 | func (v DomainName) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { 31 | var domainName types.String 32 | resp.Diagnostics.Append(tfsdk.ValueAs(ctx, req.ConfigValue, &domainName)...) 33 | if resp.Diagnostics.HasError() { 34 | return 35 | } 36 | 37 | if domainName.IsUnknown() || domainName.IsNull() { 38 | return 39 | } 40 | 41 | domainNameLower := strings.ToLower(domainName.ValueString()) 42 | if domainNameLower != domainName.ValueString() { 43 | resp.Diagnostics.AddAttributeError( 44 | req.Path, 45 | "Domain name should be lower case", 46 | fmt.Sprintf("Domain name should be lower case, but got %s", domainName.ValueString()), 47 | ) 48 | return 49 | } 50 | 51 | if utils.HasUnicodeChars(domainNameLower) { 52 | resp.Diagnostics.AddAttributeError( 53 | req.Path, 54 | "Domain name should not contain unicode characters please use punycode", 55 | fmt.Sprintf("Domain name should not contain unicode characters, but got %s", domainName.ValueString()), 56 | ) 57 | return 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/data-sources/registrant_change_check.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_registrant_change_check" 3 | --- 4 | 5 | # dnsimple\_registrant_change_check 6 | 7 | Get information on the requirements of a registrant change. 8 | 9 | ~> **Note:** The registrant change API is currently in developer preview and is subject to change. 10 | 11 | ## Example Usage 12 | 13 | Get registrant change requirements for the `example.com` domain and the contact with ID `1234`: 14 | 15 | ```hcl 16 | data "dnsimple_registrant_change_check" "example" { 17 | domain_id = "example.com" 18 | contact_id = "1234" 19 | } 20 | ``` 21 | 22 | ## Argument Reference 23 | 24 | The following arguments are supported: 25 | 26 | - `domain_id` - (Required) The name or ID of the domain. 27 | - `contact_id` - (Required) The ID of the contact you are planning to change to. 28 | 29 | ## Attributes Reference 30 | 31 | The following attributes are exported: 32 | 33 | - `contact_id` - The ID of the contact you are planning to change to. 34 | - `domain_id` - The name or ID of the domain. 35 | - `extended_attributes` - (List) A list of extended attributes that are required for the registrant change. (see [below for nested schema](#nested-schema-for-extended_attributes)) 36 | - `registry_owner_change` - (Boolean) Whether the registrant change is going to result in an owner change at the registry. 37 | 38 | ### Nested Schema for `extended_attributes` 39 | 40 | Attributes Reference: 41 | 42 | - `name` (String) - The name of the extended attribute, e.g., `x-au-registrant-id-type`. 43 | - `description` (String) - The description of the extended attribute. 44 | - `required` (Boolean) - Whether the extended attribute is required. 45 | - `options` (List) - A list of options for the extended attribute. (see [below for nested schema](#nested-schema-for-extended_attributesoptions)) 46 | 47 | ### Nested Schema for `extended_attributes.options` 48 | 49 | Attributes Reference: 50 | 51 | - `title` (String) - The human-readable title of the option, e.g., `Australian Company Number (ACN)`. 52 | - `value` (String) - The value of the option. 53 | - `description` (String) - The description of the option. 54 | 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/generic-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generic Issue 3 | about: A new issue to be triaged. 4 | title: '' 5 | labels: triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | Hi there, 11 | 12 | Thank you for opening an issue. Please note that we try to keep the Terraform issue tracker reserved for bug reports and feature requests. For general usage questions, please see: https://www.terraform.io/community.html. 13 | 14 | ### Terraform Version 15 | 16 | Run `terraform -v` to show the version. If you are not running the latest version of Terraform, please upgrade because your issue may have already been fixed. 17 | 18 | ### Affected Resource(s) 19 | 20 | Please list the resources as a list, for example: 21 | - dnsimple_zone 22 | - dnsimple_zone_record 23 | 24 | If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this. 25 | 26 | ### Terraform Configuration Files 27 | 28 | ```hcl 29 | # Copy-paste your Terraform configurations here - for large Terraform configs, 30 | # please use a service like Dropbox and share a link to the ZIP file. For 31 | # security, you can also encrypt the files using our GPG public key. 32 | ``` 33 | 34 | ### Debug Output 35 | 36 | Please provider a link to a GitHub Gist containing the complete debug output: https://www.terraform.io/docs/internals/debugging.html. Please do NOT paste the debug output in the issue; just paste a link to the Gist. 37 | 38 | ### Panic Output 39 | 40 | If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`. 41 | 42 | ### Expected Behavior 43 | 44 | What should have happened? 45 | 46 | ### Actual Behavior 47 | 48 | What actually happened? 49 | 50 | ### Steps to Reproduce 51 | 52 | Please list the steps required to reproduce the issue, for example: 53 | 1. `terraform apply` 54 | 55 | ### Important Factoids 56 | 57 | Are there anything atypical about your accounts that we should know? For example: Running in EC2 Classic? Custom version of OpenStack? Tight ACLs? 58 | 59 | ### References 60 | 61 | Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? 62 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | version: 2 4 | 5 | before: 6 | hooks: 7 | # this is just an example and not a requirement for provider building/publishing 8 | - go mod tidy 9 | 10 | builds: 11 | - env: 12 | # goreleaser does not work with CGO, it could also complicate 13 | # usage by users in CI/CD systems like Terraform Cloud where 14 | # they are unable to install libraries. 15 | - CGO_ENABLED=0 16 | mod_timestamp: '{{ .CommitTimestamp }}' 17 | flags: 18 | - -trimpath 19 | ldflags: 20 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 21 | goos: 22 | - freebsd 23 | - windows 24 | - linux 25 | - darwin 26 | goarch: 27 | - amd64 28 | - '386' 29 | - arm 30 | - arm64 31 | ignore: 32 | - goos: darwin 33 | goarch: '386' 34 | binary: '{{ .ProjectName }}_v{{ .Version }}' 35 | 36 | archives: 37 | - formats: 38 | - zip 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 40 | 41 | checksum: 42 | extra_files: 43 | - glob: 'terraform-registry-manifest.json' 44 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 45 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 46 | algorithm: sha256 47 | 48 | signs: 49 | - artifacts: checksum 50 | args: 51 | # if you are using this is a GitHub action or some other automated pipeline, you 52 | # need to pass the batch flag to indicate its not interactive. 53 | - "--batch" 54 | - "--local-user" 55 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 56 | - "--output" 57 | - "${signature}" 58 | - "--detach-sign" 59 | - "${artifact}" 60 | 61 | release: 62 | extra_files: 63 | - glob: 'terraform-registry-manifest.json' 64 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 65 | # If you want to manually examine the release before its live, uncomment this line: 66 | # draft: true 67 | 68 | changelog: 69 | disable: true 70 | -------------------------------------------------------------------------------- /internal/framework/common/zone_record_cache.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 8 | ) 9 | 10 | var zoneRecordCacheMutex = &sync.RWMutex{} 11 | 12 | type ZoneRecordCache map[string][]dnsimple.ZoneRecord 13 | 14 | func NewZoneRecordCache() ZoneRecordCache { 15 | return make(ZoneRecordCache) 16 | } 17 | 18 | func (c ZoneRecordCache) Get(zoneName string) ([]dnsimple.ZoneRecord, bool) { 19 | zoneRecordCacheMutex.RLock() 20 | defer zoneRecordCacheMutex.RUnlock() 21 | 22 | records, ok := c[zoneName] 23 | return records, ok 24 | } 25 | 26 | func (c ZoneRecordCache) Set(zoneName string, records []dnsimple.ZoneRecord) { 27 | zoneRecordCacheMutex.Lock() 28 | defer zoneRecordCacheMutex.Unlock() 29 | 30 | c[zoneName] = records 31 | } 32 | 33 | func (c ZoneRecordCache) Find(zoneName, recordName, recordType string, content string) (dnsimple.ZoneRecord, bool) { 34 | records, ok := c.Get(zoneName) 35 | if !ok { 36 | return dnsimple.ZoneRecord{}, false 37 | } 38 | 39 | for _, record := range records { 40 | if record.Name == recordName && record.Type == recordType && record.Content == content { 41 | return record, true 42 | } 43 | } 44 | 45 | return dnsimple.ZoneRecord{}, false 46 | } 47 | 48 | func (c ZoneRecordCache) Hydrate(ctx context.Context, client *dnsimple.Client, accountId string, zoneName string, options *dnsimple.ZoneRecordListOptions) error { 49 | if _, ok := c.Get(zoneName); !ok { 50 | var records []dnsimple.ZoneRecord 51 | 52 | if options == nil { 53 | options = &dnsimple.ZoneRecordListOptions{} 54 | } 55 | 56 | // Always use max page size 57 | options.PerPage = dnsimple.Int(100) 58 | // Fetch all records for the zone 59 | for { 60 | response, err := client.Zones.ListRecords(ctx, accountId, zoneName, options) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | records = append(records, response.Data...) 66 | 67 | if response.Pagination.CurrentPage >= response.Pagination.TotalPages { 68 | break 69 | } 70 | 71 | options.Page = dnsimple.Int(response.Pagination.CurrentPage + 1) 72 | } 73 | 74 | c.Set(zoneName, records) 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /docs/resources/lets_encrypt_certificate.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_lets_encrypt_certificate" 3 | --- 4 | 5 | # dnsimple\_lets_encrypt_certificate 6 | 7 | Provides a DNSimple Let's Encrypt certificate resource. 8 | 9 | ## Example Usage 10 | 11 | ```hcl 12 | resource "dnsimple_lets_encrypt_certificate" "example" { 13 | domain_id = "example.com" 14 | name = "www" 15 | auto_renew = true 16 | alternate_names = ["docs.example.com", "status.example.com"] 17 | } 18 | ``` 19 | 20 | ## Argument Reference 21 | 22 | The following arguments are supported: 23 | 24 | - `domain_id` - (Required) The domain name or ID to issue the certificate for. 25 | - `name` - (Required) The certificate name; use `""` for the root domain. Wildcard names are supported. 26 | - `alternate_names` - (Optional) List of alternate names (SANs) for the certificate. 27 | - `auto_renew` - (Required) Whether the certificate should auto-renew. 28 | - `signature_algorithm` - (Optional) The signature algorithm to use for the certificate. 29 | - `timeouts` - (Block, Optional) (see [below for nested schema](#nested-schema-for-timeouts)) 30 | 31 | ## Attributes Reference 32 | 33 | The following attributes are exported: 34 | 35 | - `id` - The certificate ID. 36 | - `years` - The number of years the certificate will last. 37 | - `state` - The state of the certificate. 38 | - `authority_identifier` - The identifying certification authority (CA). 39 | - `csr` - The certificate signing request. 40 | - `expires_at` - The datetime when the certificate will expire. 41 | - `created_at` - The datetime when the certificate was created. 42 | - `updated_at` - The datetime when the certificate was last updated. 43 | 44 | ### Nested Schema for `timeouts` 45 | 46 | Optional: 47 | 48 | - `read` (String) - The timeout for the read operation, e.g., `5m`. 49 | 50 | ## Import 51 | 52 | DNSimple Let's Encrypt certificates can be imported using the domain name and certificate ID in the format `domain_name_certificate_id`. 53 | 54 | ```bash 55 | terraform import dnsimple_lets_encrypt_certificate.example example.com_1234 56 | ``` 57 | 58 | The certificate ID can be found via the [DNSimple Certificates API](https://developer.dnsimple.com/v2/certificates/#listCertificates). 59 | -------------------------------------------------------------------------------- /docs/resources/zone_record.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_zone_record" 3 | --- 4 | 5 | # dnsimple\_zone\_record 6 | 7 | Provides a DNSimple zone record resource. 8 | 9 | ## Example Usage 10 | 11 | ```hcl 12 | # Add a record to the root domain 13 | resource "dnsimple_zone_record" "apex" { 14 | zone_name = "example.com" 15 | name = "" 16 | value = "192.0.2.1" 17 | type = "A" 18 | ttl = 3600 19 | } 20 | 21 | # Add a record to a subdomain 22 | resource "dnsimple_zone_record" "www" { 23 | zone_name = "example.com" 24 | name = "www" 25 | value = "192.0.2.1" 26 | type = "A" 27 | ttl = 3600 28 | } 29 | 30 | # Add an MX record 31 | resource "dnsimple_zone_record" "mx" { 32 | zone_name = "example.com" 33 | name = "" 34 | value = "mail.example.com" 35 | type = "MX" 36 | priority = 10 37 | ttl = 3600 38 | } 39 | ``` 40 | 41 | ## Argument Reference 42 | 43 | The following arguments are supported: 44 | 45 | - `zone_name` - (Required) The zone name to add the record to. 46 | - `name` - (Required) The name of the record. Use `""` for the root domain. 47 | - `value` - (Required) The value of the record. 48 | - `type` - (Required) The type of the record (e.g., `A`, `AAAA`, `CNAME`, `MX`, `TXT`). **The record type must be specified in UPPERCASE.** 49 | - `ttl` - (Optional) The TTL of the record. Defaults to `3600`. 50 | - `priority` - (Optional) The priority of the record. Only used for certain record types (e.g., `MX`, `SRV`). 51 | - `regions` - (Optional) A list of regions to serve the record from. You can find a list of supported values in our [developer documentation](https://developer.dnsimple.com/v2/zones/records/). 52 | 53 | 54 | ## Attributes Reference 55 | 56 | - `id` - The record ID. 57 | - `zone_id` - The zone ID of the record. 58 | - `qualified_name` - The fully qualified domain name (FQDN) of the record. 59 | - `value_normalized` - The normalized value of the record. 60 | 61 | ## Import 62 | 63 | DNSimple zone records can be imported using the zone name and numeric record ID in the format `zone_name_record_id`. 64 | 65 | **Importing record for example.com with record ID 1234:** 66 | 67 | ```bash 68 | terraform import dnsimple_zone_record.example example.com_1234 69 | ``` 70 | 71 | The record ID can be found in the URL when editing a record on the DNSimple web dashboard, or via the [DNSimple Zone Records API](https://developer.dnsimple.com/v2/zones/records/#listZoneRecords). 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DNSimple/terraform-provider 2 | 3 | ## Contributing Workflow 4 | 5 | 1. **Create a new branch** 6 | 7 | Create a feature branch from `main` for your changes: 8 | 9 | ```shell 10 | git checkout -b feature/your-feature-name 11 | ``` 12 | 13 | 2. **Make the changes** 14 | 15 | Implement your changes, following the code style guidelines below. 16 | 17 | 3. **Validate tests and linters** 18 | 19 | Before submitting your PR, ensure all checks pass: 20 | 21 | ```shell 22 | make fmtcheck # Check code formatting 23 | make errcheck # Check for unchecked errors 24 | make test # Run unit tests 25 | ``` 26 | 27 | If formatting issues are found, you can auto-fix them: 28 | 29 | ```shell 30 | make fmt # Format code with gofumpt 31 | ``` 32 | 33 | If you've modified any generated code, ensure code generation is up to date: 34 | 35 | ```shell 36 | go generate ./... 37 | ``` 38 | 39 | 4. **Create the PR** 40 | 41 | Push your branch and create a pull request against `main`. Include a clear description of your changes and reference any related issues. 42 | 43 | 5. **Follow up** 44 | 45 | - Respond to any review feedback promptly 46 | - Make requested changes and push updates to your branch 47 | - Ensure CI checks pass (tests, formatting, and static analysis) 48 | 49 | ## Code Style and Static Analysis 50 | 51 | We use several tools to maintain code quality and consistency: 52 | 53 | ### Code Formatting 54 | 55 | We use [`gofumpt`](https://github.com/mvdan/gofumpt) for code formatting, which is a stricter version of `gofmt`. 56 | 57 | - **Check formatting**: Run `make fmtcheck` to verify your code is properly formatted 58 | - **Auto-format**: Run `make fmt` to automatically format your code 59 | 60 | The build and test targets automatically run `fmtcheck` to ensure all code is properly formatted. 61 | 62 | ### Error Checking 63 | 64 | We use [`errcheck`](https://github.com/kisielk/errcheck) to ensure all errors are properly handled. 65 | 66 | - **Check for unchecked errors**: Run `make errcheck` 67 | 68 | This helps prevent bugs by ensuring all function return values, especially errors, are properly handled. 69 | 70 | ## Testing 71 | 72 | Submit unit tests for your changes. You can test your changes on your machine by [running the test suite](README.md#testing): 73 | 74 | ```shell 75 | make test 76 | ``` 77 | 78 | When you submit a PR, tests will also be run on the continuous integration environment [via GitHub Actions](https://github.com/dnsimple/terraform-provider-dnsimple/actions). 79 | -------------------------------------------------------------------------------- /internal/framework/resources/domain_resource_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 11 | "github.com/hashicorp/terraform-plugin-testing/terraform" 12 | _ "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/resources" 13 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 14 | ) 15 | 16 | func TestAccDomainResource(t *testing.T) { 17 | domainName := "test-" + os.Getenv("DNSIMPLE_DOMAIN") 18 | resourceName := "dnsimple_domain.test" 19 | 20 | resource.Test(t, resource.TestCase{ 21 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 22 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 23 | CheckDestroy: testAccCheckDomainResourceDestroy, 24 | Steps: []resource.TestStep{ 25 | { 26 | Config: testAccDomainResourceConfig(domainName), 27 | Check: resource.ComposeAggregateTestCheckFunc( 28 | resource.TestCheckResourceAttr(resourceName, "name", domainName), 29 | resource.TestCheckResourceAttr(resourceName, "state", "hosted"), 30 | ), 31 | }, 32 | { 33 | ResourceName: resourceName, 34 | ImportStateIdFunc: testAccDomainImportStateIDFunc(resourceName), 35 | ImportState: true, 36 | ImportStateVerify: true, 37 | }, 38 | // Update is a no-op 39 | // Delete testing automatically occurs in TestCase 40 | }, 41 | }) 42 | } 43 | 44 | func testAccDomainImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { 45 | return func(s *terraform.State) (string, error) { 46 | rs, ok := s.RootModule().Resources[resourceName] 47 | if !ok { 48 | return "", fmt.Errorf("Resource not found: %s", resourceName) 49 | } 50 | 51 | if rs.Primary.ID == "" { 52 | return "", errors.New("No resource ID set") 53 | } 54 | 55 | return rs.Primary.ID, nil 56 | } 57 | } 58 | 59 | func testAccCheckDomainResourceDestroy(state *terraform.State) error { 60 | for _, rs := range state.RootModule().Resources { 61 | if rs.Type != "dnsimple_domain" { 62 | continue 63 | } 64 | 65 | domainName := rs.Primary.Attributes["name"] 66 | _, err := dnsimpleClient.Domains.GetDomain(context.Background(), testAccAccount, domainName) 67 | 68 | if err == nil { 69 | return fmt.Errorf("domain still exists") 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | func testAccDomainResourceConfig(domainName string) string { 76 | return fmt.Sprintf(` 77 | resource "dnsimple_domain" "test" { 78 | name = %[1]q 79 | }`, domainName) 80 | } 81 | -------------------------------------------------------------------------------- /internal/framework/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 11 | "github.com/google/uuid" 12 | "github.com/hashicorp/terraform-plugin-framework/diag" 13 | "github.com/hashicorp/terraform-plugin-framework/path" 14 | ) 15 | 16 | func GetDefaultFromEnv(key, fallback string) string { 17 | if value, ok := os.LookupEnv(key); ok { 18 | return value 19 | } 20 | return fallback 21 | } 22 | 23 | // RandomName generates a random domain name using a UUID v7 with an optional suffix. 24 | // 25 | // It returns a string in the format "uuid.extension" or "uuid-suffix.extension" if suffix is provided. 26 | // Falls back to UUID v4 if v7 generation fails. 27 | func RandomName(extension string, suffix string) string { 28 | u, err := uuid.NewV7() 29 | if err != nil { 30 | // Fallback to v4 if v7 generation fails 31 | u = uuid.New() 32 | } 33 | name := u.String() 34 | if suffix != "" { 35 | name = name + "-" + suffix 36 | } 37 | return name + "." + extension 38 | } 39 | 40 | func HasUnicodeChars(s string) bool { 41 | for _, r := range s { 42 | if r > 127 { 43 | return true 44 | } 45 | } 46 | return false 47 | } 48 | 49 | func RetryWithTimeout(ctx context.Context, fn func() (error, bool), timeout time.Duration, delay time.Duration) error { 50 | deadline := time.Now().Add(timeout) 51 | for { 52 | err, suspend := fn() 53 | if err == nil { 54 | return nil 55 | } 56 | 57 | if suspend { 58 | return err 59 | } 60 | 61 | if time.Now().After(deadline) { 62 | return err 63 | } 64 | 65 | select { 66 | case <-ctx.Done(): 67 | return ctx.Err() 68 | case <-time.After(delay): 69 | continue 70 | } 71 | } 72 | } 73 | 74 | func AttributeErrorsToDiagnostics(err *dnsimple.ErrorResponse) diag.Diagnostics { 75 | diagnostics := diag.Diagnostics{} 76 | 77 | diagnostics.AddError( 78 | "DNSimple API returned an error", 79 | err.Message, 80 | ) 81 | 82 | for field, errors := range err.AttributeErrors { 83 | terraformField := TranslateFieldFromAPIToTerraform(field) 84 | 85 | diagnostics.AddAttributeError( 86 | path.Root(terraformField), 87 | fmt.Sprintf("DNSimple API validation error for field %s", terraformField), 88 | strings.Join(errors, ", "), 89 | ) 90 | } 91 | 92 | return diagnostics 93 | } 94 | 95 | func TranslateFieldFromAPIToTerraform(field string) string { 96 | switch field { 97 | case "record_type": 98 | return "type" 99 | case "content": 100 | return "value" 101 | default: 102 | return field 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/framework/modifiers/stringplanmodifiers_test.go: -------------------------------------------------------------------------------- 1 | package modifiers_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/path" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/modifiers" 12 | ) 13 | 14 | func TestStringDefaultValue(t *testing.T) { 15 | t.Parallel() 16 | 17 | type testCase struct { 18 | plannedValue types.String 19 | currentValue types.String 20 | defaultValue string 21 | expectedValue types.String 22 | expectError bool 23 | } 24 | tests := map[string]testCase{ 25 | "non-default non-Null string": { 26 | plannedValue: types.StringValue("gamma"), 27 | currentValue: types.StringValue("beta"), 28 | defaultValue: "alpha", 29 | expectedValue: types.StringValue("gamma"), 30 | }, 31 | "non-default non-Null string, current Null": { 32 | plannedValue: types.StringValue("gamma"), 33 | currentValue: types.StringNull(), 34 | defaultValue: "alpha", 35 | expectedValue: types.StringValue("gamma"), 36 | }, 37 | "non-default Null string, current Null": { 38 | plannedValue: types.StringNull(), 39 | currentValue: types.StringValue("beta"), 40 | defaultValue: "alpha", 41 | expectedValue: types.StringValue("alpha"), 42 | }, 43 | "default string": { 44 | plannedValue: types.StringNull(), 45 | currentValue: types.StringValue("alpha"), 46 | defaultValue: "alpha", 47 | expectedValue: types.StringValue("alpha"), 48 | }, 49 | "default string on create": { 50 | plannedValue: types.StringNull(), 51 | currentValue: types.StringNull(), 52 | defaultValue: "alpha", 53 | expectedValue: types.StringValue("alpha"), 54 | }, 55 | } 56 | 57 | for name, test := range tests { 58 | name, test := name, test 59 | t.Run(name, func(t *testing.T) { 60 | t.Parallel() 61 | 62 | ctx := context.Background() 63 | request := planmodifier.StringRequest{ 64 | Path: path.Root("test"), 65 | PlanValue: test.plannedValue, 66 | StateValue: test.currentValue, 67 | } 68 | response := planmodifier.StringResponse{ 69 | PlanValue: request.PlanValue, 70 | } 71 | modifiers.StringDefaultValue(test.defaultValue).PlanModifyString(ctx, request, &response) 72 | 73 | if !response.Diagnostics.HasError() && test.expectError { 74 | t.Fatal("expected error, got no error") 75 | } 76 | 77 | if response.Diagnostics.HasError() && !test.expectError { 78 | t.Fatalf("got unexpected error: %s", response.Diagnostics) 79 | } 80 | 81 | assert.Equal(t, test.expectedValue, response.PlanValue) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/framework/resources/registered_domain/import.go: -------------------------------------------------------------------------------- 1 | package registered_domain 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/path" 9 | "github.com/hashicorp/terraform-plugin-framework/resource" 10 | "github.com/hashicorp/terraform-plugin-framework/types" 11 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 12 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" 13 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 14 | ) 15 | 16 | func (r *RegisteredDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 17 | parts := strings.Split(req.ID, "_") 18 | domainName := parts[0] 19 | 20 | usingDomainAndRegistrationID := len(parts) == 2 21 | if usingDomainAndRegistrationID { 22 | domainRegistrationID := parts[1] 23 | 24 | domainRegistrationResponse, err := r.config.Client.Registrar.GetDomainRegistration(ctx, r.config.AccountID, domainName, domainRegistrationID) 25 | if err != nil { 26 | resp.Diagnostics.AddError( 27 | "failed to import DNSimple Domain Registration", 28 | fmt.Sprintf("Unable to find domain registration with ID '%s': %s", domainRegistrationID, err.Error()), 29 | ) 30 | return 31 | } 32 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain_registration").AtName("id"), domainRegistrationResponse.Data.ID)...) 33 | } else { 34 | resp.Private.SetKey(ctx, "skip_domain_registration", []byte(`true`)) 35 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain_registration"), basetypes.NewObjectNull(common.DomainRegistrationAttrType))...) 36 | } 37 | 38 | domainResponse, err := r.config.Client.Domains.GetDomain(ctx, r.config.AccountID, domainName) 39 | if err != nil { 40 | resp.Diagnostics.AddError( 41 | "failed to import DNSimple Domain", 42 | fmt.Sprintf("Unable to find domain '%s': %s", domainName, err.Error()), 43 | ) 44 | return 45 | } 46 | 47 | if domainResponse.Data.State != consts.DomainStateRegistered && !usingDomainAndRegistrationID { 48 | resp.Diagnostics.AddError( 49 | "failed to import DNSimple Domain", 50 | fmt.Sprintf("Domain '%s' is not registered. Domain must be registered before it can be imported", domainName), 51 | ) 52 | return 53 | } 54 | 55 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), domainResponse.Data.ID)...) 56 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), domainResponse.Data.Name)...) 57 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("contact_id"), domainResponse.Data.RegistrantID)...) 58 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("registrant_change"), types.ObjectNull(common.RegistrantChangeAttrType))...) 59 | } 60 | -------------------------------------------------------------------------------- /docs/resources/contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_contact" 3 | --- 4 | 5 | # dnsimple\_contact 6 | 7 | Provides a DNSimple contact resource. 8 | 9 | ## Example Usage 10 | 11 | ```hcl 12 | resource "dnsimple_contact" "example" { 13 | label = "Main Contact" 14 | first_name = "John" 15 | last_name = "Doe" 16 | organization_name = "Example Inc" 17 | job_title = "Manager" 18 | address1 = "123 Main Street" 19 | address2 = "Suite 100" 20 | city = "San Francisco" 21 | state_province = "California" 22 | postal_code = "94105" 23 | country = "US" 24 | phone = "+1.4155551234" 25 | fax = "+1.4155555678" 26 | email = "john@example.com" 27 | } 28 | ``` 29 | 30 | ## Argument Reference 31 | 32 | The following arguments are supported: 33 | 34 | - `label` - (Optional) A descriptive label for the contact to help identify it. 35 | - `first_name` - (Required) The first name of the contact person. 36 | - `last_name` - (Required) The last name of the contact person. 37 | - `organization_name` - (Optional) The name of the organization or company associated with the contact. 38 | - `job_title` - (Optional) The job title or position of the contact person within the organization. 39 | - `address1` - (Required) The primary address line (street address, building number, etc.). 40 | - `address2` - (Optional) The secondary address line (apartment, suite, floor, etc.). 41 | - `city` - (Required) The city where the contact is located. 42 | - `state_province` - (Required) The state, province, or region where the contact is located. 43 | - `postal_code` - (Required) The postal code, ZIP code, or equivalent for the contact's location. 44 | - `country` - (Required) The two-letter ISO country code (e.g., "US", "CA", "IT") for the contact's location. 45 | - `phone` - (Required) The contact's phone number. Use international format with country code (e.g., "+1.4012345678" for US numbers). 46 | - `fax` - (Optional) The contact's fax number. Use international format with country code (e.g., "+1.8491234567" for US numbers). 47 | - `email` - (Required) The contact's email address. 48 | 49 | ## Attributes Reference 50 | 51 | - `id` - The ID of this resource. 52 | - `account_id` - The account ID for the contact. 53 | - `phone_normalized` - The phone number, normalized. 54 | - `fax_normalized` - The fax number, normalized. 55 | - `created_at` - Timestamp representing when this contact was created. 56 | - `updated_at` - Timestamp representing when this contact was updated. 57 | 58 | 59 | ## Import 60 | 61 | DNSimple contacts can be imported using their numeric ID. 62 | 63 | ```bash 64 | terraform import dnsimple_contact.example 5678 65 | ``` 66 | 67 | The contact ID can be found within the [DNSimple Contacts API](https://developer.dnsimple.com/v2/contacts/#listContacts). Check out [Authentication](https://developer.dnsimple.com/v2/#authentication) in API Overview for available options. 68 | -------------------------------------------------------------------------------- /internal/framework/modifiers/setplanmodifier_test.go: -------------------------------------------------------------------------------- 1 | package modifiers_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/path" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/modifiers" 12 | ) 13 | 14 | func TestSetTrimSuffixValue(t *testing.T) { 15 | t.Parallel() 16 | 17 | serializeList := func(elements []string) types.Set { 18 | listValue, _ := types.SetValueFrom(context.Background(), types.StringType, elements) 19 | return listValue 20 | } 21 | 22 | type testCase struct { 23 | plannedValue types.Set 24 | stateValue types.Set 25 | configValue types.Set 26 | expectedValue types.Set 27 | expectError bool 28 | } 29 | tests := map[string]testCase{ 30 | "trim `.` in string value ": { 31 | plannedValue: serializeList([]string{"beta.alpha.", "gamma.omega"}), 32 | stateValue: types.SetNull(types.StringType), 33 | configValue: serializeList([]string{"beta.alpha.", "gamma.omega"}), 34 | expectedValue: serializeList([]string{"beta.alpha", "gamma.omega"}), 35 | }, 36 | "when state is Null": { 37 | plannedValue: serializeList([]string{"beta.alpha.", "gamma.omega."}), 38 | stateValue: types.SetNull(types.StringType), 39 | configValue: serializeList([]string{"beta.alpha.", "gamma.omega."}), 40 | expectedValue: serializeList([]string{"beta.alpha", "gamma.omega"}), 41 | }, 42 | "when plan is Null": { 43 | plannedValue: types.SetNull(types.StringType), 44 | stateValue: types.SetNull(types.StringType), 45 | configValue: serializeList([]string{"beta.alpha.", "gamma.omega."}), 46 | expectedValue: types.SetNull(types.StringType), 47 | }, 48 | "when plan is Unknown": { 49 | plannedValue: types.SetUnknown(types.StringType), 50 | stateValue: types.SetNull(types.StringType), 51 | configValue: serializeList([]string{"beta.alpha.", "gamma.omega."}), 52 | expectedValue: types.SetUnknown(types.StringType), 53 | }, 54 | } 55 | 56 | for name, test := range tests { 57 | name, test := name, test 58 | t.Run(name, func(t *testing.T) { 59 | t.Parallel() 60 | 61 | ctx := context.Background() 62 | request := planmodifier.SetRequest{ 63 | Path: path.Root("test"), 64 | PlanValue: test.plannedValue, 65 | StateValue: test.stateValue, 66 | ConfigValue: test.configValue, 67 | } 68 | response := planmodifier.SetResponse{ 69 | PlanValue: request.PlanValue, 70 | } 71 | modifiers.SetTrimSuffixValue().PlanModifySet(ctx, request, &response) 72 | 73 | if !response.Diagnostics.HasError() && test.expectError { 74 | t.Fatal("expected error, got no error") 75 | } 76 | 77 | if response.Diagnostics.HasError() && !test.expectError { 78 | t.Fatalf("got unexpected error: %s", response.Diagnostics) 79 | } 80 | 81 | assert.Equal(t, test.expectedValue, response.PlanValue) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/framework/resources/email_forward_resource_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 12 | "github.com/hashicorp/terraform-plugin-testing/terraform" 13 | _ "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/resources" 14 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 15 | ) 16 | 17 | func TestAccEmailForwardResource(t *testing.T) { 18 | domainName := os.Getenv("DNSIMPLE_DOMAIN") 19 | resourceName := "dnsimple_email_forward.test" 20 | 21 | resource.Test(t, resource.TestCase{ 22 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 23 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 24 | CheckDestroy: testAccCheckEmailForwardResourceDestroy, 25 | Steps: []resource.TestStep{ 26 | { 27 | Config: testAccEmailForwardResourceConfig(domainName), 28 | Check: resource.ComposeAggregateTestCheckFunc( 29 | resource.TestCheckResourceAttrSet(resourceName, "id"), 30 | resource.TestCheckResourceAttrSet(resourceName, "alias_email"), 31 | resource.TestCheckResourceAttr(resourceName, "domain", domainName), 32 | resource.TestCheckResourceAttr(resourceName, "alias_name", "hello"), 33 | resource.TestCheckResourceAttr(resourceName, "destination_email", "hi@hey.com"), 34 | ), 35 | }, 36 | { 37 | ResourceName: resourceName, 38 | ImportStateIdFunc: testAccEmailForwardImportStateIDFunc(resourceName), 39 | ImportState: true, 40 | ImportStateVerify: true, 41 | }, 42 | // Update is a no-op 43 | // Delete testing automatically occurs in TestCase 44 | }, 45 | }) 46 | } 47 | 48 | func testAccCheckEmailForwardResourceDestroy(state *terraform.State) error { 49 | for _, rs := range state.RootModule().Resources { 50 | if rs.Type != "dnsimple_email_forward" { 51 | continue 52 | } 53 | 54 | domainName := rs.Primary.Attributes["name"] 55 | forwardId, err := strconv.ParseInt(rs.Primary.Attributes["id"], 10, 64) 56 | if err != nil { 57 | return fmt.Errorf("error parsing email forward id: %s", err) 58 | } 59 | _, err = dnsimpleClient.Domains.GetEmailForward(context.Background(), testAccAccount, domainName, forwardId) 60 | 61 | if err == nil { 62 | return fmt.Errorf("record still exists") 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func testAccEmailForwardImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { 69 | return func(s *terraform.State) (string, error) { 70 | rs, ok := s.RootModule().Resources[resourceName] 71 | if !ok { 72 | return "", fmt.Errorf("Resource not found: %s", resourceName) 73 | } 74 | 75 | if rs.Primary.ID == "" { 76 | return "", errors.New("No resource ID set") 77 | } 78 | 79 | return fmt.Sprintf("%s_%s", rs.Primary.Attributes["domain"], rs.Primary.ID), nil 80 | } 81 | } 82 | 83 | func testAccEmailForwardResourceConfig(domainName string) string { 84 | return fmt.Sprintf(` 85 | resource "dnsimple_email_forward" "test" { 86 | domain = %[1]q 87 | alias_name = "hello" 88 | destination_email = "hi@hey.com" 89 | }`, domainName) 90 | } 91 | -------------------------------------------------------------------------------- /internal/framework/resources/zone_resource_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-testing/terraform" 11 | _ "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/resources" 12 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 13 | ) 14 | 15 | func TestAccZoneResource(t *testing.T) { 16 | zoneName := os.Getenv("DNSIMPLE_DOMAIN") 17 | resourceName := "dnsimple_zone.test" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 21 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: testAccZoneResourceConfig(zoneName), 25 | Check: resource.ComposeAggregateTestCheckFunc( 26 | resource.TestCheckResourceAttr(resourceName, "name", zoneName), 27 | resource.TestCheckResourceAttr(resourceName, "reverse", "false"), 28 | resource.TestCheckResourceAttr(resourceName, "secondary", "false"), 29 | resource.TestCheckResourceAttr(resourceName, "active", "true"), 30 | ), 31 | }, 32 | { 33 | Config: testAccZoneResourceConfigWithActive(zoneName, false), 34 | Check: resource.ComposeAggregateTestCheckFunc( 35 | resource.TestCheckResourceAttr(resourceName, "name", zoneName), 36 | resource.TestCheckResourceAttr(resourceName, "reverse", "false"), 37 | resource.TestCheckResourceAttr(resourceName, "secondary", "false"), 38 | resource.TestCheckResourceAttr(resourceName, "active", "false"), 39 | ), 40 | }, 41 | { 42 | Config: testAccZoneResourceConfigWithActive(zoneName, true), 43 | Check: resource.ComposeAggregateTestCheckFunc( 44 | resource.TestCheckResourceAttr(resourceName, "name", zoneName), 45 | resource.TestCheckResourceAttr(resourceName, "reverse", "false"), 46 | resource.TestCheckResourceAttr(resourceName, "secondary", "false"), 47 | resource.TestCheckResourceAttr(resourceName, "active", "true"), 48 | ), 49 | }, 50 | { 51 | ResourceName: resourceName, 52 | ImportStateIdFunc: testAccZoneImportStateIDFunc(resourceName), 53 | ImportState: true, 54 | ImportStateVerify: true, 55 | }, 56 | // Delete testing automatically occurs in TestCase 57 | }, 58 | }) 59 | } 60 | 61 | func testAccZoneImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { 62 | return func(s *terraform.State) (string, error) { 63 | rs, ok := s.RootModule().Resources[resourceName] 64 | if !ok { 65 | return "", fmt.Errorf("Resource not found: %s", resourceName) 66 | } 67 | 68 | if rs.Primary.ID == "" { 69 | return "", errors.New("No resource ID set") 70 | } 71 | 72 | return rs.Primary.ID, nil 73 | } 74 | } 75 | 76 | func testAccZoneResourceConfig(zoneName string) string { 77 | return fmt.Sprintf(` 78 | resource "dnsimple_zone" "test" { 79 | name = %[1]q 80 | }`, zoneName) 81 | } 82 | 83 | func testAccZoneResourceConfigWithActive(zoneName string, active bool) string { 84 | return fmt.Sprintf(` 85 | resource "dnsimple_zone" "test" { 86 | name = %[1]q 87 | active = %[2]t 88 | }`, zoneName, active) 89 | } 90 | -------------------------------------------------------------------------------- /internal/framework/datasources/zone_data_source.go: -------------------------------------------------------------------------------- 1 | package datasources 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/datasource" 8 | "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 11 | ) 12 | 13 | // Ensure provider defined types fully satisfy framework interfaces. 14 | var _ datasource.DataSource = &ZoneDataSource{} 15 | 16 | func NewZoneDataSource() datasource.DataSource { 17 | return &ZoneDataSource{} 18 | } 19 | 20 | // ZoneDataSource defines the data source implementation. 21 | type ZoneDataSource struct { 22 | config *common.DnsimpleProviderConfig 23 | } 24 | 25 | // ZoneDataSourceModel describes the data source data model. 26 | type ZoneDataSourceModel struct { 27 | Id types.Int64 `tfsdk:"id"` 28 | AccountId types.Int64 `tfsdk:"account_id"` 29 | Name types.String `tfsdk:"name"` 30 | Reverse types.Bool `tfsdk:"reverse"` 31 | } 32 | 33 | func (d *ZoneDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { 34 | resp.TypeName = req.ProviderTypeName + "_zone" 35 | } 36 | 37 | func (d *ZoneDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { 38 | resp.Schema = schema.Schema{ 39 | // This description is used by the documentation generator and the language server. 40 | MarkdownDescription: "DNSimple zone data source", 41 | 42 | Attributes: map[string]schema.Attribute{ 43 | "id": common.IDInt64Attribute(), 44 | "account_id": schema.Int64Attribute{ 45 | MarkdownDescription: "DNSimple Account ID to which the zone belongs to", 46 | Computed: true, 47 | }, 48 | "name": schema.StringAttribute{ 49 | MarkdownDescription: "Zone Name", 50 | Required: true, 51 | }, 52 | "reverse": schema.BoolAttribute{ 53 | MarkdownDescription: "True if the zone is a reverse zone", 54 | Computed: true, 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func (d *ZoneDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { 61 | // Prevent panic if the provider has not been configured. 62 | if req.ProviderData == nil { 63 | return 64 | } 65 | 66 | config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) 67 | 68 | if !ok { 69 | resp.Diagnostics.AddError( 70 | "Unexpected Data Source Configure Type", 71 | fmt.Sprintf("Expected *common.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), 72 | ) 73 | 74 | return 75 | } 76 | 77 | d.config = config 78 | } 79 | 80 | func (d *ZoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { 81 | var data ZoneDataSourceModel 82 | 83 | // Read Terraform configuration data into the model 84 | resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) 85 | 86 | if resp.Diagnostics.HasError() { 87 | return 88 | } 89 | 90 | response, err := d.config.Client.Zones.GetZone(ctx, d.config.AccountID, data.Name.ValueString()) 91 | if err != nil { 92 | resp.Diagnostics.AddError( 93 | "failed to read DNSimple Zone", 94 | err.Error(), 95 | ) 96 | return 97 | } 98 | 99 | data.Id = types.Int64Value(response.Data.ID) 100 | data.AccountId = types.Int64Value(response.Data.AccountID) 101 | data.Name = types.StringValue(response.Data.Name) 102 | data.Reverse = types.BoolValue(response.Data.Reverse) 103 | 104 | // Save data into Terraform state 105 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 106 | } 107 | -------------------------------------------------------------------------------- /internal/framework/resources/registered_domain/read.go: -------------------------------------------------------------------------------- 1 | package registered_domain 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 9 | "github.com/hashicorp/terraform-plugin-framework/resource" 10 | ) 11 | 12 | func (r *RegisteredDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 13 | var data *RegisteredDomainResourceModel 14 | 15 | // Read Terraform prior state data into the model 16 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 17 | 18 | if resp.Diagnostics.HasError() { 19 | return 20 | } 21 | 22 | domainRegistration, diags := getDomainRegistration(ctx, data) 23 | if diags.HasError() { 24 | resp.Diagnostics.Append(diags...) 25 | return 26 | } 27 | 28 | var domainRegistrationResponse *dnsimple.DomainRegistrationResponse 29 | var err error 30 | if !domainRegistration.Id.IsNull() { 31 | domainRegistrationId := strconv.Itoa(int(domainRegistration.Id.ValueInt64())) 32 | domainRegistrationResponse, err = r.config.Client.Registrar.GetDomainRegistration(ctx, r.config.AccountID, data.Name.ValueString(), domainRegistrationId) 33 | if err != nil { 34 | resp.Diagnostics.AddError( 35 | "failed to read DNSimple Domain Registration", 36 | fmt.Sprintf("Unable to read domain registration for domain '%s' (registration ID: %d): %s", data.Name.ValueString(), domainRegistration.Id.ValueInt64(), err.Error()), 37 | ) 38 | return 39 | } 40 | } 41 | 42 | registrantChange, diags := getRegistrantChange(ctx, data) 43 | if diags.HasError() { 44 | resp.Diagnostics.Append(diags...) 45 | return 46 | } 47 | 48 | var registrantChangeResponse *dnsimple.RegistrantChangeResponse 49 | if !registrantChange.Id.IsNull() { 50 | registrantChangeResponse, err = r.config.Client.Registrar.GetRegistrantChange(ctx, r.config.AccountID, int(registrantChange.Id.ValueInt64())) 51 | if err != nil { 52 | resp.Diagnostics.AddError( 53 | "failed to read DNSimple Registrant Change", 54 | fmt.Sprintf("Unable to read registrant change with ID %d: %s", registrantChange.Id.ValueInt64(), err.Error()), 55 | ) 56 | return 57 | } 58 | 59 | registrantChangeObject, diags := r.registrantChangeAPIResponseToObject(ctx, registrantChangeResponse.Data) 60 | if diags.HasError() { 61 | resp.Diagnostics.Append(diags...) 62 | return 63 | } 64 | data.RegistrantChange = registrantChangeObject 65 | } 66 | 67 | domainResponse, err := r.config.Client.Domains.GetDomain(ctx, r.config.AccountID, data.Name.ValueString()) 68 | if err != nil { 69 | resp.Diagnostics.AddError( 70 | "failed to read DNSimple Domain", 71 | fmt.Sprintf("Unable to read domain '%s': %s", data.Name.ValueString(), err.Error()), 72 | ) 73 | return 74 | } 75 | 76 | dnssecResponse, err := r.config.Client.Domains.GetDnssec(ctx, r.config.AccountID, data.Name.ValueString()) 77 | if err != nil { 78 | resp.Diagnostics.AddError( 79 | "failed to read DNSimple Domain DNSSEC status", 80 | fmt.Sprintf("Unable to read DNSSEC status for domain '%s': %s", data.Name.ValueString(), err.Error()), 81 | ) 82 | return 83 | } 84 | 85 | transferLockResponse, err := r.config.Client.Registrar.GetDomainTransferLock(ctx, r.config.AccountID, data.Name.ValueString()) 86 | if err != nil { 87 | resp.Diagnostics.AddError( 88 | "failed to read DNSimple Domain Transfer Lock status", 89 | fmt.Sprintf("Unable to read transfer lock status for domain '%s': %s", data.Name.ValueString(), err.Error()), 90 | ) 91 | return 92 | } 93 | 94 | if domainRegistrationResponse == nil { 95 | diags = r.updateModelFromAPIResponse(ctx, data, nil, domainResponse.Data, dnssecResponse.Data, transferLockResponse.Data) 96 | } else { 97 | diags = r.updateModelFromAPIResponse(ctx, data, domainRegistrationResponse.Data, domainResponse.Data, dnssecResponse.Data, transferLockResponse.Data) 98 | } 99 | 100 | if diags != nil && diags.HasError() { 101 | resp.Diagnostics.Append(diags...) 102 | return 103 | } 104 | 105 | // Save updated data into Terraform state 106 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 107 | } 108 | -------------------------------------------------------------------------------- /internal/framework/resources/domain_ds_record_resource_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 12 | "github.com/hashicorp/terraform-plugin-testing/terraform" 13 | _ "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/resources" 14 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 15 | ) 16 | 17 | func TestAccDomainDsRecordResource(t *testing.T) { 18 | domainName := os.Getenv("DNSIMPLE_DOMAIN") 19 | resourceName := "dnsimple_ds_record.test" 20 | 21 | resource.Test(t, resource.TestCase{ 22 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 23 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 24 | CheckDestroy: testAccCheckDomainDsRecordResourceDestroy, 25 | Steps: []resource.TestStep{ 26 | { 27 | Config: testAccDomainDsRecordResourceConfig(domainName), 28 | Check: resource.ComposeAggregateTestCheckFunc( 29 | resource.TestCheckResourceAttrSet(resourceName, "id"), 30 | resource.TestCheckResourceAttrSet(resourceName, "domain"), 31 | resource.TestCheckResourceAttr(resourceName, "algorithm", "8"), 32 | resource.TestCheckResourceAttr(resourceName, "digest", "C3D49CB83734B22CF3EF9A193B94302FA3BB68013E3E149786D40CDC1BBACD93"), 33 | resource.TestCheckResourceAttr(resourceName, "digest_type", "2"), 34 | resource.TestCheckResourceAttr(resourceName, "keytag", "51301"), 35 | resource.TestCheckResourceAttr(resourceName, "public_key", "AwEAAd4gdAYAeCnAsYYStm/eWd6uRn5XvT14D9DDM9TbmCvLKCuRA6WYz7suLAziJ5hvk2I7aTOVK8Wd1fDmVxHXGg0Jd6P2+GQpg7AGghD+oLeg0I7AesSIKO3o1ffr58x6iIsxVZ+fcC7G6vdr/d8oIJ/SZdAvghQnCNmCm49HLoN6bWJWNJIXzmxFrptvfgfB4B+PVzbquZrJ0W10KrD394U="), 36 | resource.TestCheckResourceAttrSet(resourceName, "created_at"), 37 | resource.TestCheckResourceAttrSet(resourceName, "updated_at"), 38 | ), 39 | }, 40 | { 41 | ResourceName: resourceName, 42 | ImportStateIdFunc: testAccDomainDsRecordImportStateIDFunc(resourceName), 43 | ImportState: true, 44 | ImportStateVerify: true, 45 | }, 46 | // Updates are a no-op 47 | // Delete testing automatically occurs in TestCase 48 | }, 49 | }) 50 | } 51 | 52 | func testAccDomainDsRecordImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { 53 | return func(s *terraform.State) (string, error) { 54 | rs, ok := s.RootModule().Resources[resourceName] 55 | if !ok { 56 | return "", fmt.Errorf("Resource not found: %s", resourceName) 57 | } 58 | 59 | if rs.Primary.ID == "" { 60 | return "", errors.New("No resource ID set") 61 | } 62 | 63 | return fmt.Sprintf("%s_%s", rs.Primary.Attributes["domain"], rs.Primary.ID), nil 64 | } 65 | } 66 | 67 | func testAccCheckDomainDsRecordResourceDestroy(state *terraform.State) error { 68 | for _, rs := range state.RootModule().Resources { 69 | if rs.Type != "dnsimple_ds_record" { 70 | continue 71 | } 72 | 73 | dsIdRaw := rs.Primary.Attributes["id"] 74 | domain := rs.Primary.Attributes["domain"] 75 | 76 | dsId, err := strconv.ParseInt(dsIdRaw, 10, 64) 77 | if err != nil { 78 | return fmt.Errorf("failed to convert domain delegation signer record ID to int: %s", err) 79 | } 80 | 81 | _, err = dnsimpleClient.Domains.GetDelegationSignerRecord(context.Background(), testAccAccount, domain, dsId) 82 | 83 | if err == nil { 84 | return fmt.Errorf("domain delegation signer record still exists") 85 | } 86 | } 87 | return nil 88 | } 89 | 90 | func testAccDomainDsRecordResourceConfig(domainName string) string { 91 | return fmt.Sprintf(` 92 | resource "dnsimple_ds_record" "test" { 93 | domain = %[1]q 94 | algorithm = "8" 95 | digest = "C3D49CB83734B22CF3EF9A193B94302FA3BB68013E3E149786D40CDC1BBACD93" 96 | digest_type = "2" 97 | keytag = "51301" 98 | public_key = "AwEAAd4gdAYAeCnAsYYStm/eWd6uRn5XvT14D9DDM9TbmCvLKCuRA6WYz7suLAziJ5hvk2I7aTOVK8Wd1fDmVxHXGg0Jd6P2+GQpg7AGghD+oLeg0I7AesSIKO3o1ffr58x6iIsxVZ+fcC7G6vdr/d8oIJ/SZdAvghQnCNmCm49HLoN6bWJWNJIXzmxFrptvfgfB4B+PVzbquZrJ0W10KrD394U=" 99 | }`, domainName) 100 | } 101 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - 'README.md' 10 | pull_request: 11 | types: ['opened', 'synchronize'] 12 | paths: 13 | - '**.go' 14 | - 'vendor/**' 15 | - '.github/workflows/**' 16 | workflow_dispatch: 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | build: 24 | name: Build 25 | runs-on: ubuntu-latest 26 | timeout-minutes: 5 27 | steps: 28 | - name: Check out code into the Go module directory 29 | uses: actions/checkout@v6.0.1 30 | - uses: actions/setup-go@v6.1.0 31 | with: 32 | go-version-file: "go.mod" 33 | cache: true 34 | - name: Get dependencies 35 | run: go mod download 36 | - name: Build 37 | run: go build -v . 38 | 39 | generate: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v6.0.1 43 | - uses: actions/setup-go@v6.1.0 44 | with: 45 | go-version-file: "go.mod" 46 | cache: true 47 | - uses: hashicorp/setup-terraform@v3.1.2 48 | with: 49 | terraform_wrapper: false 50 | - run: go generate ./... 51 | - name: git diff 52 | run: | 53 | git diff --compact-summary --exit-code || \ 54 | (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) 55 | 56 | test: 57 | name: Terraform Provider Acceptance Tests 58 | needs: build 59 | runs-on: ubuntu-latest 60 | timeout-minutes: 15 61 | strategy: 62 | fail-fast: false 63 | matrix: 64 | terraform: 65 | - "1.12.*" 66 | - "1.13.*" 67 | - "1.14.*" 68 | include: 69 | - terraform: "1.12.*" 70 | domain: "dnsimple-3-0-terraform.bio" 71 | registrant_contact_id: 14323 72 | registrant_change_domain: "dnsimple-tf-ci-1.eu" 73 | - terraform: "1.13.*" 74 | domain: "dnsimple-3-1-terraform.bio" 75 | registrant_contact_id: 14323 76 | registrant_change_domain: "dnsimple-tf-ci-2.eu" 77 | - terraform: "1.14.*" 78 | domain: "dnsimple-3-2-terraform.bio" 79 | registrant_contact_id: 14323 80 | registrant_change_domain: "dnsimple-tf-ci-3.eu" 81 | steps: 82 | - uses: actions/checkout@v6.0.1 83 | - uses: actions/setup-go@v6.1.0 84 | with: 85 | go-version-file: "go.mod" 86 | cache: true 87 | - uses: hashicorp/setup-terraform@v3.1.2 88 | with: 89 | terraform_version: ${{ matrix.terraform }} 90 | terraform_wrapper: false 91 | - run: go mod download 92 | - env: 93 | TF_ACC: 1 94 | DNSIMPLE_SANDBOX: true 95 | DNSIMPLE_ACCOUNT: ${{ secrets.DNSIMPLE_ACCOUNT }} 96 | DNSIMPLE_TOKEN: ${{ secrets.DNSIMPLE_TOKEN }} 97 | DNSIMPLE_DOMAIN: ${{ matrix.domain }} 98 | DNSIMPLE_CONTACT_ID: ${{ secrets.DNSIMPLE_CONTACT_ID }} 99 | DNSIMPLE_REGISTRANT_CHANGE_DOMAIN: ${{ matrix.registrant_change_domain }} 100 | DNSIMPLE_REGISTRANT_CHANGE_CONTACT_ID: ${{ matrix.registrant_contact_id }} 101 | run: go test -v ./internal/... -timeout 10m 102 | timeout-minutes: 10 103 | 104 | cleanup: 105 | name: Cleanup 106 | needs: test 107 | runs-on: ubuntu-latest 108 | timeout-minutes: 5 109 | steps: 110 | - uses: actions/checkout@v6.0.1 111 | - uses: actions/setup-go@v6.1.0 112 | with: 113 | go-version-file: "go.mod" 114 | cache: true 115 | - run: make sweep 116 | env: 117 | DNSIMPLE_SANDBOX: true 118 | DNSIMPLE_ACCOUNT: ${{ secrets.DNSIMPLE_ACCOUNT }} 119 | DNSIMPLE_TOKEN: ${{ secrets.DNSIMPLE_TOKEN }} 120 | DNSIMPLE_CLEANUP_DOMAINS: true 121 | DNSIMPLE_DOMAINS_TO_KEEP: "dnsimple-3-0-terraform.bio,dnsimple-3-1-terraform.bio,dnsimple-3-2-terraform.bio,dnsimple-tf-ci-1.eu,dnsimple-tf-ci-2.eu,dnsimple-tf-ci-3.eu" 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Provider for DNSimple 2 | 3 | [![Terraform Registry](https://img.shields.io/badge/terraform-registry-623CE4?logo=terraform)](https://registry.terraform.io/providers/dnsimple/dnsimple) 4 | [![License](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE) 5 | 6 | The Terraform DNSimple provider allows you to manage DNSimple resources using Terraform. 7 | 8 | ## Requirements 9 | 10 | - [Terraform](https://www.terraform.io/downloads.html) >= 1.12 11 | - [Go](https://golang.org/doc/install) >= 1.18 (to build the provider from source) 12 | 13 | ## Installation 14 | 15 | The provider is available on the [Terraform Registry](https://registry.terraform.io/providers/dnsimple/dnsimple). Add the following to your Terraform configuration: 16 | 17 | ```hcl 18 | terraform { 19 | required_providers { 20 | dnsimple = { 21 | source = "dnsimple/dnsimple" 22 | version = "~> 1.0" 23 | } 24 | } 25 | } 26 | 27 | provider "dnsimple" { 28 | token = var.dnsimple_token 29 | account = var.dnsimple_account 30 | sandbox = true # Set to false for production 31 | } 32 | ``` 33 | 34 | Then run: 35 | 36 | ```shell 37 | terraform init 38 | ``` 39 | 40 | ## Documentation 41 | 42 | Full documentation is available on the [Terraform Registry](https://registry.terraform.io/providers/dnsimple/dnsimple/latest/docs). 43 | 44 | ## Quick Start 45 | 46 | After installing the provider, configure it with your DNSimple credentials: 47 | 48 | ```hcl 49 | provider "dnsimple" { 50 | token = "your-api-token" 51 | account = "your-account-id" 52 | sandbox = true 53 | } 54 | 55 | resource "dnsimple_zone" "example" { 56 | name = "example.com" 57 | } 58 | 59 | resource "dnsimple_zone_record" "www" { 60 | zone_id = dnsimple_zone.example.id 61 | name = "www" 62 | type = "A" 63 | value = "1.2.3.4" 64 | } 65 | ``` 66 | 67 | ## Development 68 | 69 | ### Getting Started 70 | 71 | Clone the repository: 72 | 73 | ```shell 74 | git clone git@github.com:dnsimple/terraform-provider-dnsimple.git 75 | cd terraform-provider-dnsimple 76 | ``` 77 | 78 | Build the provider: 79 | 80 | ```shell 81 | make build 82 | ``` 83 | 84 | This will build the provider and place the binary in `$GOPATH/bin`. 85 | 86 | ### Testing 87 | 88 | Run the unit tests: 89 | 90 | ```shell 91 | make test 92 | ``` 93 | 94 | Run the acceptance tests (requires DNSimple API credentials): 95 | 96 | ```shell 97 | DNSIMPLE_ACCOUNT=12345 DNSIMPLE_TOKEN="your-token" DNSIMPLE_DOMAIN=example.com DNSIMPLE_SANDBOX=true make testacc 98 | ``` 99 | 100 | **Note:** Acceptance tests create real resources and may incur costs. 101 | 102 | #### Testing Let's Encrypt Resources 103 | 104 | The sandbox environment does not support certificate operations. To test `dnsimple_lets_encrypt_certificate` resources, run tests in production: 105 | 106 | ```shell 107 | DNSIMPLE_SANDBOX=false DNSIMPLE_CERTIFICATE_NAME=www DNSIMPLE_CERTIFICATE_ID=123 make testacc 108 | ``` 109 | 110 | ### Sideloading the Provider 111 | 112 | To use a locally built version of the provider: 113 | 114 | 1. Install the provider: 115 | 116 | ```shell 117 | make install 118 | ``` 119 | 120 | 2. Create a symlink to the Terraform plugins directory: 121 | 122 | ```shell 123 | # Replace darwin_arm64 with your architecture 124 | mkdir -p ~/.terraform.d/plugins/terraform.local/dnsimple/dnsimple/0.1.0/darwin_arm64 125 | ln -s "$GOBIN/terraform-provider-dnsimple" ~/.terraform.d/plugins/terraform.local/dnsimple/dnsimple/0.1.0/darwin_arm64/ 126 | ``` 127 | 128 | 3. Configure Terraform to use the local provider: 129 | 130 | ```hcl 131 | terraform { 132 | required_providers { 133 | dnsimple = { 134 | source = "terraform.local/dnsimple/dnsimple" 135 | version = "0.1.0" 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | 4. Test with the example configuration: 142 | 143 | ```shell 144 | cd example 145 | terraform init && terraform apply 146 | ``` 147 | 148 | ## Contributing 149 | 150 | We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to this project. 151 | 152 | ## License 153 | 154 | This project is licensed under the Mozilla Public License 2.0. See [LICENSE](LICENSE) for details. 155 | 156 | ## Resources 157 | 158 | - [Terraform Registry](https://registry.terraform.io/providers/dnsimple/dnsimple) 159 | - [DNSimple API Documentation](https://developer.dnsimple.com/) 160 | - [DNSimple Support](https://support.dnsimple.com/) 161 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/terraform-providers/terraform-provider-dnsimple 2 | 3 | require ( 4 | github.com/dnsimple/dnsimple-go/v7 v7.0.1 5 | github.com/google/uuid v1.6.0 6 | github.com/hashicorp/terraform-plugin-docs v0.24.0 7 | github.com/hashicorp/terraform-plugin-framework v1.17.0 8 | github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0 9 | github.com/hashicorp/terraform-plugin-go v0.29.0 10 | github.com/hashicorp/terraform-plugin-log v0.10.0 11 | github.com/hashicorp/terraform-plugin-testing v1.14.0 12 | github.com/stretchr/testify v1.11.1 13 | golang.org/x/oauth2 v0.34.0 14 | ) 15 | 16 | require ( 17 | github.com/BurntSushi/toml v1.2.1 // indirect 18 | github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect 19 | github.com/Masterminds/goutils v1.1.1 // indirect 20 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 21 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 22 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 23 | github.com/agext/levenshtein v1.2.2 // indirect 24 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 25 | github.com/armon/go-radix v1.0.0 // indirect 26 | github.com/bgentry/speakeasy v0.1.0 // indirect 27 | github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect 28 | github.com/cloudflare/circl v1.6.1 // indirect 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 30 | github.com/fatih/color v1.18.0 // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/go-cmp v0.7.0 // indirect 33 | github.com/google/go-querystring v1.1.0 // indirect 34 | github.com/hashicorp/cli v1.1.7 // indirect 35 | github.com/hashicorp/errwrap v1.1.0 // indirect 36 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 37 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 38 | github.com/hashicorp/go-cty v1.5.0 // indirect 39 | github.com/hashicorp/go-hclog v1.6.3 // indirect 40 | github.com/hashicorp/go-multierror v1.1.1 // indirect 41 | github.com/hashicorp/go-plugin v1.7.0 // indirect 42 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 43 | github.com/hashicorp/go-uuid v1.0.3 // indirect 44 | github.com/hashicorp/go-version v1.7.0 // indirect 45 | github.com/hashicorp/hc-install v0.9.2 // indirect 46 | github.com/hashicorp/hcl/v2 v2.24.0 // indirect 47 | github.com/hashicorp/logutils v1.0.0 // indirect 48 | github.com/hashicorp/terraform-exec v0.24.0 // indirect 49 | github.com/hashicorp/terraform-json v0.27.2 // indirect 50 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 // indirect 51 | github.com/hashicorp/terraform-registry-address v0.4.0 // indirect 52 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 53 | github.com/hashicorp/yamux v0.1.2 // indirect 54 | github.com/huandu/xstrings v1.3.3 // indirect 55 | github.com/imdario/mergo v0.3.15 // indirect 56 | github.com/mattn/go-colorable v0.1.14 // indirect 57 | github.com/mattn/go-isatty v0.0.20 // indirect 58 | github.com/mattn/go-runewidth v0.0.9 // indirect 59 | github.com/mitchellh/copystructure v1.2.0 // indirect 60 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 61 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 62 | github.com/mitchellh/mapstructure v1.5.0 // indirect 63 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 64 | github.com/oklog/run v1.2.0 // indirect 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 66 | github.com/posener/complete v1.2.3 // indirect 67 | github.com/shopspring/decimal v1.4.0 // indirect 68 | github.com/spf13/cast v1.5.0 // indirect 69 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 70 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 71 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 72 | github.com/yuin/goldmark v1.7.7 // indirect 73 | github.com/yuin/goldmark-meta v1.1.0 // indirect 74 | github.com/zclconf/go-cty v1.17.0 // indirect 75 | go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect 76 | golang.org/x/crypto v0.45.0 // indirect 77 | golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect 78 | golang.org/x/mod v0.29.0 // indirect 79 | golang.org/x/net v0.47.0 // indirect 80 | golang.org/x/sync v0.18.0 // indirect 81 | golang.org/x/sys v0.38.0 // indirect 82 | golang.org/x/text v0.31.0 // indirect 83 | golang.org/x/tools v0.38.0 // indirect 84 | google.golang.org/appengine v1.6.8 // indirect 85 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect 86 | google.golang.org/grpc v1.75.1 // indirect 87 | google.golang.org/protobuf v1.36.9 // indirect 88 | gopkg.in/yaml.v2 v2.3.0 // indirect 89 | gopkg.in/yaml.v3 v3.0.1 // indirect 90 | ) 91 | 92 | go 1.24.4 93 | -------------------------------------------------------------------------------- /internal/framework/resources/registered_domain/modifiers.go: -------------------------------------------------------------------------------- 1 | package registered_domain 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 7 | "github.com/hashicorp/terraform-plugin-framework/types" 8 | "github.com/hashicorp/terraform-plugin-framework/types/basetypes" 9 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" 10 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 11 | ) 12 | 13 | type domainRegistrationState struct{} 14 | 15 | // DomainRegistrationState return a object plan modifier that sets the specified value if the planned value is Null. 16 | func DomainRegistrationState() planmodifier.Object { 17 | return domainRegistrationState{} 18 | } 19 | 20 | func (m domainRegistrationState) Description(context.Context) string { 21 | return "If the domain registration state is not registered, set it to registered. Unless the state is a failing state" 22 | } 23 | 24 | func (m domainRegistrationState) MarkdownDescription(ctx context.Context) string { 25 | return m.Description(ctx) 26 | } 27 | 28 | func (m domainRegistrationState) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { 29 | if value, diags := req.Private.GetKey(ctx, "skip_domain_registration"); diags.HasError() { 30 | resp.Diagnostics.Append(diags...) 31 | } else { 32 | if string(value) == "true" { 33 | resp.PlanValue = basetypes.NewObjectNull(common.DomainRegistrationAttrType) 34 | return 35 | } 36 | } 37 | 38 | if !req.ConfigValue.IsNull() || req.PlanValue.IsUnknown() || req.PlanValue.IsNull() { 39 | return 40 | } 41 | 42 | // Check if the domain registrstion status is as expected 43 | domainRegistration := &common.DomainRegistration{} 44 | if diags := req.PlanValue.As(ctx, domainRegistration, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true}); diags.HasError() { 45 | resp.Diagnostics.Append(diags...) 46 | return 47 | } 48 | 49 | // If the domain registration state is a failing state, do not attempt to update it 50 | if domainRegistration.State.ValueString() == consts.DomainStateFailed { 51 | return 52 | } 53 | 54 | // If the domain registration state is a cancelled state, do not attempt to update it 55 | if domainRegistration.State.ValueString() == consts.DomainStateCancelling || domainRegistration.State.ValueString() == consts.DomainStateCancelled { 56 | return 57 | } 58 | 59 | // If the domain registration state is not registered, set it to registered 60 | // this will trigger a plan change and result in an update so we can attempt to sync 61 | if domainRegistration.State.ValueString() == consts.DomainStateRegistered { 62 | return 63 | } 64 | domainRegistration.State = types.StringValue(consts.DomainStateRegistered) 65 | 66 | obj, diags := types.ObjectValueFrom(ctx, common.DomainRegistrationAttrType, domainRegistration) 67 | 68 | if diags.HasError() { 69 | resp.Diagnostics.Append(diags...) 70 | return 71 | } 72 | 73 | resp.PlanValue = obj 74 | } 75 | 76 | type registrantChangeState struct{} 77 | 78 | // RegistrantChangeState return a object plan modifier that sets the specified value if the planned value is Null. 79 | func RegistrantChangeState() planmodifier.String { 80 | return registrantChangeState{} 81 | } 82 | 83 | func (m registrantChangeState) Description(context.Context) string { 84 | return "If the registrant change state is not completed, set it to completed. Unless the state is a cancelled or cancelling state" 85 | } 86 | 87 | func (m registrantChangeState) MarkdownDescription(ctx context.Context) string { 88 | return m.Description(ctx) 89 | } 90 | 91 | func (m registrantChangeState) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { 92 | if !req.ConfigValue.IsNull() || req.PlanValue.IsUnknown() || req.PlanValue.IsNull() { 93 | return 94 | } 95 | 96 | state := req.PlanValue.ValueString() 97 | 98 | // If the registrant change state is a cancelled state, do not attempt to update it 99 | if state == consts.RegistrantChangeStateCancelling || state == consts.RegistrantChangeStateCancelled { 100 | return 101 | } 102 | 103 | // If the registrant change state is not completed, set it to completed 104 | // this will trigger a plan change and result in an update so we can attempt to sync 105 | if state == consts.RegistrantChangeStateCompleted { 106 | return 107 | } 108 | 109 | resp.PlanValue = types.StringValue(consts.RegistrantChangeStateCompleted) 110 | } 111 | -------------------------------------------------------------------------------- /internal/framework/resources/domain_delegation_resource_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-testing/terraform" 11 | _ "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/resources" 12 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 13 | ) 14 | 15 | func TestAccDomainDelegationResource(t *testing.T) { 16 | domainId := os.Getenv("DNSIMPLE_DOMAIN") 17 | resourceName := "dnsimple_domain_delegation.test" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 21 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 22 | CheckDestroy: testAccCheckDomainDelegationResourceDestroy, 23 | Steps: []resource.TestStep{ 24 | { 25 | Config: testAccDomainDelegationResourceConfig(domainId), 26 | Check: resource.ComposeAggregateTestCheckFunc( 27 | resource.TestCheckResourceAttrSet(resourceName, "id"), 28 | resource.TestCheckResourceAttr(resourceName, "domain", domainId), 29 | resource.TestCheckResourceAttr(resourceName, "name_servers.#", "2"), 30 | resource.TestCheckTypeSetElemAttr(resourceName, "name_servers.*", "ns2.dnsimple-edge.net"), 31 | resource.TestCheckTypeSetElemAttr(resourceName, "name_servers.*", "ns4.dnsimple-edge.org"), 32 | ), 33 | }, 34 | { 35 | Config: testAccDomainDelegationResourceConfigReversed(domainId), 36 | ExpectNonEmptyPlan: false, 37 | PlanOnly: true, 38 | Check: resource.ComposeAggregateTestCheckFunc( 39 | resource.TestCheckResourceAttrSet(resourceName, "id"), 40 | resource.TestCheckResourceAttr(resourceName, "domain", domainId), 41 | resource.TestCheckResourceAttr(resourceName, "name_servers.#", "2"), 42 | resource.TestCheckTypeSetElemAttr(resourceName, "name_servers.*", "ns2.dnsimple-edge.net"), 43 | resource.TestCheckTypeSetElemAttr(resourceName, "name_servers.*", "ns4.dnsimple-edge.org"), 44 | ), 45 | }, 46 | { 47 | Config: testAccDomainDelegationResourceConfigWithSuffix(domainId), 48 | ExpectNonEmptyPlan: false, 49 | PlanOnly: true, 50 | Check: resource.ComposeAggregateTestCheckFunc( 51 | resource.TestCheckResourceAttrSet(resourceName, "id"), 52 | resource.TestCheckResourceAttr(resourceName, "domain", domainId), 53 | resource.TestCheckResourceAttr(resourceName, "name_servers.#", "2"), 54 | resource.TestCheckTypeSetElemAttr(resourceName, "name_servers.*", "ns2.dnsimple-edge.net"), 55 | resource.TestCheckTypeSetElemAttr(resourceName, "name_servers.*", "ns4.dnsimple-edge.org"), 56 | ), 57 | }, 58 | { 59 | ResourceName: resourceName, 60 | ImportStateIdFunc: testAccDomainDelegationImportStateIDFunc(resourceName), 61 | ImportState: true, 62 | ImportStateVerify: true, 63 | }, 64 | // Delete testing automatically occurs in TestCase 65 | }, 66 | }) 67 | } 68 | 69 | // Deleting simply reliquishes control from Terraform and leaves server state intact. 70 | func testAccCheckDomainDelegationResourceDestroy(state *terraform.State) error { 71 | return nil 72 | } 73 | 74 | func testAccDomainDelegationImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { 75 | return func(s *terraform.State) (string, error) { 76 | rs, ok := s.RootModule().Resources[resourceName] 77 | if !ok { 78 | return "", fmt.Errorf("Resource not found: %s", resourceName) 79 | } 80 | 81 | if rs.Primary.ID == "" { 82 | return "", errors.New("No resource ID set") 83 | } 84 | 85 | return rs.Primary.ID, nil 86 | } 87 | } 88 | 89 | func testAccDomainDelegationResourceConfig(domainId string) string { 90 | return fmt.Sprintf(` 91 | resource "dnsimple_domain_delegation" "test" { 92 | domain = %[1]q 93 | name_servers = ["ns2.dnsimple-edge.net", "ns4.dnsimple-edge.org"] 94 | }`, domainId) 95 | } 96 | 97 | func testAccDomainDelegationResourceConfigReversed(domainId string) string { 98 | return fmt.Sprintf(` 99 | resource "dnsimple_domain_delegation" "test" { 100 | domain = %[1]q 101 | name_servers = ["ns4.dnsimple-edge.org", "ns2.dnsimple-edge.net"] 102 | }`, domainId) 103 | } 104 | 105 | func testAccDomainDelegationResourceConfigWithSuffix(domainId string) string { 106 | return fmt.Sprintf(` 107 | resource "dnsimple_domain_delegation" "test" { 108 | domain = %[1]q 109 | name_servers = ["ns4.dnsimple-edge.org.", "ns2.dnsimple-edge.net"] 110 | }`, domainId) 111 | } 112 | -------------------------------------------------------------------------------- /internal/framework/validators/record_type_test.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/path" 8 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 9 | "github.com/hashicorp/terraform-plugin-framework/types" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestRecordType_ValidateString(t *testing.T) { 14 | t.Parallel() 15 | 16 | type testCase struct { 17 | value types.String 18 | expectError bool 19 | errorCount int 20 | } 21 | 22 | tests := map[string]testCase{ 23 | "valid uppercase A": { 24 | value: types.StringValue("A"), 25 | expectError: false, 26 | }, 27 | "valid uppercase AAAA": { 28 | value: types.StringValue("AAAA"), 29 | expectError: false, 30 | }, 31 | "valid uppercase CNAME": { 32 | value: types.StringValue("CNAME"), 33 | expectError: false, 34 | }, 35 | "valid uppercase MX": { 36 | value: types.StringValue("MX"), 37 | expectError: false, 38 | }, 39 | "valid uppercase TXT": { 40 | value: types.StringValue("TXT"), 41 | expectError: false, 42 | }, 43 | "valid uppercase SRV": { 44 | value: types.StringValue("SRV"), 45 | expectError: false, 46 | }, 47 | "valid uppercase NS": { 48 | value: types.StringValue("NS"), 49 | expectError: false, 50 | }, 51 | "valid uppercase PTR": { 52 | value: types.StringValue("PTR"), 53 | expectError: false, 54 | }, 55 | "valid uppercase SOA": { 56 | value: types.StringValue("SOA"), 57 | expectError: false, 58 | }, 59 | "invalid lowercase a": { 60 | value: types.StringValue("a"), 61 | expectError: true, 62 | errorCount: 1, 63 | }, 64 | "invalid lowercase mx": { 65 | value: types.StringValue("mx"), 66 | expectError: true, 67 | errorCount: 1, 68 | }, 69 | "invalid mixed case Aa": { 70 | value: types.StringValue("Aa"), 71 | expectError: true, 72 | errorCount: 1, 73 | }, 74 | "invalid mixed case Mx": { 75 | value: types.StringValue("Mx"), 76 | expectError: true, 77 | errorCount: 1, 78 | }, 79 | "invalid mixed case cNAME": { 80 | value: types.StringValue("cNAME"), 81 | expectError: true, 82 | errorCount: 1, 83 | }, 84 | "invalid mixed case TXt": { 85 | value: types.StringValue("TXt"), 86 | expectError: true, 87 | errorCount: 1, 88 | }, 89 | "null value": { 90 | value: types.StringNull(), 91 | expectError: false, 92 | }, 93 | "unknown value": { 94 | value: types.StringUnknown(), 95 | expectError: false, 96 | }, 97 | "empty string": { 98 | value: types.StringValue(""), 99 | expectError: false, 100 | }, 101 | } 102 | 103 | for name, test := range tests { 104 | name, test := name, test 105 | t.Run(name, func(t *testing.T) { 106 | t.Parallel() 107 | 108 | ctx := context.Background() 109 | request := validator.StringRequest{ 110 | Path: path.Root("type"), 111 | ConfigValue: test.value, 112 | } 113 | response := validator.StringResponse{} 114 | 115 | RecordType{}.ValidateString(ctx, request, &response) 116 | 117 | if !response.Diagnostics.HasError() && test.expectError { 118 | t.Fatal("expected error, got no error") 119 | } 120 | 121 | if response.Diagnostics.HasError() && !test.expectError { 122 | t.Fatalf("got unexpected error: %s", response.Diagnostics) 123 | } 124 | 125 | if test.expectError && test.errorCount > 0 { 126 | assert.Equal(t, test.errorCount, response.Diagnostics.ErrorsCount(), "expected %d error(s), got %d", test.errorCount, response.Diagnostics.ErrorsCount()) 127 | 128 | // Verify the error message contains the expected text 129 | errorSummary := response.Diagnostics.Errors()[0].Summary() 130 | assert.Equal(t, "Record type must be uppercase", errorSummary) 131 | 132 | // Verify the error detail contains the suggestion 133 | errorDetail := response.Diagnostics.Errors()[0].Detail() 134 | assert.Contains(t, errorDetail, "UPPERCASE") 135 | assert.Contains(t, errorDetail, test.value.ValueString()) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestRecordType_Description(t *testing.T) { 142 | t.Parallel() 143 | 144 | ctx := context.Background() 145 | validator := RecordType{} 146 | 147 | description := validator.Description(ctx) 148 | assert.Equal(t, "record type must be specified in UPPERCASE", description) 149 | 150 | markdownDescription := validator.MarkdownDescription(ctx) 151 | assert.Equal(t, description, markdownDescription) 152 | } 153 | -------------------------------------------------------------------------------- /docs/resources/registered_domain.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "DNSimple: dnsimple_registered_domain" 3 | --- 4 | 5 | # dnsimple\_registered\_domain 6 | 7 | Provides a DNSimple registered domain resource. 8 | 9 | ## Example Usage 10 | 11 | The simplest example below requires a contact (existing or a new one to be created) and basic domain information. 12 | 13 | ```hcl 14 | resource "dnsimple_contact" "alice_main" { 15 | label = "Alice" 16 | first_name = "Alice" 17 | last_name = "Appleseed" 18 | organization_name = "Contoso" 19 | job_title = "Manager" 20 | address1 = "Level 1, 2 Main St" 21 | city = "San Francisco" 22 | state_province = "California" 23 | postal_code = "90210" 24 | country = "US" 25 | phone = "+1.401239523" 26 | email = "apple@contoso.com" 27 | } 28 | 29 | resource "dnsimple_registered_domain" "example_com" { 30 | name = "example.com" 31 | 32 | contact_id = dnsimple_contact.alice_main.id 33 | } 34 | ``` 35 | 36 | #### Example with more settings 37 | 38 | ```hcl 39 | resource "dnsimple_registered_domain" "example_com" { 40 | name = "example.com" 41 | 42 | contact_id = dnsimple_contact.alice_main.id 43 | auto_renew_enabled = true 44 | transfer_lock_enabled = true 45 | whois_privacy_enabled = true 46 | dnssec_enabled = false 47 | } 48 | ``` 49 | 50 | #### Example with extended attributes 51 | 52 | Some domain extensions require additional information during registration. You can check if a domain extension requires extended attributes using the [TLD Extended Attributes API](https://developer.dnsimple.com/v2/tlds/#getTldExtendedAttributes). 53 | 54 | ```hcl 55 | resource "dnsimple_registered_domain" "example_bio" { 56 | name = "example.bio" 57 | 58 | contact_id = dnsimple_contact.alice_main.id 59 | auto_renew_enabled = true 60 | 61 | extended_attributes = { 62 | "bio_agree" = "I Agree" 63 | } 64 | } 65 | ``` 66 | 67 | 68 | ## Argument Reference 69 | 70 | The following arguments are supported: 71 | 72 | - `name` - (Required) The domain name to be registered. 73 | - `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, which may result in a [60-day lock](https://support.dnsimple.com/articles/icann-60-day-lock-registrant-change/). 74 | - `auto_renew_enabled` - (Optional) Whether the domain should be set to auto-renew (default: `false`). 75 | - `whois_privacy_enabled` - (Optional) Whether the domain should have WHOIS privacy enabled (default: `false`). 76 | - `dnssec_enabled` - (Optional) Whether the domain should have DNSSEC enabled (default: `false`). 77 | - `transfer_lock_enabled` - (Optional) Whether the domain transfer lock protection is enabled (default: `true`). 78 | - `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. 79 | - `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`. 80 | - `timeouts` - (Optional) (see [below for nested schema](#nested-schema-for-timeouts)). 81 | 82 | ## Attributes Reference 83 | 84 | - `id` - The ID of this resource. 85 | - `unicode_name` - The domain name in Unicode format. 86 | - `state` - The state of the domain. 87 | - `domain_registration` - The domain registration details. (see [below for nested schema](#nested-schema-for-domain_registration)) 88 | 89 | ### Nested Schema for `timeouts` 90 | 91 | Optional: 92 | 93 | - `create` (String) - The timeout for the read operation e.g. `5m` 94 | - `update` (String) - The timeout for the read operation e.g. `5m` 95 | 96 | ### Nested Schema for `domain_registration` 97 | 98 | Attributes Reference: 99 | 100 | - `id` (Number) - The ID of the domain registration. 101 | - `state` (String) - The state of the domain registration. 102 | - `period` (Number) - The registration period in years. 103 | 104 | ## Import 105 | 106 | DNSimple registered domains can be imported using their domain name and **optionally** with domain registration ID. 107 | 108 | **Importing registered domain example.com:** 109 | 110 | ```bash 111 | terraform import dnsimple_registered_domain.example example.com 112 | ``` 113 | 114 | **Importing registered domain example.com with domain registration ID 1234:** 115 | 116 | ```bash 117 | terraform import dnsimple_registered_domain.example example.com_1234 118 | ``` 119 | 120 | ~> **Note:** At present there is no way to retrieve the domain registration ID from the DNSimple API or UI. You will need to have noted the ID when you created the domain registration. Prefer using the domain name only when importing. 121 | -------------------------------------------------------------------------------- /internal/framework/resources/contact_resource_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 11 | "github.com/hashicorp/terraform-plugin-testing/terraform" 12 | _ "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/resources" 13 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 14 | ) 15 | 16 | func TestAccContactResource(t *testing.T) { 17 | resourceName := "dnsimple_contact.test" 18 | 19 | resource.Test(t, resource.TestCase{ 20 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 21 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 22 | CheckDestroy: testAccCheckContactResourceDestroy, 23 | Steps: []resource.TestStep{ 24 | { 25 | Config: testAccContactResourceConfig(), 26 | Check: resource.ComposeAggregateTestCheckFunc( 27 | resource.TestCheckResourceAttr(resourceName, "first_name", "Alice"), 28 | resource.TestCheckResourceAttr(resourceName, "last_name", "Appleseed"), 29 | resource.TestCheckResourceAttr(resourceName, "address1", "123 Main St"), 30 | resource.TestCheckResourceAttr(resourceName, "city", "San Francisco"), 31 | resource.TestCheckResourceAttr(resourceName, "state_province", "CA"), 32 | resource.TestCheckResourceAttr(resourceName, "postal_code", "94105"), 33 | resource.TestCheckResourceAttr(resourceName, "country", "US"), 34 | resource.TestCheckResourceAttr(resourceName, "phone", "+1.5555555555"), 35 | resource.TestCheckResourceAttr(resourceName, "email", "alice.appleseed@example.com"), 36 | ), 37 | }, 38 | { 39 | Config: testAccContactResourceConfigUpdated(), 40 | Check: resource.ComposeAggregateTestCheckFunc( 41 | resource.TestCheckResourceAttr(resourceName, "first_name", "Alice"), 42 | resource.TestCheckResourceAttr(resourceName, "last_name", "Townseed"), 43 | resource.TestCheckResourceAttr(resourceName, "address1", "123 Main St"), 44 | resource.TestCheckResourceAttr(resourceName, "city", "San Francisco"), 45 | resource.TestCheckResourceAttr(resourceName, "state_province", "CA"), 46 | resource.TestCheckResourceAttr(resourceName, "postal_code", "90210"), 47 | resource.TestCheckResourceAttr(resourceName, "country", "US"), 48 | resource.TestCheckResourceAttr(resourceName, "phone", "+1.5555555555"), 49 | resource.TestCheckResourceAttr(resourceName, "email", "alice.appleseed@example.com"), 50 | ), 51 | }, 52 | { 53 | ResourceName: resourceName, 54 | ImportStateIdFunc: testAccContactImportStateIDFunc(resourceName), 55 | ImportState: true, 56 | // We cannot use this as we store `phone` and `fax` into `phone_normalized` and `fax_normalized`. Therefore, we write out the checks manually. 57 | ImportStateVerify: false, 58 | Check: resource.ComposeAggregateTestCheckFunc( 59 | resource.TestCheckResourceAttr(resourceName, "first_name", "Alice"), 60 | resource.TestCheckResourceAttr(resourceName, "last_name", "Townseed"), 61 | resource.TestCheckResourceAttr(resourceName, "address1", "123 Main St"), 62 | resource.TestCheckResourceAttr(resourceName, "city", "San Francisco"), 63 | resource.TestCheckResourceAttr(resourceName, "state_province", "CA"), 64 | resource.TestCheckResourceAttr(resourceName, "postal_code", "90210"), 65 | resource.TestCheckResourceAttr(resourceName, "country", "US"), 66 | resource.TestCheckResourceAttr(resourceName, "phone_normalized", "+1.5555555555"), 67 | resource.TestCheckResourceAttr(resourceName, "email", "alice.appleseed@example.com"), 68 | ), 69 | }, 70 | // Delete testing automatically occurs in TestCase 71 | }, 72 | }) 73 | } 74 | 75 | func testAccContactImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { 76 | return func(s *terraform.State) (string, error) { 77 | rs, ok := s.RootModule().Resources[resourceName] 78 | if !ok { 79 | return "", fmt.Errorf("Resource not found: %s", resourceName) 80 | } 81 | 82 | if rs.Primary.ID == "" { 83 | return "", errors.New("No resource ID set") 84 | } 85 | 86 | return rs.Primary.ID, nil 87 | } 88 | } 89 | 90 | func testAccCheckContactResourceDestroy(state *terraform.State) error { 91 | for _, rs := range state.RootModule().Resources { 92 | if rs.Type != "dnsimple_contact" { 93 | continue 94 | } 95 | 96 | contactIdRaw := rs.Primary.Attributes["id"] 97 | contactId, err := strconv.ParseInt(contactIdRaw, 10, 64) 98 | if err != nil { 99 | return fmt.Errorf("failed to convert contact ID to int: %s", err) 100 | } 101 | 102 | _, err = dnsimpleClient.Contacts.GetContact(context.Background(), testAccAccount, contactId) 103 | 104 | if err == nil { 105 | return fmt.Errorf("contact still exists") 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | func testAccContactResourceConfig() string { 112 | // Required attributes only. 113 | return ` 114 | resource "dnsimple_contact" "test" { 115 | first_name = "Alice" 116 | last_name = "Appleseed" 117 | address1 = "123 Main St" 118 | city = "San Francisco" 119 | state_province = "CA" 120 | postal_code = "94105" 121 | country = "US" 122 | phone = "+1.5555555555" 123 | email = "alice.appleseed@example.com" 124 | }` 125 | } 126 | 127 | func testAccContactResourceConfigUpdated() string { 128 | // Required attributes only. 129 | return ` 130 | resource "dnsimple_contact" "test" { 131 | first_name = "Alice" 132 | last_name = "Townseed" 133 | address1 = "123 Main St" 134 | city = "San Francisco" 135 | state_province = "CA" 136 | postal_code = "90210" 137 | country = "US" 138 | phone = "+1.5555555555" 139 | email = "alice.appleseed@example.com" 140 | }` 141 | } 142 | -------------------------------------------------------------------------------- /internal/framework/resources/lets_encrypt_certificate_resource_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 12 | "github.com/hashicorp/terraform-plugin-testing/terraform" 13 | _ "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/resources" 14 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/test_utils" 15 | ) 16 | 17 | func TestAccLetsEncryptCertificateResource(t *testing.T) { 18 | if os.Getenv("DNSIMPLE_SANDBOX") != "false" { 19 | t.Skip("DNSIMPLE_SANDBOX is not set to `false` (read in CONTRIBUTING.md how to run this test)") 20 | return 21 | } 22 | resourceName := "dnsimple_lets_encrypt_certificate.test" 23 | 24 | domainId := os.Getenv("DNSIMPLE_DOMAIN") 25 | certName := os.Getenv("DNSIMPLE_CERTIFICATE_NAME") 26 | certAltNamesRaw := os.Getenv("DNSIMPLE_CERTIFICATE_ALTERNATE_NAMES") 27 | certAutoRenew := os.Getenv("DNSIMPLE_CERTIFICATE_AUTO_RENEW") == "1" 28 | certSigAlg := os.Getenv("DNSIMPLE_CERTIFICATE_SIGNATURE_ALGORITHM") 29 | 30 | certAltNames := strings.Split(certAltNamesRaw, ",") 31 | 32 | resource.Test(t, resource.TestCase{ 33 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 34 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 35 | CheckDestroy: testAccCheckLetsEncryptCertificateResourceDestroy, 36 | Steps: []resource.TestStep{ 37 | { 38 | Config: testAccLetsEncryptCertificateResourceConfig(domainId, certAutoRenew, certName, certAltNames, certSigAlg), 39 | Check: resource.ComposeAggregateTestCheckFunc( 40 | resource.TestCheckResourceAttrSet(resourceName, "id"), 41 | resource.TestCheckResourceAttr(resourceName, "domain_id", domainId), 42 | resource.TestCheckResourceAttr(resourceName, "name", certName), 43 | resource.TestCheckResourceAttr(resourceName, "alternate_names.#", fmt.Sprintf("%d", len(certAltNames))), 44 | resource.TestCheckResourceAttrSet(resourceName, "years"), 45 | resource.TestCheckResourceAttrSet(resourceName, "state"), 46 | resource.TestCheckResourceAttrSet(resourceName, "authority_identifier"), 47 | resource.TestCheckResourceAttr(resourceName, "auto_renew", strconv.FormatBool(certAutoRenew)), 48 | resource.TestCheckResourceAttrSet(resourceName, "created_at"), 49 | resource.TestCheckResourceAttrSet(resourceName, "updated_at"), 50 | resource.TestCheckResourceAttrSet(resourceName, "csr"), 51 | resource.TestCheckResourceAttr(resourceName, "signature_algorithm", certSigAlg), 52 | ), 53 | }, 54 | // Update is a no-op 55 | // Delete testing automatically occurs in TestCase 56 | }, 57 | }) 58 | } 59 | 60 | func TestAccLetsEncryptCertificateResource_NoAlternateNames(t *testing.T) { 61 | if os.Getenv("DNSIMPLE_SANDBOX") != "false" { 62 | t.Skip("DNSIMPLE_SANDBOX is not set to `false` (read in CONTRIBUTING.md how to run this test)") 63 | return 64 | } 65 | resourceName := "dnsimple_lets_encrypt_certificate.test" 66 | 67 | domainId := os.Getenv("DNSIMPLE_DOMAIN") 68 | certName := os.Getenv("DNSIMPLE_CERTIFICATE_NAME") 69 | certAutoRenew := os.Getenv("DNSIMPLE_CERTIFICATE_AUTO_RENEW") == "1" 70 | certSigAlg := os.Getenv("DNSIMPLE_CERTIFICATE_SIGNATURE_ALGORITHM") 71 | 72 | resource.Test(t, resource.TestCase{ 73 | PreCheck: func() { test_utils.TestAccPreCheck(t) }, 74 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 75 | CheckDestroy: testAccCheckLetsEncryptCertificateResourceDestroy, 76 | Steps: []resource.TestStep{ 77 | { 78 | Config: testAccLetsEncryptCertificateResourceConfig_NoAlternateNames(domainId, certAutoRenew, certName, certSigAlg), 79 | Check: resource.ComposeAggregateTestCheckFunc( 80 | resource.TestCheckResourceAttrSet(resourceName, "id"), 81 | resource.TestCheckResourceAttr(resourceName, "domain_id", domainId), 82 | resource.TestCheckResourceAttr(resourceName, "name", certName), 83 | resource.TestCheckResourceAttr(resourceName, "alternate_names.#", "0"), 84 | resource.TestCheckResourceAttrSet(resourceName, "years"), 85 | resource.TestCheckResourceAttrSet(resourceName, "state"), 86 | resource.TestCheckResourceAttrSet(resourceName, "authority_identifier"), 87 | resource.TestCheckResourceAttr(resourceName, "auto_renew", strconv.FormatBool(certAutoRenew)), 88 | resource.TestCheckResourceAttrSet(resourceName, "created_at"), 89 | resource.TestCheckResourceAttrSet(resourceName, "updated_at"), 90 | resource.TestCheckResourceAttrSet(resourceName, "csr"), 91 | resource.TestCheckResourceAttr(resourceName, "signature_algorithm", certSigAlg), 92 | ), 93 | }, 94 | // Update is a no-op 95 | // Delete testing automatically occurs in TestCase 96 | }, 97 | }) 98 | } 99 | 100 | // We cannot delete certificates from the server. 101 | func testAccCheckLetsEncryptCertificateResourceDestroy(state *terraform.State) error { 102 | return nil 103 | } 104 | 105 | func testAccLetsEncryptCertificateResourceConfig(domainId string, autoRenew bool, name string, alternateNames []string, signatureAlgorithm string) string { 106 | alternateNamesRaw, err := json.Marshal(alternateNames) 107 | if err != nil { 108 | panic(err) 109 | } 110 | return fmt.Sprintf(` 111 | resource "dnsimple_lets_encrypt_certificate" "test" { 112 | domain_id = %[1]q 113 | auto_renew = %[2]t 114 | name = %[3]q 115 | alternate_names = %[4]s 116 | signature_algorithm = %[5]q 117 | }`, domainId, autoRenew, name, string(alternateNamesRaw), signatureAlgorithm) 118 | } 119 | 120 | func testAccLetsEncryptCertificateResourceConfig_NoAlternateNames(domainId string, autoRenew bool, name string, signatureAlgorithm string) string { 121 | return fmt.Sprintf(` 122 | resource "dnsimple_lets_encrypt_certificate" "test" { 123 | domain_id = %[1]q 124 | auto_renew = %[2]t 125 | name = %[3]q 126 | signature_algorithm = %[4]q 127 | }`, domainId, autoRenew, name, signatureAlgorithm) 128 | } 129 | -------------------------------------------------------------------------------- /tools/sweep/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 10 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" 11 | ) 12 | 13 | func main() { 14 | token := os.Getenv("DNSIMPLE_TOKEN") 15 | account := os.Getenv("DNSIMPLE_ACCOUNT") 16 | sandbox := os.Getenv("DNSIMPLE_SANDBOX") 17 | 18 | if sandbox != "true" { 19 | panic("DNSIMPLE_SANDBOX must be set to true") 20 | } 21 | 22 | dnsimpleClient := dnsimple.NewClient(dnsimple.StaticTokenHTTPClient(context.Background(), token)) 23 | dnsimpleClient.UserAgent = "terraform-provider-dnsimple/test" 24 | dnsimpleClient.BaseURL = consts.BaseURLSandbox 25 | 26 | cancelContactChanges(context.Background(), dnsimpleClient, account) 27 | cleanupDomains(context.Background(), dnsimpleClient, account) 28 | } 29 | 30 | // RegistrantChangeCancelStates is a list of states that can be cancelled 31 | var RegistrantChangeCancelStates = []string{ 32 | consts.RegistrantChangeStateNew, 33 | consts.RegistrantChangeStatePending, 34 | } 35 | 36 | func cancelContactChanges(ctx context.Context, dnsimpleClient *dnsimple.Client, account string) { 37 | domainName := os.Getenv("DNSIMPLE_REGISTRANT_CHANGE_DOMAIN") 38 | 39 | if domainName == "" { 40 | fmt.Println("Skipping registrant change cleanup as DNSIMPLE_REGISTRANT_CHANGE_DOMAIN is not set") 41 | return 42 | } 43 | 44 | // Get the domain ID 45 | domainResponse, err := dnsimpleClient.Domains.GetDomain(ctx, account, domainName) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | listOptions := &dnsimple.RegistrantChangeListOptions{ 51 | State: dnsimple.String(consts.RegistrantChangeStateNew), 52 | ListOptions: dnsimple.ListOptions{ 53 | PerPage: dnsimple.Int(100), 54 | }, 55 | } 56 | 57 | contactChanges, err := dnsimpleClient.Registrar.ListRegistrantChange(ctx, account, listOptions) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | for _, contactChange := range contactChanges.Data { 63 | if !contains(RegistrantChangeCancelStates, contactChange.State) { 64 | continue 65 | } 66 | 67 | if contactChange.DomainId != int(domainResponse.Data.ID) { 68 | continue 69 | } 70 | 71 | fmt.Printf("Cancelling registrant change for %s id=%d state=%s\n", domainName, contactChange.Id, contactChange.State) 72 | _, err := dnsimpleClient.Registrar.DeleteRegistrantChange(ctx, account, contactChange.Id) 73 | if err != nil { 74 | panic(err) 75 | } 76 | } 77 | 78 | if contactChanges.Pagination.TotalPages > 1 { 79 | for page := 2; page <= contactChanges.Pagination.TotalPages; page++ { 80 | listOptions := &dnsimple.RegistrantChangeListOptions{ 81 | State: dnsimple.String(consts.RegistrantChangeStateNew), 82 | ListOptions: dnsimple.ListOptions{ 83 | Page: dnsimple.Int(page), 84 | PerPage: dnsimple.Int(100), 85 | }, 86 | } 87 | 88 | contactChanges, err := dnsimpleClient.Registrar.ListRegistrantChange(ctx, account, listOptions) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | for _, contactChange := range contactChanges.Data { 94 | if !contains(RegistrantChangeCancelStates, contactChange.State) { 95 | continue 96 | } 97 | 98 | if contactChange.DomainId != int(domainResponse.Data.ID) { 99 | continue 100 | } 101 | 102 | fmt.Printf("Cancelling registrant change for %s id=%d state=%s\n", domainName, contactChange.Id, contactChange.State) 103 | _, err := dnsimpleClient.Registrar.DeleteRegistrantChange(ctx, account, contactChange.Id) 104 | if err != nil { 105 | panic(err) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | func contains(s []string, e string) bool { 113 | for _, a := range s { 114 | if a == e { 115 | return true 116 | } 117 | } 118 | 119 | return false 120 | } 121 | 122 | func cleanupDomains(ctx context.Context, dnsimpleClient *dnsimple.Client, account string) { 123 | cleanupEnabled := os.Getenv("DNSIMPLE_CLEANUP_DOMAINS") 124 | 125 | if cleanupEnabled != "true" { 126 | fmt.Println("Skipping domain cleanup as DNSIMPLE_CLEANUP_DOMAINS is not set") 127 | return 128 | } 129 | 130 | domainsToKeep := os.Getenv("DNSIMPLE_DOMAINS_TO_KEEP") 131 | var domainsToKeepList []string 132 | if domainsToKeep != "" { 133 | // Split the comma separated list of domains to keep 134 | domainsToKeepList = strings.Split(domainsToKeep, ",") 135 | } 136 | 137 | listOptions := &dnsimple.DomainListOptions{ 138 | ListOptions: dnsimple.ListOptions{ 139 | PerPage: dnsimple.Int(100), 140 | }, 141 | } 142 | 143 | domains, err := dnsimpleClient.Domains.ListDomains(ctx, account, listOptions) 144 | if err != nil { 145 | panic(err) 146 | } 147 | 148 | for _, domain := range domains.Data { 149 | if contains(domainsToKeepList, domain.Name) { 150 | continue 151 | } 152 | 153 | fmt.Printf("Deleting domain %s\n", domain.Name) 154 | _, err := dnsimpleClient.Domains.DeleteDomain(ctx, account, domain.Name) 155 | if err != nil && strings.Contains(err.Error(), "The domain cannot be deleted because it is either being registered or is transferring in") { 156 | fmt.Printf("Skipping domain %s because it is being registered or is transferring in\n", domain.Name) 157 | continue 158 | } else if err != nil { 159 | panic(err) 160 | } 161 | } 162 | 163 | if domains.Pagination.TotalPages > 1 { 164 | for page := 2; page <= domains.Pagination.TotalPages; page++ { 165 | listOptions := &dnsimple.DomainListOptions{ 166 | ListOptions: dnsimple.ListOptions{ 167 | Page: dnsimple.Int(page), 168 | PerPage: dnsimple.Int(100), 169 | }, 170 | } 171 | 172 | domains, err := dnsimpleClient.Domains.ListDomains(ctx, account, listOptions) 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | for _, domain := range domains.Data { 178 | if contains(domainsToKeepList, domain.Name) { 179 | continue 180 | } 181 | 182 | fmt.Printf("Deleting domain %s\n", domain.Name) 183 | _, err := dnsimpleClient.Domains.DeleteDomain(ctx, account, domain.Name) 184 | if err != nil { 185 | panic(err) 186 | } 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /internal/framework/resources/registered_domain/create.go: -------------------------------------------------------------------------------- 1 | package registered_domain 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | 9 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/hashicorp/terraform-plugin-framework/types" 12 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" 13 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 14 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" 15 | ) 16 | 17 | func (r *RegisteredDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 18 | var data *RegisteredDomainResourceModel 19 | 20 | // Read Terraform plan data into the model 21 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 22 | 23 | if resp.Diagnostics.HasError() { 24 | return 25 | } 26 | 27 | domainAttributes := dnsimple.RegisterDomainInput{ 28 | RegistrantID: int(data.ContactId.ValueInt64()), 29 | } 30 | 31 | if !data.AutoRenewEnabled.IsNull() { 32 | domainAttributes.EnableAutoRenewal = data.AutoRenewEnabled.ValueBool() 33 | } 34 | 35 | if !data.WhoisPrivacyEnabled.IsNull() { 36 | domainAttributes.EnableWhoisPrivacy = data.WhoisPrivacyEnabled.ValueBool() 37 | } 38 | 39 | if !data.PremiumPrice.IsNull() { 40 | domainAttributes.PremiumPrice = data.PremiumPrice.ValueString() 41 | } 42 | 43 | if !data.ExtendedAttributes.IsNull() { 44 | extendedAttrs := make(map[string]string) 45 | diags := data.ExtendedAttributes.ElementsAs(ctx, &extendedAttrs, false) 46 | resp.Diagnostics.Append(diags...) 47 | if resp.Diagnostics.HasError() { 48 | return 49 | } 50 | domainAttributes.ExtendedAttributes = extendedAttrs 51 | } 52 | 53 | registerDomainResponse, err := r.config.Client.Registrar.RegisterDomain(ctx, r.config.AccountID, data.Name.ValueString(), &domainAttributes) 54 | if err != nil { 55 | var errorResponse *dnsimple.ErrorResponse 56 | if errors.As(err, &errorResponse) { 57 | resp.Diagnostics.Append(utils.AttributeErrorsToDiagnostics(errorResponse)...) 58 | return 59 | } 60 | 61 | resp.Diagnostics.AddError( 62 | "failed to register DNSimple Domain", 63 | err.Error(), 64 | ) 65 | return 66 | } 67 | 68 | if registerDomainResponse.Data.State != consts.DomainStateRegistered { 69 | convergenceState, err := tryToConvergeRegistration(ctx, data, &resp.Diagnostics, r, strconv.Itoa(int(registerDomainResponse.Data.ID))) 70 | if convergenceState == RegistrationFailed { 71 | // Response is already populated with the error we can safely return 72 | return 73 | } 74 | 75 | if convergenceState == RegistrationConvergenceTimeout { 76 | domainRegistration, secondaryErr := r.config.Client.Registrar.GetDomainRegistration(ctx, r.config.AccountID, data.Name.ValueString(), strconv.Itoa(int(registerDomainResponse.Data.ID))) 77 | 78 | if secondaryErr != nil { 79 | resp.Diagnostics.AddError( 80 | "failed to read DNSimple Domain Registration", 81 | fmt.Sprintf("Unable to read domain registration for domain '%s': %s", data.Name.ValueString(), secondaryErr.Error()), 82 | ) 83 | return 84 | } 85 | 86 | domainResponse, secondaryErr := r.config.Client.Domains.GetDomain(ctx, r.config.AccountID, data.Name.ValueString()) 87 | 88 | if secondaryErr != nil { 89 | resp.Diagnostics.AddError( 90 | "failed to read DNSimple Domain", 91 | fmt.Sprintf("Unable to read domain '%s': %s", data.Name.ValueString(), secondaryErr.Error()), 92 | ) 93 | return 94 | } 95 | 96 | // Commit the partial state to the model 97 | r.updateModelFromAPIResponsePartialCreate(ctx, data, domainRegistration.Data, domainResponse.Data) 98 | // Save data into Terraform state 99 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 100 | 101 | // Exit with warning to prevent the state from being tainted 102 | resp.Diagnostics.AddWarning( 103 | "failed to converge on domain registration", 104 | err.Error(), 105 | ) 106 | return 107 | } 108 | } 109 | 110 | // Domain registration was successful, we can now proceed with the rest of the resource 111 | 112 | if !data.DNSSECEnabled.IsNull() && data.DNSSECEnabled.ValueBool() { 113 | diags := r.setDNSSEC(ctx, data) 114 | if diags.HasError() { 115 | resp.Diagnostics.Append(diags...) 116 | return 117 | } 118 | } 119 | 120 | if !data.TransferLockEnabled.IsNull() && data.TransferLockEnabled.ValueBool() { 121 | diags := r.setTransferLock(ctx, data) 122 | if diags.HasError() { 123 | resp.Diagnostics.Append(diags...) 124 | return 125 | } 126 | } 127 | 128 | domainResponse, err := r.config.Client.Domains.GetDomain(ctx, r.config.AccountID, data.Name.ValueString()) 129 | if err != nil { 130 | resp.Diagnostics.AddError( 131 | "failed to read DNSimple Domain", 132 | fmt.Sprintf("Unable to read domain '%s': %s", data.Name.ValueString(), err.Error()), 133 | ) 134 | return 135 | } 136 | 137 | dnssecResponse, err := r.config.Client.Domains.GetDnssec(ctx, r.config.AccountID, data.Name.ValueString()) 138 | if err != nil { 139 | resp.Diagnostics.AddError( 140 | "failed to read DNSimple Domain DNSSEC status", 141 | fmt.Sprintf("Unable to read DNSSEC status for domain '%s': %s", data.Name.ValueString(), err.Error()), 142 | ) 143 | return 144 | } 145 | 146 | transferLockResponse, err := r.config.Client.Registrar.GetDomainTransferLock(ctx, r.config.AccountID, data.Name.ValueString()) 147 | if err != nil { 148 | resp.Diagnostics.AddError( 149 | "failed to read DNSimple Domain Transfer Lock status", 150 | fmt.Sprintf("Unable to read transfer lock status for domain '%s': %s", data.Name.ValueString(), err.Error()), 151 | ) 152 | return 153 | } 154 | 155 | diags := r.updateModelFromAPIResponse(ctx, data, registerDomainResponse.Data, domainResponse.Data, dnssecResponse.Data, transferLockResponse.Data) 156 | if diags != nil && diags.HasError() { 157 | resp.Diagnostics.Append(diags...) 158 | return 159 | } 160 | 161 | data.RegistrantChange = types.ObjectNull(common.RegistrantChangeAttrType) 162 | 163 | // Save data into Terraform state 164 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 165 | } 166 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: "Provider: DNSimple" 3 | --- 4 | 5 | # DNSimple Provider 6 | 7 | The DNSimple provider allows you to manage DNS records, domains, certificates, and other DNSimple resources using Terraform. 8 | 9 | This provider enables you to treat your DNS and domain infrastructure as code, making it easier to version, review, and manage your DNSimple resources alongside your other infrastructure. 10 | 11 | [![IMAGE_ALT](https://img.youtube.com/vi/cTWP1MWA-0c/0.jpg)](https://www.youtube.com/watch?v=cTWP1MWA-0c) 12 | 13 | ## Requirements 14 | 15 | - [Terraform](https://www.terraform.io/downloads.html) >= 1.12 16 | - DNSimple account with API access 17 | 18 | ## Installation 19 | 20 | Add the DNSimple provider to your Terraform configuration: 21 | 22 | ```hcl 23 | terraform { 24 | required_version = ">= 1.12" 25 | 26 | required_providers { 27 | dnsimple = { 28 | source = "dnsimple/dnsimple" 29 | version = "~> 1.9" 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | Then run `terraform init` to download the provider. 36 | 37 | ## Authentication 38 | 39 | The provider requires authentication credentials to interact with the DNSimple API. You can provide credentials in several ways: 40 | 41 | 1. **Provider configuration** (recommended for development) 42 | 2. **Environment variables** (recommended for CI/CD and production) 43 | 44 | ### Using Provider Configuration 45 | 46 | ```hcl 47 | provider "dnsimple" { 48 | token = var.dnsimple_token 49 | account = var.dnsimple_account 50 | } 51 | ``` 52 | 53 | ### Using Environment Variables 54 | 55 | ```bash 56 | export DNSIMPLE_TOKEN="your-api-token" 57 | export DNSIMPLE_ACCOUNT="your-account-id" 58 | ``` 59 | 60 | See the [Argument Reference](#argument-reference) section below for all configuration options. 61 | 62 | ## Example Usage 63 | 64 | Configure the provider: 65 | 66 | ```hcl 67 | terraform { 68 | required_version = ">= 1.12" 69 | 70 | required_providers { 71 | dnsimple = { 72 | source = "dnsimple/dnsimple" 73 | version = "~> 1.9" 74 | } 75 | } 76 | } 77 | 78 | provider "dnsimple" { 79 | token = var.dnsimple_token 80 | account = var.dnsimple_account 81 | } 82 | 83 | variable "dnsimple_token" { 84 | description = "DNSimple API Token" 85 | type = string 86 | sensitive = true 87 | } 88 | 89 | variable "dnsimple_account" { 90 | description = "DNSimple Account ID" 91 | type = string 92 | } 93 | ``` 94 | 95 | Now use the available resources to perform actions like managing DNS records or registering domains. 96 | 97 | To manage your DNS records: 98 | 99 | ```hcl 100 | # Create a zone 101 | resource "dnsimple_zone" "example" { 102 | name = "example.com" 103 | } 104 | 105 | # Create DNS records 106 | resource "dnsimple_zone_record" "www" { 107 | zone_name = dnsimple_zone.example.name 108 | name = "www" 109 | value = "192.0.2.1" 110 | type = "A" 111 | ttl = 3600 112 | } 113 | 114 | resource "dnsimple_zone_record" "apex" { 115 | zone_name = dnsimple_zone.example.name 116 | name = "" 117 | value = "192.0.2.1" 118 | type = "A" 119 | ttl = 3600 120 | } 121 | ``` 122 | 123 | To register a domain: 124 | 125 | ```hcl 126 | # Create a contact for domain registration 127 | resource "dnsimple_contact" "registrant" { 128 | label = "Main Contact" 129 | first_name = "John" 130 | last_name = "Doe" 131 | organization_name = "Example Inc" 132 | address1 = "123 Main Street" 133 | city = "San Francisco" 134 | state_province = "California" 135 | postal_code = "94105" 136 | country = "US" 137 | phone = "+1.4155551234" 138 | email = "john@example.com" 139 | } 140 | 141 | # Register a domain 142 | resource "dnsimple_registered_domain" "example_com" { 143 | name = "example.com" 144 | contact_id = dnsimple_contact.registrant.id 145 | 146 | auto_renew_enabled = true 147 | whois_privacy_enabled = true 148 | transfer_lock_enabled = true 149 | } 150 | ``` 151 | 152 | For more elaborate use cases, and to learn more about the capabilities offered by the DNSimple Terraform provider, view the individual resource and data source pages. 153 | 154 | ## Argument Reference 155 | 156 | The following arguments are supported in the provider configuration: 157 | 158 | - **`token`** (Required) - The DNSimple [API v2 token](https://support.dnsimple.com/articles/api-access-token/). Can be provided via the `DNSIMPLE_TOKEN` environment variable. You can use either a User or Account token, but an Account token is recommended for better security and access control. 159 | 160 | - **`account`** (Required) - The ID of the account associated with the token. Can be provided via the `DNSIMPLE_ACCOUNT` environment variable. 161 | 162 | - **`sandbox`** (Optional) - Set to `true` to connect to the API [sandbox environment](https://developer.dnsimple.com/sandbox/) for testing. Can be provided via the `DNSIMPLE_SANDBOX` environment variable. Defaults to `false`. 163 | 164 | - **`prefetch`** (Optional) - Set to `true` to enable prefetching zone records when dealing with large configurations. This is useful when you are dealing with API rate limitations given your number of zones and zone records. Can be provided via the `DNSIMPLE_PREFETCH` environment variable. Defaults to `false`. 165 | 166 | - **`user_agent`** (Optional) - Custom string to append to the user agent used for sending HTTP requests to the API. Useful for identifying your automation or integration. 167 | 168 | ## Getting Help 169 | 170 | - [Support article](https://support.dnsimple.com/articles/terraform-provider/) - Official support documentation 171 | - [Developer API documentation](https://developer.dnsimple.com/) - Complete API reference 172 | - [GitHub Repository](https://github.com/dnsimple/terraform-provider-dnsimple) - Source code and issue tracker 173 | 174 | ## Related Articles 175 | 176 | - [Introducing DNSimple's Terraform Provider](https://blog.dnsimple.com/2021/12/introducing-dnsimple-terraform-provider/) 177 | - [DNSimple, Terraform & Sentinel — A Guide to Policy as Code](https://blog.dnsimple.com/2023/05/policy-as-code/) 178 | - [Manage Domain Transfer Locking and Contacts in Terraform](https://blog.dnsimple.com/2023/06/terraform-domain-registrations/) 179 | - [How We Manage Domain and DNS Management with Infrastructure as Code](https://blog.dnsimple.com/2025/11/managing-domains-terraform-dnsimple/) 180 | -------------------------------------------------------------------------------- /internal/framework/datasources/registrant_change_check_data_source.go: -------------------------------------------------------------------------------- 1 | package datasources 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 8 | "github.com/hashicorp/terraform-plugin-framework/attr" 9 | "github.com/hashicorp/terraform-plugin-framework/datasource" 10 | "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 11 | "github.com/hashicorp/terraform-plugin-framework/types" 12 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 13 | ) 14 | 15 | // Ensure provider defined types fully satisfy framework interfaces. 16 | var _ datasource.DataSource = &RegistrantChangeCheckDataSource{} 17 | 18 | func NewRegistrantChangeCheckDataSource() datasource.DataSource { 19 | return &RegistrantChangeCheckDataSource{} 20 | } 21 | 22 | // RegistrantChangeCheckDataSource defines the data source implementation. 23 | type RegistrantChangeCheckDataSource struct { 24 | config *common.DnsimpleProviderConfig 25 | } 26 | 27 | // RegistrantChangeCheckDataSourceModel describes the data source data model. 28 | type RegistrantChangeCheckDataSourceModel struct { 29 | Id types.String `tfsdk:"id"` 30 | ContactId types.String `tfsdk:"contact_id"` 31 | DomainId types.String `tfsdk:"domain_id"` 32 | ExtendedAttributes []ExtendedAttribute `tfsdk:"extended_attributes"` 33 | RegistryOwnerChange types.Bool `tfsdk:"registry_owner_change"` 34 | } 35 | 36 | type ExtendedAttribute struct { 37 | Name types.String `tfsdk:"name"` 38 | Description types.String `tfsdk:"description"` 39 | Required types.Bool `tfsdk:"required"` 40 | Options []ExtendedAttributeOption `tfsdk:"options"` 41 | } 42 | 43 | type ExtendedAttributeOption struct { 44 | Title types.String `tfsdk:"title"` 45 | Value types.String `tfsdk:"value"` 46 | Description types.String `tfsdk:"description"` 47 | } 48 | 49 | func (d *RegistrantChangeCheckDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { 50 | resp.TypeName = req.ProviderTypeName + "_registrant_change_check" 51 | } 52 | 53 | func (d *RegistrantChangeCheckDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { 54 | resp.Schema = schema.Schema{ 55 | // This description is used by the documentation generator and the language server. 56 | MarkdownDescription: "DNSimple registrant change check data source", 57 | 58 | Attributes: map[string]schema.Attribute{ 59 | "id": common.IDStringAttribute(), 60 | "contact_id": schema.StringAttribute{ 61 | MarkdownDescription: "DNSimple contact ID for which the registrant change check is being performed", 62 | Required: true, 63 | }, 64 | "domain_id": schema.StringAttribute{ 65 | MarkdownDescription: "DNSimple domain ID for which the registrant change check is being performed", 66 | Required: true, 67 | }, 68 | "extended_attributes": schema.ListAttribute{ 69 | MarkdownDescription: "Extended attributes for the registrant change", 70 | ElementType: types.ObjectType{ 71 | AttrTypes: map[string]attr.Type{ 72 | "name": types.StringType, 73 | "description": types.StringType, 74 | "required": types.BoolType, 75 | "options": types.ListType{ 76 | ElemType: types.ObjectType{ 77 | AttrTypes: map[string]attr.Type{ 78 | "title": types.StringType, 79 | "value": types.StringType, 80 | "description": types.StringType, 81 | }, 82 | }, 83 | }, 84 | }, 85 | }, 86 | Computed: true, 87 | }, 88 | "registry_owner_change": schema.BoolAttribute{ 89 | MarkdownDescription: "True if the registrant change will result in a registry owner change", 90 | Computed: true, 91 | }, 92 | }, 93 | } 94 | } 95 | 96 | func (d *RegistrantChangeCheckDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { 97 | // Prevent panic if the provider has not been configured. 98 | if req.ProviderData == nil { 99 | return 100 | } 101 | 102 | config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) 103 | 104 | if !ok { 105 | resp.Diagnostics.AddError( 106 | "Unexpected Data Source Configure Type", 107 | fmt.Sprintf("Expected *common.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), 108 | ) 109 | 110 | return 111 | } 112 | 113 | d.config = config 114 | } 115 | 116 | func (d *RegistrantChangeCheckDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { 117 | var data RegistrantChangeCheckDataSourceModel 118 | 119 | // Read Terraform configuration data into the model 120 | resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) 121 | 122 | if resp.Diagnostics.HasError() { 123 | return 124 | } 125 | 126 | requestInput := dnsimple.CheckRegistrantChangeInput{ 127 | ContactId: data.ContactId.ValueString(), 128 | DomainId: data.DomainId.ValueString(), 129 | } 130 | 131 | response, err := d.config.Client.Registrar.CheckRegistrantChange(ctx, d.config.AccountID, &requestInput) 132 | if err != nil { 133 | resp.Diagnostics.AddError( 134 | "failed to check DNSimple Registrant Change", 135 | err.Error(), 136 | ) 137 | return 138 | } 139 | 140 | contactIdString := fmt.Sprintf("%d", response.Data.ContactId) 141 | data.Id = types.StringValue(data.DomainId.ValueString() + ":" + contactIdString) 142 | data.ContactId = types.StringValue(contactIdString) 143 | data.ExtendedAttributes = make([]ExtendedAttribute, len(response.Data.ExtendedAttributes)) 144 | for i, extendedAttribute := range response.Data.ExtendedAttributes { 145 | data.ExtendedAttributes[i].Name = types.StringValue(extendedAttribute.Name) 146 | data.ExtendedAttributes[i].Description = types.StringValue(extendedAttribute.Description) 147 | data.ExtendedAttributes[i].Required = types.BoolValue(extendedAttribute.Required) 148 | data.ExtendedAttributes[i].Options = make([]ExtendedAttributeOption, len(extendedAttribute.Options)) 149 | for j, option := range extendedAttribute.Options { 150 | data.ExtendedAttributes[i].Options[j].Title = types.StringValue(option.Title) 151 | data.ExtendedAttributes[i].Options[j].Value = types.StringValue(option.Value) 152 | data.ExtendedAttributes[i].Options[j].Description = types.StringValue(option.Description) 153 | } 154 | } 155 | data.RegistryOwnerChange = types.BoolValue(response.Data.RegistryOwnerChange) 156 | 157 | // Save data into Terraform state 158 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 159 | } 160 | -------------------------------------------------------------------------------- /docs/guides/resource-migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_title: Migrate from deprecated resources 3 | --- 4 | 5 | # Migrate from Deprecated Resources 6 | 7 | Learn how to migrate resources that have been deprecated or renamed in favor of a new resource. 8 | 9 | ## Overview 10 | 11 | When a resource is deprecated or renamed in a new version of the provider, Terraform will not be able to find the resource and will fail to run. To migrate to the new resource, you will need to manipulate the state file and configuration files. 12 | 13 | -> **Note:** This guide only covers migrating resources that have a replacement resource. If a resource has been removed without a replacement, you will need to manually remove the resource from the state file using `terraform state rm`. 14 | 15 | ## Migration Steps 16 | 17 | The migration process consists of three main steps: 18 | 19 | 1. **Obtain the resource ID** - Identify the unique identifier for the resource you're migrating 20 | 2. **Remove the old resource from state** - Remove the deprecated resource from Terraform's state file 21 | 3. **Update configuration and import** - Update your configuration files to use the new resource and import it into state 22 | 23 | ## Example Migration 24 | 25 | For the purpose of this guide, we will demonstrate migrating from `dnsimple_record` (deprecated) to `dnsimple_zone_record` (current). 26 | 27 | ### Step 1: Obtain the Resource ID 28 | 29 | Before starting the migration, you need to identify the resource ID. The resource ID can be found in several ways: 30 | 31 | - **From the Terraform state file** - Use `terraform console` to query the state 32 | - **From the DNSimple UI** - View the resource in the DNSimple web dashboard 33 | - **From the DNSimple API** - Query the API directly 34 | 35 | To retrieve the ID from the state file, you can use the following command: 36 | 37 | ```shell 38 | echo dnsimple_record.demo.id | terraform console 39 | ``` 40 | 41 | -> **Note:** To list all the resources tracked in the state, you can use the `terraform state list` command. 42 | 43 | For `dnsimple_zone_record`, the import ID format is `_`. You may need to construct this ID from separate values. Refer to each resource's documentation for the specific import ID format. 44 | 45 | ### Step 2: Remove the Old Resource from State 46 | 47 | Remove the deprecated resource from Terraform's state file using the `terraform state rm` command: 48 | 49 | ```shell 50 | terraform state rm dnsimple_record.demo 51 | ``` 52 | 53 | Expected output: 54 | 55 | ``` 56 | Removed dnsimple_record.demo 57 | Successfully removed 1 resource instance(s). 58 | ``` 59 | 60 | -> **Important:** This command only removes the resource from Terraform's state file. It does not delete the actual resource in DNSimple. The resource will continue to exist and function normally. 61 | 62 | ### Step 3: Update Configuration and Import 63 | 64 | Update your configuration files to use the new resource. Note any differences in attribute names or required fields between the old and new resources. 65 | 66 | **Old resource configuration:** 67 | 68 | ```hcl 69 | locals { 70 | vegan_pizza = "vegan.pizza" 71 | } 72 | 73 | resource "dnsimple_record" "demo" { 74 | domain = local.vegan_pizza 75 | name = "demo" 76 | value = "2.3.4.5" 77 | type = "A" 78 | ttl = 3600 79 | } 80 | ``` 81 | 82 | **New resource configuration:** 83 | 84 | ```hcl 85 | locals { 86 | vegan_pizza = "vegan.pizza" 87 | } 88 | 89 | resource "dnsimple_zone_record" "demo" { 90 | zone_name = local.vegan_pizza 91 | name = "demo" 92 | value = "2.3.4.5" 93 | type = "A" 94 | ttl = 3600 95 | } 96 | ``` 97 | 98 | Notice that `domain` has been changed to `zone_name` in the new resource. 99 | 100 | Now import the resource into the state using the `terraform import` command with the resource ID you obtained earlier: 101 | 102 | ```shell 103 | terraform import dnsimple_zone_record.demo vegan.pizza_2879253 104 | ``` 105 | 106 | Expected output: 107 | 108 | ``` 109 | dnsimple_zone_record.demo: Importing from ID "vegan.pizza_2879253"... 110 | dnsimple_zone_record.demo: Import prepared! 111 | Prepared dnsimple_zone_record for import 112 | dnsimple_zone_record.demo: Refreshing state... [id=2879253] 113 | 114 | Import successful! 115 | 116 | The resources that were imported are shown above. These resources are now in 117 | your Terraform state and will henceforth be managed by Terraform. 118 | ``` 119 | 120 | -> **Note:** The resource ID for `dnsimple_zone_record` is in the format `_`. For example, `vegan.pizza_2645561`. Refer to the [resource documentation](https://registry.terraform.io/providers/dnsimple/dnsimple/latest/docs/resources/zone_record#import) for more details. 121 | 122 | ### Step 4: Verify the Migration 123 | 124 | After importing the resource, run `terraform plan` to verify that Terraform recognizes the resource and that no changes are needed: 125 | 126 | ```shell 127 | terraform plan 128 | ``` 129 | 130 | Expected output: 131 | 132 | ``` 133 | dnsimple_zone_record.demo: Refreshing state... [id=2879253] 134 | 135 | No changes. Your infrastructure matches the configuration. 136 | 137 | Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed. 138 | ``` 139 | 140 | If `terraform plan` shows differences, review the configuration to ensure all attributes match the imported resource. You may need to adjust attribute values or add missing attributes. 141 | 142 | ## Troubleshooting 143 | 144 | ### Import ID Format 145 | 146 | If the import fails, verify that you're using the correct import ID format for the resource. Each resource type has a specific format documented in its resource page. Common formats include: 147 | 148 | - `_` for zone records 149 | - Domain names for domain resources 150 | - Numeric IDs for other resources 151 | 152 | ### Configuration Mismatches 153 | 154 | If `terraform plan` shows changes after import, compare the imported resource attributes with your configuration. Common issues include: 155 | 156 | - Attribute name changes (e.g., `domain` → `zone_name`) 157 | - Default value differences 158 | - Missing optional attributes that were set in the original resource 159 | 160 | ### Multiple Resources 161 | 162 | When migrating multiple resources, repeat the process for each resource. You can automate this with scripts, but be careful to verify each migration individually. 163 | 164 | ## Additional Resources 165 | 166 | - [Terraform Import Documentation](https://www.terraform.io/docs/import/index.html) - Official Terraform import guide 167 | - [Terraform State Management](https://www.terraform.io/docs/state/index.html) - Understanding Terraform state 168 | - [DNSimple Provider Documentation](https://registry.terraform.io/providers/dnsimple/dnsimple/latest/docs) - Complete provider reference 169 | -------------------------------------------------------------------------------- /internal/framework/resources/domain_resource.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 9 | "github.com/hashicorp/terraform-plugin-framework/path" 10 | "github.com/hashicorp/terraform-plugin-framework/resource" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 14 | "github.com/hashicorp/terraform-plugin-framework/types" 15 | "github.com/hashicorp/terraform-plugin-log/tflog" 16 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 17 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" 18 | ) 19 | 20 | // Ensure the implementation satisfies the expected interfaces. 21 | var ( 22 | _ resource.Resource = &DomainResource{} 23 | _ resource.ResourceWithConfigure = &DomainResource{} 24 | _ resource.ResourceWithImportState = &DomainResource{} 25 | ) 26 | 27 | func NewDomainResource() resource.Resource { 28 | return &DomainResource{} 29 | } 30 | 31 | // DomainResource defines the resource implementation. 32 | type DomainResource struct { 33 | config *common.DnsimpleProviderConfig 34 | } 35 | 36 | // DomainResourceModel describes the resource data model. 37 | type DomainResourceModel struct { 38 | Name types.String `tfsdk:"name"` 39 | AccountId types.Int64 `tfsdk:"account_id"` 40 | RegistrantId types.Int64 `tfsdk:"registrant_id"` 41 | UnicodeName types.String `tfsdk:"unicode_name"` 42 | State types.String `tfsdk:"state"` 43 | AutoRenew types.Bool `tfsdk:"auto_renew"` 44 | PrivateWhois types.Bool `tfsdk:"private_whois"` 45 | Id types.Int64 `tfsdk:"id"` 46 | } 47 | 48 | func (r *DomainResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 49 | resp.TypeName = req.ProviderTypeName + "_domain" 50 | } 51 | 52 | func (r *DomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 53 | resp.Schema = schema.Schema{ 54 | // This description is used by the documentation generator and the language server. 55 | MarkdownDescription: "DNSimple domain resource", 56 | Attributes: map[string]schema.Attribute{ 57 | "name": schema.StringAttribute{ 58 | Required: true, 59 | PlanModifiers: []planmodifier.String{ 60 | stringplanmodifier.RequiresReplace(), 61 | }, 62 | }, 63 | "account_id": schema.Int64Attribute{ 64 | Computed: true, 65 | }, 66 | "registrant_id": schema.Int64Attribute{ 67 | Computed: true, 68 | }, 69 | "unicode_name": schema.StringAttribute{ 70 | Computed: true, 71 | }, 72 | "state": schema.StringAttribute{ 73 | Computed: true, 74 | }, 75 | "auto_renew": schema.BoolAttribute{ 76 | Computed: true, 77 | }, 78 | "private_whois": schema.BoolAttribute{ 79 | Computed: true, 80 | }, 81 | "id": common.IDInt64Attribute(), 82 | }, 83 | } 84 | } 85 | 86 | func (r *DomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 87 | // Prevent panic if the provider has not been configured. 88 | if req.ProviderData == nil { 89 | return 90 | } 91 | 92 | config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) 93 | 94 | if !ok { 95 | resp.Diagnostics.AddError( 96 | "Unexpected Resource Configure Type", 97 | fmt.Sprintf("Expected *common.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), 98 | ) 99 | 100 | return 101 | } 102 | 103 | r.config = config 104 | } 105 | 106 | func (r *DomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 107 | var data *DomainResourceModel 108 | 109 | // Read Terraform plan data into the model 110 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 111 | 112 | if resp.Diagnostics.HasError() { 113 | return 114 | } 115 | 116 | domainAttributes := dnsimple.Domain{ 117 | Name: data.Name.ValueString(), 118 | } 119 | 120 | response, err := r.config.Client.Domains.CreateDomain(ctx, r.config.AccountID, domainAttributes) 121 | if err != nil { 122 | var errorResponse *dnsimple.ErrorResponse 123 | if errors.As(err, &errorResponse) { 124 | resp.Diagnostics.Append(utils.AttributeErrorsToDiagnostics(errorResponse)...) 125 | return 126 | } 127 | 128 | resp.Diagnostics.AddError( 129 | "failed to create DNSimple Domain", 130 | err.Error(), 131 | ) 132 | return 133 | } 134 | 135 | r.updateModelFromAPIResponse(response.Data, data) 136 | 137 | // Save data into Terraform state 138 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 139 | } 140 | 141 | func (r *DomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 142 | var data *DomainResourceModel 143 | 144 | // Read Terraform prior state data into the model 145 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 146 | 147 | if resp.Diagnostics.HasError() { 148 | return 149 | } 150 | 151 | response, err := r.config.Client.Domains.GetDomain(ctx, r.config.AccountID, data.Name.ValueString()) 152 | if err != nil { 153 | resp.Diagnostics.AddError( 154 | "failed to read DNSimple Domain", 155 | fmt.Sprintf("Unable to read domain '%s': %s", data.Name.ValueString(), err.Error()), 156 | ) 157 | return 158 | } 159 | 160 | r.updateModelFromAPIResponse(response.Data, data) 161 | 162 | // Save updated data into Terraform state 163 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 164 | } 165 | 166 | func (r *DomainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 167 | // No-op 168 | } 169 | 170 | func (r *DomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 171 | var data *DomainResourceModel 172 | 173 | // Read Terraform prior state data into the model 174 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 175 | 176 | if resp.Diagnostics.HasError() { 177 | return 178 | } 179 | 180 | tflog.Info(ctx, fmt.Sprintf("Deleting DNSimple Domain: %s, %s", data.Name, data.Id)) 181 | 182 | _, err := r.config.Client.Domains.DeleteDomain(ctx, r.config.AccountID, data.Name.ValueString()) 183 | if err != nil { 184 | resp.Diagnostics.AddError( 185 | "failed to delete DNSimple Domain", 186 | fmt.Sprintf("Unable to delete domain '%s': %s", data.Name.ValueString(), err.Error()), 187 | ) 188 | return 189 | } 190 | } 191 | 192 | func (r *DomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 193 | response, err := r.config.Client.Domains.GetDomain(ctx, r.config.AccountID, req.ID) 194 | if err != nil { 195 | resp.Diagnostics.AddError( 196 | "failed to import DNSimple Domain", 197 | fmt.Sprintf("Unable to find domain '%s': %s", req.ID, err.Error()), 198 | ) 199 | return 200 | } 201 | 202 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), response.Data.ID)...) 203 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), response.Data.Name)...) 204 | } 205 | 206 | func (r *DomainResource) updateModelFromAPIResponse(domain *dnsimple.Domain, data *DomainResourceModel) { 207 | data.Id = types.Int64Value(domain.ID) 208 | data.Name = types.StringValue(domain.Name) 209 | data.AccountId = types.Int64Value(domain.AccountID) 210 | data.RegistrantId = types.Int64Value(domain.RegistrantID) 211 | data.UnicodeName = types.StringValue(domain.UnicodeName) 212 | data.State = types.StringValue(domain.State) 213 | data.AutoRenew = types.BoolValue(domain.AutoRenew) 214 | data.PrivateWhois = types.BoolValue(domain.PrivateWhois) 215 | } 216 | -------------------------------------------------------------------------------- /internal/framework/resources/domain_delegation_resource.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 9 | "github.com/hashicorp/terraform-plugin-framework/diag" 10 | "github.com/hashicorp/terraform-plugin-framework/path" 11 | "github.com/hashicorp/terraform-plugin-framework/resource" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/types" 16 | "github.com/hashicorp/terraform-plugin-log/tflog" 17 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 18 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/modifiers" 19 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" 20 | ) 21 | 22 | // Ensure the implementation satisfies the expected interfaces. 23 | var ( 24 | _ resource.Resource = &DomainDelegationResource{} 25 | _ resource.ResourceWithConfigure = &DomainDelegationResource{} 26 | _ resource.ResourceWithImportState = &DomainDelegationResource{} 27 | ) 28 | 29 | func NewDomainDelegationResource() resource.Resource { 30 | return &DomainDelegationResource{} 31 | } 32 | 33 | // DomainDelegationResource defines the resource implementation. 34 | type DomainDelegationResource struct { 35 | config *common.DnsimpleProviderConfig 36 | } 37 | 38 | // DomainDelegationResourceModel describes the resource data model. 39 | type DomainDelegationResourceModel struct { 40 | Id types.String `tfsdk:"id"` 41 | Domain types.String `tfsdk:"domain"` 42 | NameServers types.Set `tfsdk:"name_servers"` 43 | } 44 | 45 | func (r *DomainDelegationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 46 | resp.TypeName = req.ProviderTypeName + "_domain_delegation" 47 | } 48 | 49 | func (r *DomainDelegationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 50 | resp.Schema = schema.Schema{ 51 | // This description is used by the documentation generator and the language server. 52 | MarkdownDescription: "DNSimple domain delegation resource", 53 | Attributes: map[string]schema.Attribute{ 54 | "id": common.IDStringAttribute(), 55 | "domain": schema.StringAttribute{ 56 | Required: true, 57 | PlanModifiers: []planmodifier.String{ 58 | stringplanmodifier.RequiresReplace(), 59 | }, 60 | }, 61 | "name_servers": schema.SetAttribute{ 62 | Required: true, 63 | ElementType: types.StringType, 64 | PlanModifiers: []planmodifier.Set{ 65 | modifiers.SetTrimSuffixValue(), 66 | }, 67 | }, 68 | }, 69 | } 70 | } 71 | 72 | func (r *DomainDelegationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 73 | // Prevent panic if the provider has not been configured. 74 | if req.ProviderData == nil { 75 | return 76 | } 77 | 78 | config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) 79 | 80 | if !ok { 81 | resp.Diagnostics.AddError( 82 | "Unexpected Resource Configure Type", 83 | fmt.Sprintf("Expected *common.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), 84 | ) 85 | 86 | return 87 | } 88 | 89 | r.config = config 90 | } 91 | 92 | func (r *DomainDelegationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 93 | var data *DomainDelegationResourceModel 94 | 95 | // Read Terraform plan data into the model 96 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 97 | 98 | if resp.Diagnostics.HasError() { 99 | return 100 | } 101 | 102 | nameServers := dnsimple.Delegation{} 103 | resp.Diagnostics.Append(data.NameServers.ElementsAs(ctx, &nameServers, false)...) 104 | 105 | tflog.Debug(ctx, "creating domain delegation", map[string]interface{}{"name servers": nameServers}) 106 | 107 | _, err := r.config.Client.Registrar.ChangeDomainDelegation(ctx, r.config.AccountID, data.Domain.ValueString(), &nameServers) 108 | if err != nil { 109 | var errorResponse *dnsimple.ErrorResponse 110 | if errors.As(err, &errorResponse) { 111 | resp.Diagnostics.Append(utils.AttributeErrorsToDiagnostics(errorResponse)...) 112 | return 113 | } 114 | 115 | resp.Diagnostics.AddError( 116 | "failed to create DNSimple Domain Delegation", 117 | err.Error(), 118 | ) 119 | return 120 | } 121 | 122 | data.Id = data.Domain 123 | 124 | tflog.Info(ctx, "created domain delegation", map[string]interface{}{"domain": data.Domain}) 125 | 126 | // Save data into Terraform state 127 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 128 | } 129 | 130 | func (r *DomainDelegationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 131 | // Read Terraform prior state data into the model. 132 | var data *DomainDelegationResourceModel 133 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 134 | if resp.Diagnostics.HasError() { 135 | return 136 | } 137 | 138 | response, err := r.config.Client.Registrar.GetDomainDelegation(ctx, r.config.AccountID, data.Domain.ValueString()) 139 | if err != nil { 140 | resp.Diagnostics.AddError( 141 | "failed to read DNSimple Domain Delegation", 142 | fmt.Sprintf("Unable to read domain delegation for domain '%s': %s", data.Domain.ValueString(), err.Error()), 143 | ) 144 | return 145 | } 146 | 147 | resp.Diagnostics.Append(r.updateModelFromAPIResponse(ctx, response.Data, data)...) 148 | 149 | // Save updated data into Terraform state. 150 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 151 | } 152 | 153 | func (r *DomainDelegationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 154 | var data *DomainDelegationResourceModel 155 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 156 | if resp.Diagnostics.HasError() { 157 | return 158 | } 159 | 160 | nameServers := dnsimple.Delegation{} 161 | resp.Diagnostics.Append(data.NameServers.ElementsAs(ctx, &nameServers, false)...) 162 | if resp.Diagnostics.HasError() { 163 | return 164 | } 165 | 166 | response, err := r.config.Client.Registrar.ChangeDomainDelegation(ctx, r.config.AccountID, data.Domain.ValueString(), &nameServers) 167 | if err != nil { 168 | resp.Diagnostics.AddError( 169 | "failed to update DNSimple Domain Delegation", 170 | fmt.Sprintf("Unable to update domain delegation for domain '%s': %s", data.Domain.ValueString(), err.Error()), 171 | ) 172 | return 173 | } 174 | 175 | tflog.Debug(ctx, "domain delegation updated", map[string]interface{}{"data": response.Data}) 176 | 177 | data.Id = data.Domain 178 | 179 | resp.Diagnostics.Append(r.updateModelFromAPIResponse(ctx, response.Data, data)...) 180 | 181 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 182 | } 183 | 184 | func (r *DomainDelegationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 185 | // No-op 186 | tflog.Info(ctx, "deleting a domain delegation simply deletes the Terraform state, and the current domain delegation will remain as is and no longer be managed by Terraform") 187 | } 188 | 189 | func (r *DomainDelegationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 190 | domainId := req.ID 191 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), domainId)...) 192 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain"), domainId)...) 193 | } 194 | 195 | func (r *DomainDelegationResource) updateModelFromAPIResponse(ctx context.Context, delegation *dnsimple.Delegation, data *DomainDelegationResourceModel) diag.Diagnostics { 196 | nameServers, diag := types.SetValueFrom(ctx, types.StringType, delegation) 197 | data.NameServers = nameServers 198 | return diag 199 | } 200 | -------------------------------------------------------------------------------- /internal/framework/resources/email_forward_resource.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 11 | "github.com/hashicorp/terraform-plugin-framework/path" 12 | "github.com/hashicorp/terraform-plugin-framework/resource" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 16 | "github.com/hashicorp/terraform-plugin-framework/types" 17 | "github.com/hashicorp/terraform-plugin-log/tflog" 18 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 19 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" 20 | ) 21 | 22 | // Ensure the implementation satisfies the expected interfaces. 23 | var ( 24 | _ resource.Resource = &EmailForwardResource{} 25 | _ resource.ResourceWithConfigure = &EmailForwardResource{} 26 | _ resource.ResourceWithImportState = &EmailForwardResource{} 27 | ) 28 | 29 | func NewEmailForwardResource() resource.Resource { 30 | return &EmailForwardResource{} 31 | } 32 | 33 | // EmailForwardResource defines the resource implementation. 34 | type EmailForwardResource struct { 35 | config *common.DnsimpleProviderConfig 36 | } 37 | 38 | // EmailForwardResourceModel describes the resource data model. 39 | type EmailForwardResourceModel struct { 40 | Domain types.String `tfsdk:"domain"` 41 | AliasName types.String `tfsdk:"alias_name"` 42 | AliasEmail types.String `tfsdk:"alias_email"` 43 | DestinationEmail types.String `tfsdk:"destination_email"` 44 | Id types.Int64 `tfsdk:"id"` 45 | } 46 | 47 | func (r *EmailForwardResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 48 | resp.TypeName = req.ProviderTypeName + "_email_forward" 49 | } 50 | 51 | func (r *EmailForwardResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 52 | resp.Schema = schema.Schema{ 53 | // This description is used by the documentation generator and the language server. 54 | MarkdownDescription: "DNSimple email forward resource", 55 | Attributes: map[string]schema.Attribute{ 56 | "domain": schema.StringAttribute{ 57 | Required: true, 58 | PlanModifiers: []planmodifier.String{ 59 | stringplanmodifier.RequiresReplace(), 60 | }, 61 | }, 62 | "alias_name": schema.StringAttribute{ 63 | Required: true, 64 | PlanModifiers: []planmodifier.String{ 65 | stringplanmodifier.RequiresReplace(), 66 | }, 67 | }, 68 | "alias_email": schema.StringAttribute{ 69 | Computed: true, 70 | }, 71 | "destination_email": schema.StringAttribute{ 72 | Required: true, 73 | PlanModifiers: []planmodifier.String{ 74 | stringplanmodifier.RequiresReplace(), 75 | }, 76 | }, 77 | "id": common.IDInt64Attribute(), 78 | }, 79 | } 80 | } 81 | 82 | func (r *EmailForwardResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 83 | // Prevent panic if the provider has not been configured. 84 | if req.ProviderData == nil { 85 | return 86 | } 87 | 88 | config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) 89 | 90 | if !ok { 91 | resp.Diagnostics.AddError( 92 | "Unexpected Resource Configure Type", 93 | fmt.Sprintf("Expected *common.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), 94 | ) 95 | 96 | return 97 | } 98 | 99 | r.config = config 100 | } 101 | 102 | func (r *EmailForwardResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 103 | var data *EmailForwardResourceModel 104 | 105 | // Read Terraform plan data into the model 106 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 107 | 108 | if resp.Diagnostics.HasError() { 109 | return 110 | } 111 | 112 | domainAttributes := dnsimple.EmailForward{ 113 | AliasName: data.AliasName.ValueString(), 114 | DestinationEmail: data.DestinationEmail.ValueString(), 115 | } 116 | 117 | tflog.Debug(ctx, "creating DNSimple EmailForward", map[string]interface{}{"attributes": domainAttributes}) 118 | 119 | response, err := r.config.Client.Domains.CreateEmailForward(ctx, r.config.AccountID, data.Domain.ValueString(), domainAttributes) 120 | if err != nil { 121 | var errorResponse *dnsimple.ErrorResponse 122 | if errors.As(err, &errorResponse) { 123 | resp.Diagnostics.Append(utils.AttributeErrorsToDiagnostics(errorResponse)...) 124 | return 125 | } 126 | 127 | resp.Diagnostics.AddError( 128 | "failed to create DNSimple Email Forward", 129 | err.Error(), 130 | ) 131 | return 132 | } 133 | 134 | r.updateModelFromAPIResponse(response.Data, data) 135 | 136 | tflog.Info(ctx, "created DNSimple EmailForward", map[string]interface{}{"id": data.Id.ValueInt64()}) 137 | 138 | // Save data into Terraform state 139 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 140 | } 141 | 142 | func (r *EmailForwardResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 143 | var data *EmailForwardResourceModel 144 | 145 | // Read Terraform prior state data into the model 146 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 147 | 148 | if resp.Diagnostics.HasError() { 149 | return 150 | } 151 | 152 | response, err := r.config.Client.Domains.GetEmailForward(ctx, r.config.AccountID, data.Domain.ValueString(), data.Id.ValueInt64()) 153 | if err != nil { 154 | resp.Diagnostics.AddError( 155 | "failed to read DNSimple Email Forward", 156 | fmt.Sprintf("Unable to read email forward with ID %d: %s", data.Id.ValueInt64(), err.Error()), 157 | ) 158 | return 159 | } 160 | 161 | r.updateModelFromAPIResponse(response.Data, data) 162 | 163 | // Save updated data into Terraform state 164 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 165 | } 166 | 167 | func (r *EmailForwardResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 168 | // No-op 169 | tflog.Info(ctx, "DNSimple does not support updating email forwards") 170 | } 171 | 172 | func (r *EmailForwardResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 173 | var data *EmailForwardResourceModel 174 | 175 | // Read Terraform prior state data into the model 176 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 177 | 178 | if resp.Diagnostics.HasError() { 179 | return 180 | } 181 | 182 | tflog.Info(ctx, fmt.Sprintf("Deleting DNSimple EmailForward: %d", data.Id.ValueInt64())) 183 | 184 | _, err := r.config.Client.Domains.DeleteEmailForward(ctx, r.config.AccountID, data.Domain.ValueString(), data.Id.ValueInt64()) 185 | if err != nil { 186 | resp.Diagnostics.AddError( 187 | "failed to delete DNSimple Email Forward", 188 | fmt.Sprintf("Unable to delete email forward with ID %d: %s", data.Id.ValueInt64(), err.Error()), 189 | ) 190 | return 191 | } 192 | } 193 | 194 | func (r *EmailForwardResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 195 | parts := strings.Split(req.ID, "_") 196 | if len(parts) != 2 { 197 | resp.Diagnostics.AddError( 198 | "invalid import ID", 199 | fmt.Sprintf("Invalid import ID format '%s'. Expected format: '_'", req.ID), 200 | ) 201 | return 202 | } 203 | domainName := parts[0] 204 | recordID := parts[1] 205 | 206 | id, err := strconv.ParseInt(recordID, 10, 64) 207 | if err != nil { 208 | resp.Diagnostics.AddError( 209 | "invalid import ID", 210 | fmt.Sprintf("Unable to parse email forward ID '%s' as integer. Expected a numeric ID", recordID), 211 | ) 212 | return 213 | } 214 | 215 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) 216 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain"), domainName)...) 217 | } 218 | 219 | func (r *EmailForwardResource) updateModelFromAPIResponse(emailForward *dnsimple.EmailForward, data *EmailForwardResourceModel) { 220 | data.Id = types.Int64Value(emailForward.ID) 221 | if emailForward.AliasEmail != "" { 222 | data.AliasName = types.StringValue(strings.Split(emailForward.AliasEmail, "@")[0]) 223 | data.AliasEmail = types.StringValue(emailForward.AliasEmail) 224 | } else if emailForward.AliasName != "" { 225 | data.AliasName = types.StringValue(emailForward.AliasName) 226 | data.AliasEmail = types.StringValue(emailForward.AliasName + "@" + data.Domain.ValueString()) 227 | } 228 | data.DestinationEmail = types.StringValue(emailForward.DestinationEmail) 229 | } 230 | -------------------------------------------------------------------------------- /internal/framework/resources/domain_ds_record_resource.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/dnsimple/dnsimple-go/v7/dnsimple" 11 | "github.com/hashicorp/terraform-plugin-framework/path" 12 | "github.com/hashicorp/terraform-plugin-framework/resource" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 15 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 16 | "github.com/hashicorp/terraform-plugin-framework/types" 17 | "github.com/hashicorp/terraform-plugin-log/tflog" 18 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 19 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" 20 | ) 21 | 22 | // Ensure the implementation satisfies the expected interfaces. 23 | var ( 24 | _ resource.Resource = &DsRecordResource{} 25 | _ resource.ResourceWithConfigure = &DsRecordResource{} 26 | _ resource.ResourceWithImportState = &DsRecordResource{} 27 | ) 28 | 29 | func NewDsRecordResource() resource.Resource { 30 | return &DsRecordResource{} 31 | } 32 | 33 | // DsRecordResource defines the resource implementation. 34 | type DsRecordResource struct { 35 | config *common.DnsimpleProviderConfig 36 | } 37 | 38 | // DsRecordResourceModel describes the resource data model. 39 | type DsRecordResourceModel struct { 40 | Id types.Int64 `tfsdk:"id"` 41 | Domain types.String `tfsdk:"domain"` 42 | Algorithm types.String `tfsdk:"algorithm"` 43 | Digest types.String `tfsdk:"digest"` 44 | DigestType types.String `tfsdk:"digest_type"` 45 | Keytag types.String `tfsdk:"keytag"` 46 | PublicKey types.String `tfsdk:"public_key"` 47 | CreatedAt types.String `tfsdk:"created_at"` 48 | UpdatedAt types.String `tfsdk:"updated_at"` 49 | } 50 | 51 | func (r *DsRecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 52 | resp.TypeName = req.ProviderTypeName + "_ds_record" 53 | } 54 | 55 | func (r *DsRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 56 | resp.Schema = schema.Schema{ 57 | // This description is used by the documentation generator and the language server. 58 | MarkdownDescription: "DNSimple domain delegation signer record resource", 59 | Attributes: map[string]schema.Attribute{ 60 | "id": common.IDInt64Attribute(), 61 | "domain": schema.StringAttribute{ 62 | Required: true, 63 | PlanModifiers: []planmodifier.String{ 64 | stringplanmodifier.RequiresReplace(), 65 | }, 66 | }, 67 | "algorithm": schema.StringAttribute{ 68 | Required: true, 69 | PlanModifiers: []planmodifier.String{ 70 | stringplanmodifier.RequiresReplace(), 71 | }, 72 | }, 73 | "digest": schema.StringAttribute{ 74 | Optional: true, 75 | PlanModifiers: []planmodifier.String{ 76 | stringplanmodifier.RequiresReplace(), 77 | }, 78 | }, 79 | "digest_type": schema.StringAttribute{ 80 | Optional: true, 81 | PlanModifiers: []planmodifier.String{ 82 | stringplanmodifier.RequiresReplace(), 83 | }, 84 | }, 85 | "keytag": schema.StringAttribute{ 86 | Optional: true, 87 | PlanModifiers: []planmodifier.String{ 88 | stringplanmodifier.RequiresReplace(), 89 | }, 90 | }, 91 | "public_key": schema.StringAttribute{ 92 | Optional: true, 93 | PlanModifiers: []planmodifier.String{ 94 | stringplanmodifier.RequiresReplace(), 95 | }, 96 | }, 97 | "created_at": schema.StringAttribute{ 98 | Computed: true, 99 | }, 100 | "updated_at": schema.StringAttribute{ 101 | Computed: true, 102 | }, 103 | }, 104 | } 105 | } 106 | 107 | func (r *DsRecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 108 | // Prevent panic if the provider has not been configured. 109 | if req.ProviderData == nil { 110 | return 111 | } 112 | 113 | config, ok := req.ProviderData.(*common.DnsimpleProviderConfig) 114 | 115 | if !ok { 116 | resp.Diagnostics.AddError( 117 | "Unexpected Resource Configure Type", 118 | fmt.Sprintf("Expected *common.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), 119 | ) 120 | 121 | return 122 | } 123 | 124 | r.config = config 125 | } 126 | 127 | func (r *DsRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 128 | var data *DsRecordResourceModel 129 | 130 | // Read Terraform plan data into the model 131 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 132 | 133 | if resp.Diagnostics.HasError() { 134 | return 135 | } 136 | 137 | dsAttributes := dnsimple.DelegationSignerRecord{ 138 | Algorithm: data.Algorithm.ValueString(), 139 | Digest: data.Digest.ValueString(), 140 | DigestType: data.DigestType.ValueString(), 141 | Keytag: data.Keytag.ValueString(), 142 | PublicKey: data.PublicKey.ValueString(), 143 | } 144 | 145 | response, err := r.config.Client.Domains.CreateDelegationSignerRecord(ctx, r.config.AccountID, data.Domain.ValueString(), dsAttributes) 146 | if err != nil { 147 | var errorResponse *dnsimple.ErrorResponse 148 | if errors.As(err, &errorResponse) { 149 | resp.Diagnostics.Append(utils.AttributeErrorsToDiagnostics(errorResponse)...) 150 | return 151 | } 152 | 153 | resp.Diagnostics.AddError( 154 | "failed to create DNSimple DS Record", 155 | err.Error(), 156 | ) 157 | return 158 | } 159 | 160 | r.updateModelFromAPIResponse(response.Data, data) 161 | 162 | // Save data into Terraform state 163 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 164 | } 165 | 166 | func (r *DsRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 167 | var data *DsRecordResourceModel 168 | 169 | // Read Terraform prior state data into the model 170 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 171 | 172 | if resp.Diagnostics.HasError() { 173 | return 174 | } 175 | 176 | response, err := r.config.Client.Domains.GetDelegationSignerRecord(ctx, r.config.AccountID, data.Domain.ValueString(), data.Id.ValueInt64()) 177 | if err != nil { 178 | resp.Diagnostics.AddError( 179 | "failed to read DNSimple DS Record", 180 | fmt.Sprintf("Unable to read DS record with ID %d: %s", data.Id.ValueInt64(), err.Error()), 181 | ) 182 | return 183 | } 184 | 185 | r.updateModelFromAPIResponse(response.Data, data) 186 | 187 | // Save updated data into Terraform state 188 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 189 | } 190 | 191 | func (r *DsRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 192 | // No-op 193 | tflog.Info(ctx, "delegation signer records cannot be updated") 194 | } 195 | 196 | func (r *DsRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 197 | var data *DsRecordResourceModel 198 | 199 | // Read Terraform prior state data into the model 200 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 201 | 202 | if resp.Diagnostics.HasError() { 203 | return 204 | } 205 | 206 | tflog.Info(ctx, fmt.Sprintf("Deleting DNSimple domain delegation signer record: %s", data.Id)) 207 | 208 | _, err := r.config.Client.Domains.DeleteDelegationSignerRecord(ctx, r.config.AccountID, data.Domain.ValueString(), data.Id.ValueInt64()) 209 | if err != nil { 210 | resp.Diagnostics.AddError( 211 | "failed to delete DNSimple DS Record", 212 | fmt.Sprintf("Unable to delete DS record with ID %d: %s", data.Id.ValueInt64(), err.Error()), 213 | ) 214 | } 215 | } 216 | 217 | func (r *DsRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 218 | parts := strings.Split(req.ID, "_") 219 | if len(parts) != 2 { 220 | resp.Diagnostics.AddError( 221 | "invalid import ID", 222 | fmt.Sprintf("Invalid import ID format '%s'. Expected format: '_'", req.ID), 223 | ) 224 | } 225 | domain := parts[0] 226 | dsIdRaw := parts[1] 227 | 228 | dsId, err := strconv.ParseInt(dsIdRaw, 10, 64) 229 | if err != nil { 230 | resp.Diagnostics.AddError( 231 | "invalid import ID", 232 | fmt.Sprintf("Unable to parse DS record ID '%s' as integer. Expected a numeric ID", dsIdRaw), 233 | ) 234 | return 235 | } 236 | 237 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), dsId)...) 238 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("domain"), domain)...) 239 | } 240 | 241 | func (r *DsRecordResource) updateModelFromAPIResponse(ds *dnsimple.DelegationSignerRecord, data *DsRecordResourceModel) { 242 | data.Id = types.Int64Value(ds.ID) 243 | data.Algorithm = types.StringValue(ds.Algorithm) 244 | data.Digest = types.StringValue(ds.Digest) 245 | data.DigestType = types.StringValue(ds.DigestType) 246 | data.Keytag = types.StringValue(ds.Keytag) 247 | data.PublicKey = types.StringValue(ds.PublicKey) 248 | data.CreatedAt = types.StringValue(ds.CreatedAt) 249 | data.UpdatedAt = types.StringValue(ds.UpdatedAt) 250 | } 251 | -------------------------------------------------------------------------------- /internal/framework/resources/registered_domain/schema.go: -------------------------------------------------------------------------------- 1 | package registered_domain 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/terraform-plugin-framework/resource" 7 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 8 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" 9 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" 10 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" 11 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 12 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 13 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 14 | "github.com/hashicorp/terraform-plugin-framework/types" 15 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" 16 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/modifiers" 17 | "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/validators" 18 | ) 19 | 20 | // Ensure the implementation satisfies the expected interfaces. 21 | var ( 22 | _ resource.Resource = &RegisteredDomainResource{} 23 | _ resource.ResourceWithConfigure = &RegisteredDomainResource{} 24 | _ resource.ResourceWithImportState = &RegisteredDomainResource{} 25 | ) 26 | 27 | func NewRegisteredDomainResource() resource.Resource { 28 | return &RegisteredDomainResource{} 29 | } 30 | 31 | // RegisteredDomainResource defines the resource implementation. 32 | type RegisteredDomainResource struct { 33 | config *common.DnsimpleProviderConfig 34 | } 35 | 36 | // RegistrantChangeResourceModel describes the resource data model. 37 | type RegistrantChangeResourceModel struct { 38 | Id types.Int64 `tfsdk:"id"` 39 | AccountId types.Int64 `tfsdk:"account_id"` 40 | ContactId types.Int64 `tfsdk:"contact_id"` 41 | DomainId types.String `tfsdk:"domain_id"` 42 | State types.String `tfsdk:"state"` 43 | ExtendedAttributes types.Map `tfsdk:"extended_attributes"` 44 | RegistryOwnerChange types.Bool `tfsdk:"registry_owner_change"` 45 | IrtLockLiftedBy types.String `tfsdk:"irt_lock_lifted_by"` 46 | } 47 | 48 | // DomainResourceModel describes the resource data model. 49 | type RegisteredDomainResourceModel struct { 50 | Name types.String `tfsdk:"name"` 51 | AccountId types.Int64 `tfsdk:"account_id"` 52 | UnicodeName types.String `tfsdk:"unicode_name"` 53 | State types.String `tfsdk:"state"` 54 | AutoRenewEnabled types.Bool `tfsdk:"auto_renew_enabled"` 55 | WhoisPrivacyEnabled types.Bool `tfsdk:"whois_privacy_enabled"` 56 | DNSSECEnabled types.Bool `tfsdk:"dnssec_enabled"` 57 | TransferLockEnabled types.Bool `tfsdk:"transfer_lock_enabled"` 58 | ContactId types.Int64 `tfsdk:"contact_id"` 59 | ExpiresAt types.String `tfsdk:"expires_at"` 60 | ExtendedAttributes types.Map `tfsdk:"extended_attributes"` 61 | PremiumPrice types.String `tfsdk:"premium_price"` 62 | DomainRegistration types.Object `tfsdk:"domain_registration"` 63 | RegistrantChange types.Object `tfsdk:"registrant_change"` 64 | Timeouts types.Object `tfsdk:"timeouts"` 65 | Id types.Int64 `tfsdk:"id"` 66 | } 67 | 68 | func (r *RegisteredDomainResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 69 | resp.TypeName = req.ProviderTypeName + "_registered_domain" 70 | } 71 | 72 | func (r *RegisteredDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 73 | resp.Schema = schema.Schema{ 74 | // This description is used by the documentation generator and the language server. 75 | MarkdownDescription: "DNSimple domain resource", 76 | Attributes: map[string]schema.Attribute{ 77 | "name": schema.StringAttribute{ 78 | Required: true, 79 | Validators: []validator.String{ 80 | validators.DomainName{}, 81 | }, 82 | PlanModifiers: []planmodifier.String{ 83 | stringplanmodifier.RequiresReplace(), 84 | }, 85 | }, 86 | "account_id": schema.Int64Attribute{ 87 | Computed: true, 88 | }, 89 | "unicode_name": schema.StringAttribute{ 90 | Computed: true, 91 | }, 92 | "state": schema.StringAttribute{ 93 | Computed: true, 94 | }, 95 | "auto_renew_enabled": schema.BoolAttribute{ 96 | Optional: true, 97 | Computed: true, 98 | }, 99 | "whois_privacy_enabled": schema.BoolAttribute{ 100 | Optional: true, 101 | Computed: true, 102 | }, 103 | "dnssec_enabled": schema.BoolAttribute{ 104 | Optional: true, 105 | Computed: true, 106 | }, 107 | "transfer_lock_enabled": schema.BoolAttribute{ 108 | Optional: true, 109 | Computed: true, 110 | }, 111 | "contact_id": schema.Int64Attribute{ 112 | Required: true, 113 | }, 114 | "expires_at": schema.StringAttribute{ 115 | Computed: true, 116 | }, 117 | "extended_attributes": schema.MapAttribute{ 118 | Optional: true, 119 | ElementType: types.StringType, 120 | }, 121 | "premium_price": schema.StringAttribute{ 122 | Optional: true, 123 | }, 124 | "domain_registration": schema.SingleNestedAttribute{ 125 | Description: "The domain registration details.", 126 | Computed: true, 127 | Attributes: map[string]schema.Attribute{ 128 | "period": schema.Int64Attribute{ 129 | Computed: true, 130 | }, 131 | "state": schema.StringAttribute{ 132 | Computed: true, 133 | Optional: true, 134 | }, 135 | "id": common.IDInt64Attribute(), 136 | }, 137 | PlanModifiers: []planmodifier.Object{ 138 | DomainRegistrationState(), 139 | }, 140 | }, 141 | "registrant_change": schema.SingleNestedAttribute{ 142 | Description: "The registrant change details.", 143 | Computed: true, 144 | Attributes: map[string]schema.Attribute{ 145 | "id": common.IDInt64Attribute(), 146 | "account_id": schema.Int64Attribute{ 147 | MarkdownDescription: "DNSimple Account ID to which the registrant change belongs to", 148 | Computed: true, 149 | }, 150 | "contact_id": schema.Int64Attribute{ 151 | MarkdownDescription: "DNSimple contact ID for which the registrant change is being performed", 152 | Computed: true, 153 | PlanModifiers: []planmodifier.Int64{ 154 | int64planmodifier.RequiresReplace(), 155 | }, 156 | }, 157 | "domain_id": schema.StringAttribute{ 158 | MarkdownDescription: "DNSimple domain ID for which the registrant change is being performed", 159 | Computed: true, 160 | PlanModifiers: []planmodifier.String{ 161 | stringplanmodifier.RequiresReplace(), 162 | }, 163 | }, 164 | "state": schema.StringAttribute{ 165 | MarkdownDescription: "State of the registrant change", 166 | PlanModifiers: []planmodifier.String{ 167 | RegistrantChangeState(), 168 | }, 169 | Computed: true, 170 | Optional: true, 171 | }, 172 | "extended_attributes": schema.MapAttribute{ 173 | MarkdownDescription: "Extended attributes for the registrant change", 174 | ElementType: types.StringType, 175 | Computed: true, 176 | PlanModifiers: []planmodifier.Map{ 177 | mapplanmodifier.RequiresReplaceIfConfigured(), 178 | }, 179 | }, 180 | "registry_owner_change": schema.BoolAttribute{ 181 | MarkdownDescription: "True if the registrant change will result in a registry owner change", 182 | Computed: true, 183 | }, 184 | "irt_lock_lifted_by": schema.StringAttribute{ 185 | MarkdownDescription: "Date when the registrant change lock was lifted for the domain", 186 | Computed: true, 187 | }, 188 | }, 189 | }, 190 | "timeouts": schema.SingleNestedAttribute{ 191 | MarkdownDescription: "Timeouts for operations, given as a parsable string as in `10m` or `30s`.", 192 | Optional: true, 193 | Attributes: map[string]schema.Attribute{ 194 | "create": schema.StringAttribute{ 195 | Optional: true, 196 | Description: "Create timeout.", 197 | Validators: []validator.String{ 198 | validators.Duration{}, 199 | }, 200 | PlanModifiers: []planmodifier.String{ 201 | modifiers.StringDefaultValue("10m"), 202 | }, 203 | }, 204 | "update": schema.StringAttribute{ 205 | Optional: true, 206 | Description: "Update timeout.", 207 | Validators: []validator.String{ 208 | validators.Duration{}, 209 | }, 210 | PlanModifiers: []planmodifier.String{ 211 | modifiers.StringDefaultValue("30s"), 212 | }, 213 | }, 214 | "delete": schema.StringAttribute{ 215 | Optional: true, 216 | Description: "Delete timeout (currently unused).", 217 | Validators: []validator.String{ 218 | validators.Duration{}, 219 | }, 220 | PlanModifiers: []planmodifier.String{ 221 | modifiers.StringDefaultValue("30s"), 222 | }, 223 | }, 224 | }, 225 | PlanModifiers: []planmodifier.Object{ 226 | objectplanmodifier.UseStateForUnknown(), 227 | }, 228 | }, 229 | "id": common.IDInt64Attribute(), 230 | }, 231 | } 232 | } 233 | --------------------------------------------------------------------------------