diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59fb41ad..1a2b635e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: jobs: check: + name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -24,6 +25,100 @@ jobs: uses: golangci/golangci-lint-action@v2 with: version: v1.41.1 + skip-pkg-cache: true - name: Test run: make test + + start-runner: + name: Start self-hosted EC2 runner + needs: check + runs-on: ubuntu-latest + outputs: + label: ${{ steps.start-ec2-runner.outputs.label }} + ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }} + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + - name: Start EC2 runner + id: start-ec2-runner + uses: namecheap/ec2-github-runner@main + with: + mode: start + github-token: ${{ secrets.GH_TOKEN }} + ec2-image-owner: 699717368611 + ec2-image-filters: > + [ + { "Name": "name", "Values": ["nc-amzn2-ami-hvm-x86_64-gp2-master-*"] } + ] + ec2-instance-type: t3.nano + subnet-id: subnet-01c4ff5a + security-group-id: sg-106ec76d + eip-allocation-id: eipalloc-1796f61b + iam-role-name: AmazonSSMRoleForInstancesQuickSetup + aws-resource-tags: > + [ + { "Key": "Name", "Value": "github_runner" }, + { "Key": "GitHubRepository", "Value": "${{ github.repository }}" } + ] + + acceptance_test: + name: Acceptance test + runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner + needs: start-runner + concurrency: acceptance_test # allow to run the only one instance of the current acceptance_test job + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + run: | + curl https://dl.google.com/go/go1.16.6.linux-amd64.tar.gz -o go1.16.6.linux-amd64.tar.gz + rm -rf .go-instance + mkdir .go-instance + tar -C .go-instance -xzf go1.16.6.linux-amd64.tar.gz + echo "#!/bin/bash" >> go-env.sh + echo "" >> go-env.sh + echo "export PATH=$PATH:$(pwd)/.go-instance/go/bin" >> go-env.sh + echo "export HOME=$(pwd)" >> go-env.sh + chmod +x go-env.sh + source go-env.sh + go version + go env GOPATH + + - name: Acceptance Test + run: | + export CGO_ENABLED=0 + source go-env.sh + make testacc + env: + NAMECHEAP_USER_NAME: ncexampleuser + NAMECHEAP_API_USER: ncexampleuser + NAMECHEAP_TEST_DOMAIN: test-domain.live + NAMECHEAP_USE_SANDBOX: true + NAMECHEAP_API_KEY: ${{ secrets.NAMECHEAP_API_KEY }} + + stop-runner: + name: Stop self-hosted EC2 runner + needs: + - start-runner + - acceptance_test + runs-on: ubuntu-latest + if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + - name: Stop EC2 runner + uses: namecheap/ec2-github-runner@main + with: + mode: stop + github-token: ${{ secrets.GH_TOKEN }} + label: ${{ needs.start-runner.outputs.label }} + ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..56522cb9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing to Terraform - Namecheap Provider + +You're welcome to start a discussion about required features, file an issue or submit a work in progress (WIP) pull +request. Feel free to ask us for help. We'll do our best to guide you and help you to get on it. + +## Tests + +### Running unit tests + +To run unit tests, execute the following command: + +```shell +$ make test +``` + +### Running acceptance tests + +Before going forward, you must set up the following environment variables: + +```dotenv +NAMECHEAP_USER_NAME=user_name +NAMECHEAP_API_USER=user_name +NAMECHEAP_API_KEY=api_key +NAMECHEAP_TEST_DOMAIN=my-domain.com +NAMECHEAP_USE_SANDBOX=true # optional +``` + +To simplify testing, you can sign up a free account on +our [Sandbox](https://www.namecheap.com/support/knowledgebase/article.aspx/763/63/what-is-sandbox/) environment, +purchase (for free) the fake domain and use the credentials from there for testing environment described below. + +**NOTE:** Do not forget to set up `NAMECHEAP_USE_SANDBOX=true` for sandbox account! + +**NOTE:** Make sure you have whitelisted your public IP address! Follow +our [API Documentation](https://www.namecheap.com/support/api/intro/) to get info about whitelisting IP. + +Run acceptance tests: + +```shell +$ make testacc +``` diff --git a/Makefile b/Makefile index 43f2a643..d5bb4a7f 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,12 @@ check: go vet ./... test: - go test -v ./... + go test -v ./... -count=1 + +# Please set the following ENV variables for this test: +# NAMECHEAP_USER_NAME, NAMECHEAP_API_USER, NAMECHEAP_API_KEY, NAMECHEAP_TEST_DOMAIN, NAMECHEAP_USE_SANDBOX (optional, default is false) +testacc: + TF_ACC=1 go test -v ./... -run=TestAcc -count=1 build: go build -o ${BINARY} diff --git a/README.md b/README.md index ae15d6b4..64adc29b 100644 --- a/README.md +++ b/README.md @@ -63,4 +63,4 @@ resource "namecheap_domain_records" "domain2-com" { ### Contributing -You're welcome to post issues and send your pull requests. +To contribute, please read our [contributing](CONTRIBUTING.md) docs. diff --git a/docs/guides/namecheap_domain_records_guide.md b/docs/guides/namecheap_domain_records_guide.md index 8eaccaac..bfafb7bc 100644 --- a/docs/guides/namecheap_domain_records_guide.md +++ b/docs/guides/namecheap_domain_records_guide.md @@ -34,6 +34,9 @@ The same workflow works for both: `record` items and `nameservers`. -> This is the default behavior, however, we recommend to set the mode explicitly. +~> Upon creating a new resource with the records or nameservers that already exist, the provider will throw a duplicate +error. + ### `OVERWRITE` Unlike [MERGE](#merge), `OVERWRITE` always removes existing records and force overwrites with provided in terraform diff --git a/docs/resources/domain_records.md b/docs/resources/domain_records.md index 72101cab..decdc7dc 100644 --- a/docs/resources/domain_records.md +++ b/docs/resources/domain_records.md @@ -45,7 +45,7 @@ resource "namecheap_domain_records" "my-domain2-com" { ## Argument Reference - `domain` - (Required) Purchased available domain name on your account -- `mode` - (Optional) Possible values: `MERGE` (default), `OVERWRITE`. +- `mode` - (Optional) Possible values: `MERGE` (default), `OVERWRITE` - `email_type` - (Optional) Possible values: NONE, FWD, MXE, MX, OX. Conflicts with `nameservers` - `record` - (Optional) (see [below for nested schema](#nestedblock--record)) Might contain one or more `record` records. Conflicts with `nameservers` @@ -60,3 +60,5 @@ resource "namecheap_domain_records" "my-domain2-com" { - `type` - (Required) Possible values: A, AAAA, ALIAS, CAA, CNAME, MX, MXE, NS, TXT, URL, URL301, FRAME - `mx_pref` - (Optional) MX preference for host. Applicable for MX records only - `ttl` - (Optional) Time to live for all record types. Possible values: any value between 60 to 60000 + +~> It is strongly recommended to set `address`, `hostname`, `nameservers` in lower case to prevent undefined behavior! diff --git a/go.mod b/go.mod index c68b5682..b1bb3deb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/namecheap/terraform-provider-namecheap require ( + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0 github.com/namecheap/go-namecheap-sdk/v2 v2.0.1 github.com/stretchr/testify v1.7.0 diff --git a/namecheap/namecheap_domain_record.go b/namecheap/namecheap_domain_record.go index 90716b1f..fddb3ad4 100644 --- a/namecheap/namecheap_domain_record.go +++ b/namecheap/namecheap_domain_record.go @@ -30,6 +30,7 @@ func resourceNamecheapDomainRecords() *schema.Resource { "domain": { Type: schema.TypeString, Required: true, + ForceNew: true, ValidateFunc: validation.StringIsNotEmpty, Description: "Purchased available domain name on your account", }, @@ -86,9 +87,8 @@ func resourceNamecheapDomainRecords() *schema.Resource { }, "nameservers": { ConflictsWith: []string{"email_type", "record"}, - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, - MinItems: 1, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringIsNotEmpty, @@ -101,9 +101,8 @@ func resourceNamecheapDomainRecords() *schema.Resource { func resourceRecordCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*namecheap.Client) - domain := data.Get("domain").(string) - mode := data.Get("mode").(string) - data.SetId(domain) + domain := strings.ToLower(data.Get("domain").(string)) + mode := strings.ToUpper(data.Get("mode").(string)) var emailType *string var records []interface{} @@ -119,45 +118,47 @@ func resourceRecordCreate(ctx context.Context, data *schema.ResourceData, meta i } if nameserversRaw, ok := data.GetOk("nameservers"); ok { - nameservers = nameserversRaw.([]interface{}) + nameservers = nameserversRaw.(*schema.Set).List() } - if strings.EqualFold(mode, ncModeMerge) && records != nil { - err := createRecordsMerge(domain, emailType, records, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge && records != nil { + diags := createRecordsMerge(domain, emailType, records, client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeOverwrite) && records != nil { - err := createRecordsOverwrite(domain, emailType, records, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeOverwrite && records != nil { + diags := createRecordsOverwrite(domain, emailType, records, client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeMerge) && nameservers != nil { - err := createNameserversMerge(domain, convertInterfacesToString(nameservers), client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge && nameservers != nil { + diags := createNameserversMerge(domain, convertInterfacesToString(nameservers), client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeOverwrite) && nameservers != nil { - err := createNameserversOverwrite(domain, convertInterfacesToString(nameservers), client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeOverwrite && nameservers != nil { + diags := createNameserversOverwrite(domain, convertInterfacesToString(nameservers), client) + if diags.HasError() { + return diags } } + data.SetId(domain) + return nil } func resourceRecordRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*namecheap.Client) - domain := data.Get("domain").(string) - mode := data.Get("mode").(string) + domain := strings.ToLower(data.Get("domain").(string)) + mode := strings.ToUpper(data.Get("mode").(string)) var emailType *string var records []interface{} @@ -173,7 +174,7 @@ func resourceRecordRead(ctx context.Context, data *schema.ResourceData, meta int } if nameserversRaw, ok := data.GetOk("nameservers"); ok { - nameservers = nameserversRaw.([]interface{}) + nameservers = nameserversRaw.(*schema.Set).List() } // We must read nameservers status before hosts. @@ -185,28 +186,28 @@ func resourceRecordRead(ctx context.Context, data *schema.ResourceData, meta int } if !*nsResponse.DomainDNSGetListResult.IsUsingOurDNS { - if strings.EqualFold(mode, ncModeMerge) { - realNameservers, err := readNameserversMerge(domain, convertInterfacesToString(nameservers), client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge { + realNameservers, diags := readNameserversMerge(domain, convertInterfacesToString(nameservers), client) + if diags.HasError() { + return diags } _ = data.Set("nameservers", *realNameservers) } - if strings.EqualFold(mode, ncModeOverwrite) { - realNameservers, err := readNameserversOverwrite(domain, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeOverwrite { + realNameservers, diags := readNameserversOverwrite(domain, client) + if diags.HasError() { + return diags } _ = data.Set("nameservers", *realNameservers) } _ = data.Set("record", []interface{}{}) } else { - if strings.EqualFold(mode, ncModeMerge) { - realRecords, realEmailType, err := readRecordsMerge(domain, records, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge { + realRecords, realEmailType, diags := readRecordsMerge(domain, records, client) + if diags.HasError() { + return diags } _ = data.Set("record", *realRecords) @@ -215,10 +216,10 @@ func resourceRecordRead(ctx context.Context, data *schema.ResourceData, meta int } } - if strings.EqualFold(mode, ncModeOverwrite) { - realRecords, realEmailType, err := readRecordsOverwrite(domain, records, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeOverwrite { + realRecords, realEmailType, diags := readRecordsOverwrite(domain, records, client) + if diags.HasError() { + return diags } _ = data.Set("record", *realRecords) if emailType != nil { @@ -226,7 +227,10 @@ func resourceRecordRead(ctx context.Context, data *schema.ResourceData, meta int } } - _ = data.Set("nameservers", []string{}) + if nameservers != nil { + _ = data.Set("nameservers", []string{}) + } + } return nil @@ -235,8 +239,8 @@ func resourceRecordRead(ctx context.Context, data *schema.ResourceData, meta int func resourceRecordUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*namecheap.Client) - domain := data.Get("domain").(string) - mode := data.Get("mode").(string) + domain := strings.ToLower(data.Get("domain").(string)) + mode := strings.ToUpper(data.Get("mode").(string)) oldRecordsRaw, newRecordsRaw := data.GetChange("record") oldNameserversRaw, newNameserversRaw := data.GetChange("nameservers") @@ -244,8 +248,8 @@ func resourceRecordUpdate(ctx context.Context, data *schema.ResourceData, meta i oldRecords := oldRecordsRaw.(*schema.Set).List() newRecords := newRecordsRaw.(*schema.Set).List() - oldNameservers := oldNameserversRaw.([]interface{}) - newNameservers := newNameserversRaw.([]interface{}) + oldNameservers := oldNameserversRaw.(*schema.Set).List() + newNameservers := newNameserversRaw.(*schema.Set).List() oldRecordsLen := len(oldRecords) newRecordsLen := len(newRecords) @@ -269,7 +273,7 @@ func resourceRecordUpdate(ctx context.Context, data *schema.ResourceData, meta i // then reset nameservers before applying records. // This case is possible when user removed nameservers lines and pasted records, so before applying records, // we must reset nameservers to defaults, otherwise we will face API exception - if (strings.EqualFold(mode, ncModeOverwrite) && oldNameserversLen != 0 && newNameserversLen == 0) || + if (mode == ncModeOverwrite && oldNameserversLen != 0 && newNameserversLen == 0) || // This condition resolves the issue if a user set up records on TF file, but in fact, manually enabled custom DNS. // Before applying records, we have to set default DNS (!*nsResponse.DomainDNSGetListResult.IsUsingOurDNS && newNameserversLen == 0) { @@ -279,63 +283,63 @@ func resourceRecordUpdate(ctx context.Context, data *schema.ResourceData, meta i } } - if strings.EqualFold(mode, ncModeMerge) && oldNameserversLen != 0 && newNameserversLen == 0 { - err := updateNameserversMerge(domain, convertInterfacesToString(oldNameservers), convertInterfacesToString(newNameservers), client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge && oldNameserversLen != 0 && newNameserversLen == 0 { + diags := updateNameserversMerge(domain, convertInterfacesToString(oldNameservers), convertInterfacesToString(newNameservers), client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeMerge) && (newRecordsLen != 0 || oldRecordsLen != 0) { - err := updateRecordsMerge(domain, emailType, oldRecords, newRecords, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge && (newRecordsLen != 0 || oldRecordsLen != 0) { + diags := updateRecordsMerge(domain, emailType, oldRecords, newRecords, client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeOverwrite) && (newRecordsLen != 0 || oldRecordsLen != 0) { - err := createRecordsOverwrite(domain, emailType, newRecords, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeOverwrite && (newRecordsLen != 0 || oldRecordsLen != 0) { + diags := createRecordsOverwrite(domain, emailType, newRecords, client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeOverwrite) && newNameserversLen != 0 { - err := createNameserversOverwrite(domain, convertInterfacesToString(newNameservers), client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeOverwrite && newNameserversLen != 0 { + diags := createNameserversOverwrite(domain, convertInterfacesToString(newNameservers), client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeMerge) && newNameserversLen != 0 { - err := updateNameserversMerge(domain, convertInterfacesToString(oldNameservers), convertInterfacesToString(newNameservers), client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge && newNameserversLen != 0 { + diags := updateNameserversMerge(domain, convertInterfacesToString(oldNameservers), convertInterfacesToString(newNameservers), client) + if diags.HasError() { + return diags } } // If user wants to control email type only while records & nameservers are absent, // then we have to update just an email status if emailType != nil && oldNameserversLen == 0 && newNameserversLen == 0 && oldRecordsLen == 0 && newRecordsLen == 0 { - if strings.EqualFold(mode, ncModeOverwrite) { - err := createRecordsOverwrite(domain, emailType, []interface{}{}, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeOverwrite { + diags := createRecordsOverwrite(domain, emailType, []interface{}{}, client) + if diags.HasError() { + return diags } } - if strings.EqualFold(mode, ncModeMerge) { - err := createRecordsMerge(domain, emailType, []interface{}{}, client) - if err != nil { - return diag.FromErr(err) + if mode == ncModeMerge { + diags := createRecordsMerge(domain, emailType, []interface{}{}, client) + if diags.HasError() { + return diags } } } // For overwrite mode, when no nameservers and records, and email type is not set, then we have to reset it to NONE - if emailType == nil && strings.EqualFold(mode, ncModeOverwrite) && oldNameserversLen == 0 && newNameserversLen == 0 && oldRecordsLen == 0 && newRecordsLen == 0 { - err := createRecordsOverwrite(domain, nil, []interface{}{}, client) - if err != nil { - return diag.FromErr(err) + if emailType == nil && mode == ncModeOverwrite && oldNameserversLen == 0 && newNameserversLen == 0 && oldRecordsLen == 0 && newRecordsLen == 0 { + diags := createRecordsOverwrite(domain, nil, []interface{}{}, client) + if diags.HasError() { + return diags } } @@ -344,8 +348,9 @@ func resourceRecordUpdate(ctx context.Context, data *schema.ResourceData, meta i func resourceRecordDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*namecheap.Client) - domain := data.Get("domain").(string) - mode := data.Get("mode").(string) + + domain := strings.ToLower(data.Get("domain").(string)) + mode := strings.ToUpper(data.Get("mode").(string)) var records []interface{} var nameservers []interface{} @@ -355,38 +360,26 @@ func resourceRecordDelete(ctx context.Context, data *schema.ResourceData, meta i } if nameserversRaw, ok := data.GetOk("nameservers"); ok { - nameservers = nameserversRaw.([]interface{}) + nameservers = nameserversRaw.(*schema.Set).List() } recordsLen := len(records) nameserversLen := len(nameservers) - if strings.EqualFold(mode, ncModeMerge) && recordsLen != 0 { - err := deleteRecordsMerge(domain, records, client) - if err != nil { - return diag.FromErr(err) - } + if mode == ncModeMerge && recordsLen != 0 { + return deleteRecordsMerge(domain, records, client) } - if strings.EqualFold(mode, ncModeOverwrite) && recordsLen != 0 { - err := deleteRecordsOverwrite(domain, client) - if err != nil { - return diag.FromErr(err) - } + if mode == ncModeOverwrite && recordsLen != 0 { + return deleteRecordsOverwrite(domain, client) } - if strings.EqualFold(mode, ncModeMerge) && nameserversLen != 0 { - err := deleteNameserversMerge(domain, convertInterfacesToString(nameservers), client) - if err != nil { - return diag.FromErr(err) - } + if mode == ncModeMerge && nameserversLen != 0 { + return deleteNameserversMerge(domain, convertInterfacesToString(nameservers), client) } - if strings.EqualFold(mode, ncModeOverwrite) && nameserversLen != 0 { - err := deleteNameserversOverwrite(domain, client) - if err != nil { - return diag.FromErr(err) - } + if mode == ncModeOverwrite && nameserversLen != 0 { + return deleteNameserversOverwrite(domain, client) } return nil diff --git a/namecheap/namecheap_domain_record_functions.go b/namecheap/namecheap_domain_record_functions.go index 8f5ee1e9..5be1b229 100644 --- a/namecheap/namecheap_domain_record_functions.go +++ b/namecheap/namecheap_domain_record_functions.go @@ -2,6 +2,8 @@ package namecheap_provider import ( "fmt" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/namecheap/go-namecheap-sdk/v2/namecheap" "strings" ) @@ -9,16 +11,16 @@ import ( // createNameserversMerge has the following behaviour: // - if nameservers have been set manually, then this method merge the provided ones with manually set // - else this is overwriting existent ones -func createNameserversMerge(domain string, nameservers []string, client *namecheap.Client) error { +func createNameserversMerge(domain string, nameservers []string, client *namecheap.Client) diag.Diagnostics { nsResponse, err := client.DomainsDNS.GetList(domain) if err != nil { - return err + return diag.FromErr(err) } if *nsResponse.DomainDNSGetListResult.IsUsingOurDNS { _, err := client.DomainsDNS.SetCustom(domain, nameservers) if err != nil { - return err + return diag.FromErr(err) } } else { var newNameservers []string @@ -26,11 +28,29 @@ func createNameserversMerge(domain string, nameservers []string, client *nameche newNameservers = append(newNameservers, *nsResponse.DomainDNSGetListResult.Nameservers...) } - newNameservers = append(newNameservers, nameservers...) + for index, nameserver := range nameservers { + if nsResponse.DomainDNSGetListResult.Nameservers != nil { + for _, remoteNameserver := range *nsResponse.DomainDNSGetListResult.Nameservers { + if strings.EqualFold(nameserver, remoteNameserver) { + return diag.Diagnostics{diag.Diagnostic{ + Severity: diag.Error, + Summary: "Duplicate nameserver", + Detail: fmt.Sprintf("Nameserver %s is already exist!", nameserver), + AttributePath: cty.Path{ + cty.GetAttrStep{Name: "nameservers"}, + cty.IndexStep{Key: cty.NumberIntVal(int64(index))}, + }, + }} + } + } + } + + newNameservers = append(newNameservers, nameserver) + } _, err := client.DomainsDNS.SetCustom(domain, newNameservers) if err != nil { - return err + return diag.FromErr(err) } } @@ -38,10 +58,10 @@ func createNameserversMerge(domain string, nameservers []string, client *nameche } // createNameserversOverwrite force overwrites the nameservers -func createNameserversOverwrite(domain string, nameservers []string, client *namecheap.Client) error { +func createNameserversOverwrite(domain string, nameservers []string, client *namecheap.Client) diag.Diagnostics { _, err := client.DomainsDNS.SetCustom(domain, nameservers) if err != nil { - return err + return diag.FromErr(err) } return nil @@ -49,10 +69,10 @@ func createNameserversOverwrite(domain string, nameservers []string, client *nam // readNameserversMerge read real nameservers, check whether there's available the current ones, return only // the records from currentNameservers argument that are really exist -func readNameserversMerge(domain string, currentNameservers []string, client *namecheap.Client) (*[]string, error) { +func readNameserversMerge(domain string, currentNameservers []string, client *namecheap.Client) (*[]string, diag.Diagnostics) { nsResponse, err := client.DomainsDNS.GetList(domain) if err != nil { - return nil, err + return nil, diag.FromErr(err) } var foundNameservers []string @@ -72,10 +92,10 @@ func readNameserversMerge(domain string, currentNameservers []string, client *na } // readNameserversOverwrite returns remote real nameservers -func readNameserversOverwrite(domain string, client *namecheap.Client) (*[]string, error) { +func readNameserversOverwrite(domain string, client *namecheap.Client) (*[]string, diag.Diagnostics) { nsResponse, err := client.DomainsDNS.GetList(domain) if err != nil { - return nil, err + return nil, diag.FromErr(err) } if *nsResponse.DomainDNSGetListResult.IsUsingOurDNS || nsResponse.DomainDNSGetListResult.Nameservers == nil { @@ -87,10 +107,10 @@ func readNameserversOverwrite(domain string, client *namecheap.Client) (*[]strin // readNameserversOverwrite fetches real nameservers from API, remove previousNameservers records, insert currentNameservers // thus, we have a merge between manually set ones via Namecheap Domain Control Panel and via terraform -func updateNameserversMerge(domain string, previousNameservers []string, currentNameservers []string, client *namecheap.Client) error { +func updateNameserversMerge(domain string, previousNameservers []string, currentNameservers []string, client *namecheap.Client) diag.Diagnostics { nsResponse, err := client.DomainsDNS.GetList(domain) if err != nil { - return err + return diag.FromErr(err) } var newNameservers []string @@ -114,20 +134,20 @@ func updateNameserversMerge(domain string, previousNameservers []string, current newNameservers = append(newNameservers, currentNameservers...) if len(newNameservers) == 1 { - return fmt.Errorf("unable to proceed with one remained nameserver, you must have at least 2 nameservers") + return diag.Errorf("Unable to proceed with one remained nameserver, you must have at least 2 nameservers") } if len(newNameservers) == 0 { _, err := client.DomainsDNS.SetDefault(domain) if err != nil { - return nil + return diag.FromErr(err) } return nil } _, err = client.DomainsDNS.SetCustom(domain, newNameservers) if err != nil { - return err + return diag.FromErr(err) } return nil @@ -136,10 +156,10 @@ func updateNameserversMerge(domain string, previousNameservers []string, current // deleteNameserversMerge deletes the only nameservers that have been set in terraform file // NOTE: be sure that after executing this method at least 2 nameservers should remain otherwise you will have a error // NOTE: if there's remained 0 nameservers, the default ones will be set -func deleteNameserversMerge(domain string, previousNameservers []string, client *namecheap.Client) error { +func deleteNameserversMerge(domain string, previousNameservers []string, client *namecheap.Client) diag.Diagnostics { nsResponse, err := client.DomainsDNS.GetList(domain) if err != nil { - return err + return diag.FromErr(err) } if *nsResponse.DomainDNSGetListResult.IsUsingOurDNS { @@ -148,7 +168,7 @@ func deleteNameserversMerge(domain string, previousNameservers []string, client } if nsResponse.DomainDNSGetListResult.Nameservers == nil { - return fmt.Errorf("invalid nameservers response (this is internal error, please report us about it)") + return diag.Errorf("Invalid nameservers response (this is internal error, please report us about it)") } var remainNameservers []string @@ -168,47 +188,50 @@ func deleteNameserversMerge(domain string, previousNameservers []string, client } if len(remainNameservers) == 1 { - return fmt.Errorf("unable to proceed with one remained nameserver, you must have at least 2 nameservers") + return diag.Errorf("Unable to proceed with one remained nameserver, you must have at least 2 nameservers") } if len(remainNameservers) == 0 { _, err := client.DomainsDNS.SetDefault(domain) if err != nil { - return nil + return diag.FromErr(err) } return nil } _, err = client.DomainsDNS.SetCustom(domain, remainNameservers) if err != nil { - return err + return diag.FromErr(err) } return nil } // deleteNameserversOverwrite resets nameservers settings to default (set default Namecheap's nameservers) -func deleteNameserversOverwrite(domain string, client *namecheap.Client) error { +func deleteNameserversOverwrite(domain string, client *namecheap.Client) diag.Diagnostics { _, err := client.DomainsDNS.SetDefault(domain) if err != nil { - return nil + return diag.FromErr(err) } return nil } // createRecordsMerge merges new records with already existing ones on Namecheap -func createRecordsMerge(domain string, emailType *string, records []interface{}, client *namecheap.Client) error { +func createRecordsMerge(domain string, emailType *string, records []interface{}, client *namecheap.Client) diag.Diagnostics { remoteRecordsResponse, err := client.DomainsDNS.GetHosts(domain) if err != nil { - return err + return diag.FromErr(err) } + recordsConverted := convertRecordTypeSetToDomainRecords(&records) + newRecordsMap := make(map[string]*namecheap.DomainsDNSHostRecord) var newDomainRecords []namecheap.DomainsDNSHostRecord if remoteRecordsResponse.DomainDNSGetHostsResult.Hosts != nil { filteredRemoteRecords := filterDefaultParkingRecords(remoteRecordsResponse.DomainDNSGetHostsResult.Hosts, &domain) for _, remoteRecord := range *filteredRemoteRecords { + remoteRecordHash := hashRecord(*remoteRecord.Name, *remoteRecord.Type, *remoteRecord.Address) domainRecord := namecheap.DomainsDNSHostRecord{ HostName: remoteRecord.Name, RecordType: remoteRecord.Type, @@ -217,13 +240,34 @@ func createRecordsMerge(domain string, emailType *string, records []interface{}, TTL: remoteRecord.TTL, } - newDomainRecords = append(newDomainRecords, domainRecord) + newRecordsMap[remoteRecordHash] = &domainRecord } } - recordsConverted := convertRecordTypeSetToDomainRecords(&records) + for _, record := range *recordsConverted { + fixedAddress, err := getFixedAddressOfRecord(&record) + if err != nil { + return diag.FromErr(err) + } + recordHash := hashRecord(*record.HostName, *record.RecordType, *fixedAddress) + + if newRecordsMap[recordHash] != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Duplicate record", + Detail: fmt.Sprintf("Record %s is already exist!", stringifyNCRecord(&record)), + }, + } + } - newDomainRecords = append(newDomainRecords, *recordsConverted...) + newRecord := record + newRecordsMap[recordHash] = &newRecord + } + + for _, record := range newRecordsMap { + newDomainRecords = append(newDomainRecords, *record) + } _, err = client.DomainsDNS.SetHosts(&namecheap.DomainsDNSSetHostsArgs{ Domain: namecheap.String(domain), @@ -233,14 +277,14 @@ func createRecordsMerge(domain string, emailType *string, records []interface{}, Tag: nil, }) if err != nil { - return err + return diag.FromErr(err) } return nil } // createRecordsOverwrite overwrites existing records with provided new ones -func createRecordsOverwrite(domain string, emailType *string, records []interface{}, client *namecheap.Client) error { +func createRecordsOverwrite(domain string, emailType *string, records []interface{}, client *namecheap.Client) diag.Diagnostics { domainRecords := convertRecordTypeSetToDomainRecords(&records) emailTypeValue := namecheap.String(namecheap.EmailTypeNone) @@ -256,18 +300,18 @@ func createRecordsOverwrite(domain string, emailType *string, records []interfac Tag: nil, }) if err != nil { - return err + return diag.FromErr(err) } - return nil + return diag.Diagnostics{} } // readRecordsMerge reads all remote records, return only the currentRecords that are exist in remote records // NOTE: method has address fix. Refer to getFixedAddressOfRecord -func readRecordsMerge(domain string, currentRecords []interface{}, client *namecheap.Client) (*[]map[string]interface{}, *string, error) { +func readRecordsMerge(domain string, currentRecords []interface{}, client *namecheap.Client) (*[]map[string]interface{}, *string, diag.Diagnostics) { remoteRecordsResponse, err := client.DomainsDNS.GetHosts(domain) if err != nil { - return nil, nil, err + return nil, nil, diag.FromErr(err) } currentRecordsConverted := convertRecordTypeSetToDomainRecords(¤tRecords) @@ -278,13 +322,13 @@ func readRecordsMerge(domain string, currentRecords []interface{}, client *namec for _, currentRecord := range *currentRecordsConverted { currentRecordAddressFixed, err := getFixedAddressOfRecord(¤tRecord) if err != nil { - return nil, nil, err + return nil, nil, diag.FromErr(err) } currentRecordHash := hashRecord(*currentRecord.HostName, *currentRecord.RecordType, *currentRecordAddressFixed) for _, remoteRecord := range *remoteRecordsResponse.DomainDNSGetHostsResult.Hosts { remoteRecordHash := hashRecord(*remoteRecord.Name, *remoteRecord.Type, *remoteRecord.Address) - if strings.EqualFold(currentRecordHash, remoteRecordHash) { + if currentRecordHash == remoteRecordHash { remoteRecord.Address = currentRecord.Address foundRecords = append(foundRecords, *convertDomainRecordDetailedToTypeSetRecord(&remoteRecord)) break @@ -298,10 +342,10 @@ func readRecordsMerge(domain string, currentRecords []interface{}, client *namec // readRecordsOverwrite returns the records that are exist on Namecheap // NOTE: method has address fix. Refer to getFixedAddressOfRecord -func readRecordsOverwrite(domain string, currentRecords []interface{}, client *namecheap.Client) (*[]map[string]interface{}, *string, error) { +func readRecordsOverwrite(domain string, currentRecords []interface{}, client *namecheap.Client) (*[]map[string]interface{}, *string, diag.Diagnostics) { remoteRecordsResponse, err := client.DomainsDNS.GetHosts(domain) if err != nil { - return nil, nil, err + return nil, nil, diag.FromErr(err) } currentRecordsConverted := convertRecordTypeSetToDomainRecords(¤tRecords) @@ -315,13 +359,13 @@ func readRecordsOverwrite(domain string, currentRecords []interface{}, client *n for _, currentRecord := range *currentRecordsConverted { currentRecordAddressFixed, err := getFixedAddressOfRecord(¤tRecord) if err != nil { - return nil, nil, err + return nil, nil, diag.FromErr(err) } currentRecordHash := hashRecord(*currentRecord.HostName, *currentRecord.RecordType, *currentRecordAddressFixed) - if strings.EqualFold(currentRecordHash, remoteRecordHash) { - remoteRecord.Address = currentRecord.Address + if currentRecordHash == remoteRecordHash { + *remoteRecord.Address = *currentRecord.Address break } @@ -336,10 +380,10 @@ func readRecordsOverwrite(domain string, currentRecords []interface{}, client *n // updateRecordsMerge fetches remote records, remove previousRecords from remote, add currentRecords and return the final list // NOTE: method has address fix. Refer to getFixedAddressOfRecord -func updateRecordsMerge(domain string, emailType *string, previousRecords []interface{}, currentRecords []interface{}, client *namecheap.Client) error { +func updateRecordsMerge(domain string, emailType *string, previousRecords []interface{}, currentRecords []interface{}, client *namecheap.Client) diag.Diagnostics { remoteRecordsResponse, err := client.DomainsDNS.GetHosts(domain) if err != nil { - return err + return diag.FromErr(err) } var newRecordList []namecheap.DomainsDNSHostRecord @@ -354,7 +398,7 @@ func updateRecordsMerge(domain string, emailType *string, previousRecords []inte for _, prevRecord := range *previousRecordsMapped { prevRecordAddressFixed, err := getFixedAddressOfRecord(&prevRecord) if err != nil { - return err + return diag.FromErr(err) } prevRecordHash := hashRecord(*prevRecord.HostName, *prevRecord.RecordType, *prevRecordAddressFixed) if strings.EqualFold(remoteRecordHash, prevRecordHash) { @@ -385,7 +429,7 @@ func updateRecordsMerge(domain string, emailType *string, previousRecords []inte Tag: nil, }) if err != nil { - return err + return diag.FromErr(err) } return nil @@ -393,10 +437,10 @@ func updateRecordsMerge(domain string, emailType *string, previousRecords []inte // deleteRecordsMerge removes only previousRecords from remote records // NOTE: method has address fix. Refer to internal.GetFixedAddressOfRecord -func deleteRecordsMerge(domain string, previousRecords []interface{}, client *namecheap.Client) error { +func deleteRecordsMerge(domain string, previousRecords []interface{}, client *namecheap.Client) diag.Diagnostics { remoteRecordsResponse, err := client.DomainsDNS.GetHosts(domain) if err != nil { - return err + return diag.FromErr(err) } var remainedRecords []namecheap.DomainsDNSHostRecord @@ -410,7 +454,7 @@ func deleteRecordsMerge(domain string, previousRecords []interface{}, client *na for _, prevRecord := range *previousRecordsMapped { prevRecordAddressFixed, err := getFixedAddressOfRecord(&prevRecord) if err != nil { - return err + return diag.FromErr(err) } prevRecordHash := hashRecord(*prevRecord.HostName, *prevRecord.RecordType, *prevRecordAddressFixed) if strings.EqualFold(remoteRecordHash, prevRecordHash) { @@ -439,14 +483,14 @@ func deleteRecordsMerge(domain string, previousRecords []interface{}, client *na Tag: nil, }) if err != nil { - return err + return diag.FromErr(err) } return nil } // deleteRecordsOverwrite removes all records -func deleteRecordsOverwrite(domain string, client *namecheap.Client) error { +func deleteRecordsOverwrite(domain string, client *namecheap.Client) diag.Diagnostics { var records []namecheap.DomainsDNSHostRecord _, err := client.DomainsDNS.SetHosts(&namecheap.DomainsDNSSetHostsArgs{ @@ -457,7 +501,7 @@ func deleteRecordsOverwrite(domain string, client *namecheap.Client) error { Tag: nil, }) if err != nil { - return err + return diag.FromErr(err) } return nil @@ -524,7 +568,7 @@ func fixCAAIodefAddressValue(address *string) (*string, error) { } if len(addressValuesFixed) != 3 { - return nil, fmt.Errorf("invalid value \"%s\"", *address) + return nil, fmt.Errorf("Invalid value \"%s\"", *address) } addressValuesFixed[2] = fmt.Sprintf(`"%s"`, addressValuesFixed[2]) @@ -574,3 +618,9 @@ func filterDefaultParkingRecords(records *[]namecheap.DomainsDNSHostRecordDetail return &filteredRecords } + +// stringifyNCRecord returns a string with hostname, record type and address of the record +// This function mostly serves to print error details for user +func stringifyNCRecord(record *namecheap.DomainsDNSHostRecord) string { + return fmt.Sprintf("{hostname = %s, type = %s, address = %s}", *record.HostName, *record.RecordType, *record.Address) +} diff --git a/namecheap/namecheap_domain_record_test.go b/namecheap/namecheap_domain_record_test.go new file mode 100644 index 00000000..1be5aa60 --- /dev/null +++ b/namecheap/namecheap_domain_record_test.go @@ -0,0 +1,440 @@ +package namecheap_provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/namecheap/go-namecheap-sdk/v2/namecheap" + "regexp" + "testing" +) + +func resetDomainNameservers(t *testing.T) { + _, err := namecheapSDKClient.DomainsDNS.SetDefault(*testAccDomain) + if err != nil { + t.Fatal(err) + } +} + +func resetDomainRecords(t *testing.T) { + _, err := namecheapSDKClient.DomainsDNS.SetHosts(&namecheap.DomainsDNSSetHostsArgs{ + Domain: namecheap.String(*testAccDomain), + EmailType: namecheap.String("NONE"), + }) + if err != nil { + t.Fatal(err) + } +} + +func setDomainRecords(t *testing.T, emailType *string, records *[]namecheap.DomainsDNSHostRecord) { + _, err := namecheapSDKClient.DomainsDNS.SetHosts(&namecheap.DomainsDNSSetHostsArgs{ + Domain: namecheap.String(*testAccDomain), + Records: records, + EmailType: emailType, + }) + if err != nil { + t.Fatal(err) + } +} + +func setDomainNameservers(t *testing.T, nameservers *[]string) { + _, err := namecheapSDKClient.DomainsDNS.SetCustom(*testAccDomain, *nameservers) + if err != nil { + t.Fatal(err) + } +} + +func testAccDomainRecordsAPIFetch(response *namecheap.DomainsDNSGetHostsCommandResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + resp, err := namecheapSDKClient.DomainsDNS.GetHosts(*testAccDomain) + if err != nil { + return err + } + + *response = *resp + + return nil + } +} + +func testAccDomainNameserversAPIFetch(response *namecheap.DomainsDNSGetListCommandResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + resp, err := namecheapSDKClient.DomainsDNS.GetList(*testAccDomain) + if err != nil { + return err + } + + *response = *resp + + return nil + } +} + +func testAccDomainRecordsLength(response *namecheap.DomainsDNSGetHostsCommandResponse, expectedLength int) resource.TestCheckFunc { + return func(s *terraform.State) error { + if response == nil || response.DomainDNSGetHostsResult == nil { + return fmt.Errorf("Empty response") + } + + if expectedLength == 0 { + if response.DomainDNSGetHostsResult.Hosts == nil { + return nil + } + + if len(*response.DomainDNSGetHostsResult.Domain) != 0 { + return fmt.Errorf("Expected %d records", expectedLength) + } + } else { + if response.DomainDNSGetHostsResult.Hosts == nil || len(*response.DomainDNSGetHostsResult.Hosts) != expectedLength { + return fmt.Errorf("Expected %d records", expectedLength) + } + } + + return nil + } +} + +func testAccDomainRecordsContain(response *namecheap.DomainsDNSGetHostsCommandResponse, record *namecheap.DomainsDNSHostRecordDetailed) resource.TestCheckFunc { + return func(s *terraform.State) error { + if response.DomainDNSGetHostsResult.Hosts == nil { + return fmt.Errorf("Doesn't contain expected record") + } + + for _, currentRecord := range *response.DomainDNSGetHostsResult.Hosts { + if equalDomainRecord(¤tRecord, record) { + return nil + } + } + + return fmt.Errorf("Doesn't contain expected record") + + } +} + +func testAccDomainNameserversLength(response *namecheap.DomainsDNSGetListCommandResponse, expectedLength int) resource.TestCheckFunc { + return func(state *terraform.State) error { + if response == nil || response.DomainDNSGetListResult == nil { + return fmt.Errorf("Empty response") + } + + if expectedLength == 0 { + if response.DomainDNSGetListResult.Nameservers == nil { + return nil + } + + if len(*response.DomainDNSGetListResult.Nameservers) != 0 { + return fmt.Errorf("Expected %d nameservers", expectedLength) + } + } else { + if response.DomainDNSGetListResult.Nameservers == nil || len(*response.DomainDNSGetListResult.Nameservers) != expectedLength { + return fmt.Errorf("Expected %d nameservers", expectedLength) + } + } + + return nil + } +} + +func testAccDomainNameserversContain(response *namecheap.DomainsDNSGetListCommandResponse, nameserver string) resource.TestCheckFunc { + return func(state *terraform.State) error { + if *response.DomainDNSGetListResult.IsUsingOurDNS { + return fmt.Errorf("Expected custom nameservers, but found default") + } + + for _, currentNameserver := range *response.DomainDNSGetListResult.Nameservers { + if currentNameserver == nameserver { + return nil + } + } + + return fmt.Errorf("Doesn't contain expected nameserver") + } +} + +func testAccDomainNameserversDefault(response *namecheap.DomainsDNSGetListCommandResponse) resource.TestCheckFunc { + return func(state *terraform.State) error { + if response == nil || response.DomainDNSGetListResult == nil { + return fmt.Errorf("Empty response") + } + + if !*response.DomainDNSGetListResult.IsUsingOurDNS { + return fmt.Errorf("Expected default nameservers, but found custom") + } + + return nil + } +} + +// equalDomainRecord compares only Name, Type, Address, TTL, MXPref fields only +func equalDomainRecord(sRec *namecheap.DomainsDNSHostRecordDetailed, dRec *namecheap.DomainsDNSHostRecordDetailed) bool { + return *sRec.Name == *dRec.Name && + *sRec.Type == *dRec.Type && + *sRec.Address == *dRec.Address && + *sRec.TTL == *dRec.TTL && + *sRec.MXPref == *dRec.MXPref +} + +func TestAccNamecheapDomainRecords_CreateMerge(t *testing.T) { + t.Run("create_records_on_empty", func(t *testing.T) { + var domainRecordsResp namecheap.DomainsDNSGetHostsCommandResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + resetDomainNameservers(t) + resetDomainRecords(t) + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccDomainRecordsAPIFetch(&domainRecordsResp), + testAccDomainRecordsLength(&domainRecordsResp, 0), + ), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "namecheap_domain_records" "my-domain" { + domain = "%s" + mode = "MERGE" + + record { + hostname = "sub1" + type = "A" + address = "11.11.11.11" + } + } + `, *testAccDomain), + Check: resource.ComposeTestCheckFunc( + testAccDomainRecordsAPIFetch(&domainRecordsResp), + testAccDomainRecordsLength(&domainRecordsResp, 1), + testAccDomainRecordsContain(&domainRecordsResp, &namecheap.DomainsDNSHostRecordDetailed{ + Name: namecheap.String("sub1"), + Type: namecheap.String("A"), + Address: namecheap.String("11.11.11.11"), + MXPref: namecheap.Int(10), + TTL: namecheap.Int(1799), + }), + ), + }, + }, + }) + }) + + t.Run("create_records_if_exists", func(t *testing.T) { + var domainRecordsResp namecheap.DomainsDNSGetHostsCommandResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + resetDomainNameservers(t) + setDomainRecords(t, namecheap.String(namecheap.EmailTypeNone), &[]namecheap.DomainsDNSHostRecord{ + { + HostName: namecheap.String("sub1"), + RecordType: namecheap.String(namecheap.RecordTypeA), + Address: namecheap.String("22.22.22.22"), + TTL: namecheap.Int(1799), + }, + }) + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccDomainRecordsAPIFetch(&domainRecordsResp), + testAccDomainRecordsLength(&domainRecordsResp, 1), + testAccDomainRecordsContain(&domainRecordsResp, &namecheap.DomainsDNSHostRecordDetailed{ + Name: namecheap.String("sub1"), + Type: namecheap.String(namecheap.RecordTypeA), + Address: namecheap.String("22.22.22.22"), + MXPref: namecheap.Int(10), + TTL: namecheap.Int(1799), + }), + ), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "namecheap_domain_records" "my-domain" { + domain = "%s" + mode = "MERGE" + + record { + hostname = "sub2" + type = "A" + address = "33.33.33.33" + } + } + `, *testAccDomain), + Check: resource.ComposeTestCheckFunc( + testAccDomainRecordsAPIFetch(&domainRecordsResp), + testAccDomainRecordsLength(&domainRecordsResp, 2), + testAccDomainRecordsContain(&domainRecordsResp, &namecheap.DomainsDNSHostRecordDetailed{ + Name: namecheap.String("sub1"), + Type: namecheap.String(namecheap.RecordTypeA), + Address: namecheap.String("22.22.22.22"), + MXPref: namecheap.Int(10), + TTL: namecheap.Int(1799), + }), + testAccDomainRecordsContain(&domainRecordsResp, &namecheap.DomainsDNSHostRecordDetailed{ + Name: namecheap.String("sub2"), + Type: namecheap.String(namecheap.RecordTypeA), + Address: namecheap.String("33.33.33.33"), + MXPref: namecheap.Int(10), + TTL: namecheap.Int(1799), + }), + ), + }, + }, + }) + }) + + t.Run("create_records_on_conflict", func(t *testing.T) { + var domainRecordsResp namecheap.DomainsDNSGetHostsCommandResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + resetDomainNameservers(t) + setDomainRecords(t, namecheap.String(namecheap.EmailTypeNone), &[]namecheap.DomainsDNSHostRecord{ + { + HostName: namecheap.String("sub1"), + RecordType: namecheap.String(namecheap.RecordTypeA), + Address: namecheap.String("22.22.22.22"), + TTL: namecheap.Int(1799), + }, + }) + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccDomainRecordsAPIFetch(&domainRecordsResp), + testAccDomainRecordsLength(&domainRecordsResp, 1), + testAccDomainRecordsContain(&domainRecordsResp, &namecheap.DomainsDNSHostRecordDetailed{ + Name: namecheap.String("sub1"), + Type: namecheap.String(namecheap.RecordTypeA), + Address: namecheap.String("22.22.22.22"), + MXPref: namecheap.Int(10), + TTL: namecheap.Int(1799), + }), + ), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "namecheap_domain_records" "my-domain" { + domain = "%s" + mode = "MERGE" + + record { + hostname = "sub1" + type = "A" + address = "22.22.22.22" + } + } + `, *testAccDomain), + ExpectError: regexp.MustCompile("Error: Duplicate record"), + }, + }, + }) + }) + + t.Run("create_ns_on_empty", func(t *testing.T) { + var domainRecordsResp namecheap.DomainsDNSGetListCommandResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + resetDomainNameservers(t) + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccDomainNameserversAPIFetch(&domainRecordsResp), + testAccDomainNameserversDefault(&domainRecordsResp), + ), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "namecheap_domain_records" "my-domain" { + domain = "%s" + mode = "MERGE" + + nameservers = [ + "dns1.namecheaphosting.com", + "dns2.namecheaphosting.com", + ] + } + `, *testAccDomain), + Check: resource.ComposeTestCheckFunc( + testAccDomainNameserversAPIFetch(&domainRecordsResp), + testAccDomainNameserversLength(&domainRecordsResp, 2), + testAccDomainNameserversContain(&domainRecordsResp, "dns1.namecheaphosting.com"), + testAccDomainNameserversContain(&domainRecordsResp, "dns2.namecheaphosting.com"), + ), + }, + }, + }) + }) + + t.Run("create_ns_if_exists", func(t *testing.T) { + var domainNameserversResponse namecheap.DomainsDNSGetListCommandResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + setDomainNameservers(t, &[]string{"ns-380.awsdns-47.com", "ns-1076.awsdns-06.org"}) + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccDomainNameserversAPIFetch(&domainNameserversResponse), + testAccDomainNameserversLength(&domainNameserversResponse, 2), + testAccDomainNameserversContain(&domainNameserversResponse, "ns-380.awsdns-47.com"), + testAccDomainNameserversContain(&domainNameserversResponse, "ns-1076.awsdns-06.org"), + ), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "namecheap_domain_records" "my-domain" { + domain = "%s" + mode = "MERGE" + + nameservers = [ + "dns1.namecheaphosting.com", + "dns2.namecheaphosting.com", + ] + } + `, *testAccDomain), + Check: resource.ComposeTestCheckFunc( + testAccDomainNameserversAPIFetch(&domainNameserversResponse), + testAccDomainNameserversLength(&domainNameserversResponse, 4), + testAccDomainNameserversContain(&domainNameserversResponse, "ns-380.awsdns-47.com"), + testAccDomainNameserversContain(&domainNameserversResponse, "ns-1076.awsdns-06.org"), + testAccDomainNameserversContain(&domainNameserversResponse, "dns1.namecheaphosting.com"), + testAccDomainNameserversContain(&domainNameserversResponse, "dns2.namecheaphosting.com"), + ), + }, + }, + }) + }) + + t.Run("create_ns_on_conflict", func(t *testing.T) { + var domainNameserversResponse namecheap.DomainsDNSGetListCommandResponse + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + setDomainNameservers(t, &[]string{"ns-380.awsdns-47.com", "ns-1076.awsdns-06.org", "dns1.namecheaphosting.com"}) + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: resource.ComposeTestCheckFunc( + testAccDomainNameserversAPIFetch(&domainNameserversResponse), + testAccDomainNameserversLength(&domainNameserversResponse, 2), + testAccDomainNameserversContain(&domainNameserversResponse, "ns-380.awsdns-47.com"), + testAccDomainNameserversContain(&domainNameserversResponse, "ns-1076.awsdns-06.org"), + testAccDomainNameserversContain(&domainNameserversResponse, "dns1.namecheaphosting.com"), + ), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "namecheap_domain_records" "my-domain" { + domain = "%s" + mode = "MERGE" + + nameservers = [ + "dns1.namecheaphosting.com", + ] + } + `, *testAccDomain), + ExpectError: regexp.MustCompile("Error: Duplicate nameserver"), + }, + }, + }) + }) +} diff --git a/namecheap/provider_test.go b/namecheap/provider_test.go new file mode 100644 index 00000000..eae13ab4 --- /dev/null +++ b/namecheap/provider_test.go @@ -0,0 +1,83 @@ +package namecheap_provider + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/namecheap/go-namecheap-sdk/v2/namecheap" + "github.com/stretchr/testify/assert" + "os" + "strings" + "testing" +) + +var testAccNamecheapProvider *schema.Provider +var testAccProviderFactories map[string]func() (*schema.Provider, error) +var namecheapSDKClient *namecheap.Client +var testAccDomain *string + +func init() { + testAccNamecheapProvider = Provider() + testAccProviderFactories = map[string]func() (*schema.Provider, error){ + "namecheap": func() (*schema.Provider, error) { + return testAccNamecheapProvider, nil + }, + } + namecheapSDKClient = namecheap.NewClient(&namecheap.ClientOptions{ + UserName: os.Getenv("NAMECHEAP_USER_NAME"), + ApiUser: os.Getenv("NAMECHEAP_API_USER"), + ApiKey: os.Getenv("NAMECHEAP_API_KEY"), + ClientIp: "0.0.0.0", + UseSandbox: strings.EqualFold(os.Getenv("NAMECHEAP_USE_SANDBOX"), "true"), + }) + + testDomain := os.Getenv("NAMECHEAP_TEST_DOMAIN") + testAccDomain = &testDomain +} + +func TestAccProviderImpl(t *testing.T) { + skipTestIfNoTFAccFlag(t) + assert.NotNil(t, testAccNamecheapProvider) +} + +func TestAccSDKImpl(t *testing.T) { + skipTestIfNoTFAccFlag(t) + assert.NotNil(t, namecheapSDKClient) +} + +func TestAccDomainImpl(t *testing.T) { + skipTestIfNoTFAccFlag(t) + assert.NotNil(t, testAccDomain) + assert.NotEmpty(t, *testAccDomain) +} + +func TestAccDomainAvailability(t *testing.T) { + skipTestIfNoTFAccFlag(t) + resp, err := namecheapSDKClient.Domains.GetList(&namecheap.DomainsGetListArgs{ + SearchTerm: namecheap.String(*testAccDomain), + }) + if err != nil { + t.Fatal(err) + } + + if resp.Domains == nil { + t.Fatal("Empty response") + } + + found := false + + for _, domain := range *resp.Domains { + if *domain.Name == *testAccDomain { + found = true + break + } + } + + if !found { + t.Fatalf(`Domain "%s" is unavailable`, *testAccDomain) + } +} + +func skipTestIfNoTFAccFlag(t *testing.T) { + if os.Getenv("TF_ACC") != "1" { + t.Skip("Skipped unless env 'TF_ACC' set") + } +}