From 963f2837d1bb5c52384dd2a12883ce7dcc6d435c Mon Sep 17 00:00:00 2001 From: abiondevelopment <161724514+abiondevelopment@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:49:32 +0100 Subject: [PATCH] Initial Abion Terraform plugin (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added initial support for resources, datasources and imports for A, AAAA, CNAME, MX, NS, PTR, SRV and TXT records --------- Co-authored-by: Johan Körner --- .copywrite.hcl | 21 + .github/CODEOWNERS | 1 + .github/CODE_OF_CONDUCT.md | 5 + .github/dependabot.yml | 12 + .github/workflows/issue-comment-triage.yml | 21 + .github/workflows/lock.yml | 21 + .github/workflows/release.yml | 41 ++ .github/workflows/test.yml | 196 +++++ .gitignore | 35 + .golangci.yml | 31 + .goreleaser.yml | 61 ++ GNUmakefile | 21 + LICENSE | 13 + README.md | 136 +++- docs/data-sources/dns_a_record.md | 60 ++ docs/data-sources/dns_aaaa_record.md | 60 ++ docs/data-sources/dns_cname_record.md | 60 ++ docs/data-sources/dns_mx_record.md | 61 ++ docs/data-sources/dns_ns_record.md | 60 ++ docs/data-sources/dns_ptr_record.md | 60 ++ docs/data-sources/dns_srv_record.md | 63 ++ docs/data-sources/dns_txt_record.md | 60 ++ docs/index.md | 27 + docs/resources/dns_a_record.md | 75 ++ docs/resources/dns_aaaa_record.md | 75 ++ docs/resources/dns_cname_record.md | 67 ++ docs/resources/dns_mx_record.md | 79 ++ docs/resources/dns_ns_record.md | 75 ++ docs/resources/dns_ptr_record.md | 75 ++ docs/resources/dns_srv_record.md | 87 +++ docs/resources/dns_txt_record.md | 75 ++ examples/README.md | 9 + .../abion_dns_a_record/data-source.tf | 20 + .../abion_dns_aaaa_record/data-source.tf | 20 + .../abion_dns_cname_record/data-source.tf | 20 + .../abion_dns_mx_record/data-source.tf | 20 + .../abion_dns_ns_record/data-source.tf | 20 + .../abion_dns_ptr_record/data-source.tf | 20 + .../abion_dns_srv_record/data-source.tf | 20 + .../abion_dns_txt_record/data-source.tf | 20 + examples/provider/provider.tf | 3 + .../resources/abion_dns_a_record/import.sh | 2 + .../resources/abion_dns_a_record/resource.tf | 29 + .../resources/abion_dns_aaaa_record/import.sh | 2 + .../abion_dns_aaaa_record/resource.tf | 29 + .../abion_dns_cname_record/import.sh | 2 + .../abion_dns_cname_record/resource.tf | 21 + .../resources/abion_dns_mx_record/import.sh | 2 + .../resources/abion_dns_mx_record/resource.tf | 32 + .../resources/abion_dns_ns_record/import.sh | 2 + .../resources/abion_dns_ns_record/resource.tf | 29 + .../resources/abion_dns_ptr_record/import.sh | 2 + .../abion_dns_ptr_record/resource.tf | 29 + .../resources/abion_dns_srv_record/import.sh | 2 + .../abion_dns_srv_record/resource.tf | 38 + .../resources/abion_dns_txt_record/import.sh | 2 + .../abion_dns_txt_record/resource.tf | 29 + go.mod | 65 ++ go.sum | 229 ++++++ internal/client/client.go | 221 ++++++ internal/client/types.go | 75 ++ internal/provider/dns_a_record_data_source.go | 166 +++++ .../provider/dns_a_record_data_source_test.go | 128 ++++ internal/provider/dns_a_record_resource.go | 275 +++++++ .../provider/dns_a_record_resource_test.go | 154 ++++ .../provider/dns_aaaa_record_data_source.go | 167 +++++ .../dns_aaaa_record_data_source_test.go | 128 ++++ internal/provider/dns_aaaa_record_resource.go | 274 +++++++ .../provider/dns_aaaa_record_resource_test.go | 154 ++++ .../provider/dns_cname_record_data_source.go | 165 +++++ .../dns_cname_record_data_source_test.go | 122 +++ .../provider/dns_cname_record_resource.go | 271 +++++++ .../dns_cname_record_resource_test.go | 105 +++ .../provider/dns_mx_record_data_source.go | 179 +++++ .../dns_mx_record_data_source_test.go | 131 ++++ internal/provider/dns_mx_record_resource.go | 286 +++++++ .../provider/dns_mx_record_resource_test.go | 171 +++++ .../provider/dns_ns_record_data_source.go | 167 +++++ .../dns_ns_record_data_source_test.go | 99 +++ internal/provider/dns_ns_record_resource.go | 274 +++++++ .../provider/dns_ns_record_resource_test.go | 116 +++ .../provider/dns_ptr_record_data_source.go | 166 +++++ .../dns_ptr_record_data_source_test.go | 128 ++++ internal/provider/dns_ptr_record_resource.go | 275 +++++++ .../provider/dns_ptr_record_resource_test.go | 154 ++++ .../provider/dns_srv_record_data_source.go | 199 +++++ .../dns_srv_record_data_source_test.go | 137 ++++ internal/provider/dns_srv_record_resource.go | 294 ++++++++ .../provider/dns_srv_record_resource_test.go | 205 ++++++ .../provider/dns_txt_record_data_source.go | 167 +++++ .../dns_txt_record_data_source_test.go | 127 ++++ internal/provider/dns_txt_record_resource.go | 274 +++++++ .../provider/dns_txt_record_resource_test.go | 154 ++++ internal/provider/import_helper.go | 48 ++ internal/provider/provider.go | 203 +++++ internal/provider/provider_test.go | 29 + internal/utils/record_types.go | 24 + internal/utils/terraform_convert_utils.go | 32 + main.go | 40 + terraform-registry-manifest.json | 6 + tools/go.mod | 99 +++ tools/go.sum | 696 ++++++++++++++++++ tools/tools.go | 19 + 103 files changed, 9755 insertions(+), 1 deletion(-) create mode 100644 .copywrite.hcl create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/issue-comment-triage.yml create mode 100644 .github/workflows/lock.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yml create mode 100644 GNUmakefile create mode 100644 LICENSE create mode 100644 docs/data-sources/dns_a_record.md create mode 100644 docs/data-sources/dns_aaaa_record.md create mode 100644 docs/data-sources/dns_cname_record.md create mode 100644 docs/data-sources/dns_mx_record.md create mode 100644 docs/data-sources/dns_ns_record.md create mode 100644 docs/data-sources/dns_ptr_record.md create mode 100644 docs/data-sources/dns_srv_record.md create mode 100644 docs/data-sources/dns_txt_record.md create mode 100644 docs/index.md create mode 100644 docs/resources/dns_a_record.md create mode 100644 docs/resources/dns_aaaa_record.md create mode 100644 docs/resources/dns_cname_record.md create mode 100644 docs/resources/dns_mx_record.md create mode 100644 docs/resources/dns_ns_record.md create mode 100644 docs/resources/dns_ptr_record.md create mode 100644 docs/resources/dns_srv_record.md create mode 100644 docs/resources/dns_txt_record.md create mode 100644 examples/README.md create mode 100644 examples/data-sources/abion_dns_a_record/data-source.tf create mode 100644 examples/data-sources/abion_dns_aaaa_record/data-source.tf create mode 100644 examples/data-sources/abion_dns_cname_record/data-source.tf create mode 100644 examples/data-sources/abion_dns_mx_record/data-source.tf create mode 100644 examples/data-sources/abion_dns_ns_record/data-source.tf create mode 100644 examples/data-sources/abion_dns_ptr_record/data-source.tf create mode 100644 examples/data-sources/abion_dns_srv_record/data-source.tf create mode 100644 examples/data-sources/abion_dns_txt_record/data-source.tf create mode 100644 examples/provider/provider.tf create mode 100755 examples/resources/abion_dns_a_record/import.sh create mode 100644 examples/resources/abion_dns_a_record/resource.tf create mode 100755 examples/resources/abion_dns_aaaa_record/import.sh create mode 100644 examples/resources/abion_dns_aaaa_record/resource.tf create mode 100755 examples/resources/abion_dns_cname_record/import.sh create mode 100644 examples/resources/abion_dns_cname_record/resource.tf create mode 100755 examples/resources/abion_dns_mx_record/import.sh create mode 100644 examples/resources/abion_dns_mx_record/resource.tf create mode 100755 examples/resources/abion_dns_ns_record/import.sh create mode 100644 examples/resources/abion_dns_ns_record/resource.tf create mode 100755 examples/resources/abion_dns_ptr_record/import.sh create mode 100644 examples/resources/abion_dns_ptr_record/resource.tf create mode 100755 examples/resources/abion_dns_srv_record/import.sh create mode 100644 examples/resources/abion_dns_srv_record/resource.tf create mode 100755 examples/resources/abion_dns_txt_record/import.sh create mode 100644 examples/resources/abion_dns_txt_record/resource.tf create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/client/client.go create mode 100644 internal/client/types.go create mode 100644 internal/provider/dns_a_record_data_source.go create mode 100644 internal/provider/dns_a_record_data_source_test.go create mode 100644 internal/provider/dns_a_record_resource.go create mode 100644 internal/provider/dns_a_record_resource_test.go create mode 100644 internal/provider/dns_aaaa_record_data_source.go create mode 100644 internal/provider/dns_aaaa_record_data_source_test.go create mode 100644 internal/provider/dns_aaaa_record_resource.go create mode 100644 internal/provider/dns_aaaa_record_resource_test.go create mode 100644 internal/provider/dns_cname_record_data_source.go create mode 100644 internal/provider/dns_cname_record_data_source_test.go create mode 100644 internal/provider/dns_cname_record_resource.go create mode 100644 internal/provider/dns_cname_record_resource_test.go create mode 100644 internal/provider/dns_mx_record_data_source.go create mode 100644 internal/provider/dns_mx_record_data_source_test.go create mode 100644 internal/provider/dns_mx_record_resource.go create mode 100644 internal/provider/dns_mx_record_resource_test.go create mode 100644 internal/provider/dns_ns_record_data_source.go create mode 100644 internal/provider/dns_ns_record_data_source_test.go create mode 100644 internal/provider/dns_ns_record_resource.go create mode 100644 internal/provider/dns_ns_record_resource_test.go create mode 100644 internal/provider/dns_ptr_record_data_source.go create mode 100644 internal/provider/dns_ptr_record_data_source_test.go create mode 100644 internal/provider/dns_ptr_record_resource.go create mode 100644 internal/provider/dns_ptr_record_resource_test.go create mode 100644 internal/provider/dns_srv_record_data_source.go create mode 100644 internal/provider/dns_srv_record_data_source_test.go create mode 100644 internal/provider/dns_srv_record_resource.go create mode 100644 internal/provider/dns_srv_record_resource_test.go create mode 100644 internal/provider/dns_txt_record_data_source.go create mode 100644 internal/provider/dns_txt_record_data_source_test.go create mode 100644 internal/provider/dns_txt_record_resource.go create mode 100644 internal/provider/dns_txt_record_resource_test.go create mode 100644 internal/provider/import_helper.go create mode 100644 internal/provider/provider.go create mode 100644 internal/provider/provider_test.go create mode 100644 internal/utils/record_types.go create mode 100644 internal/utils/terraform_convert_utils.go create mode 100644 main.go create mode 100644 terraform-registry-manifest.json create mode 100644 tools/go.mod create mode 100644 tools/go.sum create mode 100644 tools/tools.go diff --git a/.copywrite.hcl b/.copywrite.hcl new file mode 100644 index 0000000..2950a52 --- /dev/null +++ b/.copywrite.hcl @@ -0,0 +1,21 @@ +schema_version = 1 + +project { + license = "Apache-2.0" + copyright_year = 2024 + copyright_holder = "Abion AB" + + header_ignore = [ + # examples used within documentation (prose) + "examples/**", + + # GitHub issue template configuration + ".github/ISSUE_TEMPLATE/*.yml", + + # golangci-lint tooling configuration + ".golangci.yml", + + # GoReleaser tooling configuration + ".goreleaser.yml", + ] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0a117fd --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @abiondevelopment diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0c8b092 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. + +Please read the full text at https://www.hashicorp.com/community-guidelines diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e5f4836 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# See GitHub's documentation for more information on this file: +# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/issue-comment-triage.yml b/.github/workflows/issue-comment-triage.yml new file mode 100644 index 0000000..00017cd --- /dev/null +++ b/.github/workflows/issue-comment-triage.yml @@ -0,0 +1,21 @@ +# DO NOT EDIT - This GitHub Workflow is managed by automation +# https://github.com/hashicorp/terraform-devex-repos +name: Issue Comment Triage + +on: + issue_comment: + types: [created] + +jobs: + issue_comment_triage: + runs-on: ubuntu-latest + env: + # issue_comment events are triggered by comments on issues and pull requests. Checking the + # value of github.event.issue.pull_request tells us whether the issue is an issue or is + # actually a pull request, allowing us to dynamically set the gh subcommand: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only + COMMAND: ${{ github.event.issue.pull_request && 'pr' || 'issue' }} + GH_TOKEN: ${{ github.token }} + steps: + - name: 'Remove waiting-response on comment' + run: gh ${{ env.COMMAND }} edit ${{ github.event.issue.html_url }} --remove-label waiting-response diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 0000000..358aed1 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,21 @@ +# DO NOT EDIT - This GitHub Workflow is managed by automation +# https://github.com/hashicorp/terraform-devex-repos +name: 'Lock Threads' + +on: + schedule: + - cron: '43 20 * * *' + +jobs: + lock: + runs-on: ubuntu-latest + steps: + # NOTE: When TSCCR updates the GitHub action version, update the template workflow file to avoid drift: + # https://github.com/hashicorp/terraform-devex-repos/blob/main/modules/repo/workflows/lock.tftpl + - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 + with: + github-token: ${{ github.token }} + issue-inactive-days: '30' + issue-lock-reason: resolved + pr-inactive-days: '30' + pr-lock-reason: resolved diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..df091e6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +# Terraform Provider release workflow. +name: Release + +# This GitHub action creates a release when a tag that matches the pattern +# "v*" (e.g. v0.1.0) is created. +on: + push: + tags: + - 'v*' + +# Releases need permissions to read and write the repository contents. +# GitHub considers creating releases and uploading assets as writing contents. +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + # Allow goreleaser to access older tag information. + fetch-depth: 0 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 + id: import_gpg + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + with: + args: release --clean + env: + # GitHub sets the GITHUB_TOKEN secret automatically. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8af3fc6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,196 @@ +# Terraform Provider testing workflow. +name: Tests + +# This GitHub action runs your tests for each pull request. +on: + pull_request: + paths-ignore: + - 'README.md' + +# Testing only needs permissions to read the repository contents. +permissions: + contents: read + +jobs: + # Ensure project builds before running testing matrix + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod download + - run: go build -v . + - name: Format code + run: make fmt + - name: Verify code formatting + run: | + git diff --compact-summary --exit-code || \ + (echo; echo "Unexpected difference in directories after formatting. Run 'make fmt' command and commit."; exit 1) + - name: Run linters + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + with: + version: latest + generate: + name: Generate Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + # We need the latest version of Terraform for our documentation generation to use + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_wrapper: false + - name: Generate docs + run: make generate + - name: Verify docs + run: | + git diff --compact-summary --exit-code || \ + (echo; echo "Unexpected difference in directories after docs generation. Run 'make generate' command and commit."; exit 1) + + # Run acceptance tests in sequence for each supported Terraform CLI versions + test_1_5: + name: Terraform Provider Acceptance Tests v1.5.* + needs: build + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: '1.5.*' + terraform_wrapper: false + - run: go mod download + - env: + ABION_API_KEY: ${{ secrets.ABION_API_KEY }} + ABION_API_HOST: ${{ vars.ABION_API_HOST }} + run: make testacc + timeout-minutes: 20 + test_1_6: + name: Terraform Provider Acceptance Tests v1.6.* + needs: test_1_5 + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: '1.6.*' + terraform_wrapper: false + - run: go mod download + - env: + ABION_API_KEY: ${{ secrets.ABION_API_KEY }} + ABION_API_HOST: ${{ vars.ABION_API_HOST }} + run: make testacc + timeout-minutes: 20 + test_1_7: + name: Terraform Provider Acceptance Tests v1.7.* + needs: test_1_6 + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: '1.7.*' + terraform_wrapper: false + - run: go mod download + - env: + ABION_API_KEY: ${{ secrets.ABION_API_KEY }} + ABION_API_HOST: ${{ vars.ABION_API_HOST }} + run: make testacc + timeout-minutes: 20 + test_1_8: + name: Terraform Provider Acceptance Tests v1.8.* + needs: test_1_7 + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: '1.8.*' + terraform_wrapper: false + - run: go mod download + - env: + ABION_API_KEY: ${{ secrets.ABION_API_KEY }} + ABION_API_HOST: ${{ vars.ABION_API_HOST }} + run: make testacc + timeout-minutes: 20 + test_1_9: + name: Terraform Provider Acceptance Tests v1.9.* + needs: test_1_8 + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: '1.9.*' + terraform_wrapper: false + - run: go mod download + - env: + ABION_API_KEY: ${{ secrets.ABION_API_KEY }} + ABION_API_HOST: ${{ vars.ABION_API_HOST }} + run: make testacc + timeout-minutes: 20 + test_1_10: + name: Terraform Provider Acceptance Tests v1.10.* + needs: test_1_9 + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + cache: true + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: '1.10.*' + terraform_wrapper: false + - run: go mod download + - env: + ABION_API_KEY: ${{ secrets.ABION_API_KEY }} + ABION_API_HOST: ${{ vars.ABION_API_HOST }} + run: make testacc + timeout-minutes: 20 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd3ad8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +*.dll +*.exe +.DS_Store +example.tf +terraform.tfplan +terraform.tfstate +bin/ +dist/ +modules-dev/ +/pkg/ +website/.vagrant +website/.bundle +website/build +website/node_modules +.vagrant/ +*.backup +./*.tfstate +.terraform/ +*.log +*.bak +*~ +.*.swp +.idea +*.iml +*.test +*.iml + +website/vendor + +# Test exclusions +!command/test-fixtures/**/*.tfstate +!command/test-fixtures/**/.terraform/ + +# Keep windows files with windows line endings +*.winfile eol=crlf diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..57ad6fa --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,31 @@ +# Visit https://golangci-lint.run/ for usage documentation +# and information on other useful linters +issues: + # Maximum issues count per one linter. + # Set to 0 to disable. + max-issues-per-linter: 0 + # Maximum count of issues with the same text + # Set to 0 to disable. + max-same-issues: 0 + +linters: + disable-all: true + enable: + - durationcheck + - errcheck + - exportloopref + - forcetypeassert + - godot + - gofmt + - gosimple + - ineffassign + - makezero + - misspell + - nilerr + - predeclared + - staticcheck + - tenv + - unconvert + - unparam + - unused + - vet \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..150ddfa --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,61 @@ +# Visit https://goreleaser.com for documentation on how to customize this +# behavior. +version: 2 +before: + hooks: + # this is just an example and not a requirement for provider building/publishing + - go mod tidy +builds: +- env: + # goreleaser does not work with CGO, it could also complicate + # usage by users in CI/CD systems like HCP Terraform where + # they are unable to install libraries. + - CGO_ENABLED=0 + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - amd64 + - '386' + - arm + - arm64 + ignore: + - goos: darwin + goarch: '386' + binary: '{{ .ProjectName }}_v{{ .Version }}' +archives: +- format: zip + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' +checksum: + extra_files: + - glob: 'terraform-registry-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' + algorithm: sha256 +signs: + - artifacts: checksum + args: + # if you are using this in a GitHub action or some other automated pipeline, you + # need to pass the batch flag to indicate its not interactive. + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" +release: + extra_files: + - glob: 'terraform-registry-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + # If you want to manually examine the release before its live, uncomment this line: + # draft: true +changelog: + disable: true diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..e5787cf --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,21 @@ +default: fmt lint install generate + +build: + go build -v ./... + +install: build + go install -v ./... + +lint: + golangci-lint run + +generate: + cd tools; go generate ./... + +fmt: + gofmt -s -w -e . + +testacc: + TF_ACC=1 go test -v -count=1 -cover -timeout 120m ./... + +.PHONY: fmt lint testacc build install generate diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a347998 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 Abion AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index d3935b5..d534830 100644 --- a/README.md +++ b/README.md @@ -1 +1,135 @@ -# terraform-provider-abion \ No newline at end of file +# Terraform Provider: Abion + +The Terraform Abion Provider supports resources that performs DNS record updates and data sources for DNS records via the Abion API. +You **must** have an Abion account to retrieve an API key. Contact [Abion](https://abion.com/) for help how to create an account and API key. To access +the Abion API you also need to get your IP addresses whitelisted by Abion. The Terraform Abion Provider operates on record level so the actual zone +must exist and the API Key/account need to have access to the zone. + +## Requirements + +* [Terraform](https://www.terraform.io/downloads) +* [Go](https://go.dev/doc/install) (1.22) +* [GNU Make](https://www.gnu.org/software/make/) +* [golangci-lint](https://golangci-lint.run/usage/install/#local-installation) (optional) + +## Limitations + +* You are only allowed to make 100 updates of a zone per day. +* The zone must exist (and managed by Abion) and the API Key/account needs access to the zone. +* The supported record types: + * `A` + * `AAAA` + * `CNAME` + * `MX` + * `TXT` + * `NS` + * `SRV` + * `PTR` + + +## Using the provider + +Official documentation on how to use this provider can be found on the +[Terraform Registry](https://registry.terraform.io/providers/abion/abion/latest/docs). + +## Developing the Abion Provider + +The provided `GNUmakefile` defines commands generally useful during development, like for trigger a Golang build and install the Abion Provider, +running acceptance tests, generating documentation, code formatting and linting. + +`git clone` this repository and `cd` into its directory + +The default `make` command will execute linting, formatting, generate docs, build and install the Abion provider +```shell +make +``` + +### Building + +The `make install` target will trigger the Golang build and install the provider in your `$GOBIN` folder + +```shell +make install +``` + +### Adding Dependencies + +This provider uses [Go modules](https://github.com/golang/go/wiki/Modules). +Please see the Go documentation for the most up to date information about using Go modules. + +To add a new dependency `github.com/author/dependency` to the Abion provider: + +```shell +go get github.com/author/dependency +go mod tidy +``` + +Then commit the changes to `go.mod` and `go.sum`. + +### Testing + +In order to test the Terraform Abion Provider, run to run the full suite of acceptance tests by executing: + +```shell +ABION_API_KEY= ABION_API_HOST= make testacc +``` +Configure the ABION_API_KEY and (optional) ABION_API_HOST environment variables. + +**NOTE!** +It's important to understand that acceptance tests (`make testacc`) will actually call the Abion API and update zone records. You need a valid API KEY, access to the existing zone and also, your IP address must be whitelisted by Abion to be able to access the Abion API + +### Generating documentation + +The Terraform Abion Provider uses [terraform-plugin-docs](https://github.com/hashicorp/terraform-plugin-docs/) +to generate documentation and store it in the `docs/` directory. +Once a release is cut, the Terraform Registry will download the documentation from `docs/` +and associate it with the release version. Read more about how this works on the +[official page](https://www.terraform.io/registry/providers/docs). + +Use +```shell +make generate +``` +to ensure the documentation is re-generated with latest changes. + +### Code formatting and linting +```shell +make fmt +make lint +```` + +### Using a development build in Terraform + +If [running acceptance tests](#Testing) isn't enough, it's possible to set up a local terraform configuration +to use a development builds of the provider. This can be achieved by leveraging the Terraform CLI +[configuration file development overrides](https://www.terraform.io/cli/config/config-file#development-overrides-for-provider-developers). + +First, use `make install` to place a fresh development build of the provider in your +[`${GOBIN}`](https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies) +(defaults to `${GOPATH}/bin` or `${HOME}/go/bin` if `${GOPATH}` is not set). Repeat +this every time you make changes to the provider locally. + +Then, setup your environment following [these instructions](https://www.terraform.io/plugin/debugging#terraform-cli-development-overrides) +to make your local terraform use your local build. + +Example of a `.terraformrc` file: +``` +provider_installation { + + dev_overrides { + "abion/abion" = "" + } + + # For all other providers, install them directly from their origin provider + # registries as normal. If you omit this, Terraform will _only_ use + # the dev_overrides block, and so no other providers will be available. + direct {} +} +``` + +## Releasing + +The release process is automated via GitHub Actions, and it's defined in the Workflow +[release.yml](./.github/workflows/release.yml). + +Each release is cut by pushing a [semantically versioned](https://semver.org/) tag (e.g. `v.0.1.0`) to the main branch. diff --git a/docs/data-sources/dns_a_record.md b/docs/data-sources/dns_a_record.md new file mode 100644 index 0000000..d3ac570 --- /dev/null +++ b/docs/data-sources/dns_a_record.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_a_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS A records of the zone. +--- + +# abion_dns_a_record (Data Source) + +Use this data source to get DNS A records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_a_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_a_record.example.records +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `records` (Attributes List) The list of A records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) + + +### Nested Schema for `records` + +Required: + +- `ip_address` (String) The IPv4 address this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/data-sources/dns_aaaa_record.md b/docs/data-sources/dns_aaaa_record.md new file mode 100644 index 0000000..5725fe6 --- /dev/null +++ b/docs/data-sources/dns_aaaa_record.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_aaaa_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS AAAA records of the zone. +--- + +# abion_dns_aaaa_record (Data Source) + +Use this data source to get DNS AAAA records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_aaaa_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_aaaa_record.example.records +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `records` (Attributes List) The list of AAAA records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) + + +### Nested Schema for `records` + +Required: + +- `ip_address` (String) The IPv6 address this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/data-sources/dns_cname_record.md b/docs/data-sources/dns_cname_record.md new file mode 100644 index 0000000..c333312 --- /dev/null +++ b/docs/data-sources/dns_cname_record.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_cname_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS CNAME records of the zone. +--- + +# abion_dns_cname_record (Data Source) + +Use this data source to get DNS CNAME records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_cname_record" "example" { + zone = "example.com" + name = "@" +} + +output "example_ip_addresses" { + value = data.abion_dns_cname_record.example.record +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `record` (Attributes) CNAME record details. (see [below for nested schema](#nestedatt--record)) + + +### Nested Schema for `record` + +Required: + +- `cname` (String) The canonical name (CNAME) for the record. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/data-sources/dns_mx_record.md b/docs/data-sources/dns_mx_record.md new file mode 100644 index 0000000..9701123 --- /dev/null +++ b/docs/data-sources/dns_mx_record.md @@ -0,0 +1,61 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_mx_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS MX records of the zone. +--- + +# abion_dns_mx_record (Data Source) + +Use this data source to get DNS MX records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_mx_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_mx_record.example.records +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `records` (Attributes List) The list of MX records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) + + +### Nested Schema for `records` + +Required: + +- `host` (String) The hostname of the mail server. +- `priority` (Number) The priority in which order mail servers are tried. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/data-sources/dns_ns_record.md b/docs/data-sources/dns_ns_record.md new file mode 100644 index 0000000..ef32880 --- /dev/null +++ b/docs/data-sources/dns_ns_record.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_ns_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS NS records of the zone. +--- + +# abion_dns_ns_record (Data Source) + +Use this data source to get DNS NS records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_ns_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_nameservers" { + value = data.abion_dns_ns_record.example.records +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `records` (Attributes List) The list of NS records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) + + +### Nested Schema for `records` + +Required: + +- `nameserver` (String) The nameserver this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/data-sources/dns_ptr_record.md b/docs/data-sources/dns_ptr_record.md new file mode 100644 index 0000000..35169e4 --- /dev/null +++ b/docs/data-sources/dns_ptr_record.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_ptr_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS PTR records of the zone. +--- + +# abion_dns_ptr_record (Data Source) + +Use this data source to get DNS PTR records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_ptr_record" "example" { + zone = "example.com" + name = "10" +} + +output "example_ip_addresses" { + value = data.abion_dns_ptr_record.example.records +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `records` (Attributes List) The list of PTR records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) + + +### Nested Schema for `records` + +Required: + +- `ptr` (String) The canonical name this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/data-sources/dns_srv_record.md b/docs/data-sources/dns_srv_record.md new file mode 100644 index 0000000..5590214 --- /dev/null +++ b/docs/data-sources/dns_srv_record.md @@ -0,0 +1,63 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_srv_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS SRV records of the zone. +--- + +# abion_dns_srv_record (Data Source) + +Use this data source to get DNS SRV records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_srv_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_srv_record.example.records +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `records` (Attributes List) The list of SRV records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) + + +### Nested Schema for `records` + +Required: + +- `port` (Number) The TCP or UDP port on which the service is running. +- `priority` (Number) The priority specifying the priority of the target host. Lower values indicate higher priority. +- `target` (String) The hostname of the machine providing the service. +- `weight` (Number) A relative weight for records with the same priority. Higher weights are more preferred. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/data-sources/dns_txt_record.md b/docs/data-sources/dns_txt_record.md new file mode 100644 index 0000000..74433df --- /dev/null +++ b/docs/data-sources/dns_txt_record.md @@ -0,0 +1,60 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_txt_record Data Source - abion" +subcategory: "" +description: |- + Use this data source to get DNS TXT records of the zone. +--- + +# abion_dns_txt_record (Data Source) + +Use this data source to get DNS TXT records of the zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_txt_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_txt_data" { + value = data.abion_dns_txt_record.example.records +} +``` + + +## Schema + +### Required + +- `name` (String) The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `zone` (String) The zone the record belongs to. + +### Read-Only + +- `records` (Attributes List) The list of TXT records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) + + +### Nested Schema for `records` + +Required: + +- `txt_data` (String) The TXT data. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ad7faa9 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,27 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion Provider" +subcategory: "" +description: |- + +--- + +# abion Provider + + + +## Example Usage + +```terraform +provider "abion" { + apikey = "" +} +``` + + +## Schema + +### Optional + +- `apikey` (String, Sensitive) The Abion API key. Contact [Abion](https://abion.com) for help on how to create an account and an API key and whitelist IP addresses to be able to access the Abion API. This value can also be set using the `ABION_API_KEY` environment variable. The order of precedence: Terraform configuration value (highest priority) > environment variable (lowest priority). +- `host` (String) The Abion API host URL. If not set, defaults to `https://api.abion.com`. This value can also be set using the `ABION_API_HOST` environment variable. The order of precedence: Terraform configuration value (highest priority) > environment variable > default value. diff --git a/docs/resources/dns_a_record.md b/docs/resources/dns_a_record.md new file mode 100644 index 0000000..101dbde --- /dev/null +++ b/docs/resources/dns_a_record.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_a_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS A records of a zone. +--- + +# abion_dns_a_record (Resource) + +Use this resource to create, update and delete DNS A records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_a_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + ip_address = "203.0.113.0" + }, + { + ip_address = "203.0.113.1" + comments = "test comment" + }, + { + ip_address = "203.0.113.2" + ttl = "300" + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `records` (Attributes List) The list of A records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `records` + +Required: + +- `ip_address` (String) The IPv4 address this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS A records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_a_record.example "example.com/www" +``` diff --git a/docs/resources/dns_aaaa_record.md b/docs/resources/dns_aaaa_record.md new file mode 100644 index 0000000..4f55dbf --- /dev/null +++ b/docs/resources/dns_aaaa_record.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_aaaa_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS AAAA records of a zone. +--- + +# abion_dns_aaaa_record (Resource) + +Use this resource to create, update and delete DNS AAAA records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_aaaa_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff1" + comments = "test comment" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2" + ttl = "300" + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `records` (Attributes List) The list of AAAA records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `records` + +Required: + +- `ip_address` (String) The IPv6 address this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS AAAA records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_aaaa_record.example "example.com/www" +``` diff --git a/docs/resources/dns_cname_record.md b/docs/resources/dns_cname_record.md new file mode 100644 index 0000000..a4fb7b0 --- /dev/null +++ b/docs/resources/dns_cname_record.md @@ -0,0 +1,67 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_cname_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS CNAME records of a zone. +--- + +# abion_dns_cname_record (Resource) + +Use this resource to create, update and delete DNS CNAME records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_cname_record" "example" { + zone = "example.com" + name = "@" + record = { + cname = "www.devabion.se." + ttl = "3600" + comments = "test comment" + } +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `record` (Attributes) CNAME record details. (see [below for nested schema](#nestedatt--record)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `record` + +Required: + +- `cname` (String) The canonical name (CNAME) for the record. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS CNAME records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_cname_record.example "example.com/www" +``` diff --git a/docs/resources/dns_mx_record.md b/docs/resources/dns_mx_record.md new file mode 100644 index 0000000..970c69d --- /dev/null +++ b/docs/resources/dns_mx_record.md @@ -0,0 +1,79 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_mx_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS MX records of a zone. +--- + +# abion_dns_mx_record (Resource) + +Use this resource to create, update and delete DNS MX records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_mx_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + host = "mail1.example.com." + priority = "10" + }, + { + host = "mail2.example.com." + priority = "20" + comments = "test comment" + }, + { + host = "mail3.example.com." + priority = "30" + ttl = "300" + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `records` (Attributes List) The list of MX records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `records` + +Required: + +- `host` (String) The hostname of the mail server. +- `priority` (Number) The priority in which order mail servers are tried. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS MX records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_mx_record.example "example.com/www" +``` diff --git a/docs/resources/dns_ns_record.md b/docs/resources/dns_ns_record.md new file mode 100644 index 0000000..5687579 --- /dev/null +++ b/docs/resources/dns_ns_record.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_ns_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS NS records of a zone. +--- + +# abion_dns_ns_record (Resource) + +Use this resource to create, update and delete DNS NS records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_ns_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + nameserver = "ns1.testabiondns.se." + }, + { + nameserver = "ns2.testabiondns.se." + comments = "test comment" + }, + { + nameserver = "ns3.testabiondns.se." + ttl = "300" + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `records` (Attributes List) The list of NS records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `records` + +Required: + +- `nameserver` (String) The Nameserver address this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS NS records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_ns_record.example "example.com/www" +``` diff --git a/docs/resources/dns_ptr_record.md b/docs/resources/dns_ptr_record.md new file mode 100644 index 0000000..03a9260 --- /dev/null +++ b/docs/resources/dns_ptr_record.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_ptr_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS PTR records of a zone. +--- + +# abion_dns_ptr_record (Resource) + +Use this resource to create, update and delete DNS PTR records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_ptr_record" "example" { + zone = "example.com" + name = "10" + records = [ + { + ptr = "www.abiontest.com." + }, + { + ptr = "www.abiontest2.com." + comments = "test comment" + }, + { + ptr = "www.abiontest3.com." + ttl = "300" + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `records` (Attributes List) The list of PTR records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `records` + +Required: + +- `ptr` (String) The canonical name this record will point to. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS PTR records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_ptr_record.example "example.com/www" +``` diff --git a/docs/resources/dns_srv_record.md b/docs/resources/dns_srv_record.md new file mode 100644 index 0000000..174f359 --- /dev/null +++ b/docs/resources/dns_srv_record.md @@ -0,0 +1,87 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_srv_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS SRV records of a zone. +--- + +# abion_dns_srv_record (Resource) + +Use this resource to create, update and delete DNS SRV records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_srv_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + target = "server1.example.com." + port = "443" + priority = "1" + weight = "100" + }, + { + target = "server2.example.com." + port = "443" + priority = "100" + weight = "10" + comments = "test comment" + }, + { + target = "server3.example.com." + port = "443" + priority = "100" + weight = "30" + ttl = "300" + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `records` (Attributes List) The list of SRV records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `records` + +Required: + +- `port` (Number) The TCP or UDP port on which the service is running. +- `priority` (Number) The priority specifying the priority of the target host. Lower values indicate higher priority. +- `target` (String) The hostname of the machine providing the service. +- `weight` (Number) A relative weight for records with the same priority. Higher weights are more preferred. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS SRV records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_srv_record.example "example.com/www" +``` diff --git a/docs/resources/dns_txt_record.md b/docs/resources/dns_txt_record.md new file mode 100644 index 0000000..b0eed62 --- /dev/null +++ b/docs/resources/dns_txt_record.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "abion_dns_txt_record Resource - abion" +subcategory: "" +description: |- + Use this resource to create, update and delete DNS TXT records of a zone. +--- + +# abion_dns_txt_record (Resource) + +Use this resource to create, update and delete DNS TXT records of a zone. + +## Example Usage + +```terraform +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_txt_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + txt_data = "txt 1" + }, + { + txt_data = "txt 2" + comments = "test comment" + }, + { + txt_data = "txt 3" + ttl = "300" + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone. +- `records` (Attributes List) The list of TXT records. Records are sorted to avoid constant changing plans (see [below for nested schema](#nestedatt--records)) +- `zone` (String) The zone the record belongs to. + + +### Nested Schema for `records` + +Required: + +- `txt_data` (String) The TXT data. + +Optional: + +- `comments` (String) Comments for the record. +- `ttl` (Number) Time-to-live (TTL) for the record, in seconds. + +## Import + +Import is supported using the following syntax: + +```shell +# DNS TXT records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_txt_record.example "example.com/www" +``` diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6defa71 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Examples + +This directory contains examples that are mostly used for documentation, but can also be run/tested manually via the Terraform CLI. + +The document generation tool looks for files in the following locations by default. All other *.tf files besides the ones mentioned below are ignored by the documentation tool. This is useful for creating examples that can run and/or are testable even if some parts are not relevant for the documentation. + +* **provider/provider.tf** example file for the provider index page +* **data-sources/`full data source name`/data-source.tf** example file for the named data source page +* **resources/`full resource name`/resource.tf** example file for the named data source page diff --git a/examples/data-sources/abion_dns_a_record/data-source.tf b/examples/data-sources/abion_dns_a_record/data-source.tf new file mode 100644 index 0000000..3b07cfc --- /dev/null +++ b/examples/data-sources/abion_dns_a_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_a_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_a_record.example.records +} \ No newline at end of file diff --git a/examples/data-sources/abion_dns_aaaa_record/data-source.tf b/examples/data-sources/abion_dns_aaaa_record/data-source.tf new file mode 100644 index 0000000..51877bb --- /dev/null +++ b/examples/data-sources/abion_dns_aaaa_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_aaaa_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_aaaa_record.example.records +} diff --git a/examples/data-sources/abion_dns_cname_record/data-source.tf b/examples/data-sources/abion_dns_cname_record/data-source.tf new file mode 100644 index 0000000..f56a79b --- /dev/null +++ b/examples/data-sources/abion_dns_cname_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_cname_record" "example" { + zone = "example.com" + name = "@" +} + +output "example_ip_addresses" { + value = data.abion_dns_cname_record.example.record +} diff --git a/examples/data-sources/abion_dns_mx_record/data-source.tf b/examples/data-sources/abion_dns_mx_record/data-source.tf new file mode 100644 index 0000000..2f48177 --- /dev/null +++ b/examples/data-sources/abion_dns_mx_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_mx_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_mx_record.example.records +} \ No newline at end of file diff --git a/examples/data-sources/abion_dns_ns_record/data-source.tf b/examples/data-sources/abion_dns_ns_record/data-source.tf new file mode 100644 index 0000000..d74eab7 --- /dev/null +++ b/examples/data-sources/abion_dns_ns_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_ns_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_nameservers" { + value = data.abion_dns_ns_record.example.records +} \ No newline at end of file diff --git a/examples/data-sources/abion_dns_ptr_record/data-source.tf b/examples/data-sources/abion_dns_ptr_record/data-source.tf new file mode 100644 index 0000000..e2dd5df --- /dev/null +++ b/examples/data-sources/abion_dns_ptr_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_ptr_record" "example" { + zone = "example.com" + name = "10" +} + +output "example_ip_addresses" { + value = data.abion_dns_ptr_record.example.records +} diff --git a/examples/data-sources/abion_dns_srv_record/data-source.tf b/examples/data-sources/abion_dns_srv_record/data-source.tf new file mode 100644 index 0000000..0ac5ada --- /dev/null +++ b/examples/data-sources/abion_dns_srv_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_srv_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_ip_addresses" { + value = data.abion_dns_srv_record.example.records +} \ No newline at end of file diff --git a/examples/data-sources/abion_dns_txt_record/data-source.tf b/examples/data-sources/abion_dns_txt_record/data-source.tf new file mode 100644 index 0000000..42f5a9f --- /dev/null +++ b/examples/data-sources/abion_dns_txt_record/data-source.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +data "abion_dns_txt_record" "example" { + zone = "example.com" + name = "www" +} + +output "example_txt_data" { + value = data.abion_dns_txt_record.example.records +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf new file mode 100644 index 0000000..86f4b45 --- /dev/null +++ b/examples/provider/provider.tf @@ -0,0 +1,3 @@ +provider "abion" { + apikey = "" +} diff --git a/examples/resources/abion_dns_a_record/import.sh b/examples/resources/abion_dns_a_record/import.sh new file mode 100755 index 0000000..ddef5db --- /dev/null +++ b/examples/resources/abion_dns_a_record/import.sh @@ -0,0 +1,2 @@ +# DNS A records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_a_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_a_record/resource.tf b/examples/resources/abion_dns_a_record/resource.tf new file mode 100644 index 0000000..d64e825 --- /dev/null +++ b/examples/resources/abion_dns_a_record/resource.tf @@ -0,0 +1,29 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_a_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + ip_address = "203.0.113.0" + }, + { + ip_address = "203.0.113.1" + comments = "test comment" + }, + { + ip_address = "203.0.113.2" + ttl = "300" + }, + ] +} diff --git a/examples/resources/abion_dns_aaaa_record/import.sh b/examples/resources/abion_dns_aaaa_record/import.sh new file mode 100755 index 0000000..11b34f7 --- /dev/null +++ b/examples/resources/abion_dns_aaaa_record/import.sh @@ -0,0 +1,2 @@ +# DNS AAAA records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_aaaa_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_aaaa_record/resource.tf b/examples/resources/abion_dns_aaaa_record/resource.tf new file mode 100644 index 0000000..c2638fe --- /dev/null +++ b/examples/resources/abion_dns_aaaa_record/resource.tf @@ -0,0 +1,29 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_aaaa_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff1" + comments = "test comment" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2" + ttl = "300" + }, + ] +} diff --git a/examples/resources/abion_dns_cname_record/import.sh b/examples/resources/abion_dns_cname_record/import.sh new file mode 100755 index 0000000..c4b219f --- /dev/null +++ b/examples/resources/abion_dns_cname_record/import.sh @@ -0,0 +1,2 @@ +# DNS CNAME records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_cname_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_cname_record/resource.tf b/examples/resources/abion_dns_cname_record/resource.tf new file mode 100644 index 0000000..611b9e6 --- /dev/null +++ b/examples/resources/abion_dns_cname_record/resource.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_cname_record" "example" { + zone = "example.com" + name = "@" + record = { + cname = "www.devabion.se." + ttl = "3600" + comments = "test comment" + } +} diff --git a/examples/resources/abion_dns_mx_record/import.sh b/examples/resources/abion_dns_mx_record/import.sh new file mode 100755 index 0000000..96bba91 --- /dev/null +++ b/examples/resources/abion_dns_mx_record/import.sh @@ -0,0 +1,2 @@ +# DNS MX records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_mx_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_mx_record/resource.tf b/examples/resources/abion_dns_mx_record/resource.tf new file mode 100644 index 0000000..442ac97 --- /dev/null +++ b/examples/resources/abion_dns_mx_record/resource.tf @@ -0,0 +1,32 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_mx_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + host = "mail1.example.com." + priority = "10" + }, + { + host = "mail2.example.com." + priority = "20" + comments = "test comment" + }, + { + host = "mail3.example.com." + priority = "30" + ttl = "300" + }, + ] +} diff --git a/examples/resources/abion_dns_ns_record/import.sh b/examples/resources/abion_dns_ns_record/import.sh new file mode 100755 index 0000000..b8a660c --- /dev/null +++ b/examples/resources/abion_dns_ns_record/import.sh @@ -0,0 +1,2 @@ +# DNS NS records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_ns_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_ns_record/resource.tf b/examples/resources/abion_dns_ns_record/resource.tf new file mode 100644 index 0000000..c2dcc2c --- /dev/null +++ b/examples/resources/abion_dns_ns_record/resource.tf @@ -0,0 +1,29 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_ns_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + nameserver = "ns1.testabiondns.se." + }, + { + nameserver = "ns2.testabiondns.se." + comments = "test comment" + }, + { + nameserver = "ns3.testabiondns.se." + ttl = "300" + }, + ] +} diff --git a/examples/resources/abion_dns_ptr_record/import.sh b/examples/resources/abion_dns_ptr_record/import.sh new file mode 100755 index 0000000..8e5890d --- /dev/null +++ b/examples/resources/abion_dns_ptr_record/import.sh @@ -0,0 +1,2 @@ +# DNS PTR records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_ptr_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_ptr_record/resource.tf b/examples/resources/abion_dns_ptr_record/resource.tf new file mode 100644 index 0000000..3269b31 --- /dev/null +++ b/examples/resources/abion_dns_ptr_record/resource.tf @@ -0,0 +1,29 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_ptr_record" "example" { + zone = "example.com" + name = "10" + records = [ + { + ptr = "www.abiontest.com." + }, + { + ptr = "www.abiontest2.com." + comments = "test comment" + }, + { + ptr = "www.abiontest3.com." + ttl = "300" + }, + ] +} diff --git a/examples/resources/abion_dns_srv_record/import.sh b/examples/resources/abion_dns_srv_record/import.sh new file mode 100755 index 0000000..07a2d84 --- /dev/null +++ b/examples/resources/abion_dns_srv_record/import.sh @@ -0,0 +1,2 @@ +# DNS SRV records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_srv_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_srv_record/resource.tf b/examples/resources/abion_dns_srv_record/resource.tf new file mode 100644 index 0000000..6811e13 --- /dev/null +++ b/examples/resources/abion_dns_srv_record/resource.tf @@ -0,0 +1,38 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_srv_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + target = "server1.example.com." + port = "443" + priority = "1" + weight = "100" + }, + { + target = "server2.example.com." + port = "443" + priority = "100" + weight = "10" + comments = "test comment" + }, + { + target = "server3.example.com." + port = "443" + priority = "100" + weight = "30" + ttl = "300" + }, + ] +} diff --git a/examples/resources/abion_dns_txt_record/import.sh b/examples/resources/abion_dns_txt_record/import.sh new file mode 100755 index 0000000..ce59085 --- /dev/null +++ b/examples/resources/abion_dns_txt_record/import.sh @@ -0,0 +1,2 @@ +# DNS TXT records can be imported by specifying the string identifier. The import ID should be in the format: "zone/name". The `@` character represents the root of the zone, E.g., "example.com/@" +terraform import abion_dns_txt_record.example "example.com/www" \ No newline at end of file diff --git a/examples/resources/abion_dns_txt_record/resource.tf b/examples/resources/abion_dns_txt_record/resource.tf new file mode 100644 index 0000000..1029a0d --- /dev/null +++ b/examples/resources/abion_dns_txt_record/resource.tf @@ -0,0 +1,29 @@ +terraform { + required_providers { + abion = { + source = "abion/abion" + } + } +} + +provider "abion" { + apikey = "" +} + +resource "abion_dns_txt_record" "example" { + zone = "example.com" + name = "www" + records = [ + { + txt_data = "txt 1" + }, + { + txt_data = "txt 2" + comments = "test comment" + }, + { + txt_data = "txt 3" + ttl = "300" + }, + ] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..51dde1b --- /dev/null +++ b/go.mod @@ -0,0 +1,65 @@ +module terraform-provider-abion + +go 1.22.7 + +require ( + github.com/hashicorp/terraform-plugin-framework v1.13.0 + github.com/hashicorp/terraform-plugin-go v0.25.0 + github.com/hashicorp/terraform-plugin-log v0.9.0 + github.com/hashicorp/terraform-plugin-testing v1.10.0 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/net v0.31.0 +) + +require ( + github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect + github.com/agext/levenshtein v1.2.2 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-checkpoint v0.5.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hc-install v0.8.0 // indirect + github.com/hashicorp/hcl/v2 v2.21.0 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/terraform-exec v0.21.0 // indirect + github.com/hashicorp/terraform-json v0.22.1 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect + github.com/hashicorp/terraform-registry-address v0.2.3 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/zclconf/go-cty v1.15.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.22.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..187fe13 --- /dev/null +++ b/go.sum @@ -0,0 +1,229 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= +github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= +github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.8.0 h1:LdpZeXkZYMQhoKPCecJHlKvUkQFixN/nvyR1CdfOLjI= +github.com/hashicorp/hc-install v0.8.0/go.mod h1:+MwJYjDfCruSD/udvBmRB22Nlkwwkwf5sAB6uTIhSaU= +github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= +github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= +github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= +github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= +github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= +github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= +github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= +github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= +github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= +github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= +github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= +github.com/hashicorp/terraform-plugin-testing v1.10.0 h1:2+tmRNhvnfE4Bs8rB6v58S/VpqzGC6RCh9Y8ujdn+aw= +github.com/hashicorp/terraform-plugin-testing v1.10.0/go.mod h1:iWRW3+loP33WMch2P/TEyCxxct/ZEcCGMquSLSCVsrc= +github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= +github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..58c3583 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,221 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "golang.org/x/net/html" + "io" + "net/http" + "net/url" + "strings" + "terraform-provider-abion/internal/utils" + "time" + + log "github.com/sirupsen/logrus" +) + +const apiKeyHeader = "X-API-KEY" + +// Client the Abion API client. +type Client struct { + apiKey string + baseURL *url.URL + HTTPClient *http.Client +} + +type ApiClient interface { + GetZones(ctx context.Context, page *Pagination) (*APIResponse[[]Zone], error) + GetZone(ctx context.Context, name string) (*APIResponse[*Zone], error) + PatchZone(ctx context.Context, name string, patch ZoneRequest) (*APIResponse[*Zone], error) +} + +// NewAbionClient Creates a new Client. +func NewAbionClient(host string, apiKey string) (*Client, error) { + baseURL, err := url.Parse(host) + + if err != nil { + return nil, err + } + + return &Client{ + apiKey: apiKey, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// GetZone Returns the full information on a single zone. +func (c *Client) GetZone(ctx context.Context, name string) (*APIResponse[*Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones", name) + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, err + } + + results := &APIResponse[*Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not get zone %s: %w", name, err) + } + + return results, nil +} + +// PatchZone Updates a zone by patching it according to JSON Merge Patch format (RFC 7396). +func (c *Client) PatchZone(ctx context.Context, name string, patch ZoneRequest) (*APIResponse[*Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones", name) + + req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, patch) + if err != nil { + return nil, err + } + + results := &APIResponse[*Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not update zone %s: %w", name, err) + } + + return results, nil +} + +func (c *Client) do(req *http.Request, result any) error { + req.Header.Set(apiKeyHeader, c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("error sending request %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return parseError(resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response body %w", err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return fmt.Errorf("error unmarshalling response %w", err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + zResp := &APIResponse[any]{} + err := json.Unmarshal(raw, zResp) + if err != nil { + + err2 := tryParseHtmlError(resp, raw) + if err2 != nil { + return err2 + } + + log.Errorf("error parsing error %s", err) + return err + } + + return zResp.Error +} + +func tryParseHtmlError(resp *http.Response, raw []byte) error { + + // Handle special whitelist error + if strings.Contains(resp.Header.Get("Content-Type"), "text/html") { + // Parse the HTML response to extract the title + doc, err2 := html.Parse(strings.NewReader(string(raw))) + if err2 != nil { + log.Printf("Error parsing HTML: %s", err2) + return err2 + } + + // Traverse the HTML tree to find the tag + var title string + var traverseTitle func(*html.Node) + traverseTitle = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" { + // Extract the title content + for c := n.FirstChild; c != nil; c = c.NextSibling { + if c.Type == html.TextNode { + title = c.Data + break + } + } + } + // Continue traversing the child nodes + for c := n.FirstChild; c != nil; c = c.NextSibling { + traverseTitle(c) + } + } + + // Start traversing from the root node + traverseTitle(doc) + + if title != "" { + // Return the error message with the title + return fmt.Errorf("API error: %s", title) + } + } + return nil +} + +func CreateRecordPatchRequest(zoneName string, subDomainOrRoot string, recordType utils.RecordType, data []Record) ZoneRequest { + records := make(map[string]map[string][]Record) + + if records[subDomainOrRoot] == nil { + records[subDomainOrRoot] = make(map[string][]Record) + } + records[subDomainOrRoot][recordType.String()] = data + + patchRequest := ZoneRequest{ + Data: Zone{ + Type: "zone", + ID: zoneName, + Attributes: Attributes{ + Records: records, + }, + }, + } + return patchRequest +} diff --git a/internal/client/types.go b/internal/client/types.go new file mode 100644 index 0000000..1b8702f --- /dev/null +++ b/internal/client/types.go @@ -0,0 +1,75 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package client + +import "fmt" + +type ZoneRequest struct { + Data Zone `json:"data,omitempty"` +} + +type Pagination struct { + Offset int `json:"offset,omitempty" url:"offset"` + Limit int `json:"limit,omitempty" url:"limit"` + Total int `json:"total,omitempty" url:"total"` +} + +type APIResponse[T any] struct { + Meta *Metadata `json:"meta,omitempty"` + Data T `json:"data,omitempty"` + Error *Error `json:"error,omitempty"` +} + +type Metadata struct { + InvocationID string `json:"invocationId,omitempty"` + *Pagination +} + +type Zone struct { + Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` + Attributes Attributes `json:"attributes,omitempty"` +} + +type Attributes struct { + OrganisationID string `json:"organisationId,omitempty"` + OrganisationDescription string `json:"organisationDescription,omitempty"` + DNSTypeDescription string `json:"dnsTypeDescription,omitempty"` + Slave bool `json:"slave,omitempty"` + Pending bool `json:"pending,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Settings *Settings `json:"settings,omitempty"` + Records map[string]map[string][]Record `json:"records,omitempty"` + Redirects map[string][]Redirect `json:"redirects,omitempty"` +} + +type Settings struct { + MName string `json:"mname,omitempty"` + Refresh int `json:"refresh,omitempty"` + Expire int `json:"expire,omitempty"` + TTL int `json:"ttl,omitempty"` +} + +type Record struct { + TTL *int `json:"ttl,omitempty"` + Data string `json:"rdata,omitempty"` + Comments *string `json:"comments,omitempty"` +} + +type Redirect struct { + Path string `json:"path"` + Destination string `json:"destination"` + Status int `json:"status"` + Slugs bool `json:"slugs"` + Certificate bool `json:"certificate"` +} + +type Error struct { + Status int `json:"status"` + Message string `json:"message"` +} + +func (e *Error) Error() string { + return fmt.Sprintf("api error: status=%d, message=%s", e.Status, e.Message) +} diff --git a/internal/provider/dns_a_record_data_source.go b/internal/provider/dns_a_record_data_source.go new file mode 100644 index 0000000..70e5ea2 --- /dev/null +++ b/internal/provider/dns_a_record_data_source.go @@ -0,0 +1,166 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsARecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsARecordDataSource{} +) + +// NewDnsARecordDataSource is a helper function to simplify the provider implementation. +func NewDnsARecordDataSource() datasource.DataSource { + return &dnsARecordDataSource{} +} + +// dnsARecordDataSource is the data source implementation. +type dnsARecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsARecordModel maps the data source schema data. +type dnsARecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Records []ARecordData `tfsdk:"records"` +} + +type ARecordData struct { + IPAddress types.String `tfsdk:"ip_address"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsARecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypeA +} + +// Metadata returns the data source type name. +func (d *dnsARecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_a_record" +} + +// Schema defines the schema for the data source. +func (d *dnsARecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS A records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Computed: true, + Description: "The list of A records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "ip_address": schema.StringAttribute{ + Required: true, + Description: "The IPv4 address this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsARecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsARecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + state.Records = []ARecordData{} + for _, record := range records { + state.Records = append(state.Records, ARecordData{ + IPAddress: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/dns_a_record_data_source_test.go b/internal/provider/dns_a_record_data_source_test.go new file mode 100644 index 0000000..fd3c72d --- /dev/null +++ b/internal/provider/dns_a_record_data_source_test.go @@ -0,0 +1,128 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsARecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test" { + zone = "pmapitest1.com" + name = "@" + records = [ + { + ip_address = "203.0.113.0" + ttl = "3600" + comments = "test comment" + } + ] + } + + data "abion_dns_a_record" "test_data" { + zone = abion_dns_a_record.test.zone + name = abion_dns_a_record.test.name + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_a_record.test_data", "zone", "pmapitest1.com"), + resource.TestCheckResourceAttr("data.abion_dns_a_record.test_data", "name", "@"), + resource.TestCheckResourceAttr("data.abion_dns_a_record.test_data", "records.#", "1"), + + resource.TestCheckResourceAttr("data.abion_dns_a_record.test_data", "records.0.ip_address", "203.0.113.0"), + resource.TestCheckResourceAttr("data.abion_dns_a_record.test_data", "records.0.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_a_record.test_data", "records.0.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsARecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_a_record" "non_existing" { + zone = "non_existing.com" + name = "@" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsARecordNoRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify error + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test2" { + zone = "pmapitest1.com" + name = "@" + records = [ + { + ip_address = "203.0.113.0" + } + ] + } + + data "abion_dns_a_record" "test_data" { + zone = abion_dns_a_record.test2.zone + name = "xxx" + } + `, + ExpectError: regexp.MustCompile("No records exist on xxx level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsARecordNoARecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test3" { + zone = "pmapitest1.com" + name = "test3" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + } + ] + } + + data "abion_dns_a_record" "test_data" { + zone = abion_dns_aaaa_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No A records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_a_record_resource.go b/internal/provider/dns_a_record_resource.go new file mode 100644 index 0000000..214ceee --- /dev/null +++ b/internal/provider/dns_a_record_resource.go @@ -0,0 +1,275 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsARecordResource{} + _ resource.ResourceWithConfigure = &dnsARecordResource{} + _ resource.ResourceWithImportState = &dnsARecordResource{} +) + +// NewDnsARecordResource is a helper function to simplify the provider implementation. +func NewDnsARecordResource() resource.Resource { + return &dnsARecordResource{} +} + +// dnsARecordResource is the resource implementation. +type dnsARecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsARecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypeA +} + +// Metadata returns the resource type name. +func (r *dnsARecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_a_record" +} + +// Schema defines the schema for the resource. +func (r *dnsARecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS A records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Required: true, + Description: "The list of A records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "ip_address": schema.StringAttribute{ + Required: true, + Description: "The IPv4 address this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsARecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsARecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsARecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsARecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + state.Records = []ARecordData{} + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + for _, record := range records { + state.Records = append(state.Records, ARecordData{ + IPAddress: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsARecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsARecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsARecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // update or add the new records according to the plan + patchRequest := r.createARecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsARecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsARecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by removing the records + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsARecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsARecordResource) createARecordCreateUpdateRequest(plan dnsARecordModel) abionclient.ZoneRequest { + var data []abionclient.Record + for _, record := range plan.Records { + record := abionclient.Record{ + Data: record.IPAddress.ValueString(), + TTL: utils.Int32ToIntPointer(record.TTL), + Comments: record.Comments.ValueStringPointer(), + } + data = append(data, record) + } + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_a_record_resource_test.go b/internal/provider/dns_a_record_resource_test.go new file mode 100644 index 0000000..61ed50a --- /dev/null +++ b/internal/provider/dns_a_record_resource_test.go @@ -0,0 +1,154 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsARecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on root level + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test" { + zone = "pmapitest1.com" + name = "@" + records = [ + { + ip_address = "203.0.113.0" + }, + { + ip_address = "203.0.113.1" + ttl = "3600" + }, + { + ip_address = "203.0.113.2" + comments = "test comment" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_a_record.test", "zone", "pmapitest1.com"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "name", "@"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.0.ip_address", "203.0.113.0"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.1.ip_address", "203.0.113.1"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.1.ttl", "3600"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.2.ip_address", "203.0.113.2"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.2.comments", "test comment"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.2.ttl"), + ), + }, + // Create and Read testing on subdomain level, move records from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test" { + zone = "pmapitest1.com" + name = "test" + records = [ + { + ip_address = "203.0.113.0" + }, + { + ip_address = "203.0.113.1" + }, + { + ip_address = "203.0.113.2" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_a_record.test", "zone", "pmapitest1.com"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.0.ip_address", "203.0.113.0"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.1.ip_address", "203.0.113.1"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.2.ip_address", "203.0.113.2"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.2.comments"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.2.ttl"), + ), + }, + + // Update and Read testing on subdomain level, remove ip address + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test" { + zone = "pmapitest1.com" + name = "test" + records = [ + { + ip_address = "203.0.113.0" + }, + { + ip_address = "203.0.113.1" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_a_record.test", "zone", "pmapitest1.com"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.#", "2"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.0.ip_address", "203.0.113.0"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.0.comments"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.0.ttl"), + + resource.TestCheckResourceAttr("abion_dns_a_record.test", "records.1.ip_address", "203.0.113.1"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_a_record.test", "records.1.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsARecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test2" { + zone = "non_existing.com" + name = "@" + records = [ + { + ip_address = "203.0.113.0" + }, + ] + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_aaaa_record_data_source.go b/internal/provider/dns_aaaa_record_data_source.go new file mode 100644 index 0000000..5c448e9 --- /dev/null +++ b/internal/provider/dns_aaaa_record_data_source.go @@ -0,0 +1,167 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsAAAARecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsAAAARecordDataSource{} +) + +// NewDnsAAAARecordDataSource is a helper function to simplify the provider implementation. +func NewDnsAAAARecordDataSource() datasource.DataSource { + return &dnsAAAARecordDataSource{} +} + +// dnsAAAARecordDataSource is the data source implementation. +type dnsAAAARecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsAAAARecordModel maps the data source schema data. +type dnsAAAARecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Records []AAAARecordData `tfsdk:"records"` +} + +type AAAARecordData struct { + IPAddress types.String `tfsdk:"ip_address"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsAAAARecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypeAAAA +} + +// Metadata returns the data source type name. +func (d *dnsAAAARecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_aaaa_record" +} + +// Schema defines the schema for the data source. +func (d *dnsAAAARecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS AAAA records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Computed: true, + Description: "The list of AAAA records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "ip_address": schema.StringAttribute{ + Required: true, + Description: "The IPv6 address this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsAAAARecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsAAAARecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + state.Records = []AAAARecordData{} + for _, record := range records { + state.Records = append(state.Records, AAAARecordData{ + IPAddress: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/dns_aaaa_record_data_source_test.go b/internal/provider/dns_aaaa_record_data_source_test.go new file mode 100644 index 0000000..3896675 --- /dev/null +++ b/internal/provider/dns_aaaa_record_data_source_test.go @@ -0,0 +1,128 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsAAAARecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test" { + zone = "pmapitest2.com" + name = "@" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + ttl = "3600" + comments = "test comment" + } + ] + } + + data "abion_dns_aaaa_record" "test_data" { + zone = abion_dns_aaaa_record.test.zone + name = abion_dns_aaaa_record.test.name + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_aaaa_record.test_data", "zone", "pmapitest2.com"), + resource.TestCheckResourceAttr("data.abion_dns_aaaa_record.test_data", "name", "@"), + resource.TestCheckResourceAttr("data.abion_dns_aaaa_record.test_data", "records.#", "1"), + + resource.TestCheckResourceAttr("data.abion_dns_aaaa_record.test_data", "records.0.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0"), + resource.TestCheckResourceAttr("data.abion_dns_aaaa_record.test_data", "records.0.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_aaaa_record.test_data", "records.0.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsAAAARecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_aaaa_record" "non_existing" { + zone = "non_existing.com" + name = "@" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsAAAARecordNoRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify error + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test2" { + zone = "pmapitest2.com" + name = "@" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + } + ] + } + + data "abion_dns_aaaa_record" "test_data" { + zone = abion_dns_aaaa_record.test2.zone + name = "xxx" + } + `, + ExpectError: regexp.MustCompile("No records exist on xxx level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsAAAARecordNoARecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test3" { + zone = "pmapitest2.com" + name = "test3" + records = [ + { + ip_address = "203.0.113.0" + } + ] + } + + data "abion_dns_aaaa_record" "test_data" { + zone = abion_dns_a_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No AAAA records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_aaaa_record_resource.go b/internal/provider/dns_aaaa_record_resource.go new file mode 100644 index 0000000..a8f7e31 --- /dev/null +++ b/internal/provider/dns_aaaa_record_resource.go @@ -0,0 +1,274 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsAAAARecordResource{} + _ resource.ResourceWithConfigure = &dnsAAAARecordResource{} + _ resource.ResourceWithImportState = &dnsAAAARecordResource{} +) + +// NewDnsAAAARecordResource is a helper function to simplify the provider implementation. +func NewDnsAAAARecordResource() resource.Resource { + return &dnsAAAARecordResource{} +} + +// dnsAAAARecordResource is the resource implementation. +type dnsAAAARecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsAAAARecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypeAAAA +} + +// Metadata returns the resource type name. +func (r *dnsAAAARecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_aaaa_record" +} + +// Schema defines the schema for the resource. +func (r *dnsAAAARecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS AAAA records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Required: true, + Description: "The list of AAAA records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "ip_address": schema.StringAttribute{ + Required: true, + Description: "The IPv6 address this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsAAAARecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsAAAARecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsAAAARecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsAAAARecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + state.Records = []AAAARecordData{} + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + for _, record := range records { + state.Records = append(state.Records, AAAARecordData{ + IPAddress: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsAAAARecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsAAAARecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsAAAARecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsAAAARecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsAAAARecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsAAAARecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsAAAARecordResource) createARecordCreateUpdateRequest(plan dnsAAAARecordModel) abionclient.ZoneRequest { + var data []abionclient.Record + for _, record := range plan.Records { + record := abionclient.Record{ + Data: record.IPAddress.ValueString(), + TTL: utils.Int32ToIntPointer(record.TTL), + Comments: record.Comments.ValueStringPointer(), + } + data = append(data, record) + } + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_aaaa_record_resource_test.go b/internal/provider/dns_aaaa_record_resource_test.go new file mode 100644 index 0000000..e7b5ab8 --- /dev/null +++ b/internal/provider/dns_aaaa_record_resource_test.go @@ -0,0 +1,154 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsAAAARecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on root level + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test" { + zone = "pmapitest2.com" + name = "@" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff1" + ttl = "3600" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2" + comments = "test comment" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "zone", "pmapitest2.com"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "name", "@"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.0.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.1.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff1"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.1.ttl", "3600"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.2.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.2.ttl"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.2.comments", "test comment"), + ), + }, + // Create and Read testing on subdomain level, move records from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test" { + zone = "pmapitest2.com" + name = "test" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff1" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "zone", "pmapitest2.com"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.0.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.1.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff1"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.2.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.2.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.2.comments"), + ), + }, + + // Update and Read testing on subdomain level, remove ip address + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test" { + zone = "pmapitest2.com" + name = "test" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + }, + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "zone", "pmapitest2.com"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.#", "2"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.0.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_aaaa_record.test", "records.1.ip_address", "2001:db8:ffff:ffff:ffff:ffff:ffff:fff2"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_aaaa_record.test", "records.1.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsAAAARecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test2" { + zone = "non_existing.com" + name = "@" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + }, + ] + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_cname_record_data_source.go b/internal/provider/dns_cname_record_data_source.go new file mode 100644 index 0000000..0c60ff1 --- /dev/null +++ b/internal/provider/dns_cname_record_data_source.go @@ -0,0 +1,165 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsCNameRecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsCNameRecordDataSource{} +) + +// NewDnsCNameRecordDataSource is a helper function to simplify the provider implementation. +func NewDnsCNameRecordDataSource() datasource.DataSource { + return &dnsCNameRecordDataSource{} +} + +// dnsCNameRecordDataSource is the data source implementation. +type dnsCNameRecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsCNameRecordModel maps the data source schema data. +type dnsCNameRecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Record *CNameRecordData `tfsdk:"record"` +} + +type CNameRecordData struct { + CName types.String `tfsdk:"cname"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsCNameRecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypeCName +} + +// Metadata returns the data source type name. +func (d *dnsCNameRecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_cname_record" +} + +// Schema defines the schema for the data source. +func (d *dnsCNameRecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS CNAME records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "record": schema.SingleNestedAttribute{ + Description: "CNAME record details.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "cname": schema.StringAttribute{ + Description: "The canonical name (CNAME) for the record.", + Required: true, + }, + "ttl": schema.Int32Attribute{ + Description: "Time-to-live (TTL) for the record, in seconds.", + Computed: true, + Optional: true, + }, + "comments": schema.StringAttribute{ + Description: "Comments for the record.", + Computed: true, + Optional: true, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsCNameRecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsCNameRecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + // only one cname per level is supported, pick first element. + state.Record = &CNameRecordData{ + CName: types.StringValue(records[0].Data), + Comments: utils.StringPointerToTerraformString(records[0].Comments), + TTL: utils.IntPointerToInt32(records[0].TTL), + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/dns_cname_record_data_source_test.go b/internal/provider/dns_cname_record_data_source_test.go new file mode 100644 index 0000000..8240656 --- /dev/null +++ b/internal/provider/dns_cname_record_data_source_test.go @@ -0,0 +1,122 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsCNameRecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_cname_record" "test" { + zone = "pmapitest3.com" + name = "@" + record = { + cname = "www.test.com." + ttl = "3600" + comments = "test comment" + } + } + + data "abion_dns_cname_record" "test_data" { + zone = abion_dns_cname_record.test.zone + name = abion_dns_cname_record.test.name + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_cname_record.test_data", "zone", "pmapitest3.com"), + resource.TestCheckResourceAttr("data.abion_dns_cname_record.test_data", "name", "@"), + resource.TestCheckResourceAttr("data.abion_dns_cname_record.test_data", "record.cname", "www.test.com."), + resource.TestCheckResourceAttr("data.abion_dns_cname_record.test_data", "record.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_cname_record.test_data", "record.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsCNameRecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_cname_record" "non_existing" { + zone = "non_existing.com" + name = "@" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsCNameRecordNoRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify error + { + Config: providerConfig + ` + resource "abion_dns_cname_record" "test2" { + zone = "pmapitest3.com" + name = "@" + record = { + cname = "www.test.com" + } + } + + data "abion_dns_cname_record" "test_data" { + zone = abion_dns_cname_record.test2.zone + name = "xxx" + } + `, + ExpectError: regexp.MustCompile("No records exist on xxx level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsCNameRecordNoARecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test3" { + zone = "pmapitest3.com" + name = "test3" + records = [ + { + ip_address = "203.0.113.0" + }, + ] + } + + data "abion_dns_cname_record" "test_data" { + zone = abion_dns_a_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No CNAME records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_cname_record_resource.go b/internal/provider/dns_cname_record_resource.go new file mode 100644 index 0000000..09d989a --- /dev/null +++ b/internal/provider/dns_cname_record_resource.go @@ -0,0 +1,271 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsCNameRecordResource{} + _ resource.ResourceWithConfigure = &dnsCNameRecordResource{} + _ resource.ResourceWithImportState = &dnsCNameRecordResource{} +) + +// NewDnsCNameRecordResource is a helper function to simplify the provider implementation. +func NewDnsCNameRecordResource() resource.Resource { + return &dnsCNameRecordResource{} +} + +// dnsCNameRecordResource is the resource implementation. +type dnsCNameRecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsCNameRecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypeCName +} + +// Metadata returns the resource type name. +func (r *dnsCNameRecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_cname_record" +} + +// Schema defines the schema for the resource. +func (r *dnsCNameRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS CNAME records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "record": schema.SingleNestedAttribute{ + Description: "CNAME record details.", + Required: true, + Attributes: map[string]schema.Attribute{ + "cname": schema.StringAttribute{ + Description: "The canonical name (CNAME) for the record.", + Required: true, + }, + "ttl": schema.Int32Attribute{ + Description: "Time-to-live (TTL) for the record, in seconds.", + Optional: true, + }, + "comments": schema.StringAttribute{ + Description: "Comments for the record.", + Optional: true, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsCNameRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsCNameRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsCNameRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsCNameRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + if len(records) > 0 { + // only one cname per level is supported, pick first element. + state.Record = &CNameRecordData{ + CName: types.StringValue(records[0].Data), + Comments: utils.StringPointerToTerraformString(records[0].Comments), + TTL: utils.IntPointerToInt32(records[0].TTL), + } + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsCNameRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsCNameRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsCNameRecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsCNameRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsCNameRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsCNameRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsCNameRecordResource) createARecordCreateUpdateRequest(plan dnsCNameRecordModel) abionclient.ZoneRequest { + + record := abionclient.Record{ + Data: plan.Record.CName.ValueString(), + Comments: plan.Record.Comments.ValueStringPointer(), + TTL: utils.Int32ToIntPointer(plan.Record.TTL), + } + + data := []abionclient.Record{record} + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_cname_record_resource_test.go b/internal/provider/dns_cname_record_resource_test.go new file mode 100644 index 0000000..404630d --- /dev/null +++ b/internal/provider/dns_cname_record_resource_test.go @@ -0,0 +1,105 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsCNameRecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on root level + { + Config: providerConfig + ` + resource "abion_dns_cname_record" "test" { + zone = "pmapitest3.com" + name = "@" + record = { + cname = "www.test.com." + ttl = "3600" + comments = "test comment" + } + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "zone", "pmapitest3.com"), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "name", "@"), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "record.cname", "www.test.com."), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "record.ttl", "3600"), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "record.comments", "test comment"), + ), + }, + // Create and Read testing on subdomain level, move record from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_cname_record" "test" { + zone = "pmapitest3.com" + name = "test" + record = { + cname = "www.test.com." + } + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "zone", "pmapitest3.com"), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "record.cname", "www.test.com."), + resource.TestCheckNoResourceAttr("abion_dns_cname_record.test", "record.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_cname_record.test", "record.comments"), + ), + }, + + // Update and Read testing on subdomain level, change cname + { + Config: providerConfig + ` + resource "abion_dns_cname_record" "test" { + zone = "pmapitest3.com" + name = "test" + record = { + cname = "www.test3.com." + } + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "zone", "pmapitest3.com"), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_cname_record.test", "record.cname", "www.test3.com."), + resource.TestCheckNoResourceAttr("abion_dns_cname_record.test", "record.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_cname_record.test", "record.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsCNameRecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_cname_record" "test2" { + zone = "non_existing.com" + name = "@" + record = { + cname = "still_non_existing.com" + } + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_mx_record_data_source.go b/internal/provider/dns_mx_record_data_source.go new file mode 100644 index 0000000..6175dae --- /dev/null +++ b/internal/provider/dns_mx_record_data_source.go @@ -0,0 +1,179 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "strconv" + "strings" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsMXRecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsMXRecordDataSource{} +) + +// NewDnsMXRecordDataSource is a helper function to simplify the provider implementation. +func NewDnsMXRecordDataSource() datasource.DataSource { + return &dnsMXRecordDataSource{} +} + +// dnsMXRecordDataSource is the data source implementation. +type dnsMXRecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsMXRecordModel maps the data source schema data. +type dnsMXRecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Records []MXRecordData `tfsdk:"records"` +} + +type MXRecordData struct { + Priority types.Int32 `tfsdk:"priority"` + Host types.String `tfsdk:"host"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsMXRecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypeMX +} + +// Metadata returns the data source type name. +func (d *dnsMXRecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_mx_record" +} + +// Schema defines the schema for the data source. +func (d *dnsMXRecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS MX records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Computed: true, + Description: "The list of MX records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + Required: true, + Description: "The hostname of the mail server.", + }, + "priority": schema.Int32Attribute{ + Required: true, + Description: "The priority in which order mail servers are tried.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsMXRecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsMXRecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + state.Records = []MXRecordData{} + for _, record := range records { + + // MX record data follow the pattern "priority host". + parts := strings.Fields(record.Data) + priority, _ := strconv.Atoi(parts[0]) + + state.Records = append(state.Records, MXRecordData{ + Priority: types.Int32Value(int32(priority)), + Host: types.StringValue(parts[1]), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/dns_mx_record_data_source_test.go b/internal/provider/dns_mx_record_data_source_test.go new file mode 100644 index 0000000..20f7fd0 --- /dev/null +++ b/internal/provider/dns_mx_record_data_source_test.go @@ -0,0 +1,131 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsMXRecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_mx_record" "test" { + zone = "pmapitest4.com" + name = "@" + records = [ + { + host = "mail1.pmapitest4.com." + priority = "10" + ttl = "3600" + comments = "test comment" + } + ] + } + + data "abion_dns_mx_record" "test_data" { + zone = abion_dns_mx_record.test.zone + name = abion_dns_mx_record.test.name + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_mx_record.test_data", "zone", "pmapitest4.com"), + resource.TestCheckResourceAttr("data.abion_dns_mx_record.test_data", "name", "@"), + resource.TestCheckResourceAttr("data.abion_dns_mx_record.test_data", "records.#", "1"), + + resource.TestCheckResourceAttr("data.abion_dns_mx_record.test_data", "records.0.host", "mail1.pmapitest4.com."), + resource.TestCheckResourceAttr("data.abion_dns_mx_record.test_data", "records.0.priority", "10"), + resource.TestCheckResourceAttr("data.abion_dns_mx_record.test_data", "records.0.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_mx_record.test_data", "records.0.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsMXRecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_mx_record" "non_existing" { + zone = "non_existing.com" + name = "@" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsMXRecordNoRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify error + { + Config: providerConfig + ` + resource "abion_dns_mx_record" "test2" { + zone = "pmapitest4.com" + name = "@" + records = [ + { + host = "mail.pmapitest4.com." + priority = "10" + } + ] + } + + data "abion_dns_mx_record" "test_data" { + zone = abion_dns_mx_record.test2.zone + name = "xxx" + } + `, + ExpectError: regexp.MustCompile("No records exist on xxx level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsMXRecordNoARecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test3" { + zone = "pmapitest4.com" + name = "test3" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + } + ] + } + + data "abion_dns_mx_record" "test_data" { + zone = abion_dns_aaaa_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No MX records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_mx_record_resource.go b/internal/provider/dns_mx_record_resource.go new file mode 100644 index 0000000..c240ec7 --- /dev/null +++ b/internal/provider/dns_mx_record_resource.go @@ -0,0 +1,286 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "strconv" + "strings" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsMXRecordResource{} + _ resource.ResourceWithConfigure = &dnsMXRecordResource{} + _ resource.ResourceWithImportState = &dnsMXRecordResource{} +) + +// NewDnsMXRecordResource is a helper function to simplify the provider implementation. +func NewDnsMXRecordResource() resource.Resource { + return &dnsMXRecordResource{} +} + +// dnsMXRecordResource is the resource implementation. +type dnsMXRecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsMXRecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypeMX +} + +// Metadata returns the resource type name. +func (r *dnsMXRecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_mx_record" +} + +// Schema defines the schema for the resource. +func (r *dnsMXRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS MX records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Required: true, + Description: "The list of MX records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + Required: true, + Description: "The hostname of the mail server.", + }, + "priority": schema.Int32Attribute{ + Required: true, + Description: "The priority in which order mail servers are tried.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsMXRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsMXRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createMXRecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsMXRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsMXRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + state.Records = []MXRecordData{} + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + for _, record := range records { + // MX record data follow the pattern "priority host". + parts := strings.Fields(record.Data) + priority, _ := strconv.Atoi(parts[0]) + + state.Records = append(state.Records, MXRecordData{ + Priority: types.Int32Value(int32(priority)), + Host: types.StringValue(parts[1]), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsMXRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsMXRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsMXRecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // update or add the new records according to the plan + patchRequest := r.createMXRecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsMXRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsMXRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by removing the records + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsMXRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsMXRecordResource) createMXRecordCreateUpdateRequest(plan dnsMXRecordModel) abionclient.ZoneRequest { + var data []abionclient.Record + for _, record := range plan.Records { + record := abionclient.Record{ + Data: record.Priority.String() + " " + record.Host.ValueString(), + TTL: utils.Int32ToIntPointer(record.TTL), + Comments: record.Comments.ValueStringPointer(), + } + data = append(data, record) + } + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_mx_record_resource_test.go b/internal/provider/dns_mx_record_resource_test.go new file mode 100644 index 0000000..3d7f4ed --- /dev/null +++ b/internal/provider/dns_mx_record_resource_test.go @@ -0,0 +1,171 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsMXRecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on root level + { + Config: providerConfig + ` + resource "abion_dns_mx_record" "test" { + zone = "pmapitest4.com" + name = "@" + records = [ + { + host = "mail1.pmapitest4.com." + priority = "10" + }, + { + host = "mail2.pmapitest4.com." + priority = "20" + comments = "test comment" + }, + { + host = "mail3.pmapitest4.com." + priority = "30" + ttl = "3600" + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "zone", "pmapitest4.com"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "name", "@"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.0.host", "mail1.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.0.priority", "10"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.1.host", "mail2.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.1.priority", "20"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.1.comments", "test comment"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.1.ttl"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.2.host", "mail3.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.2.priority", "30"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.2.ttl", "3600"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.2.comments"), + ), + }, + // Create and Read testing on subdomain level, move records from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_mx_record" "test" { + zone = "pmapitest4.com" + name = "test" + records = [ + { + host = "mail1.pmapitest4.com." + priority = "10" + }, + { + host = "mail2.pmapitest4.com." + priority = "20" + }, + { + host = "mail3.pmapitest4.com." + priority = "30" + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "zone", "pmapitest4.com"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.0.host", "mail1.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.0.priority", "10"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.1.host", "mail2.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.1.priority", "20"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.2.host", "mail3.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.2.priority", "30"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.2.comments"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.2.ttl"), + ), + }, + + // Update and Read testing on subdomain level, remove mx record + { + Config: providerConfig + ` + resource "abion_dns_mx_record" "test" { + zone = "pmapitest4.com" + name = "test" + records = [ + { + host = "mail1.pmapitest4.com." + priority = "10" + }, + { + host = "mail3.pmapitest4.com." + priority = "30" + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "zone", "pmapitest4.com"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.#", "2"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.0.host", "mail1.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.0.priority", "10"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.0.comments"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.0.ttl"), + + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.1.host", "mail3.pmapitest4.com."), + resource.TestCheckResourceAttr("abion_dns_mx_record.test", "records.1.priority", "30"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_mx_record.test", "records.1.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsMXRecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_mx_record" "test2" { + zone = "non_existing.com" + name = "@" + records = [ + { + host = "mail1.pmapitest4.com." + priority = "10" + }, + ] + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_ns_record_data_source.go b/internal/provider/dns_ns_record_data_source.go new file mode 100644 index 0000000..07570ef --- /dev/null +++ b/internal/provider/dns_ns_record_data_source.go @@ -0,0 +1,167 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsNSRecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsNSRecordDataSource{} +) + +// NewDnsNSRecordDataSource is a helper function to simplify the provider implementation. +func NewDnsNSRecordDataSource() datasource.DataSource { + return &dnsNSRecordDataSource{} +} + +// dnsNSRecordDataSource is the data source implementation. +type dnsNSRecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsNSRecordModel maps the data source schema data. +type dnsNSRecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Records []NSRecordData `tfsdk:"records"` +} + +type NSRecordData struct { + Nameserver types.String `tfsdk:"nameserver"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsNSRecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypeNS +} + +// Metadata returns the data source type name. +func (d *dnsNSRecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_ns_record" +} + +// Schema defines the schema for the data source. +func (d *dnsNSRecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS NS records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Computed: true, + Description: "The list of NS records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nameserver": schema.StringAttribute{ + Required: true, + Description: "The nameserver this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsNSRecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsNSRecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + state.Records = []NSRecordData{} + for _, record := range records { + state.Records = append(state.Records, NSRecordData{ + Nameserver: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/dns_ns_record_data_source_test.go b/internal/provider/dns_ns_record_data_source_test.go new file mode 100644 index 0000000..5cca5ec --- /dev/null +++ b/internal/provider/dns_ns_record_data_source_test.go @@ -0,0 +1,99 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsNSRecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_ns_record" "test" { + zone = "pmapitest5.com" + name = "www" + records = [ + { + nameserver = "ns1.testabiondns.se." + ttl = "3600" + comments = "test comment" + } + ] + } + + data "abion_dns_ns_record" "test_data" { + zone = abion_dns_ns_record.test.zone + name = "www" + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_ns_record.test_data", "zone", "pmapitest5.com"), + resource.TestCheckResourceAttr("data.abion_dns_ns_record.test_data", "name", "www"), + resource.TestCheckResourceAttr("data.abion_dns_ns_record.test_data", "records.#", "1"), + + resource.TestCheckResourceAttr("data.abion_dns_ns_record.test_data", "records.0.nameserver", "ns1.testabiondns.se."), + resource.TestCheckResourceAttr("data.abion_dns_ns_record.test_data", "records.0.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_ns_record.test_data", "records.0.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsNSRecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_ns_record" "non_existing" { + zone = "non_existing.com" + name = "www" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsNSRecordNoNSRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test3" { + zone = "pmapitest5.com" + name = "test3" + records = [ + { + ip_address = "203.0.113.0" + } + ] + } + + data "abion_dns_ns_record" "test_data" { + zone = abion_dns_a_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No NS records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_ns_record_resource.go b/internal/provider/dns_ns_record_resource.go new file mode 100644 index 0000000..2b721ef --- /dev/null +++ b/internal/provider/dns_ns_record_resource.go @@ -0,0 +1,274 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsNSRecordResource{} + _ resource.ResourceWithConfigure = &dnsNSRecordResource{} + _ resource.ResourceWithImportState = &dnsNSRecordResource{} +) + +// NewDnsNSRecordResource is a helper function to simplify the provider implementation. +func NewDnsNSRecordResource() resource.Resource { + return &dnsNSRecordResource{} +} + +// dnsNSRecordResource is the resource implementation. +type dnsNSRecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsNSRecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypeNS +} + +// Metadata returns the resource type name. +func (r *dnsNSRecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_ns_record" +} + +// Schema defines the schema for the resource. +func (r *dnsNSRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS NS records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Required: true, + Description: "The list of NS records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nameserver": schema.StringAttribute{ + Required: true, + Description: "The Nameserver address this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsNSRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsNSRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsNSRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsNSRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + state.Records = []NSRecordData{} + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + for _, record := range records { + state.Records = append(state.Records, NSRecordData{ + Nameserver: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsNSRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsNSRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsNSRecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsNSRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsNSRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsNSRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsNSRecordResource) createARecordCreateUpdateRequest(plan dnsNSRecordModel) abionclient.ZoneRequest { + var data []abionclient.Record + for _, record := range plan.Records { + record := abionclient.Record{ + Data: record.Nameserver.ValueString(), + TTL: utils.Int32ToIntPointer(record.TTL), + Comments: record.Comments.ValueStringPointer(), + } + data = append(data, record) + } + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_ns_record_resource_test.go b/internal/provider/dns_ns_record_resource_test.go new file mode 100644 index 0000000..204fe41 --- /dev/null +++ b/internal/provider/dns_ns_record_resource_test.go @@ -0,0 +1,116 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsNSRecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on subdomain level, move records from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_ns_record" "test" { + zone = "pmapitest5.com" + name = "test" + records = [ + { + nameserver = "ns1.testabiondns.se." + }, + { + nameserver = "ns2.testabiondns.se." + ttl = "3600" + }, + { + nameserver = "ns3.testabiondns.se." + comments = "test comment" + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "zone", "pmapitest5.com"), + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.0.nameserver", "ns1.testabiondns.se."), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.1.nameserver", "ns2.testabiondns.se."), + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.1.ttl", "3600"), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.2.nameserver", "ns3.testabiondns.se."), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.2.ttl"), + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.2.comments", "test comment"), + ), + }, + + // Update and Read testing on subdomain level, remove ip address + { + Config: providerConfig + ` + resource "abion_dns_ns_record" "test" { + zone = "pmapitest5.com" + name = "test" + records = [ + { + nameserver = "ns1.testabiondns.se." + }, + { + nameserver = "ns3.testabiondns.se." + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "zone", "pmapitest5.com"), + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.#", "2"), + + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.0.nameserver", "ns1.testabiondns.se."), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_ns_record.test", "records.1.nameserver", "ns3.testabiondns.se."), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ns_record.test", "records.1.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsNSRecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_ns_record" "test2" { + zone = "non_existing.com" + name = "test" + records = [ + { + nameserver = "ns1.testabiondns.se." + }, + ] + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_ptr_record_data_source.go b/internal/provider/dns_ptr_record_data_source.go new file mode 100644 index 0000000..7a3b148 --- /dev/null +++ b/internal/provider/dns_ptr_record_data_source.go @@ -0,0 +1,166 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsPTRRecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsPTRRecordDataSource{} +) + +// NewDnsPTRRecordDataSource is a helper function to simplify the provider implementation. +func NewDnsPTRRecordDataSource() datasource.DataSource { + return &dnsPTRRecordDataSource{} +} + +// dnsPTRRecordDataSource is the data source implementation. +type dnsPTRRecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsPTRRecordModel maps the data source schema data. +type dnsPTRRecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Records []PTRRecordData `tfsdk:"records"` +} + +type PTRRecordData struct { + PTR types.String `tfsdk:"ptr"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsPTRRecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypePTR +} + +// Metadata returns the data source type name. +func (d *dnsPTRRecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_ptr_record" +} + +// Schema defines the schema for the data source. +func (d *dnsPTRRecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS PTR records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Computed: true, + Description: "The list of PTR records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "ptr": schema.StringAttribute{ + Required: true, + Description: "The canonical name this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsPTRRecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsPTRRecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + state.Records = []PTRRecordData{} + for _, record := range records { + state.Records = append(state.Records, PTRRecordData{ + PTR: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/dns_ptr_record_data_source_test.go b/internal/provider/dns_ptr_record_data_source_test.go new file mode 100644 index 0000000..9340bf8 --- /dev/null +++ b/internal/provider/dns_ptr_record_data_source_test.go @@ -0,0 +1,128 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsPTRRecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_ptr_record" "test" { + zone = "pmapitest6.com" + name = "@" + records = [ + { + ptr = "203.0.113.0.www.pmapitest6.com." + ttl = "3600" + comments = "test comment" + } + ] + } + + data "abion_dns_ptr_record" "test_data" { + zone = abion_dns_ptr_record.test.zone + name = abion_dns_ptr_record.test.name + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_ptr_record.test_data", "zone", "pmapitest6.com"), + resource.TestCheckResourceAttr("data.abion_dns_ptr_record.test_data", "name", "@"), + resource.TestCheckResourceAttr("data.abion_dns_ptr_record.test_data", "records.#", "1"), + + resource.TestCheckResourceAttr("data.abion_dns_ptr_record.test_data", "records.0.ptr", "203.0.113.0.www.pmapitest6.com."), + resource.TestCheckResourceAttr("data.abion_dns_ptr_record.test_data", "records.0.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_ptr_record.test_data", "records.0.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsPTRRecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_ptr_record" "non_existing" { + zone = "non_existing.com" + name = "@" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsPTRRecordNoRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify error + { + Config: providerConfig + ` + resource "abion_dns_ptr_record" "test2" { + zone = "pmapitest6.com" + name = "@" + records = [ + { + ptr = "203.0.113.0.www.pmapitest6.com." + } + ] + } + + data "abion_dns_ptr_record" "test_data" { + zone = abion_dns_ptr_record.test2.zone + name = "xxx" + } + `, + ExpectError: regexp.MustCompile("No records exist on xxx level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsPTRRecordNoARecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test3" { + zone = "pmapitest6.com" + name = "test3" + records = [ + { + ip_address = "203.0.113.0" + } + ] + } + + data "abion_dns_ptr_record" "test_data" { + zone = abion_dns_a_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No PTR records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_ptr_record_resource.go b/internal/provider/dns_ptr_record_resource.go new file mode 100644 index 0000000..ff81c41 --- /dev/null +++ b/internal/provider/dns_ptr_record_resource.go @@ -0,0 +1,275 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsPTRRecordResource{} + _ resource.ResourceWithConfigure = &dnsPTRRecordResource{} + _ resource.ResourceWithImportState = &dnsPTRRecordResource{} +) + +// NewDnsPTRRecordResource is a helper function to simplify the provider implementation. +func NewDnsPTRRecordResource() resource.Resource { + return &dnsPTRRecordResource{} +} + +// dnsPTRRecordResource is the resource implementation. +type dnsPTRRecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsPTRRecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypePTR +} + +// Metadata returns the resource type name. +func (r *dnsPTRRecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_ptr_record" +} + +// Schema defines the schema for the resource. +func (r *dnsPTRRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS PTR records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Required: true, + Description: "The list of PTR records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "ptr": schema.StringAttribute{ + Required: true, + Description: "The canonical name this record will point to.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsPTRRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsPTRRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createPTRRecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsPTRRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsPTRRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + state.Records = []PTRRecordData{} + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + for _, record := range records { + state.Records = append(state.Records, PTRRecordData{ + PTR: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsPTRRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsPTRRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsPTRRecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // update or add the new records according to the plan + patchRequest := r.createPTRRecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsPTRRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsPTRRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by removing the records + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsPTRRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsPTRRecordResource) createPTRRecordCreateUpdateRequest(plan dnsPTRRecordModel) abionclient.ZoneRequest { + var data []abionclient.Record + for _, record := range plan.Records { + record := abionclient.Record{ + Data: record.PTR.ValueString(), + TTL: utils.Int32ToIntPointer(record.TTL), + Comments: record.Comments.ValueStringPointer(), + } + data = append(data, record) + } + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_ptr_record_resource_test.go b/internal/provider/dns_ptr_record_resource_test.go new file mode 100644 index 0000000..b3cca33 --- /dev/null +++ b/internal/provider/dns_ptr_record_resource_test.go @@ -0,0 +1,154 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsPTRRecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on root level + { + Config: providerConfig + ` + resource "abion_dns_ptr_record" "test" { + zone = "pmapitest6.com" + name = "@" + records = [ + { + ptr = "www.example0.com." + }, + { + ptr = "www.example1.com." + ttl = "3600" + }, + { + ptr = "www.example2.com." + comments = "test comment" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "zone", "pmapitest6.com"), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "name", "@"), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.0.ptr", "www.example0.com."), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.1.ptr", "www.example1.com."), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.1.ttl", "3600"), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.2.ptr", "www.example2.com."), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.2.ttl"), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.2.comments", "test comment"), + ), + }, + // Create and Read testing on subdomain level, move records from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_ptr_record" "test" { + zone = "pmapitest6.com" + name = "test" + records = [ + { + ptr = "www.example0.com." + }, + { + ptr = "www.example1.com." + }, + { + ptr = "www.example2.com." + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "zone", "pmapitest6.com"), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.0.ptr", "www.example0.com."), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.1.ptr", "www.example1.com."), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.2.ptr", "www.example2.com."), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.2.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.2.comments"), + ), + }, + + // Update and Read testing on subdomain level, remove ip address + { + Config: providerConfig + ` + resource "abion_dns_ptr_record" "test" { + zone = "pmapitest6.com" + name = "test" + records = [ + { + ptr = "www.example0.com." + }, + { + ptr = "www.example2.com." + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "zone", "pmapitest6.com"), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.#", "2"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.0.ptr", "www.example0.com."), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_ptr_record.test", "records.1.ptr", "www.example2.com."), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_ptr_record.test", "records.1.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsPTRRecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_ptr_record" "test2" { + zone = "non_existing.com" + name = "@" + records = [ + { + ptr = "www.example0.com." + }, + ] + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_srv_record_data_source.go b/internal/provider/dns_srv_record_data_source.go new file mode 100644 index 0000000..6b9d693 --- /dev/null +++ b/internal/provider/dns_srv_record_data_source.go @@ -0,0 +1,199 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "strconv" + "strings" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsSRVRecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsSRVRecordDataSource{} +) + +// NewDnsSRVRecordDataSource is a helper function to simplify the provider implementation. +func NewDnsSRVRecordDataSource() datasource.DataSource { + return &dnsSRVRecordDataSource{} +} + +// dnsSRVRecordDataSource is the data source implementation. +type dnsSRVRecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsSRVRecordModel maps the data source schema data. +type dnsSRVRecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Records []SRVRecordData `tfsdk:"records"` +} + +type SRVRecordData struct { + Priority types.Int32 `tfsdk:"priority"` + Weight types.Int32 `tfsdk:"weight"` + Port types.Int32 `tfsdk:"port"` + Target types.String `tfsdk:"target"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsSRVRecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypeSRV +} + +// Metadata returns the data source type name. +func (d *dnsSRVRecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_srv_record" +} + +// Schema defines the schema for the data source. +func (d *dnsSRVRecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS SRV records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Computed: true, + Description: "The list of SRV records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target": schema.StringAttribute{ + Required: true, + Description: "The hostname of the machine providing the service.", + }, + "port": schema.Int32Attribute{ + Required: true, + Description: "The TCP or UDP port on which the service is running.", + }, + "priority": schema.Int32Attribute{ + Required: true, + Description: "The priority specifying the priority of the target host. Lower values indicate higher priority.", + }, + "weight": schema.Int32Attribute{ + Required: true, + Description: "A relative weight for records with the same priority. Higher weights are more preferred.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsSRVRecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsSRVRecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + state.Records = []SRVRecordData{} + for _, record := range records { + + priority, weight, port, target := splitSrvRecordData(record) + + state.Records = append(state.Records, SRVRecordData{ + Priority: types.Int32Value(int32(priority)), + Weight: types.Int32Value(int32(weight)), + Port: types.Int32Value(int32(port)), + Target: types.StringValue(target), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func splitSrvRecordData(record abionclient.Record) (int, int, int, string) { + // SRV record data follow the pattern "priority weight port target". + parts := strings.Fields(record.Data) + priority, _ := strconv.Atoi(parts[0]) + weight, _ := strconv.Atoi(parts[1]) + port, _ := strconv.Atoi(parts[2]) + target := parts[3] + return priority, weight, port, target +} diff --git a/internal/provider/dns_srv_record_data_source_test.go b/internal/provider/dns_srv_record_data_source_test.go new file mode 100644 index 0000000..2d8a5b9 --- /dev/null +++ b/internal/provider/dns_srv_record_data_source_test.go @@ -0,0 +1,137 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsSRVRecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_srv_record" "test" { + zone = "pmapitest7.com" + name = "@" + records = [ + { + target = "server1.pmapitest7.com." + port = "443" + priority = "1" + weight = "100" + comments = "test comment" + ttl = "3600" + }, + ] + } + + data "abion_dns_srv_record" "test_data" { + zone = abion_dns_srv_record.test.zone + name = abion_dns_srv_record.test.name + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "zone", "pmapitest7.com"), + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "name", "@"), + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "records.#", "1"), + + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "records.0.target", "server1.pmapitest7.com."), + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "records.0.port", "443"), + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "records.0.priority", "1"), + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "records.0.weight", "100"), + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "records.0.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_srv_record.test_data", "records.0.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsSRVRecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_srv_record" "non_existing" { + zone = "non_existing.com" + name = "@" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsSRVRecordNoRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify error + { + Config: providerConfig + ` + resource "abion_dns_srv_record" "test2" { + zone = "pmapitest7.com" + name = "@" + records = [ + { + target = "server1.pmapitest7.com." + port = "443" + priority = "1" + weight = "100" + }, + ] + } + + data "abion_dns_srv_record" "test_data" { + zone = abion_dns_srv_record.test2.zone + name = "xxx" + } + `, + ExpectError: regexp.MustCompile("No records exist on xxx level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsSRVRecordNoARecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_aaaa_record" "test3" { + zone = "pmapitest7.com" + name = "test3" + records = [ + { + ip_address = "2001:db8:ffff:ffff:ffff:ffff:ffff:fff0" + } + ] + } + + data "abion_dns_srv_record" "test_data" { + zone = abion_dns_aaaa_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No SRV records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_srv_record_resource.go b/internal/provider/dns_srv_record_resource.go new file mode 100644 index 0000000..eb70b72 --- /dev/null +++ b/internal/provider/dns_srv_record_resource.go @@ -0,0 +1,294 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsSRVRecordResource{} + _ resource.ResourceWithConfigure = &dnsSRVRecordResource{} + _ resource.ResourceWithImportState = &dnsSRVRecordResource{} +) + +// NewDnsSRVRecordResource is a helper function to simplify the provider implementation. +func NewDnsSRVRecordResource() resource.Resource { + return &dnsSRVRecordResource{} +} + +// dnsSRVRecordResource is the resource implementation. +type dnsSRVRecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsSRVRecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypeSRV +} + +// Metadata returns the resource type name. +func (r *dnsSRVRecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_srv_record" +} + +// Schema defines the schema for the resource. +func (r *dnsSRVRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS SRV records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Required: true, + Description: "The list of SRV records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "target": schema.StringAttribute{ + Required: true, + Description: "The hostname of the machine providing the service.", + }, + "port": schema.Int32Attribute{ + Required: true, + Description: "The TCP or UDP port on which the service is running.", + }, + "priority": schema.Int32Attribute{ + Required: true, + Description: "The priority specifying the priority of the target host. Lower values indicate higher priority.", + }, + "weight": schema.Int32Attribute{ + Required: true, + Description: "A relative weight for records with the same priority. Higher weights are more preferred.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsSRVRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsSRVRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createSRVRecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsSRVRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsSRVRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + state.Records = []SRVRecordData{} + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + for _, record := range records { + + // SRV record data follow the pattern "priority weight port target". + priority, weight, port, target := splitSrvRecordData(record) + + state.Records = append(state.Records, SRVRecordData{ + Priority: types.Int32Value(int32(priority)), + Weight: types.Int32Value(int32(weight)), + Port: types.Int32Value(int32(port)), + Target: types.StringValue(target), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsSRVRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsSRVRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsSRVRecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // update or add the new records according to the plan + patchRequest := r.createSRVRecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsSRVRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsSRVRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by removing the records + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsSRVRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsSRVRecordResource) createSRVRecordCreateUpdateRequest(plan dnsSRVRecordModel) abionclient.ZoneRequest { + var data []abionclient.Record + for _, record := range plan.Records { + record := abionclient.Record{ + Data: record.Priority.String() + " " + record.Weight.String() + " " + record.Port.String() + " " + record.Target.ValueString(), + TTL: utils.Int32ToIntPointer(record.TTL), + Comments: record.Comments.ValueStringPointer(), + } + data = append(data, record) + } + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_srv_record_resource_test.go b/internal/provider/dns_srv_record_resource_test.go new file mode 100644 index 0000000..0bc1cc3 --- /dev/null +++ b/internal/provider/dns_srv_record_resource_test.go @@ -0,0 +1,205 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsSRVRecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on root level + { + Config: providerConfig + ` + resource "abion_dns_srv_record" "test" { + zone = "pmapitest7.com" + name = "@" + records = [ + { + target = "server1.pmapitest7.com." + port = "443" + priority = "1" + weight = "100" + }, + { + target = "server2.pmapitest7.com." + port = "5103" + priority = "100" + weight = "10" + comments = "test comment" + }, + { + target = "server3.pmapitest7.com." + port = "443" + priority = "100" + weight = "30" + ttl = "3600" + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "zone", "pmapitest7.com"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "name", "@"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.target", "server1.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.port", "443"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.priority", "1"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.weight", "100"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.target", "server2.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.port", "5103"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.priority", "100"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.weight", "10"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.comments", "test comment"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.1.ttl"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.target", "server3.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.port", "443"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.priority", "100"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.weight", "30"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.ttl", "3600"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.2.comments"), + ), + }, + // Create and Read testing on subdomain level, move records from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_srv_record" "test" { + zone = "pmapitest7.com" + name = "test" + records = [ + { + target = "server1.pmapitest7.com." + port = "443" + priority = "1" + weight = "100" + }, + { + target = "server2.pmapitest7.com." + port = "5103" + priority = "100" + weight = "10" + }, + { + target = "server3.pmapitest7.com." + port = "443" + priority = "100" + weight = "30" + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "zone", "pmapitest7.com"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.target", "server1.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.port", "443"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.priority", "1"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.weight", "100"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.target", "server2.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.port", "5103"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.priority", "100"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.weight", "10"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.target", "server3.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.port", "443"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.priority", "100"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.2.weight", "30"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.2.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.2.comments"), + ), + }, + + // Update and Read testing on subdomain level, remove srv record + { + Config: providerConfig + ` + resource "abion_dns_srv_record" "test" { + zone = "pmapitest7.com" + name = "test" + records = [ + { + target = "server1.pmapitest7.com." + port = "443" + priority = "1" + weight = "100" + }, + { + target = "server3.pmapitest7.com." + port = "443" + priority = "100" + weight = "30" + }, + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "zone", "pmapitest7.com"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.#", "2"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.target", "server1.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.port", "443"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.priority", "1"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.0.weight", "100"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.target", "server3.pmapitest7.com."), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.port", "443"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.priority", "100"), + resource.TestCheckResourceAttr("abion_dns_srv_record.test", "records.1.weight", "30"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_srv_record.test", "records.1.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsSRVRecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_srv_record" "test2" { + zone = "non_existing.com" + name = "@" + records = [ + { + target = "server1.pmapitest7.com." + port = "443" + priority = "1" + weight = "100" + }, + ] + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_txt_record_data_source.go b/internal/provider/dns_txt_record_data_source.go new file mode 100644 index 0000000..172d0ca --- /dev/null +++ b/internal/provider/dns_txt_record_data_source.go @@ -0,0 +1,167 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dnsTXTRecordDataSource{} + _ datasource.DataSourceWithConfigure = &dnsTXTRecordDataSource{} +) + +// NewDnsTXTRecordDataSource is a helper function to simplify the provider implementation. +func NewDnsTXTRecordDataSource() datasource.DataSource { + return &dnsTXTRecordDataSource{} +} + +// dnsTXTRecordDataSource is the data source implementation. +type dnsTXTRecordDataSource struct { + client *abionclient.Client + recordType utils.RecordType +} + +// dnsTXTRecordModel maps the data source schema data. +type dnsTXTRecordModel struct { + Zone types.String `tfsdk:"zone"` + Name types.String `tfsdk:"name"` + Records []TXTRecordData `tfsdk:"records"` +} + +type TXTRecordData struct { + TXTData types.String `tfsdk:"txt_data"` + TTL types.Int32 `tfsdk:"ttl"` + Comments types.String `tfsdk:"comments"` +} + +func (d *dnsTXTRecordDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client + d.recordType = utils.RecordTypeTXT +} + +// Metadata returns the data source type name. +func (d *dnsTXTRecordDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_txt_record" +} + +// Schema defines the schema for the data source. +func (d *dnsTXTRecordDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get DNS TXT records of the zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to fetch records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Computed: true, + Description: "The list of TXT records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "txt_data": schema.StringAttribute{ + Required: true, + Description: "The TXT data.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *dnsTXTRecordDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state dnsTXTRecordModel + + // Load zone_name from the configuration into state + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + tflog.Debug(ctx, "Getting zone details") + + // Get the zone details from Abion API + zone, err := d.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Zone from Abion API", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + if len(recordTypes) == 0 { + resp.Diagnostics.AddError( + "No records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + records := recordTypes[d.recordType.String()] + if len(records) == 0 { + resp.Diagnostics.AddError( + "No "+d.recordType.String()+" records exist on "+state.Name.ValueString()+" level.", + "", + ) + return + } + + state.Records = []TXTRecordData{} + for _, record := range records { + state.Records = append(state.Records, TXTRecordData{ + TXTData: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/dns_txt_record_data_source_test.go b/internal/provider/dns_txt_record_data_source_test.go new file mode 100644 index 0000000..bfe9671 --- /dev/null +++ b/internal/provider/dns_txt_record_data_source_test.go @@ -0,0 +1,127 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsTXTRecordDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify datasource + { + Config: providerConfig + ` + resource "abion_dns_txt_record" "test" { + zone = "pmapitest8.com" + name = "@" + records = [ + { + txt_data = "txt data 1" + ttl = "3600" + comments = "test comment" + } + ] + } + + data "abion_dns_txt_record" "test_data" { + zone = abion_dns_txt_record.test.zone + name = abion_dns_txt_record.test.name + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("data.abion_dns_txt_record.test_data", "zone", "pmapitest8.com"), + resource.TestCheckResourceAttr("data.abion_dns_txt_record.test_data", "name", "@"), + resource.TestCheckResourceAttr("data.abion_dns_txt_record.test_data", "records.#", "1"), + resource.TestCheckResourceAttr("data.abion_dns_txt_record.test_data", "records.0.txt_data", "txt data 1"), + resource.TestCheckResourceAttr("data.abion_dns_txt_record.test_data", "records.0.ttl", "3600"), + resource.TestCheckResourceAttr("data.abion_dns_txt_record.test_data", "records.0.comments", "test comment"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsTXTRecordNonExistingZoneDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + data "abion_dns_txt_record" "non_existing" { + zone = "non_existing.com" + name = "@" + } + `, + ExpectError: regexp.MustCompile("Unable to Read Zone from Abion API"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsTXTRecordNoRecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create record and verify error + { + Config: providerConfig + ` + resource "abion_dns_txt_record" "test2" { + zone = "pmapitest8.com" + name = "@" + records = [ + { + txt_data = "txt data 1" + } + ] + } + + data "abion_dns_txt_record" "test_data" { + zone = abion_dns_txt_record.test2.zone + name = "xxx" + } + `, + ExpectError: regexp.MustCompile("No records exist on xxx level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsTXTRecordNoARecordOnSubDomainLevelDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create another record type on subdomain and verify error + { + Config: providerConfig + ` + resource "abion_dns_a_record" "test3" { + zone = "pmapitest8.com" + name = "test3" + records = [ + { + ip_address = "203.0.113.0" + }, + ] + } + + data "abion_dns_txt_record" "test_data" { + zone = abion_dns_a_record.test3.zone + name = "test3" + } + `, + ExpectError: regexp.MustCompile("No TXT records exist on test3 level"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/dns_txt_record_resource.go b/internal/provider/dns_txt_record_resource.go new file mode 100644 index 0000000..7e40fbf --- /dev/null +++ b/internal/provider/dns_txt_record_resource.go @@ -0,0 +1,274 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + abionclient "terraform-provider-abion/internal/client" + "terraform-provider-abion/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &dnsTXTRecordResource{} + _ resource.ResourceWithConfigure = &dnsTXTRecordResource{} + _ resource.ResourceWithImportState = &dnsTXTRecordResource{} +) + +// NewDnsTXTRecordResource is a helper function to simplify the provider implementation. +func NewDnsTXTRecordResource() resource.Resource { + return &dnsTXTRecordResource{} +} + +// dnsTXTRecordResource is the resource implementation. +type dnsTXTRecordResource struct { + client *abionclient.Client + recordType utils.RecordType +} + +func (r *dnsTXTRecordResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*abionclient.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *abionclient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client + r.recordType = utils.RecordTypeTXT +} + +// Metadata returns the resource type name. +func (r *dnsTXTRecordResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_txt_record" +} + +// Schema defines the schema for the resource. +func (r *dnsTXTRecordResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this resource to create, update and delete DNS TXT records of a zone.", + Attributes: map[string]schema.Attribute{ + "zone": schema.StringAttribute{ + Required: true, + Description: "The zone the record belongs to.", + }, + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name to create records for. For example `@`, `www`, `ftp`, `www.east`. The `@` character represents the root of the zone.", + }, + "records": schema.ListNestedAttribute{ + Required: true, + Description: "The list of TXT records. Records are sorted to avoid constant changing plans", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "txt_data": schema.StringAttribute{ + Required: true, + Description: "The TXT data.", + }, + "ttl": schema.Int32Attribute{ + Optional: true, + Description: "Time-to-live (TTL) for the record, in seconds.", + }, + "comments": schema.StringAttribute{ + Optional: true, + Description: "Comments for the record.", + }, + }, + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *dnsTXTRecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan dnsTXTRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Creating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not create record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *dnsTXTRecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + + // Get current state + var state dnsTXTRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the zone details from Abion API + zone, err := r.client.GetZone(ctx, state.Zone.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Abion Zone", + err.Error(), + ) + return + } + + recordTypes := zone.Data.Attributes.Records[state.Name.ValueString()] + + state.Records = []TXTRecordData{} + if len(recordTypes) > 0 { + records := recordTypes[r.recordType.String()] + + for _, record := range records { + state.Records = append(state.Records, TXTRecordData{ + TXTData: types.StringValue(record.Data), + Comments: utils.StringPointerToTerraformString(record.Comments), + TTL: utils.IntPointerToInt32(record.TTL), + }) + } + } + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *dnsTXTRecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Retrieve values from plan + var plan dnsTXTRecordModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve values from current state + var state dnsTXTRecordModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := r.createARecordCreateUpdateRequest(plan) + + if plan.Name.ValueString() != state.Name.ValueString() { + // records has been moved from one level to another, remove the records from the old state level + if patchRequest.Data.Attributes.Records[state.Name.ValueString()] == nil { + patchRequest.Data.Attributes.Records[state.Name.ValueString()] = make(map[string][]abionclient.Record) + } + patchRequest.Data.Attributes.Records[state.Name.ValueString()][r.recordType.String()] = nil + } + + ctx = tflog.SetField(ctx, "zone", plan.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", plan.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Updating zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, plan.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not update record, unexpected error: "+err.Error(), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *dnsTXTRecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state dnsTXTRecordModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + patchRequest := abionclient.CreateRecordPatchRequest(state.Zone.ValueString(), state.Name.ValueString(), r.recordType, nil) + + ctx = tflog.SetField(ctx, "zone", state.Zone.ValueString()) + ctx = tflog.SetField(ctx, "name", state.Name.ValueString()) + ctx = tflog.SetField(ctx, "record_type", r.recordType.String()) + tflog.Debug(ctx, "Deleting zone "+r.recordType.String()+" record") + + // Update zone by adding the record + _, err := r.client.PatchZone(ctx, state.Zone.ValueString(), patchRequest) + + if err != nil { + resp.Diagnostics.AddError( + "Error patching zone", + "Could not delete record, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *dnsTXTRecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + importState(ctx, req, resp) +} + +func (r *dnsTXTRecordResource) createARecordCreateUpdateRequest(plan dnsTXTRecordModel) abionclient.ZoneRequest { + var data []abionclient.Record + for _, record := range plan.Records { + record := abionclient.Record{ + Data: record.TXTData.ValueString(), + TTL: utils.Int32ToIntPointer(record.TTL), + Comments: record.Comments.ValueStringPointer(), + } + data = append(data, record) + } + + return abionclient.CreateRecordPatchRequest(plan.Zone.ValueString(), plan.Name.ValueString(), r.recordType, data) +} diff --git a/internal/provider/dns_txt_record_resource_test.go b/internal/provider/dns_txt_record_resource_test.go new file mode 100644 index 0000000..0dd7d85 --- /dev/null +++ b/internal/provider/dns_txt_record_resource_test.go @@ -0,0 +1,154 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDnsTXTRecordResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing on root level + { + Config: providerConfig + ` + resource "abion_dns_txt_record" "test" { + zone = "pmapitest8.com" + name = "@" + records = [ + { + txt_data = "txt data 1" + }, + { + txt_data = "txt data 2" + ttl = "3600" + }, + { + txt_data = "txt data 3" + comments = "test comment" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "zone", "pmapitest8.com"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "name", "@"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.0.txt_data", "txt data 1"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.1.txt_data", "txt data 2"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.1.ttl", "3600"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.2.txt_data", "txt data 3"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.2.ttl"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.2.comments", "test comment"), + ), + }, + // Create and Read testing on subdomain level, move records from root to subdomain + { + Config: providerConfig + ` + resource "abion_dns_txt_record" "test" { + zone = "pmapitest8.com" + name = "test" + records = [ + { + txt_data = "txt data 1" + }, + { + txt_data = "txt data 2" + }, + { + txt_data = "txt data 3" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "zone", "pmapitest8.com"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.#", "3"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.0.txt_data", "txt data 1"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.0.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.0.comments"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.1.txt_data", "txt data 2"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.1.comments"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.2.txt_data", "txt data 3"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.2.comments"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.2.ttl"), + ), + }, + + // Update and Read testing on subdomain level, remove ip address + { + Config: providerConfig + ` + resource "abion_dns_txt_record" "test" { + zone = "pmapitest8.com" + name = "test" + records = [ + { + txt_data = "txt data 1" + }, + { + txt_data = "txt data 3" + } + ] + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "zone", "pmapitest8.com"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "name", "test"), + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.#", "2"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.0.txt_data", "txt data 1"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.0.comments"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.0.ttl"), + + resource.TestCheckResourceAttr("abion_dns_txt_record.test", "records.1.txt_data", "txt data 3"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.1.ttl"), + resource.TestCheckNoResourceAttr("abion_dns_txt_record.test", "records.1.comments"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccDnsTXTRecordNonExistingZoneResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Verify error non existing zone + { + Config: providerConfig + ` + resource "abion_dns_txt_record" "test2" { + zone = "non_existing.com" + name = "@" + records = [ + { + txt_data = "text data 1" + }, + ] + } + `, + ExpectError: regexp.MustCompile(`(?s)Error patching zone.*Zone not found`), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/internal/provider/import_helper.go b/internal/provider/import_helper.go new file mode 100644 index 0000000..5e3ea4d --- /dev/null +++ b/internal/provider/import_helper.go @@ -0,0 +1,48 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "strings" +) + +func importState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Expect the import ID to be in the format: "zone/name" + // e.g., "example.com/@", or "example.com/www" + parts := strings.Split(req.ID, "/") + + if len(parts) != 2 { + resp.Diagnostics.AddError( + "Invalid import ID format", + "Expected format `zone/name`, e.g., `example.com/@`).", + ) + return + } + + // Import the state for zone and name + zone := parts[0] + if zone == "" { + resp.Diagnostics.AddError( + "Invalid import ID format", + "`zone` is required.", + ) + return + } + + // Import the state for zone and name + name := parts[1] + if name == "" { + resp.Diagnostics.AddError( + "Invalid import ID format", + "`name` is required.", + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("zone"), zone)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go new file mode 100644 index 0000000..2ed323d --- /dev/null +++ b/internal/provider/provider.go @@ -0,0 +1,203 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "os" + abionclient "terraform-provider-abion/internal/client" +) + +// Ensure AbionDnsProvider satisfies various provider interfaces. +var _ provider.Provider = &AbionDnsProvider{} +var _ provider.ProviderWithFunctions = &AbionDnsProvider{} + +// AbionDnsProvider defines the provider implementation. +type AbionDnsProvider struct { + // version is set to the provider version on release, "dev" when the + // provider is built and ran locally, and "test" when running acceptance + // testing. + version string +} + +// AbionProviderModel describes the provider data model. +type AbionProviderModel struct { + Host types.String `tfsdk:"host"` + Apikey types.String `tfsdk:"apikey"` +} + +// Metadata returns the provider type name. +func (p *AbionDnsProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "abion" + resp.Version = p.version +} + +// Schema defines the provider-level schema for configuration data. +func (p *AbionDnsProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "host": schema.StringAttribute{ + MarkdownDescription: "The Abion API host URL. If not set, defaults to `https://api.abion.com`. " + + "This value can also be set using the `ABION_API_HOST` environment variable. " + + "The order of precedence: Terraform configuration value (highest priority) > " + + "environment variable > default value.", + Optional: true, + }, + "apikey": schema.StringAttribute{ + MarkdownDescription: "The Abion API key. Contact [Abion](https://abion.com) for help on " + + "how to create an account and an API key and whitelist IP addresses to be able to access the Abion API. " + + "This value can also be set using the `ABION_API_KEY` environment variable. " + + "The order of precedence: Terraform configuration value (highest priority) > " + + "environment variable (lowest priority). ", + Optional: true, + Sensitive: true, + }, + }, + } +} + +// Configure prepares a Abion API client for data sources and resources. +func (p *AbionDnsProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + tflog.Info(ctx, "Configuring Abion client.") + + // Retrieve provider data from configuration + var config AbionProviderModel + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if config.Host.IsUnknown() { + resp.Diagnostics.AddAttributeError( + path.Root("host"), + "Unknown Abion API Host", + "The provider cannot create the Abion API client as there is an unknown configuration value for the Abion API host. "+ + "Either target apply the source of the value first, set the value statically in the configuration, or use the ABION_API_KEY environment variable.", + ) + } + + if config.Apikey.IsUnknown() { + resp.Diagnostics.AddAttributeError( + path.Root("apikey"), + "Unknown Abion API Key", + "The provider cannot create the Abion API client as there is an unknown configuration value for the Abion API Key. "+ + "Either target apply the source of the value first, set the value statically in the configuration, or use the ABION_API_KEY environment variable.", + ) + } + + if resp.Diagnostics.HasError() { + return + } + + // Default values to environment variables, but override + // with Terraform configuration value if set. If neither set, use default abion api host + host := os.Getenv("ABION_API_HOST") + if !config.Host.IsNull() { + host = config.Host.ValueString() + } + + if host == "" { + host = "https://api.abion.com" + } + + apikey := os.Getenv("ABION_API_KEY") + + if !config.Apikey.IsNull() { + apikey = config.Apikey.ValueString() + } + + if apikey == "" { + resp.Diagnostics.AddAttributeError( + path.Root("apikey"), + "Missing Abion API Key", + "The provider cannot create the Abion API client as there is a missing or empty value for the Abion API Key. "+ + "Set the apikey value in the configuration or use the ABION_API_KEY environment variable. "+ + "If either is already set, ensure the value is not empty.", + ) + } + + if resp.Diagnostics.HasError() { + return + } + + ctx = tflog.SetField(ctx, "abion_host", host) + ctx = tflog.SetField(ctx, "abion_apikey", apikey) + ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "abion_apikey") + + tflog.Debug(ctx, "Creating Abion client") + + // Create a new Abion client using the configuration values + client, err := abionclient.NewAbionClient(host, apikey) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Abion API Client", + "An unexpected error occurred when creating the Abion API client. "+ + "If the error is not clear, please contact the provider developers.\n\n"+ + "Abion Client Error: "+err.Error(), + ) + return + } + + // Make the Abion client available during DataSource and Resource + // type Configure methods. + resp.DataSourceData = client + resp.ResourceData = client + + tflog.Info(ctx, "Configured Abion client", map[string]any{"success": true}) +} + +// Resources defines the resources implemented in the provider. +func (p *AbionDnsProvider) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + NewDnsARecordResource, + NewDnsAAAARecordResource, + NewDnsNSRecordResource, + NewDnsTXTRecordResource, + NewDnsCNameRecordResource, + NewDnsMXRecordResource, + NewDnsSRVRecordResource, + NewDnsPTRRecordResource, + } +} + +// DataSources defines the data sources implemented in the provider. +func (p *AbionDnsProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + NewDnsARecordDataSource, + NewDnsAAAARecordDataSource, + NewDnsNSRecordDataSource, + NewDnsTXTRecordDataSource, + NewDnsCNameRecordDataSource, + NewDnsMXRecordDataSource, + NewDnsSRVRecordDataSource, + NewDnsPTRRecordDataSource, + } +} + +// DataSources defines the functions implemented in the provider. +func (p *AbionDnsProvider) Functions(ctx context.Context) []func() function.Function { + //return []func() function.Function{ + // NewExampleFunction, + //} + return nil +} + +// New is a helper function to simplify provider server and testing implementation. +func New(version string) func() provider.Provider { + return func() provider.Provider { + return &AbionDnsProvider{ + version: version, + } + } +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 0000000..d937547 --- /dev/null +++ b/internal/provider/provider_test.go @@ -0,0 +1,29 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package provider + +import ( + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +const ( + // providerConfig is a shared configuration to combine with the actual + // test configuration so the abion client is properly configured. + // Use the ABION_API_KEY and (optional) ABION_API_HOST environment variables instead. + providerConfig = ` +provider "abion" { +} +` +) + +var ( + // testAccProtoV6ProviderFactories are used to instantiate a provider during + // acceptance testing. The factory function will be invoked for every Terraform + // CLI command executed to create a provider server to which the CLI can + // reattach. + testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ + "abion": providerserver.NewProtocol6WithError(New("test")()), + } +) diff --git a/internal/utils/record_types.go b/internal/utils/record_types.go new file mode 100644 index 0000000..0062ac9 --- /dev/null +++ b/internal/utils/record_types.go @@ -0,0 +1,24 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package utils + +// RecordType defines a type for DNS record types. +type RecordType string + +// Constants for DNS record types. +const ( + RecordTypeA RecordType = "A" + RecordTypeAAAA RecordType = "AAAA" + RecordTypeCName RecordType = "CNAME" + RecordTypeMX RecordType = "MX" + RecordTypeTXT RecordType = "TXT" + RecordTypeNS RecordType = "NS" + RecordTypeSRV RecordType = "SRV" + RecordTypePTR RecordType = "PTR" +) + +// String method to convert the RecordType to a string. +func (r RecordType) String() string { + return string(r) +} diff --git a/internal/utils/terraform_convert_utils.go b/internal/utils/terraform_convert_utils.go new file mode 100644 index 0000000..9ee4a3d --- /dev/null +++ b/internal/utils/terraform_convert_utils.go @@ -0,0 +1,32 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// IntPointerToInt32 convert *int to types.Int32. +func IntPointerToInt32(input *int) types.Int32 { + if input == nil { + return types.Int32Null() + } + return types.Int32Value(int32(*input)) +} + +// Int32ToIntPointer convert types.Int32 to *int. +func Int32ToIntPointer(input types.Int32) *int { + if input.IsNull() || input.IsUnknown() { + return nil + } + value := int(input.ValueInt32()) + return &value +} + +func StringPointerToTerraformString(input *string) types.String { + if input == nil { + return types.StringNull() + } + return types.StringPointerValue(input) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d833ff5 --- /dev/null +++ b/main.go @@ -0,0 +1,40 @@ +// Copyright (c) Abion AB +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "flag" + "log" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "terraform-provider-abion/internal/provider" +) + +var ( + // these will be set by the goreleaser configuration + // to appropriate values for the compiled binary. + version string = "dev" + + // goreleaser can pass other information to the main package, such as the specific commit + // https://goreleaser.com/cookbooks/using-main.version/ +) + +func main() { + var debug bool + + flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") + flag.Parse() + + opts := providerserver.ServeOpts{ + Address: "registry.terraform.io/abion/abion", + Debug: debug, + } + + err := providerserver.Serve(context.Background(), provider.New(version), opts) + + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/terraform-registry-manifest.json b/terraform-registry-manifest.json new file mode 100644 index 0000000..fec2a56 --- /dev/null +++ b/terraform-registry-manifest.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "metadata": { + "protocol_versions": ["6.0"] + } +} diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 0000000..d6b6875 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,99 @@ +module tools + +go 1.22.7 + +require ( + github.com/hashicorp/copywrite v0.19.0 + github.com/hashicorp/terraform-plugin-docs v0.20.0 +) + +require ( + github.com/AlecAivazis/survey/v2 v2.3.6 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 // indirect + github.com/cli/go-gh v1.2.1 // indirect + github.com/cli/safeexec v1.0.0 // indirect + github.com/cli/shurcooL-graphql v0.0.2 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-openapi/errors v0.20.2 // indirect + github.com/go-openapi/strfmt v0.21.3 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-github/v45 v45.2.0 // indirect + github.com/google/go-github/v53 v53.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/cli v1.1.6 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-checkpoint v0.5.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hc-install v0.9.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/terraform-exec v0.21.0 // indirect + github.com/hashicorp/terraform-json v0.23.0 // indirect + github.com/henvic/httpretty v0.0.6 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect + github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect + github.com/joho/godotenv v1.3.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/knadh/koanf v1.5.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mergestat/timediff v0.0.3 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/termenv v0.12.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/samber/lo v1.37.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/thanhpk/randstr v1.0.4 // indirect + github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect + github.com/yuin/goldmark v1.7.7 // indirect + github.com/yuin/goldmark-meta v1.1.0 // indirect + github.com/zclconf/go-cty v1.15.0 // indirect + go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect + go.mongodb.org/mongo-driver v1.10.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 0000000..8c404f4 --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,696 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= +github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= +github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= +github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 h1:yaYcGQ7yEIGbsJfW/9z7v1sLiZg/5rSNNXwmMct5XaE= +github.com/bradleyfalzon/ghinstallation/v2 v2.5.0/go.mod h1:amcvPQMrRkWNdueWOjPytGL25xQGzox7425qMgzo+Vo= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cli/go-gh v1.2.1 h1:xFrjejSsgPiwXFP6VYynKWwxLQcNJy3Twbu82ZDlR/o= +github.com/cli/go-gh v1.2.1/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/shurcooL-graphql v0.0.2 h1:rwP5/qQQ2fM0TzkUTwtt6E2LbIYf6R+39cUXTa04NYk= +github.com/cli/shurcooL-graphql v0.0.2/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= +github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= +github.com/google/go-github/v53 v53.0.0 h1:T1RyHbSnpHYnoF0ZYKiIPSgPtuJ8G6vgc0MKodXsQDQ= +github.com/google/go-github/v53 v53.0.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8= +github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4= +github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/copywrite v0.19.0 h1:f9LVxTDBfFYeQmdBpOsZ+HWknXonI8ZwubbO/RwyuCo= +github.com/hashicorp/copywrite v0.19.0/go.mod h1:6wvQH+ICDoD2bpjO1RJ6fi+h3aY5NeLEM12oTkEtFoc= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hc-install v0.9.0 h1:2dIk8LcvANwtv3QZLckxcjyF5w8KVtiMxu6G6eLhghE= +github.com/hashicorp/hc-install v0.9.0/go.mod h1:+6vOP+mf3tuGgMApVYtmsnDoKWMDcFXeTxCACYZ8SFg= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= +github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= +github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= +github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= +github.com/hashicorp/terraform-plugin-docs v0.20.0 h1:ox7rm1FN0dVZaJBUzkVVh10R1r3+FeMQWL0QopQ9d7o= +github.com/hashicorp/terraform-plugin-docs v0.20.0/go.mod h1:A/+4SVMdAkQYtIBtaxV0H7AU862TxVZk/hhKaMDQB6Y= +github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= +github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= +github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= +github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= +github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mergestat/timediff v0.0.3 h1:ucCNh4/ZrTPjFZ081PccNbhx9spymCJkFxSzgVuPU+Y= +github.com/mergestat/timediff v0.0.3/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= +github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= +github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= +github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU= +github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= +github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= +github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= +go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 0000000..088a1b6 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,19 @@ +//go:build generate + +package tools + +import ( + _ "github.com/hashicorp/copywrite" + _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" +) + +// Generate copyright headers +//go:generate go run github.com/hashicorp/copywrite headers -d .. --config ../.copywrite.hcl + +// Format Terraform code for use in documentation. +// If you do not have Terraform installed, you can remove the formatting command, but it is suggested +// to ensure the documentation is formatted properly. +//go:generate terraform fmt -recursive ../examples/ + +// Generate documentation. +//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate --provider-dir .. -provider-name abion