diff --git a/.editorconfig b/.editorconfig
index fc14c62c6d..6fa964d7e3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -109,12 +109,12 @@ indent_style = space
# Shell
# https://google.github.io/styleguide/shell.xml#Indentation
-[*.{bash,sh,zsh}]
+[*.{bash,sh,zsh,*sh.gotmpl}]
indent_size = 2
indent_style = space
# Svelte
-# https://github.com/sveltejs/svelte/blob/master/.editorconfig
+# https://github.com/sveltejs/svelte/blob/main/.editorconfig
[*.svelte]
indent_size = 2
indent_style = tab
diff --git a/.gitattributes b/.gitattributes
index e40dd0eaec..e20a6ea4b0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -29,3 +29,5 @@ go.sum text eol=lf
*.jpg binary
*.png
*.svg
+
+pkg/cloudflare-go/.changelog text eol=crlf
diff --git a/.gitbook.yaml b/.gitbook.yaml
deleted file mode 100644
index b346c76c61..0000000000
--- a/.gitbook.yaml
+++ /dev/null
@@ -1 +0,0 @@
-root: ./documentation/
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 1bd257fea2..1d62754cdb 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,6 +1,6 @@
---
name: Bug report
-about: Create a report to help us improve
+about: Create a report to help us improve.
title: ''
labels: ''
assignees: ''
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..8f3a9f3b50
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: I have a question or need support
+ url: https://docs.dnscontrol.org/
+ about: We use GitHub for tracking bugs, check our website for resources on getting help.
+ - name: I'm unsure where to go
+ url: https://groups.google.com/g/dnscontrol-discuss
+ about: If you are unsure where to go, then joining our mailing list is recommended; Just ask!
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index bbcbbe7d61..396a28d2b5 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,6 +1,6 @@
---
name: Feature request
-about: Suggest an idea for this project
+about: Suggest an idea for this project.
title: ''
labels: ''
assignees: ''
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3749f99e53..8fb2e90dc8 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -22,7 +22,6 @@ updates:
- dependency-name: "github.com/exoscale/egoscale"
- dependency-name: "github.com/ovh/go-ovh"
- dependency-name: "github.com/vultr/govultr"
- - dependency-name: "gopkg.in/ns1/ns1-go.v2"
# Maintain dependencies for Docker
- package-ecosystem: "docker"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 0789ab8253..f82f3e27fd 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,9 +1,21 @@
+## Release changelog section
+
+Help keep the release changelog clear by pre-naming the proper section in the GitHub pull request title.
+
+Some examples:
+* CICD: Add required GHA permissions for goreleaser
+* DOCS: Fixed providers with "contributor support" table
+* ROUTE53: Allow R53_ALIAS records to enable target health evaluation
+
+More examples/context can be found in the file .goreleaser.yml under the 'build' > 'changelog' key.
+!-->
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index e9429297d8..0000000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,76 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
- push:
- branches: [ "master" ]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [ "master" ]
- schedule:
- - cron: '32 19 * * 0'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'go' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Use only 'java' to analyze code written in Java, Kotlin or both
- # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
- # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
-
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # âšī¸ Command-line programs to run using the OS shell.
- # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
- # If the Autobuild fails above, remove it and uncomment the following three lines.
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
-
- # - run: |
- # echo "Run, Build Application using script"
- # ./location_of_script_within_repo/buildscript.sh
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
- with:
- category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml
new file mode 100644
index 0000000000..f6dd863b9c
--- /dev/null
+++ b/.github/workflows/pr_build.yml
@@ -0,0 +1,83 @@
+name: "PR: Run UNIT tests and Build artifacts for all platforms"
+
+# When will this pipeline be activated?
+# 1. On any pull-request, or if someone pushes to a branch called
+# "tlim_testpr".
+on:
+ pull_request:
+ workflow_dispatch:
+ # Want to trigger all the tests without making a PR?
+ # Run: git push origin main:tlim_testpr --force
+ # This will trigger a full PR test on the main branch.
+ # See https://github.com/StackExchange/dnscontrol/actions/workflows/pr_build.yml?query=branch%3Atlim_testpr
+ push:
+ branches:
+ - 'tlim_testpr'
+
+# Environment Variables
+env:
+ # cache-key: Change to force cache reset `pwsh > Get-Date -UFormat %s`
+ cache-key: 1639697695
+ # go-mod-path: Where go-mod writes temp files
+ go-mod-path: /go/pkg/mod
+ # BIND_DOMAIN: BIND is the one providers that we always test. By
+ # defining this here, we know it will always be set.
+ BIND_DOMAIN: example.com
+
+jobs:
+
+# Run unit tests:
+
+ build:
+ runs-on: ubuntu-latest
+ env:
+ TEST_RESULTS: "/tmp/test-results"
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: stable
+ - name: restore_cache
+ uses: actions/cache@v4.2.0
+ with:
+ key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
+ restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
+ path: ${{ env.go-mod-path }}
+ - run: mkdir -p "$TEST_RESULTS"
+ - name: Run unit tests
+ run: |
+ go install gotest.tools/gotestsum@latest
+ gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES
+# - name: Enforce Go Formatted Code
+# run: "[ `go fmt ./... | wc -l` -eq 0 ]"
+ - uses: actions/upload-artifact@v4.5.0
+ with:
+ name: unit-tests
+ path: ${{ env.TEST_RESULTS }}
+
+# build: Use GoReleaser to build binaries for all platforms.
+
+ # Stringer is needed because .goreleaser includes "go generate ./..."
+ - name: Install stringer
+ run: |
+ go install golang.org/x/tools/cmd/stringer@latest
+
+ -
+ id: build_binaries_tagged
+ name: Build binaries (if tagged)
+ if: github.ref_type == 'tag'
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ distribution: goreleaser
+ version: latest
+ args: build
+ -
+ id: build_binaries_not_tagged
+ name: Build binaries (not tagged)
+ if: github.ref_type != 'tag'
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ args: build --snapshot
diff --git a/.github/workflows/pr_check_git_status.yml b/.github/workflows/pr_check_git_status.yml
new file mode 100644
index 0000000000..4991fe5ccb
--- /dev/null
+++ b/.github/workflows/pr_check_git_status.yml
@@ -0,0 +1,26 @@
+name: "PR: Check git status"
+on:
+ push:
+ branches:
+ - 'tlim_testpr'
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ check-git-status:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ ref: ${{ github.event.pull_request.head.ref }}
+ - uses: actions/setup-go@v5
+ with:
+ go-version: stable
+ - run: go install golang.org/x/tools/cmd/stringer@latest
+ - run: go fmt ./...
+ - run: go mod tidy
+ - run: go generate ./...
+ - uses: CatChen/check-git-status-action@v1
+ with:
+ fail-if-not-clean: true
diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml
new file mode 100644
index 0000000000..733f731ded
--- /dev/null
+++ b/.github/workflows/pr_integration_tests.yml
@@ -0,0 +1,192 @@
+name: "PR: Run INTEGRATION tests"
+
+# When will this pipeline be activated?
+# 1. On any pull-request, or if someone pushes to a branch called
+# "tlim_testpr".
+on:
+ pull_request:
+ workflow_dispatch:
+ # Want to trigger all the tests without making a PR?
+ # Run: git push origin main:tlim_testpr --force
+ # This will trigger a full PR test on the main branch.
+ # See https://github.com/StackExchange/dnscontrol/actions/workflows/pr_integration_tests.yml?query=branch%3Atlim_testpr
+ push:
+ branches:
+ - 'tlim_testpr'
+
+# Environment Variables
+env:
+ # cache-key: Change to force cache reset `pwsh > Get-Date -UFormat %s`
+ cache-key: 1639697695
+ # go-mod-path: Where go-mod writes temp files
+ go-mod-path: /go/pkg/mod
+ # BIND_DOMAIN: BIND is the one providers that we always test. By
+ # defining this here, we know it will always be set.
+ BIND_DOMAIN: example.com
+
+jobs:
+
+# integration-test-providers: Determine which providers have a _DOMAIN variable set.
+# That variable enables testing for the provider. The results are
+# stored in a JSON blob stashed in # needs.integration-test-providers.outputs.integration_test_providers
+# where integration-tests can pick it up.
+ integration-test-providers:
+ #needs: build
+ runs-on: ubuntu-latest
+ outputs:
+ integration_test_providers: ${{ steps.get_integration_test_providers.outputs.integration_test_providers }}
+ steps:
+ - name: Set Integration Test Providers
+ id: get_integration_test_providers
+ shell: pwsh
+ run: |
+ $Providers = @()
+ $EnvContext = ConvertFrom-Json -InputObject $env:ENV_CONTEXT
+ $VarsContext = ConvertFrom-Json -InputObject $env:VARS_CONTEXT
+ $SecretsContext = ConvertFrom-Json -InputObject $env:SECRETS_CONTEXT
+ ConvertFrom-Json -InputObject $env:PROVIDERS | ForEach-Object {
+ if(($null -ne $EnvContext."$($_)_DOMAIN") -or ($null -ne $VarsContext."$($_)_DOMAIN") -or ($null -ne $SecretsContext."$($_)_DOMAIN")) {
+ $Providers += $_
+ }
+ }
+ Write-Host "Integration test providers: $Providers"
+ echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
+ env:
+ PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','HUAWEICLOUD','INWX','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']"
+ ENV_CONTEXT: ${{ toJson(env) }}
+ VARS_CONTEXT: ${{ toJson(vars) }}
+ SECRETS_CONTEXT: ${{ toJson(secrets) }}
+
+# integration-tests: Run the integration tests on any provider listed
+# in needs.integration-test-providers.outputs.integration_test_providers.
+ integration-tests:
+ if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main'
+ runs-on: ubuntu-latest
+ container:
+ image: golang:1.23
+ needs:
+ - integration-test-providers
+ env:
+ TEST_RESULTS: "/tmp/test-results"
+ GOTESTSUM_FORMAT: testname
+
+ # PROVIDER DOMAIN LIST
+ # These providers will be tested if the env variable is set.
+ # Set it to the domain name to use during the test.
+ AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }}
+ BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
+ BUNNY_DNS_DOMAIN: ${{ vars.BUNNY_DNS_DOMAIN }}
+ CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
+ CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
+ CNR_DOMAIN: ${{ vars.CNR_DOMAIN }}
+ CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
+ DIGITALOCEAN_DOMAIN: ${{ vars.DIGITALOCEAN_DOMAIN }}
+ GANDI_V5_DOMAIN: ${{ vars.GANDI_V5_DOMAIN }}
+ GCLOUD_DOMAIN: ${{ vars.GCLOUD_DOMAIN }}
+ HEDNS_DOMAIN: ${{ vars.HEDNS_DOMAIN }}
+ HEXONET_DOMAIN: ${{ vars.HEXONET_DOMAIN }}
+ HUAWEICLOUD_DOMAIN: ${{ vars.HUAWEICLOUD_DOMAIN }}
+ MYTHICBEASTS_DOMAIN: ${{ vars.MYTHICBEASTS_DOMAIN }}
+ NAMEDOTCOM_DOMAIN: ${{ vars.NAMEDOTCOM_DOMAIN }}
+ NS1_DOMAIN: ${{ vars.NS1_DOMAIN }}
+ POWERDNS_DOMAIN: ${{ vars.POWERDNS_DOMAIN }}
+ ROUTE53_DOMAIN: ${{ vars.ROUTE53_DOMAIN }}
+ SAKURACLOUD_DOMAIN: ${{ vars.SAKURACLOUD_DOMAIN }}
+ TRANSIP_DOMAIN: ${{ vars.TRANSIP_DOMAIN }}
+
+ # PROVIDER SECRET LIST
+ # The above providers have additional env variables they
+ # need for credentials and such.
+ #
+ AZURE_DNS_CLIENT_ID: ${{ secrets.AZURE_DNS_CLIENT_ID }}
+ AZURE_DNS_CLIENT_SECRET: ${{ secrets.AZURE_DNS_CLIENT_SECRET }}
+ AZURE_DNS_RESOURCE_GROUP: ${{ secrets.AZURE_DNS_RESOURCE_GROUP }}
+ AZURE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_DNS_SUBSCRIPTION_ID }}
+ AZURE_DNS_TENANT_ID: ${{ secrets.AZURE_DNS_TENANT_ID }}
+ #
+ BUNNY_DNS_API_KEY: ${{ secrets.BUNNY_DNS_API_KEY }}
+ #
+ CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
+ CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
+ #
+ CLOUDNS_AUTH_ID: ${{ secrets.CLOUDNS_AUTH_ID }}
+ CLOUDNS_AUTH_PASSWORD: ${{ secrets.CLOUDNS_AUTH_PASSWORD }}
+ #
+ CSCGLOBAL_APIKEY: ${{ secrets.CSCGLOBAL_APIKEY }}
+ CSCGLOBAL_USERTOKEN: ${{ secrets.CSCGLOBAL_USERTOKEN }}
+ #
+ CNR_UID: ${{ secrets.CNR_UID }}
+ CNR_PW: ${{ secrets.CNR_PW }}
+ CNR_ENTITY: ${{ secrets.CNR_ENTITY }}
+ #
+ DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
+ #
+ GANDI_V5_APIKEY: ${{ secrets.GANDI_V5_APIKEY }}
+ #
+ GCLOUD_EMAIL: ${{ secrets.GCLOUD_EMAIL }}
+ GCLOUD_PRIVATEKEY: ${{ secrets.GCLOUD_PRIVATEKEY }}
+ GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
+ GCLOUD_TYPE: ${{ secrets.GCLOUD_TYPE }}
+ #
+ HEDNS_PASSWORD: ${{ secrets.HEDNS_PASSWORD }}
+ HEDNS_TOTP_SECRET: ${{ secrets.HEDNS_TOTP_SECRET }}
+ HEDNS_USERNAME: ${{ secrets.HEDNS_USERNAME }}
+ #
+ HEXONET_ENTITY: ${{ secrets.HEXONET_ENTITY }}
+ HEXONET_PW: ${{ secrets.HEXONET_PW }}
+ HEXONET_UID: ${{ secrets.HEXONET_UID }}
+ #
+ HUAWEICLOUD_REGION: ${{ secrets.HUAWEICLOUD_REGION }}
+ HUAWEICLOUD_KEY_ID: ${{ secrets.HUAWEICLOUD_KEY_ID }}
+ HUAWEICLOUD_KEY: ${{ secrets.HUAWEICLOUD_KEY }}
+ #
+ MYTHICBEASTS_KEYID: ${{ secrets.MYTHICBEASTS_KEYID }}
+ MYTHICBEASTS_SECRET: ${{ secrets.MYTHICBEASTS_SECRET }}
+ #
+ NAMEDOTCOM_KEY: ${{ secrets.NAMEDOTCOM_KEY }}
+ NAMEDOTCOM_URL: ${{ secrets.NAMEDOTCOM_URL }}
+ NAMEDOTCOM_USER: ${{ secrets.NAMEDOTCOM_USER }}
+ #
+ NS1_TOKEN: ${{ secrets.NS1_TOKEN }}
+ #
+ POWERDNS_APIKEY: ${{ secrets.POWERDNS_APIKEY }}
+ POWERDNS_APIURL: ${{ secrets.POWERDNS_APIURL }}
+ POWERDNS_SERVERNAME: ${{ secrets.POWERDNS_SERVERNAME }}
+ #
+ ROUTE53_KEY: ${{ secrets.ROUTE53_KEY }}
+ ROUTE53_KEY_ID: ${{ secrets.ROUTE53_KEY_ID }}
+ #
+ SAKURACLOUD_ACCESS_TOKEN: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN }}
+ SAKURACLOUD_ACCESS_TOKEN_SECRET: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN_SECRET }}
+ #
+ TRANSIP_ACCOUNT_NAME: ${{ secrets.TRANSIP_ACCOUNT_NAME }}
+ TRANSIP_PRIVATE_KEY: ${{ secrets.TRANSIP_PRIVATE_KEY }}
+
+ concurrency:
+ group: ${{ github.workflow }}-${{ matrix.provider }}
+ strategy:
+ fail-fast: false
+ matrix:
+ provider: ${{ fromJson(needs.integration-test-providers.outputs.integration_test_providers )}}
+ steps:
+ - uses: actions/checkout@v4
+ - run: mkdir -p "$TEST_RESULTS"
+ - name: restore_cache
+ uses: actions/cache@v4.2.0
+ with:
+ key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
+ restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
+ path: ${{ env.go-mod-path }}
+ - name: Run integration tests for ${{ matrix.provider }} provider
+ run: |-
+ go install gotest.tools/gotestsum@latest
+ if [ -n "$${{ matrix.provider }}_DOMAIN" ] ; then
+ gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- -timeout 30m -v -verbose -provider ${{ matrix.provider }} -cfworkers=false
+ else
+ echo "Skip test for ${{ matrix.provider }} provider"
+ fi
+ working-directory: integrationTest
+ - uses: actions/upload-artifact@v4.5.0
+ with:
+ name: integration-tests-${{ matrix.provider }}
+ path: ${{ env.TEST_RESULTS }}
diff --git a/.github/workflows/pr_test.yml b/.github/workflows/pr_test.yml
deleted file mode 100644
index ec3601f0c4..0000000000
--- a/.github/workflows/pr_test.yml
+++ /dev/null
@@ -1,295 +0,0 @@
-name: "PR: Run all tests"
-on:
- pull_request:
- workflow_dispatch:
-
-env:
- cache-key: 1639697695 #Change to force cache reset `pwsh > Get-Date -UFormat %s`
- go-mod-path: /go/pkg/mod
-
-jobs:
- build:
- runs-on: ubuntu-latest
- container:
- image: golang:1.21
- env:
- TEST_RESULTS: "/tmp/test-results"
- steps:
- - uses: actions/checkout@v4
- - name: restore_cache
- uses: actions/cache@v3.3.2
- with:
- key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
- restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
- path: ${{ env.go-mod-path }}
- - run: mkdir -p "$TEST_RESULTS"
- - name: Run unit tests
- run: |
- go install gotest.tools/gotestsum@latest
- gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES
- - name: Enforce Go Formatted Code
- run: "[ `go fmt ./... | wc -l` -eq 0 ]"
- - uses: actions/upload-artifact@v3.1.3
- with:
- path: "/tmp/test-results"
-
-# For some reason goreleaser isn't correctly setting the version
-# string used by "dnscontrol version". Therefore, we're forcing the
-# string using the GORELEASER_CURRENT_TAG feature.
-# TODO(tlim): Use the native gorelease version mechanism.
- - name: Retrieve version
- id: version
- run: |
- echo "TAG_NAME=$(git config --global --add safe.directory /__w/dnscontrol/dnscontrol ; git describe)" >> $GITHUB_OUTPUT
- - name: Reveal version
- run: echo ${{ steps.version.outputs.TAG_NAME }}
- -
- id: build_binaries_tagged
- name: Build binaries (if tagged)
- if: github.ref_type == 'tag'
- uses: goreleaser/goreleaser-action@v5
- with:
- distribution: goreleaser
- version: latest
- args: build
- env:
- GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.TAG_NAME }}
- -
- id: build_binaries_not_tagged
- name: Build binaries (not tagged)
- if: github.ref_type != 'tag'
- uses: goreleaser/goreleaser-action@v5
- with:
- distribution: goreleaser
- version: latest
- args: build --snapshot
- env:
- GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.TAG_NAME }}
- integration-test-providers:
- needs: build
- runs-on: ubuntu-latest
- outputs:
- integration_test_providers: ${{ steps.get_integration_test_providers.outputs.integration_test_providers }}
- steps:
- - name: Set Integration Test Providers
- id: get_integration_test_providers
- shell: pwsh
- run: |
- $Providers = @()
- $EnvContext = ConvertFrom-Json -InputObject $env:ENV_CONTEXT
- $VarsContext = ConvertFrom-Json -InputObject $env:VARS_CONTEXT
- $SecretsContext = ConvertFrom-Json -InputObject $env:SECRETS_CONTEXT
- ConvertFrom-Json -InputObject $env:PROVIDERS | ForEach-Object {
- if(($null -ne $EnvContext."$($_)_DOMAIN") -or ($null -ne $VarsContext."$($_)_DOMAIN") -or ($null -ne $SecretsContext."$($_)_DOMAIN")) {
- $Providers += $_
- }
- }
- Write-Host "Integration test providers: $Providers"
- echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
- env:
- PROVIDERS: "['AZURE_DNS','BIND','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','TRANSIP']"
- ENV_CONTEXT: ${{ toJson(env) }}
- VARS_CONTEXT: ${{ toJson(vars) }}
- SECRETS_CONTEXT: ${{ toJson(secrets) }}
- integrtests-diff2:
- if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main'
- runs-on: ubuntu-latest
- container:
- image: golang:1.21
- needs:
- - integration-test-providers
- env:
- TEST_RESULTS: "/tmp/test-results"
- GOTESTSUM_FORMAT: testname
-
- # These providers will be tested if the env variable is set.
- # Set it to the domain name to use during the test.
- AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }}
- BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
- CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
- CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
- CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
- DIGITALOCEAN_DOMAIN: ${{ vars.DIGITALOCEAN_DOMAIN }}
- GANDI_V5_DOMAIN: ${{ vars.GANDI_V5_DOMAIN }}
- GCLOUD_DOMAIN: ${{ vars.GCLOUD_DOMAIN }}
- HEDNS_DOMAIN: ${{ vars.HEDNS_DOMAIN }}
- HEXONET_DOMAIN: ${{ vars.HEXONET_DOMAIN }}
- NAMEDOTCOM_DOMAIN: ${{ vars.NAMEDOTCOM_DOMAIN }}
- NS1_DOMAIN: ${{ vars.NS1_DOMAIN }}
- POWERDNS_DOMAIN: ${{ vars.POWERDNS_DOMAIN }}
- ROUTE53_DOMAIN: ${{ vars.ROUTE53_DOMAIN }}
- TRANSIP_DOMAIN: ${{ vars.TRANSIP_DOMAIN }}
-
- # The above providers have additional env variables they
- # need for credentials and such.
-
- AZURE_DNS_CLIENT_ID: ${{ secrets.AZURE_DNS_CLIENT_ID }}
- AZURE_DNS_CLIENT_SECRET: ${{ secrets.AZURE_DNS_CLIENT_SECRET }}
- AZURE_DNS_RESOURCE_GROUP: ${{ secrets.AZURE_DNS_RESOURCE_GROUP }}
- AZURE_DNS_SUBSCRIPTION_ID: ${{ secrets.AZURE_DNS_SUBSCRIPTION_ID }}
- AZURE_DNS_TENANT_ID: ${{ secrets.AZURE_DNS_TENANT_ID }}
-
- CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
- CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
-
- CLOUDNS_AUTH_ID: ${{ secrets.CLOUDNS_AUTH_ID }}
- CLOUDNS_AUTH_PASSWORD: ${{ secrets.CLOUDNS_AUTH_PASSWORD }}
-
- CSCGLOBAL_APIKEY: ${{ secrets.CSCGLOBAL_APIKEY }}
- CSCGLOBAL_USERTOKEN: ${{ secrets.CSCGLOBAL_USERTOKEN }}
-
- DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
-
- GANDI_V5_APIKEY: ${{ secrets.GANDI_V5_APIKEY }}
-
- GCLOUD_EMAIL: ${{ secrets.GCLOUD_EMAIL }}
- GCLOUD_PRIVATEKEY: ${{ secrets.GCLOUD_PRIVATEKEY }}
- GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
- GCLOUD_TYPE: ${{ secrets.GCLOUD_TYPE }}
-
- HEDNS_PASSWORD: ${{ secrets.HEDNS_PASSWORD }}
- HEDNS_TOTP_SECRET: ${{ secrets.HEDNS_TOTP_SECRET }}
- HEDNS_USERNAME: ${{ secrets.HEDNS_USERNAME }}
-
- HEXONET_ENTITY: ${{ secrets.HEXONET_ENTITY }}
- HEXONET_PW: ${{ secrets.HEXONET_PW }}
- HEXONET_UID: ${{ secrets.HEXONET_UID }}
-
- NAMEDOTCOM_KEY: ${{ secrets.NAMEDOTCOM_KEY }}
- NAMEDOTCOM_URL: ${{ secrets.NAMEDOTCOM_URL }}
- NAMEDOTCOM_USER: ${{ secrets.NAMEDOTCOM_USER }}
-
- NS1_TOKEN: ${{ secrets.NS1_TOKEN }}
-
- POWERDNS_APIKEY: ${{ secrets.POWERDNS_APIKEY }}
- POWERDNS_APIURL: ${{ secrets.POWERDNS_APIURL }}
- POWERDNS_SERVERNAME: ${{ secrets.POWERDNS_SERVERNAME }}
-
- ROUTE53_KEY: ${{ secrets.ROUTE53_KEY }}
- ROUTE53_KEY_ID: ${{ secrets.ROUTE53_KEY_ID }}
-
- TRANSIP_ACCOUNT_NAME: ${{ secrets.TRANSIP_ACCOUNT_NAME }}
- TRANSIP_PRIVATE_KEY: ${{ secrets.TRANSIP_PRIVATE_KEY }}
-
- concurrency: ${{ matrix.provider }}
- strategy:
- fail-fast: false
- matrix:
- provider: ${{ fromJson(needs.integration-test-providers.outputs.integration_test_providers )}}
- steps:
- - uses: actions/checkout@v4
- - run: mkdir -p "$TEST_RESULTS"
- - name: restore_cache
- uses: actions/cache@v3.3.2
- with:
- key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
- restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
- path: ${{ env.go-mod-path }}
- - name: Run integration tests for ${{ matrix.provider }} provider
- run: |-
- go install gotest.tools/gotestsum@latest
- if [ -n "$${{ matrix.provider }}_DOMAIN" ] ; then
- gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- -timeout 30m -v -verbose -provider ${{ matrix.provider }} -cfworkers=false
- else
- echo "Skip test for ${{ matrix.provider }} provider"
- fi
- working-directory: integrationTest
- - uses: actions/upload-artifact@v3.1.3
- with:
- path: "/tmp/test-results"
-# release:
-# if: # GitHub does not currently support regular expressions inside if conditions
-## (github.ref == 'refs/tags//v[0-9]+(\.[0-9]+)*(-.*)*/') && (github.ref != 'refs/heads//.*/')
-# runs-on: ubuntu-latest
-# container:
-# image: golang:${{ env.gover }}
-# needs:
-# - build
-# env:
-# DOCKERHUB_ACCESS_TOKEN:
-# DOCKERHUB_USERNAME:
-# steps:
-# - uses: actions/checkout@v4
-## # 'setup_remote_docker' was not transformed because there is no suitable equivalent in GitHub Actions
-# - uses: "./.github/actions/docker_check"
-# with:
-# docker-password: "${{ secrets.DOCKER_PASSWORD }}"
-# docker-username: "${{ env.DOCKER_LOGIN }}"
-# - name: restore_cache
-# uses: actions/cache@v3.3.2
-# with:
-# key: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
-# restore-keys: linux-go-${{ hashFiles('go.sum') }}-${{ env.cache-key }}
-# - name: Install goreleaser
-# run: go install github.com/goreleaser/goreleaser@latest
-# - run: goreleaser release
-# - uses: actions/upload-artifact@v3.1.3
-# with:
-# path: dist
-# - uses: actions/upload-artifact@v3.1.3
-# with:
-# path: |-
-# dist/*.rpm
-# dist/*.deb
-# upload:
-# if: # GitHub does not currently support regular expressions inside if conditions
-## (github.ref == 'refs/tags//v[0-9]+(\.[0-9]+)*(-.*)*/') && (github.ref != 'refs/heads//.*/')
-# runs-on: ubuntu-latest
-# container:
-# image: python:3.10
-# needs:
-# - release
-# env:
-# CLOUDSMITH_API_KEY:
-# CLOUDSMITH_USERNAME:
-# DOCKER_LOGIN:
-# DOCKER_PASSWORD:
-# strategy:
-# matrix:
-# arch:
-# - i386
-# - x86_64
-# - arm64
-# format:
-# distro:
-# steps:
-# - uses: actions/download-artifact@v3.0.1
-# with:
-# path: "."
-## # This item has no matching transformer
-## - cloudsmith_cloudsmith_ensure_api_key:
-## # This item has no matching transformer
-## - cloudsmith_cloudsmith_install_cli:
-## # This item has no matching transformer
-## - cloudsmith_cloudsmith_publish:
-# upload_1:
-# if: # GitHub does not currently support regular expressions inside if conditions
-## (github.ref == 'refs/tags//v[0-9]+(\.[0-9]+)*(-.*)*/') && (github.ref != 'refs/heads//.*/')
-# runs-on: ubuntu-latest
-# container:
-# image: python:3.10
-# needs:
-# - release
-# env:
-# CLOUDSMITH_API_KEY:
-# CLOUDSMITH_USERNAME:
-# DOCKER_LOGIN:
-# DOCKER_PASSWORD:
-# strategy:
-# matrix:
-# arch:
-# - i386
-# - amd64
-# - arm64
-# format:
-# distro:
-# steps:
-# - uses: actions/download-artifact@v3.0.1
-# with:
-# path: "."
-## # This item has no matching transformer
-## - cloudsmith_cloudsmith_ensure_api_key:
-## # This item has no matching transformer
-## - cloudsmith_cloudsmith_install_cli:
-## # This item has no matching transformer
-## - cloudsmith_cloudsmith_publish:
diff --git a/.github/workflows/release.yml.DISABLED b/.github/workflows/release.yml.DISABLED
deleted file mode 100644
index bb14282e33..0000000000
--- a/.github/workflows/release.yml.DISABLED
+++ /dev/null
@@ -1,103 +0,0 @@
-on:
- release:
- types: [published]
-
-name: release
-jobs:
- release:
- name: release
- runs-on: ubuntu-latest
- steps:
-
- - name: Get release
- id: get_release
- uses: bruceadams/get-release@v1.3.2
- env:
- GITHUB_TOKEN: ${{ github.token }}
-
- - name: Checkout repo
- uses: actions/checkout@v3
- with:
- fetch-depth: 0
-
- - name: Set up Go
- uses: actions/setup-go@v4
- with:
- go-version: ^1.21
-
- - name: Build binaries
- run: go run build/build.go
- env:
- CGO_ENABLED: 0
-
- - name: Get release from tag
- run: echo ::set-output name=RELEASE_VERSION::$(echo ${GITHUB_REF:11})
- id: versioner
-
- - name: Create target directory
- run: mkdir -p usr/bin
-
- - name: Copy Linux version to dnscontrol
- run: cp dnscontrol-Linux usr/bin/dnscontrol
-
- - name: Bundle RPM
- uses: bpicode/github-action-fpm@master
- with:
- fpm_args: 'usr/'
- fpm_opts: '-n dnscontrol -t rpm -s dir -v ${{ steps.versioner.outputs.RELEASE_VERSION }} --license "The MIT License (MIT)" --url "https://dnscontrol.org/" --description "DNSControl: Infrastructure as Code for DNS Zones"'
-
- - name: Bundle DEB
- uses: bpicode/github-action-fpm@master
- with:
- fpm_args: 'usr/'
- fpm_opts: '-n dnscontrol -t deb -s dir -v ${{ steps.versioner.outputs.RELEASE_VERSION }} --license "The MIT License (MIT)" --url "https://dnscontrol.org/" --description "DNSControl: Infrastructure as Code for DNS Zones"'
-
- - name: Upload dnscontrol-Darwin
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.get_release.outputs.upload_url }}
- asset_path: ./dnscontrol-Darwin
- asset_name: dnscontrol-Darwin
- asset_content_type: application/octet-stream
-
- - name: Upload dnscontrol-Linux
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.get_release.outputs.upload_url }}
- asset_path: ./dnscontrol-Linux
- asset_name: dnscontrol-Linux
- asset_content_type: application/octet-stream
-
- - name: Upload dnscontrol.exe
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.get_release.outputs.upload_url }}
- asset_path: ./dnscontrol.exe
- asset_name: dnscontrol.exe
- asset_content_type: application/octet-stream
-
- - name: Upload RPM
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.get_release.outputs.upload_url }}
- asset_path: dnscontrol-${{ steps.versioner.outputs.RELEASE_VERSION }}-1.x86_64.rpm
- asset_name: dnscontrol-${{ steps.versioner.outputs.RELEASE_VERSION }}-1.x86_64.rpm
- asset_content_type: application/x-rpm
-
- - name: Upload DEB
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.get_release.outputs.upload_url }}
- asset_path: dnscontrol_${{ steps.versioner.outputs.RELEASE_VERSION }}_amd64.deb
- asset_name: dnscontrol_${{ steps.versioner.outputs.RELEASE_VERSION }}_amd64.deb
- asset_content_type: application/vnd.debian.binary-package
diff --git a/.github/workflows/release_draft.yml b/.github/workflows/release_draft.yml
index b2bd1223f8..c9a7e94476 100644
--- a/.github/workflows/release_draft.yml
+++ b/.github/workflows/release_draft.yml
@@ -1,10 +1,11 @@
+name: "RELEASE: Make release candidate"
+
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-*
-name: "Release: Make release candidate"
jobs:
draft_release:
name: draft release
@@ -40,30 +41,23 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
- uses: actions/setup-go@v4
+ uses: actions/setup-go@v5
with:
- go-version: ^1.21
-
-# For some reason goreleaser isn't correctly setting the version
-# string used by "dnscontrol version". Therefore, we're forcing the
-# string using the GORELEASER_CURRENT_TAG feature.
-# TODO(tlim): Use the native gorelease version mechanism.
+ go-version: ^1.23
- - name: Retrieve version
- id: version
+# Stringer is needed because .goreleaser includes "go generate ./..."
+ - name: Install stringer
run: |
- echo "TAG_NAME=$(git config --global --add safe.directory /__w/dnscontrol/dnscontrol ; git describe --tags)" >> $GITHUB_OUTPUT
+ go install golang.org/x/tools/cmd/stringer@latest
- - name: Reveal version
- run: echo ${{ steps.version.outputs.TAG_NAME }}
+# use GoReleaser to build for all platforms.
-
id: release
name: Goreleaser release
- uses: goreleaser/goreleaser-action@v5
+ uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.TAG_NAME }}
diff --git a/.gitignore b/.gitignore
index 84b1e05b47..aaa4741e0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/tmp
+/commands/types/dnscontrol.d.ts
dnscontrol-Darwin
dnscontrol-Linux
dnscontrol.exe
@@ -22,8 +23,11 @@ stack.sh
.idea/
*.nupkg
.DS_Store
-.vscode/launch.json
+.vscode
.jekyll-cache
types-dnscontrol.d.ts
dist/
+
+// Test artifact:
+*.ACTUAL
diff --git a/.goreleaser.yml b/.goreleaser.yml
index ac5568685d..4cfbbcd0ba 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,3 +1,6 @@
+# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
+project_name: dnscontrol
+version: 2
builds:
-
id: build
@@ -18,10 +21,13 @@ builds:
- goos: freebsd
goarch: "386"
ldflags:
- - -linkmode=internal -s -w -X main.Version="{{ .Version }}" -X main.SHA="{{ .FullCommit }}" -X main.BuildTime={{ .Timestamp }}
+ - -linkmode=internal -s -w
+ - -X main.version={{ .Version }}
before:
hooks:
- - go mod tidy
+ - go fmt ./...
+ - go mod tidy
+ - go generate ./...
changelog:
sort: asc
use: github
@@ -33,13 +39,22 @@ changelog:
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
order: 1
- title: 'Provider-specific changes:'
- regexp: "(?i)((akamaiedge|autodns|axfrd|azure|bind|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*"
+ regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cnr|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|huaweicloud|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|vultr).*:)+.*"
order: 2
+ - title: 'Documentation:'
+ regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"
+ order: 3
+ - title: 'CI/CD:'
+ regexp: "(?i)^.*(build|ci|cicd)[(\\w)]*:+.*$"
+ order: 4
+ - title: 'Dependencies:'
+ regexp: "(?i)^.*Build\\(deps\\)*:+.*$|(?i)^.*update deps+.*$"
+ order: 5
+ - title: 'Other changes and improvements:'
+ order: 9
- title: 'Deprecation warnings:'
regexp: "(?i)^.*Deprecate[(\\w)]*:+.*$"
order: 10
- - title: 'Other changes and improvements:'
- order: 9
filters:
exclude:
- '^test:'
@@ -135,9 +150,57 @@ docker_manifests:
checksum:
name_template: 'checksums.txt'
snapshot:
- name_template: "{{ incpatch .Version }}-next"
+ version_template: "{{ incpatch .Version }}-next"
release:
draft: true
prerelease: auto
mode: append
+ footer: |
+ ## Deprecation warnings
+
+ > [!WARNING]
+ > - **REV() will switch from RFC2317 to RFC4183 in v5.0.** This is a breaking change. Warnings are output if your configuration is affected. No date has been announced for v5.0. See https://docs.dnscontrol.org/language-reference/top-level-functions/revcompat
+ > - **MSDNS maintainer needed!** Without a new volunteer, this DNS provider will lose support after April 2025. See https://github.com/StackExchange/dnscontrol/issues/2878
+ > - **NAMEDOTCOM and SOFTLAYER need maintainers!** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes.
+ > - **get-certs/ACME support is frozen and will be removed without notice between now and July 2025.** It has been unsupported since December 2022. If you don't use this feature, do not start. If you do use this feature, migrate ASAP. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400)
+
+ ## Install
+
+ #### macOS and Linux
+
+ ##### Install with [Homebrew](https://brew.sh) (recommended)
+
+ ```shell
+ brew install dnscontrol
+ ```
+
+ ##### Using with [Docker](https://www.docker.com)
+
+ You can use the Docker image from [Docker hub](https://hub.docker.com/r/stackexchange/dnscontrol/) or [GitHub Container Registry](https://github.com/stackexchange/dnscontrol/pkgs/container/dnscontrol).
+
+ ```shell
+ docker run --rm -it -v "$(pwd):/dns" ghcr.io/stackexchange/dnscontrol preview
+ ```
+
+ #### Anywhere else
+
+ Alternatively, you can install the latest binary (or the apt/rpm/deb/archlinux package) from this page.
+
+ Or, if you have Go installed, you can install the latest version of DNSControl with the following command:
+
+ ```shell
+ go install github.com/StackExchange/dnscontrol/v4@main
+ ```
+
+ ## Update
+
+ Update to the latest version depends on how you choose to install `dnscontrol` on your machine.
+
+ #### Update with [Homebrew](https://brew.sh)
+
+ ```shell
+ brew upgrade dnscontrol
+ ```
+
+ Alternatively, you can grab the latest binary (or the apt/rpm/deb package) from this page.
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ec3b1fec7c..0000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: go
-
-go:
- - 1.14.x
-
-install: pwd
-
-script:
- - go run -mod=readonly build/validate/validate.go
- - go test -mod=readonly ./...
-
-notifications:
- email:
- on_success: never # default: change
- on_failure: always # default: always
- webhooks:
- urls:
- - https://webhooks.gitter.im/e/4f27a4a85d6f4475be19
- on_success: always
- on_failure: always
- on_start: always
diff --git a/Dockerfile b/Dockerfile
index f392fc6ff1..da55c2b356 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
-FROM alpine:3.18.4@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 as RUN
+FROM alpine:3.20.3@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a as RUN
# Add runtime dependencies
# - tzdata: Go time required external dependency eg: TRANSIP and possibly others
diff --git a/OWNERS b/OWNERS
index b1b524718e..ff50183b7b 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,10 +1,13 @@
-providers/akamaiedgedns @svernick
+providers/akamaiedgedns @edglynes
providers/autodns @arnoschoon
providers/axfrddns @hnrgrgr
providers/azuredns @vatsalyagoel
+providers/azureprivatedns @matthewmgamble
providers/bind @tlimoncelli
+providers/bunnydns @ppmathis
providers/cloudflare @tresni
providers/cloudns @pragmaton
+providers/cnr @KaiSchwarz-cnic
providers/cscglobal @mikenz
providers/desec @D3luxee
providers/digitalocean @Deraen
@@ -12,6 +15,7 @@ providers/dnsimple @onlyhavecans
providers/dnsmadeeasy @vojtad
providers/doh @mikenz
providers/domainnameshop @SimenBai
+providers/dynadot @e-im
providers/easyname @tresni
providers/exoscale @pierre-emmanuelJ
providers/gandiv5 @TomOnTime
@@ -21,6 +25,7 @@ providers/hedns @rblenkinsopp
providers/hetzner @das7pad
providers/hexonet @KaiSchwarz-cnic
providers/hostingde @juliusrickert
+providers/huaweicloud @huihuimoe
providers/internetbs @pragmaton
providers/inwx @patschi
providers/linode @koesie10
@@ -39,8 +44,10 @@ providers/ovh @masterzen
providers/packetframe @hamptonmoore
providers/porkbun @imlonghao
providers/powerdns @jpbede
+providers/realtimeregister @PJEilers
providers/route53 @tresni
providers/rwth @mistererwin
+providers/sakuracloud @ttkzw
# providers/softlayer NEEDS VOLUNTEER
providers/transip @blackshadev
providers/vultr @pgaskin
diff --git a/README.md b/README.md
index 1b658f66b0..93427631ac 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# DNSControl
-[](https://github.com/StackExchange/dnscontrol/actions/workflows/build.yml)
-[](https://gitter.im/dnscontrol/Lobby)
+[](https://github.com/StackExchange/dnscontrol/actions/workflows/pr_test.yml)
[](https://groups.google.com/forum/#!forum/dnscontrol-discuss)
[](https://pkg.go.dev/github.com/StackExchange/dnscontrol/v4)
@@ -21,9 +20,12 @@ Currently supported DNS providers:
- AWS Route 53
- AXFR+DDNS
- Azure DNS
+- Azure Private DNS
- BIND
+- Bunny DNS
- Cloudflare
- ClouDNS
+- CentralNic Reseller (CNR) - formerly RRPProxy
- deSEC
- DigitalOcean
- DNS Made Easy
@@ -36,6 +38,7 @@ Currently supported DNS providers:
- Hetzner
- HEXONET
- hosting.de
+- Huawei Cloud DNS
- Hurricane Electric DNS
- INWX
- Linode
@@ -53,7 +56,9 @@ Currently supported DNS providers:
- Packetframe
- Porkbun
- PowerDNS
+- Realtime Register
- RWTH DNS-Admin
+- Sakura Cloud
- SoftLayer
- TransIP
- Vultr
@@ -62,7 +67,9 @@ Currently supported Domain Registrars:
- AWS Route 53
- CSC Global
+- CentralNic Reseller (formerly RRPProxy)
- DNSOVERHTTPS
+- Dynadot
- easyname
- Gandi
- HEXONET
@@ -73,6 +80,7 @@ Currently supported Domain Registrars:
- Name.com
- OpenSRS
- OVH
+- Realtime Register
At Stack Overflow, we use this system to manage hundreds of domains
and subdomains across multiple registrars and DNS providers.
@@ -153,15 +161,19 @@ DNSControl can be installed via packages for macOS, Linux and Windows, or from s
See [dnscontrol-action](https://github.com/koenrh/dnscontrol-action) or [gacts/install-dnscontrol](https://github.com/gacts/install-dnscontrol).
-## Deprecation warnings (updated 2023-02-18)
+## Deprecation warnings (updated 2024-03-25)
-- **32-bit binaries will no longer be distributed after September 10, 2023.** There is a proposal to stop shipping 32-bit binaries (packages and containers). If no objections are raised by Sept 10, 2023, new releases will not include them. See https://github.com/StackExchange/dnscontrol/issues/2461 for details.
-- **Call for new volunteer maintainers for NAMEDOTCOM and SOFTLAYER.** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes.
-- **ACME/Let's Encrypt support is frozen and will be removed after December 31, 2022.** The `get-certs` command (renews certs via Let's Encrypt) has no maintainer. There are other projects that do a better job. If you don't use this feature, please do not start. If you do use this feature, please plan on migrating to something else. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400)
-- **get-zones syntax changes in v3.16** Starting in [v3.16](documentation/v316.md), the command line arguments for `dnscontrol get-zones` changes. For backwards compatibility change `provider` to `-`. See documentation for details.
+- **REV() will switch from RFC2317 to RFC4183 in v5.0.** This is a breaking change. Warnings are output if your configuration is affected. No date has been announced for v5.0. See https://docs.dnscontrol.org/language-reference/top-level-functions/revcompat
+- **MSDNS maintainer needed!** Without a new volunteer, this DNS provider will lose support after April 2025. See https://github.com/StackExchange/dnscontrol/issues/2878
+- **NAMEDOTCOM and SOFTLAYER need maintainers!** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes.
+- **get-certs/ACME support is frozen and will be removed without notice between now and July 2025.** It has been unsupported since December 2022. If you don't use this feature, do not start. If you do use this feature, migrate ASAP. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400)
## More info at our website
The website: [https://docs.dnscontrol.org/](https://docs.dnscontrol.org/)
The getting started guide: [https://docs.dnscontrol.org/getting-started/getting-started](https://docs.dnscontrol.org/getting-started/getting-started)
+
+## Stargazers over time
+
+[](https://starchart.cc/StackExchange/dnscontrol)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 9ca9132c8b..0000000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-trigger:
- batch: "true"
- branches:
- include:
- - master
-
-jobs:
-
-- job: Compile
- strategy:
- maxParallel: 3
- matrix:
- Windows:
- OS: windows
- OSX:
- OS: darwin
- Linux:
- OS: linux
- steps:
- - template: build/azure-pipelines/go-env.yaml
- - script: "go run -mod=readonly build/build.go -os $(OS)"
-
-- job: "unittests"
- displayName: "Run Unit Tests"
- steps:
- - template: build/azure-pipelines/go-env.yaml
- - script: "go test -mod=readonly ./..."
-
-- job: "modtidy"
- displayName: "Check Go Modules"
- steps:
- - template: build/azure-pipelines/go-env.yaml
- - script: |
- set -e
- go mod tidy
- git status --porcelain
- git diff
- [ ! -n "$(git status --porcelain go.mod go.sum)" ] || { echo "Error: go.mod/go.sum outdated, please run go mod tidy."; false; }
-
-- job: "modvendor"
- displayName: "Check Go Vendor"
- steps:
- - template: build/azure-pipelines/go-env.yaml
- - script: |
- set -e
- go mod vendor
- git status --porcelain
- [ ! -n "$(git status --porcelain vendor)" ] || { echo "Error: Vendor does not match go.mod/go.sum, please run go mod vendor."; false; }
-
-- job: "GoFmt"
- displayName: "Check Go Formatting"
- steps:
- - template: build/azure-pipelines/go-env.yaml
- - script: |
- set -e
- go fmt ./...
- git status --porcelain
- git diff
- [ ! -n "$(git status --porcelain)" ] || { echo "Error: Go files not formatted, please run go fmt ./... ."; false; }
-
-- job: "GoGen"
- displayName: "Check Go Generate"
- steps:
- - template: build/azure-pipelines/go-env.yaml
- - script: |
- set -e
- go generate .
- git status --porcelain
- git diff
- [ ! -n "$(git status --porcelain)" ] || { echo "Error: Generated files not up to date, please run go generate . ."; false; }
diff --git a/build/azure-pipelines/choco.yaml b/build/azure-pipelines/choco.yaml
deleted file mode 100644
index aa912913de..0000000000
--- a/build/azure-pipelines/choco.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Starter pipeline
-# Start with a minimal pipeline that you can customize to build and deploy your code.
-# Add steps that build, run tests, deploy, and more:
-# https://aka.ms/yaml
-
-trigger:
-- master
-
-pool:
- vmImage: 'ubuntu-latest'
-
-steps:
-- script: echo Hello, world!
- displayName: 'Run a one-line script'
-
-- script: |
- echo Add other tasks to build, test, and deploy your project.
- echo See https://aka.ms/yaml
- displayName: 'Run a multi-line script'
diff --git a/build/azure-pipelines/go-env.yaml b/build/azure-pipelines/go-env.yaml
deleted file mode 100644
index 9d2f56c441..0000000000
--- a/build/azure-pipelines/go-env.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-# shared step for setting up go env
-# see https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/go?view=azure-devops#build-a-container-image
-steps:
-- task: GoTool@0
- inputs:
- version: '1.16'
diff --git a/build/azure-pipelines/integration.yml b/build/azure-pipelines/integration.yml
deleted file mode 100644
index c75efe39c9..0000000000
--- a/build/azure-pipelines/integration.yml
+++ /dev/null
@@ -1,121 +0,0 @@
-variables:
- wd: '$(System.DefaultWorkingDirectory)/integrationTest'
-
-trigger:
- batch: "true"
- branches:
- include:
- - pipeline
-
-# Each provider gets its' own job. These will run in parallel.
-# each job gets setup with only the env vars it needs for that run.
-# these are defined in azure pipelines web ui as secret variables.
-
-jobs:
-
-- job: Route53
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider ROUTE53
- workingDirectory: $(wd)
- env:
- R53_DOMAIN: $(R53_DOMAIN)
- R53_KEY_ID: $(R53_KEY_ID)
- R53_KEY: $(R53_KEY)
-
-- job: GCloud
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider GCLOUD
- workingDirectory: $(wd)
- env:
- GCLOUD_DOMAIN: $(GCLOUD_DOMAIN)
- GCLOUD_TYPE: $(GCLOUD_TYPE)
- GCLOUD_EMAIL: $(GCLOUD_EMAIL)
- GCLOUD_PROJECT: $(GCLOUD_PROJECT)
- GCLOUD_PRIVATEKEY: $(GCLOUD_PRIVATEKEY)
-
-- job: NameDotCom
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider NAMEDOTCOM
- workingDirectory: $(wd)
- env:
- NAMEDOTCOM_DOMAIN: $(NAMEDOTCOM_DOMAIN)
- NAMEDOTCOM_KEY: $(NAMEDOTCOM_KEY)
- NAMEDOTCOM_USER: $(NAMEDOTCOM_USER)
-
-- job: Cloudflare
- steps:
- - template: go-env.yaml
- - script: go test -v -timeout 30m -verbose -provider CLOUDFLAREAPI
- workingDirectory: $(wd)
- env:
- CF_TOKEN: $(CF_TOKEN)
-
-- job: DigitalOcean
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider DIGITALOCEAN
- workingDirectory: $(wd)
- env:
- DO_DOMAIN: $(DO_DOMAIN)
- DO_TOKEN: $(DO_TOKEN)
-
-- job: GandiV5
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider GANDI_V5
- workingDirectory: $(wd)
- env:
- GANDI_KEY: $(GANDI_V5_APIKEY)
- GANDI_DOMAIN: $(GANDI_V5_DOMAIN)
-
-# - job: GandiLive
-# steps:
-# - template: go-env.yaml
-# - script: go test -v -verbose -provider GANDI-LIVEDNS
-# workingDirectory: $(wd)
-# env:
-# GANDILIVE_KEY: $(GANDILIVE_KEY)
-# GANDILIVE_DOMAIN: $(GANDILIVE_DOMAIN)
-
-# - job: NS1
-# steps:
-# - template: go-env.yaml
-# - script: go test -v -verbose -provider NS1
-# workingDirectory: $(wd)
-# env:
-# NS1_TOKEN: $(NS1_TOKEN)
-# NS1_DOMAIN: $(NS1_DOMAIN)
-
-- job: DNSIMPLE
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider DNSIMPLE
- workingDirectory: $(wd)
- env:
- DNSIMPLE_TOKEN: $(DNSIMPLE_TOKEN)
- DNSIMPLE_DOMAIN: $(DNSIMPLE_DOMAIN)
-
-- job: Vultr
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider VULTR
- workingDirectory: $(wd)
- env:
- VULTR_DOMAIN: $(VULTR_DOMAIN)
- VULTR_TOKEN: $(VULTR_TOKEN)
-
-- job: Azure
- steps:
- - template: go-env.yaml
- - script: go test -v -verbose -provider AZURE_DNS
- workingDirectory: $(wd)
- env:
- AZURE_CLIENT_ID: $(AZ_CLIENTID)
- AZURE_CLIENT_SECRET: $(AZ_CLIENTSECRET)
- AZURE_DOMAIN: $(AZ_DOMAIN)
- AZURE_RESOURCE_GROUP: $(AZ_RSGNAME)
- AZURE_SUBSCRIPTION_ID: $(AZ_SUBSCRIPTIONID)
- AZURE_TENANT_ID: $(AZ_TENANTID)
diff --git a/build/build.go b/build/build.go
index 736618f3c3..6a5af6a10b 100644
--- a/build/build.go
+++ b/build/build.go
@@ -7,7 +7,6 @@ import (
"os"
"os/exec"
"strings"
- "time"
)
var sha = flag.String("sha", "", "SHA of current commit")
@@ -16,7 +15,7 @@ var goos = flag.String("os", "", "OS to build (linux, windows, or darwin) Defaul
func main() {
flag.Parse()
- flags := fmt.Sprintf(`-s -w -X "main.SHA=%s" -X main.BuildTime=%d`, getVersion(), time.Now().Unix())
+ flags := fmt.Sprintf(`-s -w -X "main.version=%s"`, getVersion())
pkg := "github.com/StackExchange/dnscontrol/v4"
build := func(out, goos string) {
diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go
index e204728e04..b786de97f5 100644
--- a/build/generate/featureMatrix.go
+++ b/build/generate/featureMatrix.go
@@ -38,7 +38,7 @@ func markdownTable(matrix *FeatureMatrix) (string, error) {
featureMap := matrix.Providers[providerName]
var tableDataRow []string
- tableDataRow = append(tableDataRow, "[`"+providerName+"`](providers/"+strings.ToLower(providerName)+".md)")
+ tableDataRow = append(tableDataRow, "[`"+providerName+"`](provider/"+strings.ToLower(providerName)+".md)")
for _, featureName := range matrix.Features {
tableDataRow = append(tableDataRow, featureEmoji(featureMap, featureName))
}
@@ -76,21 +76,25 @@ func matrixData() *FeatureMatrix {
OfficialSupport = "Official Support" // vs. community supported
ProviderDNSProvider = "DNS Provider"
ProviderRegistrar = "Registrar"
- DomainModifierAlias = "[`ALIAS`](functions/domain/ALIAS.md)"
- DomainModifierCaa = "[`CAA`](functions/domain/CAA.md)"
- DomainModifierDnssec = "[`AUTODNSSEC`](functions/domain/AUTODNSSEC_ON.md)"
- DomainModifierLoc = "[`LOC`](functions/domain/LOC.md)"
- DomainModifierNaptr = "[`NAPTR`](functions/domain/NAPTR.md)"
- DomainModifierPtr = "[`PTR`](functions/domain/PTR.md)"
- DomainModifierSoa = "[`SOA`](functions/domain/SOA.md)"
- DomainModifierSrv = "[`SRV`](functions/domain/SRV.md)"
- DomainModifierSshfp = "[`SSHFP`](functions/domain/SSHFP.md)"
- DomainModifierTlsa = "[`TLSA`](functions/domain/TLSA.md)"
- DomainModifierDs = "[`DS`](functions/domain/DS.md)"
- DomainModifierDhcid = "[`DHCID`](functions/domain/DHCID.md)"
+ ProviderThreadSafe = "Concurrency Verified"
+ DomainModifierAlias = "[`ALIAS`](language-reference/domain-modifiers/ALIAS.md)"
+ DomainModifierCaa = "[`CAA`](language-reference/domain-modifiers/CAA.md)"
+ DomainModifierDnssec = "[`AUTODNSSEC`](language-reference/domain-modifiers/AUTODNSSEC_ON.md)"
+ DomainModifierHTTPS = "[`HTTPS`](language-reference/domain-modifiers/HTTPS.md)"
+ DomainModifierLoc = "[`LOC`](language-reference/domain-modifiers/LOC.md)"
+ DomainModifierNaptr = "[`NAPTR`](language-reference/domain-modifiers/NAPTR.md)"
+ DomainModifierPtr = "[`PTR`](language-reference/domain-modifiers/PTR.md)"
+ DomainModifierSoa = "[`SOA`](language-reference/domain-modifiers/SOA.md)"
+ DomainModifierSrv = "[`SRV`](language-reference/domain-modifiers/SRV.md)"
+ DomainModifierSshfp = "[`SSHFP`](language-reference/domain-modifiers/SSHFP.md)"
+ DomainModifierSvcb = "[`SVCB`](language-reference/domain-modifiers/SVCB.md)"
+ DomainModifierTlsa = "[`TLSA`](language-reference/domain-modifiers/TLSA.md)"
+ DomainModifierDs = "[`DS`](language-reference/domain-modifiers/DS.md)"
+ DomainModifierDhcid = "[`DHCID`](language-reference/domain-modifiers/DHCID.md)"
+ DomainModifierDname = "[`DNAME`](language-reference/domain-modifiers/DNAME.md)"
+ DomainModifierDnskey = "[`DNSKEY`](language-reference/domain-modifiers/DNSKEY.md)"
DualHost = "dual host"
CreateDomains = "create-domains"
- NoPurge = "[`NO_PURGE`](functions/domain/NO_PURGE.md)"
GetZones = "get-zones"
)
@@ -100,21 +104,26 @@ func matrixData() *FeatureMatrix {
OfficialSupport,
ProviderDNSProvider,
ProviderRegistrar,
+ ProviderThreadSafe,
DomainModifierAlias,
DomainModifierCaa,
DomainModifierDnssec,
+ DomainModifierHTTPS,
DomainModifierLoc,
DomainModifierNaptr,
DomainModifierPtr,
DomainModifierSoa,
DomainModifierSrv,
DomainModifierSshfp,
+ DomainModifierSvcb,
DomainModifierTlsa,
DomainModifierDs,
DomainModifierDhcid,
+ DomainModifierDname,
+ DomainModifierDnskey,
DualHost,
CreateDomains,
- NoPurge,
+ //NoPurge,
GetZones,
},
}
@@ -170,6 +179,10 @@ func matrixData() *FeatureMatrix {
false,
func() bool { return providers.RegistrarTypes[providerName] != nil },
)
+ setCapability(
+ ProviderThreadSafe,
+ providers.CanConcur,
+ )
setCapability(
DomainModifierAlias,
providers.CanUseAlias,
@@ -186,10 +199,22 @@ func matrixData() *FeatureMatrix {
DomainModifierDhcid,
providers.CanUseDHCID,
)
+ setCapability(
+ DomainModifierDname,
+ providers.CanUseDNAME,
+ )
setCapability(
DomainModifierDs,
providers.CanUseDS,
)
+ setCapability(
+ DomainModifierDnskey,
+ providers.CanUseDNSKEY,
+ )
+ setCapability(
+ DomainModifierHTTPS,
+ providers.CanUseHTTPS,
+ )
setCapability(
DomainModifierLoc,
providers.CanUseLOC,
@@ -214,6 +239,10 @@ func matrixData() *FeatureMatrix {
DomainModifierSshfp,
providers.CanUseSSHFP,
)
+ setCapability(
+ DomainModifierSvcb,
+ providers.CanUseSVCB,
+ )
setCapability(
DomainModifierTlsa,
providers.CanUseTLSA,
@@ -233,17 +262,17 @@ func matrixData() *FeatureMatrix {
false,
)
- // no purge is a freaky double negative
- cantUseNOPURGE := providers.CantUseNOPURGE
- if providerNotes[cantUseNOPURGE] != nil {
- featureMap[NoPurge] = providerNotes[cantUseNOPURGE]
- } else {
- featureMap.SetSimple(
- NoPurge,
- false,
- func() bool { return !providers.ProviderHasCapability(providerName, cantUseNOPURGE) },
- )
- }
+ // // no purge is a freaky double negative
+ // cantUseNOPURGE := providers.CantUseNOPURGE
+ // if providerNotes[cantUseNOPURGE] != nil {
+ // featureMap[NoPurge] = providerNotes[cantUseNOPURGE]
+ // } else {
+ // featureMap.SetSimple(
+ // NoPurge,
+ // false,
+ // func() bool { return !providers.ProviderHasCapability(providerName, cantUseNOPURGE) },
+ // )
+ // }
matrix.Providers[providerName] = featureMap
}
return matrix
diff --git a/build/generate/functionTypes.go b/build/generate/functionTypes.go
index 62b2d68a8b..65194f2fae 100644
--- a/build/generate/functionTypes.go
+++ b/build/generate/functionTypes.go
@@ -78,15 +78,15 @@ func parseFrontMatter(content string) (map[string]interface{}, string, error) {
}
var returnTypes = map[string]string{
- "domain": "DomainModifier",
- "global": "void",
- "record": "RecordModifier",
+ "domain-modifiers": "DomainModifier",
+ "top-level-functions": "void",
+ "record-modifiers": "RecordModifier",
}
var categories = map[string]string{
- "domain": "domain-modifiers",
- "global": "top-level-functions",
- "record": "record-modifiers",
+ "domain-modifiers": "domain-modifiers",
+ "top-level-functions": "top-level-functions",
+ "record-modifiers": "record-modifiers",
}
var providerNames = map[string]string{
@@ -101,7 +101,7 @@ var providerNames = map[string]string{
func generateFunctionTypes() (string, error) {
funcs := []Function{}
- srcRoot := join("documentation", "functions")
+ srcRoot := join("documentation", "language-reference")
types, err := os.ReadDir(srcRoot)
if err != nil {
return "", err
diff --git a/build/generate/generate.go b/build/generate/generate.go
index 4753aed486..ec505c3e9b 100644
--- a/build/generate/generate.go
+++ b/build/generate/generate.go
@@ -6,6 +6,9 @@ func main() {
if err := generateFeatureMatrix(); err != nil {
log.Fatal(err)
}
+ if err := generateOwnersFile(); err != nil {
+ log.Fatal(err)
+ }
funcs, err := generateFunctionTypes()
if err != nil {
log.Fatal(err)
diff --git a/build/generate/ownersFile.go b/build/generate/ownersFile.go
new file mode 100644
index 0000000000..b9c9009ea0
--- /dev/null
+++ b/build/generate/ownersFile.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+ "github.com/StackExchange/dnscontrol/v4/providers"
+ "os"
+ "sort"
+ "strings"
+)
+
+func generateOwnersFile() error {
+ maintainers := providers.ProviderMaintainers
+ sortedProviderNames := getSortedProviderNames(maintainers)
+
+ var ownersData strings.Builder
+ for _, providerName := range sortedProviderNames {
+ providerMaintainer := maintainers[providerName]
+ if providerMaintainer == "NEEDS VOLUNTEER" {
+ ownersData.WriteString("# ")
+ }
+ ownersData.WriteString("providers/")
+ ownersData.WriteString(getProviderDirectory(providerName))
+ ownersData.WriteString(" ")
+ ownersData.WriteString(providerMaintainer)
+ ownersData.WriteString("\n")
+ }
+
+ file, err := os.Create("OWNERS")
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ _, err = file.WriteString(ownersData.String())
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func getProviderDirectory(providerName string) string {
+ // Strip the underscores from the providerName constants
+ providerDirectory := strings.ToLower(
+ strings.ReplaceAll(
+ providerName, "_", "",
+ ),
+ )
+
+ // These providers use a different directory name
+ if providerDirectory == "cloudflareapi" {
+ providerDirectory = "cloudflare"
+ }
+ if providerDirectory == "dnsoverhttps" {
+ providerDirectory = "doh"
+ }
+
+ return providerDirectory
+}
+
+func getSortedProviderNames(maintainers map[string]string) []string {
+ providerNameSorted := make([]string, 0, len(maintainers))
+ for providerNameKey := range maintainers {
+ providerNameSorted = append(providerNameSorted, providerNameKey)
+ }
+ sort.Strings(providerNameSorted)
+
+ return providerNameSorted
+}
diff --git a/build/validate/validate.go b/build/validate/validate.go
deleted file mode 100644
index de90ef68ac..0000000000
--- a/build/validate/validate.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package main
-
-import (
- "context"
- "crypto/aes"
- "crypto/cipher"
- "encoding/base64"
- "fmt"
- "os"
- "os/exec"
- "strings"
-
- "github.com/google/go-github/v35/github"
- "golang.org/x/oauth2"
-)
-
-func main() {
- failed := false
-
- run := func(ctx string, preStatus string, goodStatus string, f func() error) {
- setStatus(stPending, preStatus, ctx)
- if err := f(); err != nil {
- fmt.Println(err)
- setStatus(stError, err.Error(), ctx)
- failed = true
- } else {
- setStatus(stSuccess, goodStatus, ctx)
- }
- }
-
- run("gofmt", "Checking gofmt", "gofmt ok", checkGoFmt)
- run("gogen", "Checking go generate", "go generate ok", checkGoGenerate)
- if failed {
- os.Exit(1)
- }
-}
-
-func checkGoFmt() error {
- cmd := exec.Command("gofmt", "-s", "-l", ".")
- out, err := cmd.CombinedOutput()
- if err != nil {
- return err
- }
- if len(out) == 0 {
- return nil
- }
- files := strings.Split(string(out), "\n")
- fList := ""
- for _, f := range files {
- if strings.HasPrefix(f, "vendor") {
- continue
- }
- if fList != "" {
- fList += "\n"
- }
- fList += f
- }
- if fList == "" {
- return nil
- }
- return fmt.Errorf("the following files need to have gofmt run on them:\n%s", fList)
-}
-
-func checkGoGenerate() error {
- cmd := exec.Command("go", "generate")
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
- if err != nil {
- return err
- }
- modified, err := getModifiedFiles()
- if err != nil {
- return err
- }
- if len(modified) != 0 {
- return fmt.Errorf("ERROR: The following files are modified after go generate:\n%s", strings.Join(modified, "\n"))
- }
- return nil
-}
-
-func getModifiedFiles() ([]string, error) {
- cmd := exec.Command("git", strings.Split("diff --name-only", " ")...)
- out, err := cmd.CombinedOutput()
- if err != nil {
- return nil, err
- }
- if len(out) == 0 {
- return nil, nil
- }
- return strings.Split(string(out), "\n"), nil
-}
-
-const (
- stPending = "pending"
- stSuccess = "success"
- stError = "error"
-)
-
-func setStatus(status string, desc string, ctx string) {
- if commitish == "" || ctx == "" {
- return
- }
- client.Repositories.CreateStatus(context.Background(), "StackExchange", "dnscontrol", commitish, &github.RepoStatus{
- Context: &ctx,
- Description: &desc,
- State: &status,
- })
-}
-
-var client *github.Client
-var commitish string
-
-func init() {
- // not intended for security, just minimal obfuscation.
- key, _ := base64.StdEncoding.DecodeString("qIOy76aRcXcxm3vb82tvZqW6JoYnpncgVKx7qej1y+4=")
- iv, _ := base64.StdEncoding.DecodeString("okRtW8z6Mx04Y9yMk1cb5w==")
- garb, _ := base64.StdEncoding.DecodeString("ut8AtS6re1g7m/onk0ciIq7OxNOdZ/tsQ5ay6OfxKcARnBGY0bQ+pA==")
- c, _ := aes.NewCipher(key)
- d := cipher.NewCFBDecrypter(c, iv)
- t := make([]byte, len(garb))
- d.XORKeyStream(t, garb)
- hc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: string(t)}))
- client = github.NewClient(hc)
-
- // get current version if in travis build
- if tc := os.Getenv("TRAVIS_COMMIT"); tc != "" {
- commitish = tc
- }
-}
diff --git a/commands/commands.go b/commands/commands.go
index 0155763b25..23e46d0a7d 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -25,11 +25,6 @@ const (
var commands = []*cli.Command{}
-// These are set by/for goreleaser
-var (
- version = "dev"
-)
-
func cmd(cat string, c *cli.Command) bool {
c.Category = cat
commands = append(commands, c)
@@ -52,11 +47,12 @@ func Run(v string) int {
app.Version = version
app.Name = "dnscontrol"
app.HideVersion = true
- app.Usage = "dnscontrol is a compiler and DSL for managing dns zones"
+ app.Usage = "DNSControl is a compiler and DSL for managing dns zones"
app.Flags = []cli.Flag{
&cli.BoolFlag{
- Name: "v",
- Usage: "Enable detailed logging",
+ Name: "debug",
+ Aliases: []string{"v"},
+ Usage: "Enable debug logging",
Destination: &printer.DefaultPrinter.Verbose,
},
&cli.BoolFlag{
@@ -88,6 +84,24 @@ func Run(v string) int {
sort.Sort(cli.CommandsByName(commands))
app.Commands = commands
app.EnableBashCompletion = true
+ app.BashComplete = func(cCtx *cli.Context) {
+ // ripped from cli.DefaultCompleteWithFlags
+ var lastArg string
+
+ if len(os.Args) > 2 {
+ lastArg = os.Args[len(os.Args)-2]
+ }
+
+ if lastArg != "" {
+ if strings.HasPrefix(lastArg, "-") {
+ if !islastFlagComplete(lastArg, app.Flags) {
+ dnscontrolPrintFlagSuggestions(lastArg, app.Flags, cCtx.App.Writer)
+ return
+ }
+ }
+ }
+ dnscontrolPrintCommandSuggestions(app.Commands, cCtx.App.Writer)
+ }
if err := app.Run(os.Args); err != nil {
return 1
}
diff --git a/commands/completion-scripts/completion.bash.gotmpl b/commands/completion-scripts/completion.bash.gotmpl
new file mode 100644
index 0000000000..1f04ec5978
--- /dev/null
+++ b/commands/completion-scripts/completion.bash.gotmpl
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+: "{{.App.Name}}"
+
+# Macs have bash3 for which the bash-completion package doesn't include
+# _init_completion. This is a minimal version of that function.
+_dnscontrol_init_completion() {
+ COMPREPLY=()
+ _get_comp_words_by_ref "$@" cur prev words cword
+}
+
+_dnscontrol() {
+ if [[ "${COMP_WORDS[0]}" != "source" ]]; then
+ local cur opts base words
+ COMPREPLY=()
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ if declare -F _init_completion >/dev/null 2>&1; then
+ _init_completion -n "=:" || return
+ else
+ _dnscontrol_init_completion -n "=:" || return
+ fi
+ words=("${words[@]:0:$cword}")
+ if [[ "$cur" == "-"* ]]; then
+ requestComp="${words[*]} ${cur} --generate-bash-completion"
+ else
+ requestComp="${words[*]} --generate-bash-completion"
+ fi
+ opts=$(eval "${requestComp}" 2>/dev/null)
+ COMPREPLY=($(compgen -W "${opts}" -- ${cur}))
+ return 0
+ fi
+}
+
+complete -o bashdefault -o default -o nospace -F "_dnscontrol" "{{.App.Name}}"
diff --git a/commands/completion-scripts/completion.zsh.gotmpl b/commands/completion-scripts/completion.zsh.gotmpl
new file mode 100644
index 0000000000..1eb2191757
--- /dev/null
+++ b/commands/completion-scripts/completion.zsh.gotmpl
@@ -0,0 +1,28 @@
+#compdef {{.App.Name}}
+
+_dnscontrol() {
+ local -a opts
+ local cur
+ cur=${words[-1]}
+ if [[ "$cur" == "-"* ]]; then
+ opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
+ else
+ opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}")
+ fi
+
+ if [[ "${opts[1]}" != "" ]]; then
+ _describe 'values' opts
+ else
+ _files
+ fi
+}
+
+# Run the function the first time we are auto-loaded, otherwise the very first
+# complete wouldn't work in each shell session
+# Otherwise assume we are directly sourced and register the completion
+# (This is done by the #compdef directive in the autoloaded case.)
+if [[ "${zsh_eval_context[-1]}" == "loadautofunc" ]]; then
+ _dnscontrol "$@"
+else
+ compdef "_dnscontrol" "dnscontrol"
+fi
diff --git a/commands/completion.go b/commands/completion.go
new file mode 100644
index 0000000000..7f94348b40
--- /dev/null
+++ b/commands/completion.go
@@ -0,0 +1,169 @@
+package commands
+
+import (
+ "embed"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "strings"
+ "text/template"
+ "unicode/utf8"
+
+ "github.com/urfave/cli/v2"
+)
+
+//go:embed completion-scripts/completion.*.gotmpl
+var completionScripts embed.FS
+
+func shellCompletionCommand() *cli.Command {
+ supportedShells, templates, err := getCompletionSupportedShells()
+ if err != nil {
+ panic(err)
+ }
+ return &cli.Command{
+ Name: "shell-completion",
+ Usage: "generate shell completion scripts",
+ ArgsUsage: fmt.Sprintf("[ %s ]", strings.Join(supportedShells, " | ")),
+ Description: fmt.Sprintf("Generate shell completion script for [ %s ]", strings.Join(supportedShells, " | ")),
+ BashComplete: func(ctx *cli.Context) {
+ for _, shell := range supportedShells {
+ if strings.HasPrefix(shell, ctx.Args().First()) {
+ ctx.App.Writer.Write([]byte(shell + "\n"))
+ }
+ }
+ },
+ Action: func(ctx *cli.Context) error {
+ var inputShell string
+ if inputShell = ctx.Args().First(); inputShell == "" {
+ if inputShell = os.Getenv("SHELL"); inputShell == "" {
+ return cli.Exit(errors.New("shell not specified"), 1)
+ }
+ }
+ shellName := path.Base(inputShell) // necessary if using $SHELL, noop otherwise
+
+ template := templates[shellName]
+ if template == nil {
+ return cli.Exit(fmt.Errorf("unknown shell: %s", inputShell), 1)
+ }
+
+ err = template.Execute(ctx.App.Writer, struct {
+ App *cli.App
+ }{ctx.App})
+ if err != nil {
+ return cli.Exit(fmt.Errorf("failed to print completion script: %w", err), 1)
+ }
+ return nil
+ },
+ }
+}
+
+var _ = cmd(catUtils, shellCompletionCommand())
+
+// getCompletionSupportedShells returns a list of shells with available completions.
+// The list is generated from the embedded completion scripts.
+func getCompletionSupportedShells() (shells []string, shellCompletionScripts map[string]*template.Template, err error) {
+ scripts, err := completionScripts.ReadDir("completion-scripts")
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read completion scripts: %w", err)
+ }
+
+ shellCompletionScripts = make(map[string]*template.Template)
+
+ for _, f := range scripts {
+ fNameWithoutExtension := strings.TrimSuffix(f.Name(), ".gotmpl")
+ shellName := strings.TrimPrefix(path.Ext(fNameWithoutExtension), ".")
+
+ content, err := completionScripts.ReadFile(path.Join("completion-scripts", f.Name()))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read completion script %s", f.Name())
+ }
+
+ t := template.New(shellName)
+ t, err = t.Parse(string(content))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to parse template %s", f.Name())
+ }
+
+ shells = append(shells, shellName)
+ shellCompletionScripts[shellName] = t
+ }
+ return shells, shellCompletionScripts, nil
+}
+
+func dnscontrolPrintCommandSuggestions(commands []*cli.Command, writer io.Writer) {
+ for _, command := range commands {
+ if command.Hidden {
+ continue
+ }
+ if strings.HasSuffix(os.Getenv("SHELL"), "zsh") {
+ for _, name := range command.Names() {
+ _, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage)
+ }
+ } else {
+ for _, name := range command.Names() {
+ _, _ = fmt.Fprintf(writer, "%s\n", name)
+ }
+ }
+ }
+}
+
+func dnscontrolCliArgContains(flagName string) bool {
+ for _, name := range strings.Split(flagName, ",") {
+ name = strings.TrimSpace(name)
+ count := utf8.RuneCountInString(name)
+ if count > 2 {
+ count = 2
+ }
+ flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
+ for _, a := range os.Args {
+ if a == flag {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func dnscontrolPrintFlagSuggestions(lastArg string, flags []cli.Flag, writer io.Writer) {
+ cur := strings.TrimPrefix(lastArg, "-")
+ cur = strings.TrimPrefix(cur, "-")
+ for _, flag := range flags {
+ if bflag, ok := flag.(*cli.BoolFlag); ok && bflag.Hidden {
+ continue
+ }
+ for _, name := range flag.Names() {
+ name = strings.TrimSpace(name)
+ // this will get total count utf8 letters in flag name
+ count := utf8.RuneCountInString(name)
+ if count > 2 {
+ count = 2 // reuse this count to generate single - or -- in flag completion
+ }
+ // if flag name has more than one utf8 letter and last argument in cli has -- prefix then
+ // skip flag completion for short flags example -v or -x
+ if strings.HasPrefix(lastArg, "--") && count == 1 {
+ continue
+ }
+ // match if last argument matches this flag and it is not repeated
+ if strings.HasPrefix(name, cur) && cur != name && !dnscontrolCliArgContains(name) {
+ flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
+ _, _ = fmt.Fprintln(writer, flagCompletion)
+ }
+ }
+ }
+}
+
+func islastFlagComplete(lastArg string, flags []cli.Flag) bool {
+ cur := strings.TrimPrefix(lastArg, "-")
+ cur = strings.TrimPrefix(cur, "-")
+ for _, flag := range flags {
+ for _, name := range flag.Names() {
+ name = strings.TrimSpace(name)
+ if strings.HasPrefix(name, cur) && cur != name && !dnscontrolCliArgContains(name) {
+ return false
+ }
+ }
+ }
+ return true
+}
diff --git a/commands/completion_test.go b/commands/completion_test.go
new file mode 100644
index 0000000000..07ba8436cb
--- /dev/null
+++ b/commands/completion_test.go
@@ -0,0 +1,250 @@
+package commands
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+ "testing"
+ "text/template"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/urfave/cli/v2"
+ "golang.org/x/exp/slices"
+)
+
+type shellTestDataItem struct {
+ shellName string
+ shellPath string
+ completionScriptTemplate *template.Template
+}
+
+// setupTestShellCompletionCommand resets the buffers used to capture output and errors from the app.
+func setupTestShellCompletionCommand(app *cli.App) func(t *testing.T) {
+ return func(t *testing.T) {
+ app.Writer.(*bytes.Buffer).Reset()
+ cli.ErrWriter.(*bytes.Buffer).Reset()
+ }
+}
+
+func TestShellCompletionCommand(t *testing.T) {
+ app := cli.NewApp()
+ app.Name = "testing"
+
+ var appWriterBuffer bytes.Buffer
+ app.Writer = &appWriterBuffer // capture output from app
+
+ var appErrWriterBuffer bytes.Buffer
+ cli.ErrWriter = &appErrWriterBuffer // capture errors from app (apparently, HandleExitCoder doesn't use app.ErrWriter!?)
+
+ cli.OsExiter = func(int) {} // disable os.Exit call
+
+ app.Commands = []*cli.Command{
+ shellCompletionCommand(),
+ }
+
+ shellsAndCompletionScripts, err := testHelperGetShellsAndCompletionScripts()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(shellsAndCompletionScripts) == 0 {
+ t.Fatal("no shells found")
+ }
+
+ invalidShellTestDataItem := shellTestDataItem{
+ shellName: "invalid",
+ shellPath: "/bin/invalid",
+ }
+ for _, tt := range shellsAndCompletionScripts {
+ if tt.shellName == invalidShellTestDataItem.shellName {
+ t.Fatalf("invalidShellTestDataItem.shellName (%s) is actually a valid shell name", invalidShellTestDataItem.shellName)
+ }
+ }
+
+ // Test shell argument
+ t.Run("shellArg", func(t *testing.T) {
+ for _, tt := range shellsAndCompletionScripts {
+ t.Run(tt.shellName, func(t *testing.T) {
+ tearDownTest := setupTestShellCompletionCommand(app)
+ defer tearDownTest(t)
+
+ err := app.Run([]string{app.Name, "shell-completion", tt.shellName})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got := appWriterBuffer.String()
+ want, err := testHelperRenderTemplateFromApp(app, tt.completionScriptTemplate)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+ }
+
+ stderr := appErrWriterBuffer.String()
+ if stderr != "" {
+ t.Errorf("want no stderr, got %q", stderr)
+ }
+ })
+ }
+
+ t.Run(invalidShellTestDataItem.shellName, func(t *testing.T) {
+ tearDownTest := setupTestShellCompletionCommand(app)
+ defer tearDownTest(t)
+
+ err := app.Run([]string{app.Name, "shell-completion", "invalid"})
+
+ if err == nil {
+ t.Fatal("expected error, but didn't get one")
+ }
+
+ want := fmt.Sprintf("unknown shell: %s", invalidShellTestDataItem.shellName)
+ got := strings.TrimSpace(appErrWriterBuffer.String())
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+ }
+
+ stdout := appWriterBuffer.String()
+ if stdout != "" {
+ t.Errorf("want no stdout, got %q", stdout)
+ }
+ })
+ })
+
+ // Test $SHELL envar
+ t.Run("$SHELL", func(t *testing.T) {
+ for _, tt := range shellsAndCompletionScripts {
+ t.Run(tt.shellName, func(t *testing.T) {
+ tearDownTest := setupTestShellCompletionCommand(app)
+ defer tearDownTest(t)
+
+ t.Setenv("SHELL", tt.shellPath)
+
+ err := app.Run([]string{app.Name, "shell-completion"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got := appWriterBuffer.String()
+ want, err := testHelperRenderTemplateFromApp(app, tt.completionScriptTemplate)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+ }
+
+ stderr := appErrWriterBuffer.String()
+ if stderr != "" {
+ t.Errorf("want no stderr, got %q", stderr)
+ }
+ })
+ }
+
+ t.Run(invalidShellTestDataItem.shellName, func(t *testing.T) {
+ tearDownTest := setupTestShellCompletionCommand(app)
+ defer tearDownTest(t)
+
+ t.Setenv("SHELL", invalidShellTestDataItem.shellPath)
+
+ err := app.Run([]string{app.Name, "shell-completion"})
+ if err == nil {
+ t.Fatal("expected error, but didn't get one")
+ }
+
+ want := fmt.Sprintf("unknown shell: %s", invalidShellTestDataItem.shellPath)
+ got := strings.TrimSpace(appErrWriterBuffer.String())
+ if diff := cmp.Diff(got, want); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+ }
+
+ stdout := appWriterBuffer.String()
+ if stdout != "" {
+ t.Errorf("want no stdout, got %q", stdout)
+ }
+ })
+ })
+
+ // Test shell argument completion (meta)
+ t.Run("shell-name-completion", func(t *testing.T) {
+ type testCase struct {
+ shellArg string
+ expected []string
+ }
+ testCases := []testCase{
+ {shellArg: ""}, // empty 'shell' argument, returns all known shells (expected is filled later)
+ {shellArg: "invalid", expected: []string{""}}, // invalid shell, returns none
+ }
+
+ for _, tt := range shellsAndCompletionScripts {
+ testCases[0].expected = append(testCases[0].expected, tt.shellName)
+ for i := range tt.shellName {
+ testCases = append(testCases, testCase{
+ shellArg: tt.shellName[:i+1],
+ expected: []string{tt.shellName},
+ })
+ }
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.shellArg, func(t *testing.T) {
+ tearDownTest := setupTestShellCompletionCommand(app)
+ defer tearDownTest(t)
+ app.EnableBashCompletion = true
+ defer func() {
+ app.EnableBashCompletion = false
+ }()
+
+ err := app.Run([]string{app.Name, "shell-completion", tC.shellArg, "--generate-bash-completion"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ for _, line := range strings.Split(strings.TrimSpace(appWriterBuffer.String()), "\n") {
+ if !slices.Contains(tC.expected, line) {
+ t.Errorf("%q found, but not expected", line)
+ }
+ }
+ })
+ }
+ })
+}
+
+// testHelperGetShellsAndCompletionScripts collects all supported shells and their completion scripts and returns them
+// as a slice of shellTestDataItem.
+// The completion scripts are sourced with getCompletionSupportedShells
+func testHelperGetShellsAndCompletionScripts() ([]shellTestDataItem, error) {
+ shells, templates, err := getCompletionSupportedShells()
+ if err != nil {
+ return nil, err
+ }
+
+ var shellsAndValues []shellTestDataItem
+ for shellName, t := range templates {
+ if !slices.Contains(shells, shellName) {
+ return nil, fmt.Errorf(
+ `"%s" is not present in slice of shells from getCompletionSupportedShells`, shellName)
+ }
+ shellsAndValues = append(
+ shellsAndValues,
+ shellTestDataItem{
+ shellName: shellName,
+ shellPath: fmt.Sprintf("/bin/%s", shellName),
+ completionScriptTemplate: t,
+ },
+ )
+ }
+ return shellsAndValues, nil
+}
+
+// testHelperRenderTemplateFromApp renders a given template with a given app.
+// This is used to test the output of the CLI command against a 'known good' value.
+func testHelperRenderTemplateFromApp(app *cli.App, scriptTemplate *template.Template) (string, error) {
+ var scriptBytes bytes.Buffer
+ err := scriptTemplate.Execute(&scriptBytes, struct {
+ App *cli.App
+ }{app})
+
+ return scriptBytes.String(), err
+}
diff --git a/commands/getCerts.go b/commands/getCerts.go
index 16bd4420be..2e63b14790 100644
--- a/commands/getCerts.go
+++ b/commands/getCerts.go
@@ -24,6 +24,8 @@ var _ = cmd(catUtils, func() *cli.Command {
return exit(GetCerts(args))
},
Flags: args.flags(),
+
+ Hidden: true,
}
}())
diff --git a/commands/getZones.go b/commands/getZones.go
index abc8349b81..4ae28443b5 100644
--- a/commands/getZones.go
+++ b/commands/getZones.go
@@ -220,7 +220,7 @@ func GetZone(args GetZoneArgs) error {
fmt.Fprintf(w, `var %s = NewDnsProvider("%s", "%s");`+"\n",
dspVariableName, args.CredName, args.ProviderName)
}
- fmt.Fprintf(w, `var REG_CHANGEME = NewRegistrar("none");`+"\n")
+ fmt.Fprintf(w, `var REG_CHANGEME = NewRegistrar("none");`+"\n\n")
}
// print each zone
@@ -254,7 +254,7 @@ func GetZone(args GetZoneArgs) error {
if (rec.Type == "CNAME") && (rec.Name == "@") {
o = append(o, "// NOTE: CNAME at apex may require manual editing.")
}
- o = append(o, formatDsl(zoneName, rec, defaultTTL))
+ o = append(o, formatDsl(rec, defaultTTL))
}
out := strings.Join(o, sep)
@@ -267,14 +267,17 @@ func GetZone(args GetZoneArgs) error {
"//, NOTE: CNAME at apex may require manual editing.",
"// NOTE: CNAME at apex may require manual editing.",
)
+ fmt.Fprint(w, out)
+ fmt.Fprint(w, "\n)\n\n")
} else {
+ out = out + ","
out = strings.ReplaceAll(out,
"// NOTE: CNAME at apex may require manual editing.,",
"// NOTE: CNAME at apex may require manual editing.",
)
+ fmt.Fprint(w, out)
+ fmt.Fprint(w, "\n);\n\n")
}
- fmt.Fprint(w, out)
- fmt.Fprint(w, "\n)\n")
case "tsv":
for _, rec := range recs {
@@ -286,8 +289,12 @@ func GetZone(args GetZoneArgs) error {
}
}
+ ty := rec.Type
+ if rec.Type == "UNKNOWN" {
+ ty = rec.UnknownTypeName
+ }
fmt.Fprintf(w, "%s\t%s\t%d\tIN\t%s\t%s%s\n",
- rec.NameFQDN, rec.Name, rec.TTL, rec.Type, rec.GetTargetCombined(), cfproxy)
+ rec.NameFQDN, rec.Name, rec.TTL, ty, rec.GetTargetCombinedFunc(nil), cfproxy)
}
default:
@@ -307,7 +314,7 @@ func jsonQuoted(i string) string {
return string(b)
}
-func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) string {
+func formatDsl(rec *models.RecordConfig, defaultTTL uint32) string {
target := rec.GetTargetCombined()
@@ -330,6 +337,8 @@ func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) str
return makeCaa(rec, ttlop)
case "DS":
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest)
+ case "DNSKEY":
+ target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.DnskeyFlags, rec.DnskeyProtocol, rec.DnskeyAlgorithm, rec.DnskeyPublicKey)
case "MX":
target = fmt.Sprintf(`%d, "%s"`, rec.MxPreference, rec.GetTargetField())
case "NAPTR":
@@ -348,14 +357,12 @@ func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) str
target = fmt.Sprintf(`"%s", "%s", %d, %d, %d, %d, %d`, rec.GetTargetField(), rec.SoaMbox, rec.SoaSerial, rec.SoaRefresh, rec.SoaRetry, rec.SoaExpire, rec.SoaMinttl)
case "SRV":
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.SrvPriority, rec.SrvWeight, rec.SrvPort, rec.GetTargetField())
+ case "SVCB", "HTTPS":
+ target = fmt.Sprintf(`%d, "%s", "%s"`, rec.SvcPriority, rec.GetTargetField(), rec.SvcParams)
case "TLSA":
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.TlsaUsage, rec.TlsaSelector, rec.TlsaMatchingType, rec.GetTargetField())
case "TXT":
- if len(rec.TxtStrings) == 1 {
- target = `"` + rec.TxtStrings[0] + `"`
- } else {
- target = `["` + strings.Join(rec.TxtStrings, `", "`) + `"]`
- }
+ target = jsonQuoted(rec.GetTargetTXTJoined())
// TODO(tlim): If this is an SPF record, generate a SPF_BUILDER().
case "NS":
// NS records at the apex should be NAMESERVER() records.
@@ -367,6 +374,8 @@ func formatDsl(zonename string, rec *models.RecordConfig, defaultTTL uint32) str
target = `"` + target + `"`
case "R53_ALIAS":
return makeR53alias(rec, ttl)
+ case "UNKNOWN":
+ return makeUknown(rec, ttl)
default:
target = `"` + target + `"`
}
@@ -395,8 +404,15 @@ func makeR53alias(rec *models.RecordConfig, ttl uint32) string {
if z, ok := rec.R53Alias["zone_id"]; ok {
items = append(items, `R53_ZONE("`+z+`")`)
}
+ if e, ok := rec.R53Alias["evaluate_target_health"]; ok && e == "true" {
+ items = append(items, "R53_EVALUATE_TARGET_HEALTH(true)")
+ }
if ttl != 0 {
items = append(items, fmt.Sprintf("TTL(%d)", ttl))
}
return rec.Type + "(" + strings.Join(items, ", ") + ")"
}
+
+func makeUknown(rc *models.RecordConfig, ttl uint32) string {
+ return fmt.Sprintf(`// %s("%s", TTL(%d))`, rc.UnknownTypeName, rc.GetTargetField(), ttl)
+}
diff --git a/commands/gz_test.go b/commands/gz_test.go
index c7c56bece7..594debb839 100644
--- a/commands/gz_test.go
+++ b/commands/gz_test.go
@@ -66,13 +66,12 @@ func testFormat(t *testing.T, domain, format string) {
log.Fatal(fmt.Errorf("can't read expected %q: %w", outfile.Name(), err))
}
- // // Update got -> want
- // err = os.WriteFile(expectedFilename, got, 0644)
- // if err != nil {
- // log.Fatal(err)
- // }
-
if w, g := string(want), string(got); w != g {
+ // If the test fails, output a file showing "got"
+ err = os.WriteFile(expectedFilename+".ACTUAL", got, 0644)
+ if err != nil {
+ log.Fatal(err)
+ }
t.Errorf("testFormat mismatch (-got +want):\n%s", diff.LineDiff(g, w))
}
}
diff --git a/commands/ppreviewPush.go b/commands/ppreviewPush.go
new file mode 100644
index 0000000000..e8a16dca9a
--- /dev/null
+++ b/commands/ppreviewPush.go
@@ -0,0 +1,873 @@
+package commands
+
+import (
+ "cmp"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/bindserial"
+ "github.com/StackExchange/dnscontrol/v4/pkg/credsfile"
+ "github.com/StackExchange/dnscontrol/v4/pkg/nameservers"
+ "github.com/StackExchange/dnscontrol/v4/pkg/normalize"
+ "github.com/StackExchange/dnscontrol/v4/pkg/notifications"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
+ "github.com/StackExchange/dnscontrol/v4/pkg/zonerecs"
+ "github.com/StackExchange/dnscontrol/v4/providers"
+ "github.com/urfave/cli/v2"
+ "golang.org/x/exp/slices"
+ "golang.org/x/net/idna"
+)
+
+type zoneCache struct {
+ cache map[string]*[]string
+ sync.Mutex
+}
+
+const legacywarn = `WARNING: --cmode=legacy will go away in v4.16 or later.` +
+ ` Please test --cmode=concurrent and report any bugs ASAP.` +
+ ` See https://docs.dnscontrol.org/commands/preview-push#cmode` +
+ "\n"
+
+const ppreviewwarn = `WARNING: ppreview is going away in v4.16 or later.` +
+ ` Use "preview" instead.` +
+ "\n"
+
+const ppushwarn = `WARNING: ppush is going away in v4.16 or later.` +
+ ` Use "push" instead.` +
+ "\n"
+
+var _ = cmd(catMain, func() *cli.Command {
+ var args PPreviewArgs
+ return &cli.Command{
+ Name: "preview",
+ Usage: "read live configuration and identify changes to be made, without applying them",
+ Action: func(ctx *cli.Context) error {
+ if args.ConcurMode == "legacy" {
+ fmt.Fprint(os.Stderr, legacywarn)
+ return exit(Preview(args))
+ }
+ return exit(PPreview(args))
+ },
+ Flags: args.flags(),
+ }
+}())
+
+var _ = cmd(catMain, func() *cli.Command {
+ var args PPreviewArgs
+ return &cli.Command{
+ Name: "ppreview",
+ Usage: "Deprecated. Same as: preview --cmode=concurrent",
+ Action: func(ctx *cli.Context) error {
+ fmt.Fprint(os.Stderr, ppreviewwarn)
+ if args.ConcurMode == "legacy" {
+ return exit(Preview(args))
+ }
+ return exit(PPreview(args))
+ },
+ Flags: args.flags(),
+ }
+}())
+
+// PPreviewArgs contains all data/flags needed to run preview, independently of CLI
+type PPreviewArgs struct {
+ GetDNSConfigArgs
+ GetCredentialsArgs
+ FilterArgs
+ Notify bool
+ WarnChanges bool
+ ConcurMode string
+ NoPopulate bool
+ DePopulate bool
+ Report string
+ Full bool
+}
+
+// ReportItem is a record of corrections for a particular domain/provider/registrar.
+//type ReportItem struct {
+// Domain string `json:"domain"`
+// Corrections int `json:"corrections"`
+// Provider string `json:"provider,omitempty"`
+// Registrar string `json:"registrar,omitempty"`
+//}
+
+func (args *PPreviewArgs) flags() []cli.Flag {
+ flags := args.GetDNSConfigArgs.flags()
+ flags = append(flags, args.GetCredentialsArgs.flags()...)
+ flags = append(flags, args.FilterArgs.flags()...)
+ flags = append(flags, &cli.BoolFlag{
+ Name: "notify",
+ Destination: &args.Notify,
+ Usage: `set to true to send notifications to configured destinations`,
+ })
+ flags = append(flags, &cli.BoolFlag{
+ Name: "expect-no-changes",
+ Destination: &args.WarnChanges,
+ Usage: `set to true for non-zero return code if there are changes`,
+ })
+ flags = append(flags, &cli.StringFlag{
+ Name: "cmode",
+ Destination: &args.ConcurMode,
+ Value: "concurrent",
+ Usage: `Which providers to run concurrently: legacy, concurrent, none, all`,
+ Action: func(c *cli.Context, s string) error {
+ if !slices.Contains([]string{"legacy", "concurrent", "none", "all"}, s) {
+ fmt.Printf("%q is not a valid option for --cmode. Values are: legacy, concurrent, none, all\n", s)
+ }
+ return nil
+ },
+ })
+ flags = append(flags, &cli.BoolFlag{
+ Name: "no-populate",
+ Destination: &args.NoPopulate,
+ Usage: `Do not auto-create zones at the provider`,
+ })
+ flags = append(flags, &cli.BoolFlag{
+ Name: "depopulate",
+ Destination: &args.NoPopulate,
+ Usage: `Delete unknown zones at provider (dangerous!)`,
+ })
+ flags = append(flags, &cli.BoolFlag{
+ Name: "full",
+ Destination: &args.Full,
+ Usage: `Add headings, providers names, notifications of no changes, etc`,
+ })
+ flags = append(flags, &cli.IntFlag{
+ Name: "reportmax",
+ Hidden: true,
+ Usage: `Limit the IGNORE/NO_PURGE report to this many lines (Expermental. Will change in the future.)`,
+ Action: func(ctx *cli.Context, max int) error {
+ printer.MaxReport = max
+ return nil
+ },
+ })
+ flags = append(flags, &cli.Int64Flag{
+ Name: "bindserial",
+ Destination: &bindserial.ForcedValue,
+ Usage: `Force BIND serial numbers to this value (for reproducibility)`,
+ })
+ flags = append(flags, &cli.StringFlag{
+ Name: "report",
+ Destination: &args.Report,
+ Usage: `Generate a machine-parseable report of corrections.`,
+ })
+ return flags
+}
+
+var _ = cmd(catMain, func() *cli.Command {
+ var args PPushArgs
+ return &cli.Command{
+ Name: "push",
+ Usage: "identify changes to be made, and perform them",
+ Action: func(ctx *cli.Context) error {
+ if args.ConcurMode == "legacy" {
+ fmt.Fprint(os.Stderr, legacywarn)
+ return exit(Push(args))
+ }
+ return exit(PPush(args))
+ },
+ Flags: args.flags(),
+ }
+}())
+
+var _ = cmd(catMain, func() *cli.Command {
+ var args PPushArgs
+ return &cli.Command{
+ Name: "ppush",
+ Usage: "identify changes to be made, and perform them",
+ Action: func(ctx *cli.Context) error {
+ fmt.Fprint(os.Stderr, ppushwarn)
+ if args.ConcurMode == "legacy" {
+ return exit(Push(args))
+ }
+ return exit(PPush(args))
+ },
+ Flags: args.flags(),
+ }
+}())
+
+// PPushArgs contains all data/flags needed to run push, independently of CLI
+type PPushArgs struct {
+ PPreviewArgs
+ Interactive bool
+}
+
+func (args *PPushArgs) flags() []cli.Flag {
+ flags := args.PPreviewArgs.flags()
+ flags = append(flags, &cli.BoolFlag{
+ Name: "i",
+ Destination: &args.Interactive,
+ Usage: "Interactive. Confirm or Exclude each correction before they run",
+ })
+ return flags
+}
+
+// PPreview implements the preview subcommand.
+func PPreview(args PPreviewArgs) error {
+ return prun(args, false, false, printer.DefaultPrinter, args.Report)
+}
+
+// PPush implements the push subcommand.
+func PPush(args PPushArgs) error {
+ return prun(args.PPreviewArgs, true, args.Interactive, printer.DefaultPrinter, args.Report)
+}
+
+var pobsoleteDiff2FlagUsed = false
+
+// run is the main routine common to preview/push
+func prun(args PPreviewArgs, push bool, interactive bool, out printer.CLI, report string) error {
+
+ // This is a hack until we have the new printer replacement.
+ printer.SkinnyReport = !args.Full
+ fullMode := args.Full
+
+ if pobsoleteDiff2FlagUsed {
+ printer.Println("WARNING: Please remove obsolete --diff2 flag. This will be an error in v5 or later. See https://github.com/StackExchange/dnscontrol/issues/2262")
+ }
+
+ out.PrintfIf(fullMode, "Reading dnsconfig.js or equiv.\n")
+ cfg, err := GetDNSConfig(args.GetDNSConfigArgs)
+ if err != nil {
+ return err
+ }
+
+ out.PrintfIf(fullMode, "Reading creds.json or equiv.\n")
+ providerConfigs, err := credsfile.LoadProviderConfigs(args.CredsFile)
+ if err != nil {
+ return err
+ }
+
+ out.PrintfIf(fullMode, "Creating an in-memory model of 'desired'...\n")
+ notifier, err := PInitializeProviders(cfg, providerConfigs, args.Notify)
+ if err != nil {
+ return err
+ }
+
+ out.PrintfIf(fullMode, "Normalizing and validating 'desired'..\n")
+ errs := normalize.ValidateAndNormalizeConfig(cfg)
+ if PrintValidationErrors(errs) {
+ return fmt.Errorf("exiting due to validation errors")
+ }
+
+ zcache := NewZoneCache()
+
+ // Loop over all (or some) zones:
+ zonesToProcess := whichZonesToProcess(cfg.Domains, args.Domains)
+ zonesSerial, zonesConcurrent := splitConcurrent(zonesToProcess, args.ConcurMode)
+ out.PrintfIf(fullMode, "PHASE 1: GATHERING data\n")
+ var wg sync.WaitGroup
+ wg.Add(len(zonesConcurrent))
+ out.Printf("CONCURRENTLY gathering %d zone(s)\n", len(zonesConcurrent))
+ for _, zone := range optimizeOrder(zonesConcurrent) {
+ out.PrintfIf(fullMode, "Concurrently gathering: %q\n", zone.Name)
+ go func(zone *models.DomainConfig, args PPreviewArgs, zcache *zoneCache) {
+ defer wg.Done()
+ oneZone(zone, args, zcache)
+ }(zone, args, zcache)
+ }
+ out.Printf("SERIALLY gathering %d zone(s)\n", len(zonesSerial))
+ for _, zone := range zonesSerial {
+ out.Printf("Serially Gathering: %q\n", zone.Name)
+ oneZone(zone, args, zcache)
+ }
+ out.PrintfIf(len(zonesConcurrent) > 0, "Waiting for concurrent gathering(s) to complete...")
+ wg.Wait()
+ out.PrintfIf(len(zonesConcurrent) > 0, "DONE\n")
+
+ // Now we know what to do, print or do the tasks.
+ out.PrintfIf(fullMode, "PHASE 2: CORRECTIONS\n")
+ var totalCorrections int
+ var reportItems []*ReportItem
+ var anyErrors bool
+ for _, zone := range zonesToProcess {
+ out.StartDomain(zone.GetUniqueName())
+
+ // Process DNS provider changes:
+ providersToProcess := whichProvidersToProcess(zone.DNSProviderInstances, args.Providers)
+ for _, provider := range zone.DNSProviderInstances {
+ skip := skipProvider(provider.Name, providersToProcess)
+ out.StartDNSProvider(provider.Name, skip)
+ if !skip {
+ corrections := zone.GetCorrections(provider.Name)
+ numActions := zone.GetChangeCount(provider.Name)
+ totalCorrections += numActions
+ out.EndProvider2(provider.Name, numActions)
+ reportItems = append(reportItems, genReportItem(zone.Name, corrections, provider.Name))
+ anyErrors = cmp.Or(anyErrors, pprintOrRunCorrections(zone.Name, provider.Name, corrections, out, push, interactive, notifier, report))
+ }
+ }
+
+ // Process Registrar changes:
+ skip := skipProvider(zone.RegistrarInstance.Name, providersToProcess)
+ out.StartRegistrar(zone.RegistrarName, !skip)
+ if skip {
+ corrections := zone.GetCorrections(zone.RegistrarInstance.Name)
+ numActions := zone.GetChangeCount(zone.RegistrarInstance.Name)
+ out.EndProvider2(zone.RegistrarName, numActions)
+ totalCorrections += numActions
+ reportItems = append(reportItems, genReportItem(zone.Name, corrections, zone.RegistrarName))
+ anyErrors = cmp.Or(anyErrors, pprintOrRunCorrections(zone.Name, zone.RegistrarInstance.Name, corrections, out, push, interactive, notifier, report))
+ }
+
+ }
+
+ if os.Getenv("TEAMCITY_VERSION") != "" {
+ fmt.Fprintf(os.Stderr, "##teamcity[buildStatus status='SUCCESS' text='%d corrections']", totalCorrections)
+ }
+ rfc4183.PrintWarning()
+ notifier.Done()
+ out.Printf("Done. %d corrections.\n", totalCorrections)
+ err = writeReport(report, reportItems)
+ if err != nil {
+ return fmt.Errorf("could not write report")
+ }
+ if anyErrors {
+ return fmt.Errorf("completed with errors")
+ }
+ if totalCorrections != 0 && args.WarnChanges {
+ return fmt.Errorf("there are pending changes")
+ }
+ return nil
+}
+
+//func countActions(corrections []*models.Correction) int {
+// r := 0
+// for _, c := range corrections {
+// if c.F != nil {
+// r++
+// }
+// }
+// return r
+//}
+
+func whichZonesToProcess(domains []*models.DomainConfig, filter string) []*models.DomainConfig {
+ if filter == "" || filter == "all" {
+ return domains
+ }
+
+ permitList := strings.Split(filter, ",")
+ var picked []*models.DomainConfig
+ for _, domain := range domains {
+ if domainInList(domain.Name, permitList) {
+ picked = append(picked, domain)
+ }
+ }
+ return picked
+}
+
+// splitConcurrent takes a list of DomainConfigs and returns two lists. The
+// first list is the items that do NOT support concurrency. The second is list
+// the items that DO support concurrency.
+func splitConcurrent(domains []*models.DomainConfig, filter string) (serial []*models.DomainConfig, concurrent []*models.DomainConfig) {
+ if filter == "none" {
+ return domains, nil
+ } else if filter == "all" {
+ return nil, domains
+ }
+ for _, dc := range domains {
+ if allConcur(dc) {
+ concurrent = append(concurrent, dc)
+ } else {
+ serial = append(serial, dc)
+ }
+ }
+ return
+}
+
+// allConcur returns true if its registrar and all DNS providers support
+// concurrency. Otherwise false is returned.
+func allConcur(dc *models.DomainConfig) bool {
+ if !providers.ProviderHasCapability(dc.RegistrarInstance.ProviderType, providers.CanConcur) {
+ //fmt.Printf("WHY? %q: %+v\n", dc.Name, dc.RegistrarInstance)
+ return false
+ }
+ for _, p := range dc.DNSProviderInstances {
+ if !providers.ProviderHasCapability(p.ProviderType, providers.CanConcur) {
+ //fmt.Printf("WHY? %q: %+v\n", dc.Name, p)
+ return false
+ }
+ }
+ return true
+}
+
+// optimizeOrder returns a list of DomainConfigs so that they gather fastest.
+//
+// The current algorithm is based on the heuistic that larger zones (zones with
+// the most records) need the most time to be processed. Therefore, the largest
+// zones are moved to the front of the list.
+// This isn't perfect but it is good enough.
+func optimizeOrder(zones []*models.DomainConfig) []*models.DomainConfig {
+ slices.SortFunc(zones, func(a, b *models.DomainConfig) int {
+ return len(b.Records) - len(a.Records) // Biggest to smallest.
+ })
+
+ // // For benchmarking. Randomize the list. If you aren't better
+ // // than random, you might as well not play.
+ // rand.Shuffle(len(zones), func(i, j int) {
+ // zones[i], zones[j] = zones[j], zones[i]
+ // })
+
+ return zones
+}
+
+func oneZone(zone *models.DomainConfig, args PPreviewArgs, zc *zoneCache) {
+ // Fix the parent zone's delegation: (if able/needed)
+ delegationCorrections, dcCount := generateDelegationCorrections(zone, zone.DNSProviderInstances, zone.RegistrarInstance)
+
+ // Loop over the (selected) providers configured for that zone:
+ providersToProcess := whichProvidersToProcess(zone.DNSProviderInstances, args.Providers)
+ for _, provider := range providersToProcess {
+
+ // Populate the zones at the provider (if desired/needed/able):
+ if !args.NoPopulate {
+ populateCorrections := generatePopulateCorrections(provider, zone.Name, zc)
+ zone.StoreCorrections(provider.Name, populateCorrections)
+ }
+
+ // Update the zone's records at the provider:
+ zoneCor, rep, actualChangeCount := generateZoneCorrections(zone, provider)
+ zone.StoreCorrections(provider.Name, rep)
+ zone.StoreCorrections(provider.Name, zoneCor)
+ zone.IncrementChangeCount(provider.Name, actualChangeCount)
+ }
+
+ // Do the delegation corrections after the zones are updated.
+ zone.StoreCorrections(zone.RegistrarInstance.Name, delegationCorrections)
+ zone.IncrementChangeCount(zone.RegistrarInstance.Name, dcCount)
+}
+
+func whichProvidersToProcess(providers []*models.DNSProviderInstance, filter string) []*models.DNSProviderInstance {
+
+ if filter == "all" { // all
+ return providers
+ }
+
+ permitList := strings.Split(filter, ",")
+ var picked []*models.DNSProviderInstance
+
+ // Just the default providers:
+ if filter == "" {
+ for _, provider := range providers {
+ if provider.IsDefault {
+ picked = append(picked, provider)
+ }
+ }
+ return picked
+ }
+
+ // Just the exact matches:
+ for _, provider := range providers {
+ for _, filterItem := range permitList {
+ if provider.Name == filterItem {
+ picked = append(picked, provider)
+ }
+ }
+ }
+ return picked
+}
+
+func skipProvider(name string, providers []*models.DNSProviderInstance) bool {
+ return !slices.ContainsFunc(providers, func(p *models.DNSProviderInstance) bool {
+ return p.Name == name
+ })
+}
+
+func genReportItem(zname string, corrections []*models.Correction, pname string) *ReportItem {
+
+ // Only count the actions, not the messages.
+ cnt := 0
+ for _, cor := range corrections {
+ if cor.F != nil {
+ cnt++
+ }
+ }
+
+ r := ReportItem{
+ Domain: zname,
+ Corrections: cnt,
+ Provider: pname,
+ }
+ return &r
+}
+
+func pprintOrRunCorrections(zoneName string, providerName string, corrections []*models.Correction, out printer.CLI, push bool, interactive bool, notifier notifications.Notifier, report string) bool {
+ if len(corrections) == 0 {
+ return false
+ }
+ var anyErrors bool
+ cc := 0
+ cn := 0
+ for _, correction := range corrections {
+
+ // Print what we're about to do.
+ if correction.F == nil {
+ out.PrintReport(cn, correction)
+ cn++
+ } else {
+ out.PrintCorrection(cc, correction)
+ cc++
+ }
+
+ var err error
+ if push {
+
+ // If interactive, ask "are you sure?" and skip if not.
+ if interactive && !out.PromptToRun() {
+ continue
+ }
+
+ // If it is an action (not an informational message), notify and execute.
+ if correction.F != nil {
+ notifier.Notify(zoneName, providerName, correction.Msg, err, false)
+ err = correction.F()
+ out.EndCorrection(err)
+ if err != nil {
+ anyErrors = true
+ }
+ }
+ }
+ }
+
+ _ = report // File name to write report to. (obsolete)
+ return anyErrors
+}
+
+func writeReport(report string, reportItems []*ReportItem) error {
+ // No filename? No report.
+ if report == "" {
+ return nil
+ }
+
+ f, err := os.OpenFile(report, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ b, err := json.MarshalIndent(reportItems, "", " ")
+ if err != nil {
+ return err
+ }
+ if _, err := f.Write(b); err != nil {
+ return err
+ }
+ return nil
+}
+
+func generatePopulateCorrections(provider *models.DNSProviderInstance, zoneName string, zcache *zoneCache) []*models.Correction {
+
+ lister, ok := provider.Driver.(providers.ZoneLister)
+ if !ok {
+ return nil // We can't generate a list. No corrections are possible.
+ }
+
+ z, err := zcache.zoneList(provider.Name, lister)
+ if err != nil {
+ return []*models.Correction{{Msg: fmt.Sprintf("zoneList failed for %q: %s", provider.Name, err)}}
+ }
+ zones := *z
+
+ aceZoneName, _ := idna.ToASCII(zoneName)
+ if slices.Contains(zones, aceZoneName) {
+ return nil // zone exists. Nothing to do.
+ }
+
+ creator, ok := provider.Driver.(providers.ZoneCreator)
+ if !ok {
+ return []*models.Correction{{Msg: fmt.Sprintf("Zone %q does not exist. Can not create because %q does not implement ZoneCreator", aceZoneName, provider.Name)}}
+ }
+
+ return []*models.Correction{{
+ Msg: fmt.Sprintf("Ensuring zone %q exists in %q", aceZoneName, provider.Name),
+ F: func() error { return creator.EnsureZoneExists(aceZoneName) },
+ }}
+}
+
+func generateZoneCorrections(zone *models.DomainConfig, provider *models.DNSProviderInstance) ([]*models.Correction, []*models.Correction, int) {
+ reports, zoneCorrections, actualChangeCount, err := zonerecs.CorrectZoneRecords(provider.Driver, zone)
+ if err != nil {
+ return []*models.Correction{{Msg: fmt.Sprintf("Domain %q provider %s Error: %s", zone.Name, provider.Name, err)}}, nil, 0
+ }
+ return zoneCorrections, reports, actualChangeCount
+}
+
+func generateDelegationCorrections(zone *models.DomainConfig, providers []*models.DNSProviderInstance, _ *models.RegistrarInstance) ([]*models.Correction, int) {
+ //fmt.Printf("DEBUG: generateDelegationCorrections start zone=%q nsList = %v\n", zone.Name, zone.Nameservers)
+ nsList, err := nameservers.DetermineNameserversForProviders(zone, providers, true)
+ if err != nil {
+ return msg(fmt.Sprintf("DetermineNS: zone %q; Error: %s", zone.Name, err)), 0
+ }
+ zone.Nameservers = nsList
+ nameservers.AddNSRecords(zone)
+
+ if len(zone.Nameservers) == 0 && zone.Metadata["no_ns"] != "true" {
+ return []*models.Correction{{Msg: fmt.Sprintf("Skipping registrar %q: No nameservers declared for domain %q. Add {no_ns:'true'} to force",
+ zone.RegistrarName,
+ zone.Name,
+ )}}, 0
+ }
+
+ corrections, err := zone.RegistrarInstance.Driver.GetRegistrarCorrections(zone)
+ if err != nil {
+ return msg(fmt.Sprintf("zone %q; Rprovider %q; Error: %s", zone.Name, zone.RegistrarInstance.Name, err)), 0
+ }
+ return corrections, len(corrections)
+}
+
+func msg(s string) []*models.Correction {
+ return []*models.Correction{{Msg: s}}
+}
+
+// PInitializeProviders takes (fully processed) configuration and instantiates all providers and returns them.
+func PInitializeProviders(cfg *models.DNSConfig, providerConfigs map[string]map[string]string, notifyFlag bool) (notify notifications.Notifier, err error) {
+ var notificationCfg map[string]string
+ defer func() {
+ notify = notifications.Init(notificationCfg)
+ }()
+ if notifyFlag {
+ notificationCfg = providerConfigs["notifications"]
+ }
+ isNonDefault := map[string]bool{}
+ for name, vals := range providerConfigs {
+ // add "_exclude_from_defaults":"true" to a provider to exclude it from being run unless
+ // -providers=all or -providers=name
+ if vals["_exclude_from_defaults"] == "true" {
+ isNonDefault[name] = true
+ }
+ }
+
+ // Populate provider type ids based on values from creds.json:
+ msgs, err := ppopulateProviderTypes(cfg, providerConfigs)
+ if len(msgs) != 0 {
+ fmt.Fprintln(os.Stderr, strings.Join(msgs, "\n"))
+ }
+ if err != nil {
+ return
+ }
+
+ registrars := map[string]providers.Registrar{}
+ dnsProviders := map[string]providers.DNSServiceProvider{}
+ for _, d := range cfg.Domains {
+ if registrars[d.RegistrarName] == nil {
+ rCfg := cfg.RegistrarsByName[d.RegistrarName]
+ r, err := providers.CreateRegistrar(rCfg.Type, providerConfigs[d.RegistrarName])
+ if err != nil {
+ return nil, err
+ }
+ registrars[d.RegistrarName] = r
+ }
+ d.RegistrarInstance.Driver = registrars[d.RegistrarName]
+ d.RegistrarInstance.IsDefault = !isNonDefault[d.RegistrarName]
+ for _, pInst := range d.DNSProviderInstances {
+ if dnsProviders[pInst.Name] == nil {
+ dCfg := cfg.DNSProvidersByName[pInst.Name]
+ prov, err := providers.CreateDNSProvider(dCfg.Type, providerConfigs[dCfg.Name], dCfg.Metadata)
+ if err != nil {
+ return nil, err
+ }
+ dnsProviders[pInst.Name] = prov
+ }
+ pInst.Driver = dnsProviders[pInst.Name]
+ pInst.IsDefault = !isNonDefault[pInst.Name]
+ }
+ }
+ return
+}
+
+// pproviderTypeFieldName is the name of the field in creds.json that specifies the provider type id.
+const pproviderTypeFieldName = "TYPE"
+
+// ppurl is the documentation URL to list in the warnings related to missing provider type ids.
+const purl = "https://docs.dnscontrol.org/commands/creds-json"
+
+// ppopulateProviderTypes scans a DNSConfig for blank provider types and fills them in based on providerConfigs.
+// That is, if the provider type is "-" or "", we take that as an flag
+// that means this value should be replaced by the type found in creds.json.
+func ppopulateProviderTypes(cfg *models.DNSConfig, providerConfigs map[string]map[string]string) ([]string, error) {
+ var msgs []string
+
+ for i := range cfg.Registrars {
+ pType := cfg.Registrars[i].Type
+ pName := cfg.Registrars[i].Name
+ nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewRegistrar")
+ cfg.Registrars[i].Type = nt
+ if warnMsg != "" {
+ msgs = append(msgs, warnMsg)
+ }
+ if err != nil {
+ return msgs, err
+ }
+ }
+
+ for i := range cfg.DNSProviders {
+ pName := cfg.DNSProviders[i].Name
+ pType := cfg.DNSProviders[i].Type
+ nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewDnsProvider")
+ cfg.DNSProviders[i].Type = nt
+ if warnMsg != "" {
+ msgs = append(msgs, warnMsg)
+ }
+ if err != nil {
+ return msgs, err
+ }
+ }
+
+ // Update these fields set by
+ // commands/commands.go:preloadProviders().
+ // This is probably a layering violation. That said, the
+ // fundamental problem here is that we're storing the provider
+ // instances by string name, not by a pointer to a struct. We
+ // should clean that up someday.
+ for _, domain := range cfg.Domains { // For each domain..
+ for _, provider := range domain.DNSProviderInstances { // For each provider...
+ pName := provider.ProviderBase.Name
+ pType := provider.ProviderBase.ProviderType
+ nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewDnsProvider")
+ provider.ProviderBase.ProviderType = nt
+ if warnMsg != "" {
+ msgs = append(msgs, warnMsg)
+ }
+ if err != nil {
+ return msgs, err
+ }
+ }
+ p := domain.RegistrarInstance
+ pName := p.Name
+ pType := p.ProviderType
+ nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewRegistrar")
+ p.ProviderType = nt
+ if warnMsg != "" {
+ msgs = append(msgs, warnMsg)
+ }
+ if err != nil {
+ return msgs, err
+ }
+ }
+
+ return puniqueStrings(msgs), nil
+}
+
+// puniqueStrings takes an unsorted slice of strings and returns the
+// unique strings, in the order they first appeared in the list.
+func puniqueStrings(stringSlice []string) []string {
+ keys := make(map[string]bool)
+ list := []string{}
+ for _, entry := range stringSlice {
+ if _, ok := keys[entry]; !ok {
+ keys[entry] = true
+ list = append(list, entry)
+ }
+ }
+ return list
+}
+
+func prefineProviderType(credEntryName string, t string, credFields map[string]string, source string) (replacementType string, warnMsg string, err error) {
+
+ // t="" and t="-" are processed the same. Standardize on "-" to reduce the number of cases to check.
+ if t == "" {
+ t = "-"
+ }
+
+ // Use cases:
+ //
+ // type credsType
+ // ---- ---------
+ // - or "" GANDI lookup worked. Nothing to say.
+ // - or "" - or "" ERROR "creds.json has invalid or missing data"
+ // GANDI "" WARNING "Working but.... Please fix as follows..."
+ // GANDI GANDI INFO "working but unneeded: clean up as follows..."
+ // GANDI NAMEDOT ERROR "error mismatched: please fix as follows..."
+
+ // ERROR: Invalid.
+ // WARNING: Required change to remain compatible with 4.0
+ // INFO: Post-4.0 cleanups or other non-required changes.
+
+ if t != "-" {
+ // Old-style, dnsconfig.js specifies the type explicitly.
+ // This is supported but we suggest updates for future compatibility.
+
+ // If credFields is nil, that means there was no entry in creds.json:
+ if credFields == nil {
+ // Warn the user to update creds.json in preparation for 4.0:
+ // In 4.0 this should be an error. We could default to a
+ // provider such as "NONE" but I suspect it would be confusing
+ // to users to see references to a provider name that they did
+ // not specify.
+ return t, fmt.Sprintf(`WARNING: For future compatibility, add this entry creds.json: %q: { %q: %q }, (See %s#missing)`,
+ credEntryName, pproviderTypeFieldName, t,
+ purl,
+ ), nil
+ }
+
+ switch ct := credFields[pproviderTypeFieldName]; ct {
+ case "":
+ // Warn the user to update creds.json in preparation for 4.0:
+ // In 4.0 this should be an error.
+ return t, fmt.Sprintf(`WARNING: For future compatibility, update the %q entry in creds.json by adding: %q: %q, (See %s#missing)`,
+ credEntryName,
+ pproviderTypeFieldName, t,
+ purl,
+ ), nil
+ case "-":
+ // This should never happen. The user is specifying "-" in a place that it shouldn't be used.
+ return "-", "", fmt.Errorf(`ERROR: creds.json entry %q has invalid %q value %q (See %s#hyphen)`,
+ credEntryName, pproviderTypeFieldName, ct,
+ purl,
+ )
+ case t:
+ // creds.json file is compatible with and dnsconfig.js can be updated.
+ return ct, fmt.Sprintf(`INFO: In dnsconfig.js %s(%q, %q) can be simplified to %s(%q) (See %s#cleanup)`,
+ source, credEntryName, t,
+ source, credEntryName,
+ purl,
+ ), nil
+ default:
+ // creds.json lists a TYPE but it doesn't match what's in dnsconfig.js!
+ return t, "", fmt.Errorf(`ERROR: Mismatch found! creds.json entry %q has %q set to %q but dnsconfig.js specifies %s(%q, %q) (See %s#mismatch)`,
+ credEntryName,
+ pproviderTypeFieldName, ct,
+ source, credEntryName, t,
+ purl,
+ )
+ }
+ }
+
+ // t == "-"
+ // New-style, dnsconfig.js does not specify the type (t == "") or a
+ // command line tool accepted "-" as a positional argument for
+ // backwards compatibility.
+
+ // If credFields is nil, that means there was no entry in creds.json:
+ if credFields == nil {
+ return "", "", fmt.Errorf(`ERROR: creds.json is missing an entry called %q. Suggestion: %q: { %q: %q }, (See %s#missing)`,
+ credEntryName,
+ credEntryName, pproviderTypeFieldName, "FILL_IN_PROVIDER_TYPE",
+ purl,
+ )
+ }
+
+ // New-style, dnsconfig.js doesn't specifies the type. It will be
+ // looked up in creds.json.
+ switch ct := credFields[pproviderTypeFieldName]; ct {
+ case "":
+ return ct, "", fmt.Errorf(`ERROR: creds.json entry %q is missing: %q: %q, (See %s#fixcreds)`,
+ credEntryName,
+ pproviderTypeFieldName, "FILL_IN_PROVIDER_TYPE",
+ purl,
+ )
+ case "-":
+ // This should never happen. The user is confused and specified "-" in the wrong place!
+ return "-", "", fmt.Errorf(`ERROR: creds.json entry %q has invalid %q value %q (See %s#hyphen)`,
+ credEntryName,
+ pproviderTypeFieldName, ct,
+ purl,
+ )
+ default:
+ // use the value in creds.json (this should be the normal case)
+ return ct, "", nil
+ }
+
+}
diff --git a/commands/previewPush.go b/commands/previewPush.go
index dfb1592ab9..2a2e176cd0 100644
--- a/commands/previewPush.go
+++ b/commands/previewPush.go
@@ -10,41 +10,18 @@ import (
"golang.org/x/net/idna"
"github.com/StackExchange/dnscontrol/v4/models"
- "github.com/StackExchange/dnscontrol/v4/pkg/bindserial"
"github.com/StackExchange/dnscontrol/v4/pkg/credsfile"
"github.com/StackExchange/dnscontrol/v4/pkg/nameservers"
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
"github.com/StackExchange/dnscontrol/v4/pkg/notifications"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
"github.com/StackExchange/dnscontrol/v4/pkg/zonerecs"
"github.com/StackExchange/dnscontrol/v4/providers"
- "github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
)
-var _ = cmd(catMain, func() *cli.Command {
- var args PreviewArgs
- return &cli.Command{
- Name: "preview",
- Usage: "read live configuration and identify changes to be made, without applying them",
- Action: func(ctx *cli.Context) error {
- return exit(Preview(args))
- },
- Flags: args.flags(),
- }
-}())
-
-// PreviewArgs contains all data/flags needed to run preview, independently of CLI
-type PreviewArgs struct {
- GetDNSConfigArgs
- GetCredentialsArgs
- FilterArgs
- Notify bool
- WarnChanges bool
- NoPopulate bool
- Full bool
-}
-
+// ReportItem is a record of corrections for a particular domain/provider/registrar.
type ReportItem struct {
Domain string `json:"domain"`
Corrections int `json:"corrections"`
@@ -52,86 +29,20 @@ type ReportItem struct {
Registrar string `json:"registrar,omitempty"`
}
-func (args *PreviewArgs) flags() []cli.Flag {
- flags := args.GetDNSConfigArgs.flags()
- flags = append(flags, args.GetCredentialsArgs.flags()...)
- flags = append(flags, args.FilterArgs.flags()...)
- flags = append(flags, &cli.BoolFlag{
- Name: "notify",
- Destination: &args.Notify,
- Usage: `set to true to send notifications to configured destinations`,
- })
- flags = append(flags, &cli.BoolFlag{
- Name: "expect-no-changes",
- Destination: &args.WarnChanges,
- Usage: `set to true for non-zero return code if there are changes`,
- })
- flags = append(flags, &cli.BoolFlag{
- Name: "no-populate",
- Destination: &args.NoPopulate,
- Usage: `Use this flag to not auto-create non-existing zones at the provider`,
- })
- flags = append(flags, &cli.BoolFlag{
- Name: "full",
- Destination: &args.Full,
- Usage: `Add headings, providers names, notifications of no changes, etc`,
- })
- flags = append(flags, &cli.Int64Flag{
- Name: "bindserial",
- Destination: &bindserial.ForcedValue,
- Usage: `Force BIND serial numbers to this value (for reproducibility)`,
- })
- return flags
-}
-
-var _ = cmd(catMain, func() *cli.Command {
- var args PushArgs
- return &cli.Command{
- Name: "push",
- Usage: "identify changes to be made, and perform them",
- Action: func(ctx *cli.Context) error {
- return exit(Push(args))
- },
- Flags: args.flags(),
- }
-}())
-
-// PushArgs contains all data/flags needed to run push, independently of CLI
-type PushArgs struct {
- PreviewArgs
- Interactive bool
- Report string
-}
-
-func (args *PushArgs) flags() []cli.Flag {
- flags := args.PreviewArgs.flags()
- flags = append(flags, &cli.BoolFlag{
- Name: "i",
- Destination: &args.Interactive,
- Usage: "Interactive. Confirm or Exclude each correction before they run",
- })
- flags = append(flags, &cli.StringFlag{
- Name: "report",
- Destination: &args.Report,
- Usage: `Generate a machine-parseable report of performed corrections.`,
- })
- return flags
-}
-
// Preview implements the preview subcommand.
-func Preview(args PreviewArgs) error {
- return run(args, false, false, printer.DefaultPrinter, nil)
+func Preview(args PPreviewArgs) error {
+ return run(args, false, false, printer.DefaultPrinter, &args.Report)
}
// Push implements the push subcommand.
-func Push(args PushArgs) error {
- return run(args.PreviewArgs, true, args.Interactive, printer.DefaultPrinter, &args.Report)
+func Push(args PPushArgs) error {
+ return run(args.PPreviewArgs, true, args.Interactive, printer.DefaultPrinter, &args.Report)
}
var obsoleteDiff2FlagUsed = false
// run is the main routine common to preview/push
-func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report *string) error {
+func run(args PPreviewArgs, push bool, interactive bool, out printer.CLI, report *string) error {
// TODO: make truly CLI independent. Perhaps return results on a channel as they occur
// This is a hack until we have the new printer replacement.
@@ -195,6 +106,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report
zones, err := lister.ListZones()
if err != nil {
out.Errorf("ERROR: %s\n", err.Error())
+ anyErrors = true
return
}
aceZoneName, _ := idna.ToASCII(domain.Name)
@@ -210,6 +122,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report
// this is the actual push, ensure domain exists at DSP
if err := creator.EnsureZoneExists(domain.Name); err != nil {
out.Warnf("Error creating domain: %s\n", err)
+ anyErrors = true
continue // continue with next provider, as we couldn't create this one
}
}
@@ -219,7 +132,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report
// Correct the registrar...
- nsList, err := nameservers.DetermineNameserversForProviders(domain, providersWithExistingZone)
+ nsList, err := nameservers.DetermineNameserversForProviders(domain, providersWithExistingZone, false)
if err != nil {
out.Errorf("ERROR: %s\n", err.Error())
return
@@ -235,17 +148,17 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report
continue
}
- reports, corrections, err := zonerecs.CorrectZoneRecords(provider.Driver, domain)
- out.EndProvider(provider.Name, len(corrections), err)
+ reports, corrections, actualChangeCount, err := zonerecs.CorrectZoneRecords(provider.Driver, domain)
+ out.EndProvider(provider.Name, actualChangeCount, err)
if err != nil {
anyErrors = true
return
}
- totalCorrections += len(corrections)
+ totalCorrections += actualChangeCount
printReports(domain.Name, provider.Name, reports, out, push, notifier)
reportItems = append(reportItems, ReportItem{
Domain: domain.Name,
- Corrections: len(corrections),
+ Corrections: actualChangeCount,
Provider: provider.Name,
})
anyErrors = printOrRunCorrections(domain.Name, provider.Name, corrections, out, push, interactive, notifier) || anyErrors
@@ -282,6 +195,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report
if os.Getenv("TEAMCITY_VERSION") != "" {
fmt.Fprintf(os.Stderr, "##teamcity[buildStatus status='SUCCESS' text='%d corrections']", totalCorrections)
}
+ rfc4183.PrintWarning()
notifier.Done()
out.Printf("Done. %d corrections.\n", totalCorrections)
if anyErrors {
diff --git a/commands/printIR.go b/commands/printIR.go
index 82dc48efdf..cb463504fc 100644
--- a/commands/printIR.go
+++ b/commands/printIR.go
@@ -10,6 +10,8 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/js"
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rtypes"
"github.com/urfave/cli/v2"
)
@@ -58,6 +60,7 @@ var _ = cmd(catDebug, func() *cli.Command {
log.SetOutput(os.Stdout)
err := exit(PrintIR(pargs))
+ rfc4183.PrintWarning()
if err == nil {
fmt.Fprintf(os.Stdout, "No errors.\n")
}
@@ -126,6 +129,12 @@ func ExecuteDSL(args ExecuteDSLArgs) (*models.DNSConfig, error) {
if err != nil {
return nil, fmt.Errorf("executing %s: %w", args.JSFile, err)
}
+
+ err = rtypes.PostProcess(dnsConfig.Domains)
+ if err != nil {
+ return nil, err
+ }
+
return dnsConfig, nil
}
diff --git a/commands/r53_test.go b/commands/r53_test.go
index 46e281e6d7..27772bc3c9 100644
--- a/commands/r53_test.go
+++ b/commands/r53_test.go
@@ -68,3 +68,69 @@ func TestR53Test_2ttl(t *testing.T) {
t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
}
}
+
+func TestR53Test_3(t *testing.T) {
+ rec := models.RecordConfig{
+ Type: "R53_ALIAS",
+ Name: "foo",
+ NameFQDN: "foo.domain.tld",
+ }
+ rec.SetTarget("bar")
+ rec.R53Alias = make(map[string]string)
+ rec.R53Alias["type"] = "A"
+ rec.R53Alias["evaluate_target_health"] = "true"
+ w := `R53_ALIAS("foo", "A", "bar", R53_EVALUATE_TARGET_HEALTH(true))`
+ if g := makeR53alias(&rec, 0); g != w {
+ t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
+ }
+}
+
+func TestR53Test_3ttl(t *testing.T) {
+ rec := models.RecordConfig{
+ Type: "R53_ALIAS",
+ Name: "foo",
+ NameFQDN: "foo.domain.tld",
+ }
+ rec.SetTarget("bar")
+ rec.R53Alias = make(map[string]string)
+ rec.R53Alias["type"] = "A"
+ rec.R53Alias["evaluate_target_health"] = "true"
+ w := `R53_ALIAS("foo", "A", "bar", R53_EVALUATE_TARGET_HEALTH(true), TTL(123))`
+ if g := makeR53alias(&rec, 123); g != w {
+ t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
+ }
+}
+
+func TestR53Test_4(t *testing.T) {
+ rec := models.RecordConfig{
+ Type: "R53_ALIAS",
+ Name: "foo",
+ NameFQDN: "foo.domain.tld",
+ }
+ rec.SetTarget("bar")
+ rec.R53Alias = make(map[string]string)
+ rec.R53Alias["type"] = "A"
+ rec.R53Alias["zone_id"] = "blarg"
+ rec.R53Alias["evaluate_target_health"] = "true"
+ w := `R53_ALIAS("foo", "A", "bar", R53_ZONE("blarg"), R53_EVALUATE_TARGET_HEALTH(true))`
+ if g := makeR53alias(&rec, 0); g != w {
+ t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
+ }
+}
+
+func TestR53Test_4ttl(t *testing.T) {
+ rec := models.RecordConfig{
+ Type: "R53_ALIAS",
+ Name: "foo",
+ NameFQDN: "foo.domain.tld",
+ }
+ rec.SetTarget("bar")
+ rec.R53Alias = make(map[string]string)
+ rec.R53Alias["type"] = "A"
+ rec.R53Alias["zone_id"] = "blarg"
+ rec.R53Alias["evaluate_target_health"] = "true"
+ w := `R53_ALIAS("foo", "A", "bar", R53_ZONE("blarg"), R53_EVALUATE_TARGET_HEALTH(true), TTL(123))`
+ if g := makeR53alias(&rec, 123); g != w {
+ t.Errorf("makeR53alias failure: got `%s` want `%s`", g, w)
+ }
+}
diff --git a/commands/test_data/apex.com.zone.djs b/commands/test_data/apex.com.zone.djs
index 24fe6ccd30..a24b5c718e 100644
--- a/commands/test_data/apex.com.zone.djs
+++ b/commands/test_data/apex.com.zone.djs
@@ -1,5 +1,6 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("apex.com", REG_CHANGEME
, DnsProvider(DSP_BIND)
//, SOA("@", "ns3.serverfault.com.", "sysadmin.stackoverflow.com.", 2020022300, 3600, 600, 604800, 1440)
@@ -11,3 +12,4 @@ D("apex.com", REG_CHANGEME
, CNAME("@", "cnametest1.example.com.")
, CNAME("www", "cnametest2.example.com.")
)
+
diff --git a/commands/test_data/apex.com.zone.js b/commands/test_data/apex.com.zone.js
index 2e19a309db..cf4404be4e 100644
--- a/commands/test_data/apex.com.zone.js
+++ b/commands/test_data/apex.com.zone.js
@@ -1,5 +1,6 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("apex.com", REG_CHANGEME,
DnsProvider(DSP_BIND),
//SOA("@", "ns3.serverfault.com.", "sysadmin.stackoverflow.com.", 2020022300, 3600, 600, 604800, 1440),
@@ -9,5 +10,6 @@ D("apex.com", REG_CHANGEME,
//NAMESERVER("ns-cloud-c2.googledomains.com."),
// NOTE: CNAME at apex may require manual editing.
CNAME("@", "cnametest1.example.com."),
- CNAME("www", "cnametest2.example.com.")
-)
+ CNAME("www", "cnametest2.example.com."),
+);
+
diff --git a/commands/test_data/ds.com.zone.djs b/commands/test_data/ds.com.zone.djs
index a6fd23a01d..9283527e4f 100644
--- a/commands/test_data/ds.com.zone.djs
+++ b/commands/test_data/ds.com.zone.djs
@@ -1,7 +1,9 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("ds.com", REG_CHANGEME
, DnsProvider(DSP_BIND)
//, SOA("@", "ns3.serverfault.com.", "sysadmin.stackoverflow.com.", 2020022300, 3600, 600, 604800, 1440)
, DS("geo", 14480, 13, 2, "BB1C4B615CDED2B34347CF23710471934D972F1E34F53B54ED8D5F786202C73B")
)
+
diff --git a/commands/test_data/ds.com.zone.js b/commands/test_data/ds.com.zone.js
index 2a1b010810..365f8fdd92 100644
--- a/commands/test_data/ds.com.zone.js
+++ b/commands/test_data/ds.com.zone.js
@@ -1,7 +1,9 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("ds.com", REG_CHANGEME,
DnsProvider(DSP_BIND),
//SOA("@", "ns3.serverfault.com.", "sysadmin.stackoverflow.com.", 2020022300, 3600, 600, 604800, 1440),
- DS("geo", 14480, 13, 2, "BB1C4B615CDED2B34347CF23710471934D972F1E34F53B54ED8D5F786202C73B")
-)
+ DS("geo", 14480, 13, 2, "BB1C4B615CDED2B34347CF23710471934D972F1E34F53B54ED8D5F786202C73B"),
+);
+
diff --git a/commands/test_data/example.org.zone.djs b/commands/test_data/example.org.zone.djs
index e4b695a3ff..c4ef92e60f 100644
--- a/commands/test_data/example.org.zone.djs
+++ b/commands/test_data/example.org.zone.djs
@@ -1,5 +1,6 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("example.org", REG_CHANGEME
, DnsProvider(DSP_BIND)
, DefaultTTL(7200)
@@ -37,7 +38,7 @@ D("example.org", REG_CHANGEME
, SRV("_pop3._tcp", 0, 0, 0, ".")
, SRV("_pop3s._tcp", 0, 0, 0, ".")
, SRV("_sieve._tcp", 10, 10, 4190, "imap.example.org.")
- , TXT("dns-moreinfo", ["Fred Bloggs, TZ=America/New_York", "Chat-Service-X: @handle1", "Chat-Service-Y: federated-handle@example.org"])
+ , TXT("dns-moreinfo", "Fred Bloggs, TZ=America/New_YorkChat-Service-X: @handle1Chat-Service-Y: federated-handle@example.org")
, SRV("_pgpkey-http._tcp", 0, 0, 0, ".")
, SRV("_pgpkey-https._tcp", 0, 0, 0, ".")
, SRV("_hkp._tcp", 0, 0, 0, ".")
@@ -48,9 +49,9 @@ D("example.org", REG_CHANGEME
, AAAA("@", "2001:db8::1:1")
, TXT("_adsp._domainkey", "dkim=all")
, TXT("_dmarc", "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s")
- , TXT("d201911._domainkey", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks", "6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"])
+ , TXT("d201911._domainkey", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB")
, TXT("d201911e2._domainkey", "v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo=")
- , TXT("d202003._domainkey", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo", "pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"])
+ , TXT("d202003._domainkey", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jopv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB")
, TXT("d202003e2._domainkey", "v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg=")
, TXT("_report", "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;")
, TXT("_smtp._tls", "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org")
@@ -311,9 +312,9 @@ D("example.org", REG_CHANGEME
, A("fred", "192.0.2.93")
, AAAA("fred", "2001:db8::48:4558:5345:5256")
, TXT("fred", "v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all")
- , TXT("d201911._domainkey.fred", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz", "P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"])
+ , TXT("d201911._domainkey.fred", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/TlzP2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB")
, TXT("d201911e2._domainkey.fred", "v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A=")
- , TXT("d202003._domainkey.fred", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj", "c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"])
+ , TXT("d202003._domainkey.fred", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYjc0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB")
, TXT("d202003e2._domainkey.fred", "v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw=")
, TXT("_adsp._domainkey.fred", "dkim=all")
, TXT("_dmarc.fred", "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s")
@@ -321,9 +322,9 @@ D("example.org", REG_CHANGEME
, TXT("_smtp._tls.fred", "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org")
, TXT("_smtp-tlsrpt.fred", "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org")
, MX("mailtest", 10, "mx.example.org.")
- , TXT("d201911._domainkey.mailtest", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn", "0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"])
+ , TXT("d201911._domainkey.mailtest", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB")
, TXT("d201911e2._domainkey.mailtest", "v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y=")
- , TXT("d202003._domainkey.mailtest", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN", "aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"])
+ , TXT("d202003._domainkey.mailtest", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KNaS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB")
, TXT("d202003e2._domainkey.mailtest", "v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc=")
, TXT("_adsp._domainkey.mailtest", "dkim=all")
, TXT("_dmarc.mailtest", "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s")
@@ -353,3 +354,4 @@ D("example.org", REG_CHANGEME
, CNAME("zyxwvutsrqpo", "gv-nmlkjihgfedcba.dv.googlehosted.com.")
, CNAME("0123456789abcdef0123456789abcdef", "verify.bing.com.")
)
+
diff --git a/commands/test_data/example.org.zone.js b/commands/test_data/example.org.zone.js
index 70f253630d..52ad747f14 100644
--- a/commands/test_data/example.org.zone.js
+++ b/commands/test_data/example.org.zone.js
@@ -1,5 +1,6 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("example.org", REG_CHANGEME,
DnsProvider(DSP_BIND),
DefaultTTL(7200),
@@ -37,7 +38,7 @@ D("example.org", REG_CHANGEME,
SRV("_pop3._tcp", 0, 0, 0, "."),
SRV("_pop3s._tcp", 0, 0, 0, "."),
SRV("_sieve._tcp", 10, 10, 4190, "imap.example.org."),
- TXT("dns-moreinfo", ["Fred Bloggs, TZ=America/New_York", "Chat-Service-X: @handle1", "Chat-Service-Y: federated-handle@example.org"]),
+ TXT("dns-moreinfo", "Fred Bloggs, TZ=America/New_YorkChat-Service-X: @handle1Chat-Service-Y: federated-handle@example.org"),
SRV("_pgpkey-http._tcp", 0, 0, 0, "."),
SRV("_pgpkey-https._tcp", 0, 0, 0, "."),
SRV("_hkp._tcp", 0, 0, 0, "."),
@@ -48,9 +49,9 @@ D("example.org", REG_CHANGEME,
AAAA("@", "2001:db8::1:1"),
TXT("_adsp._domainkey", "dkim=all"),
TXT("_dmarc", "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"),
- TXT("d201911._domainkey", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks", "6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"]),
+ TXT("d201911._domainkey", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"),
TXT("d201911e2._domainkey", "v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo="),
- TXT("d202003._domainkey", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo", "pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"]),
+ TXT("d202003._domainkey", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jopv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"),
TXT("d202003e2._domainkey", "v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg="),
TXT("_report", "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"),
TXT("_smtp._tls", "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"),
@@ -311,9 +312,9 @@ D("example.org", REG_CHANGEME,
A("fred", "192.0.2.93"),
AAAA("fred", "2001:db8::48:4558:5345:5256"),
TXT("fred", "v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"),
- TXT("d201911._domainkey.fred", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz", "P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"]),
+ TXT("d201911._domainkey.fred", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/TlzP2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"),
TXT("d201911e2._domainkey.fred", "v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A="),
- TXT("d202003._domainkey.fred", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj", "c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"]),
+ TXT("d202003._domainkey.fred", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYjc0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"),
TXT("d202003e2._domainkey.fred", "v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw="),
TXT("_adsp._domainkey.fred", "dkim=all"),
TXT("_dmarc.fred", "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"),
@@ -321,9 +322,9 @@ D("example.org", REG_CHANGEME,
TXT("_smtp._tls.fred", "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"),
TXT("_smtp-tlsrpt.fred", "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"),
MX("mailtest", 10, "mx.example.org."),
- TXT("d201911._domainkey.mailtest", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn", "0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"]),
+ TXT("d201911._domainkey.mailtest", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"),
TXT("d201911e2._domainkey.mailtest", "v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y="),
- TXT("d202003._domainkey.mailtest", ["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN", "aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"]),
+ TXT("d202003._domainkey.mailtest", "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KNaS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"),
TXT("d202003e2._domainkey.mailtest", "v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc="),
TXT("_adsp._domainkey.mailtest", "dkim=all"),
TXT("_dmarc.mailtest", "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"),
@@ -351,5 +352,6 @@ D("example.org", REG_CHANGEME,
CNAME("_fedcba9876543210fedcba9876543210.go", "_45678901234abcdef45678901234abcd.ggedgsdned.acm-validations.aws."),
CNAME("opqrstuvwxyz", "gv-abcdefghijklmn.dv.googlehosted.com."),
CNAME("zyxwvutsrqpo", "gv-nmlkjihgfedcba.dv.googlehosted.com."),
- CNAME("0123456789abcdef0123456789abcdef", "verify.bing.com.")
-)
+ CNAME("0123456789abcdef0123456789abcdef", "verify.bing.com."),
+);
+
diff --git a/commands/test_data/example.org.zone.tsv b/commands/test_data/example.org.zone.tsv
index ca15d2fc8a..abae4c9693 100644
--- a/commands/test_data/example.org.zone.tsv
+++ b/commands/test_data/example.org.zone.tsv
@@ -4,7 +4,7 @@ example.org @ 7200 IN NS ns2.example.org.
example.org @ 7200 IN NS ns-a.example.net.
example.org @ 7200 IN NS friend-dns.example.com.
example.org @ 7200 IN MX 10 mx.example.org.
-example.org @ 7200 IN TXT "v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
+example.org @ 7200 IN TXT v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all
_client._smtp.example.org _client._smtp 7200 IN SRV 1 1 1 example.org.
_client._smtp.mx.example.org _client._smtp.mx 7200 IN SRV 1 2 1 mx.example.org.
_client._smtp.foo.example.org _client._smtp.foo 7200 IN SRV 1 2 1 foo.example.org.
@@ -12,7 +12,7 @@ _kerberos._tcp.example.org _kerberos._tcp 7200 IN SRV 10 1 88 kerb-service.examp
_kerberos._udp.example.org _kerberos._udp 7200 IN SRV 10 1 88 kerb-service.example.org.
_kpasswd._udp.example.org _kpasswd._udp 7200 IN SRV 10 1 464 kerb-service.example.org.
_kerberos-adm._tcp.example.org _kerberos-adm._tcp 7200 IN SRV 10 1 749 kerb-service.example.org.
-_kerberos.example.org _kerberos 7200 IN TXT "EXAMPLE.ORG"
+_kerberos.example.org _kerberos 7200 IN TXT EXAMPLE.ORG
_ldap._tcp.example.org _ldap._tcp 7200 IN SRV 0 0 0 .
_ldap._udp.example.org _ldap._udp 7200 IN SRV 0 0 0 .
_jabber._tcp.example.org _jabber._tcp 7200 IN SRV 10 2 5269 xmpp-s2s.example.org.
@@ -32,7 +32,7 @@ _imaps._tcp.example.org _imaps._tcp 7200 IN SRV 10 10 993 imap.example.org.
_pop3._tcp.example.org _pop3._tcp 7200 IN SRV 0 0 0 .
_pop3s._tcp.example.org _pop3s._tcp 7200 IN SRV 0 0 0 .
_sieve._tcp.example.org _sieve._tcp 7200 IN SRV 10 10 4190 imap.example.org.
-dns-moreinfo.example.org dns-moreinfo 7200 IN TXT "Fred Bloggs, TZ=America/New_York" "Chat-Service-X: @handle1" "Chat-Service-Y: federated-handle@example.org"
+dns-moreinfo.example.org dns-moreinfo 7200 IN TXT Fred Bloggs, TZ=America/New_YorkChat-Service-X: @handle1Chat-Service-Y: federated-handle@example.org
_pgpkey-http._tcp.example.org _pgpkey-http._tcp 7200 IN SRV 0 0 0 .
_pgpkey-https._tcp.example.org _pgpkey-https._tcp 7200 IN SRV 0 0 0 .
_hkp._tcp.example.org _hkp._tcp 7200 IN SRV 0 0 0 .
@@ -41,20 +41,20 @@ _finger._tcp.example.org _finger._tcp 7200 IN SRV 10 10 79 barbican.example.org.
_avatars-sec._tcp.example.org _avatars-sec._tcp 7200 IN SRV 10 10 443 avatars.example.org.
example.org @ 7200 IN A 192.0.2.1
example.org @ 7200 IN AAAA 2001:db8::1:1
-_adsp._domainkey.example.org _adsp._domainkey 7200 IN TXT "dkim=all"
-_dmarc.example.org _dmarc 7200 IN TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
-d201911._domainkey.example.org d201911._domainkey 7200 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks" "6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"
-d201911e2._domainkey.example.org d201911e2._domainkey 7200 IN TXT "v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo="
-d202003._domainkey.example.org d202003._domainkey 7200 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo" "pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"
-d202003e2._domainkey.example.org d202003e2._domainkey 7200 IN TXT "v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg="
-_report.example.org _report 7200 IN TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
-_smtp._tls.example.org _smtp._tls 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
-_smtp-tlsrpt.example.org _smtp-tlsrpt 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
-example.net._report._dmarc.example.org example.net._report._dmarc 7200 IN TXT "v=DMARC1"
-example.com._report._dmarc.example.org example.com._report._dmarc 7200 IN TXT "v=DMARC1"
-xn--2j5b.xn--9t4b11yi5a._report._dmarc.example.org xn--2j5b.xn--9t4b11yi5a._report._dmarc 7200 IN TXT "v=DMARC1"
-special.test._report._dmarc.example.org special.test._report._dmarc 7200 IN TXT "v=DMARC1"
-xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc.example.org xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc 7200 IN TXT "v=DMARC1"
+_adsp._domainkey.example.org _adsp._domainkey 7200 IN TXT dkim=all
+_dmarc.example.org _dmarc 7200 IN TXT v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s
+d201911._domainkey.example.org d201911._domainkey 7200 IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB
+d201911e2._domainkey.example.org d201911e2._domainkey 7200 IN TXT v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo=
+d202003._domainkey.example.org d202003._domainkey 7200 IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jopv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB
+d202003e2._domainkey.example.org d202003e2._domainkey 7200 IN TXT v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg=
+_report.example.org _report 7200 IN TXT r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;
+_smtp._tls.example.org _smtp._tls 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
+_smtp-tlsrpt.example.org _smtp-tlsrpt 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
+example.net._report._dmarc.example.org example.net._report._dmarc 7200 IN TXT v=DMARC1
+example.com._report._dmarc.example.org example.com._report._dmarc 7200 IN TXT v=DMARC1
+xn--2j5b.xn--9t4b11yi5a._report._dmarc.example.org xn--2j5b.xn--9t4b11yi5a._report._dmarc 7200 IN TXT v=DMARC1
+special.test._report._dmarc.example.org special.test._report._dmarc 7200 IN TXT v=DMARC1
+xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc.example.org xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc 7200 IN TXT v=DMARC1
*._smimecert.example.org *._smimecert 7200 IN CNAME _ourca-smimea.example.org.
b._dns-sd._udp.example.org b._dns-sd._udp 7200 IN PTR field.example.org.
lb._dns-sd._udp.example.org lb._dns-sd._udp 7200 IN PTR field.example.org.
@@ -265,9 +265,9 @@ mx.example.org mx 7200 IN A 192.0.2.25
mx.example.org mx 7200 IN AAAA 2001:db8::48:4558:736d:7470
mx.ipv4.example.org mx.ipv4 7200 IN A 192.0.2.25
mx.ipv6.example.org mx.ipv6 7200 IN AAAA 2001:db8::48:4558:736d:7470
-mx.example.org mx 7200 IN TXT "v=spf1 a include:_spflarge.example.net -all"
-_mta-sts.example.org _mta-sts 7200 IN TXT "v=STSv1; id=20191231r1;"
-mta-sts.example.org mta-sts 7200 IN TXT "v=STSv1; id=20191231r1;"
+mx.example.org mx 7200 IN TXT v=spf1 a include:_spflarge.example.net -all
+_mta-sts.example.org _mta-sts 7200 IN TXT v=STSv1; id=20191231r1;
+mta-sts.example.org mta-sts 7200 IN TXT v=STSv1; id=20191231r1;
mta-sts.example.org mta-sts 7200 IN A 192.0.2.93
mta-sts.example.org mta-sts 7200 IN AAAA 2001:db8::48:4558:5345:5256
xmpp.ipv6.example.org xmpp.ipv6 7200 IN AAAA 2001:db8::f0ab:cdef:1234:f00f
@@ -297,34 +297,34 @@ news-feed.example.org news-feed 7200 IN AAAA 2001:db8::48:4558:6e6e:7470
go.example.org go 7200 IN CNAME abcdefghijklmn.cloudfront.net.
foo.example.org foo 7200 IN A 192.0.2.200
gladys.example.org gladys 7200 IN MX 10 mx.example.org.
-_adsp._domainkey.gladys.example.org _adsp._domainkey.gladys 7200 IN TXT "dkim=all"
-_dmarc.gladys.example.org _dmarc.gladys 7200 IN TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
-_report.gladys.example.org _report.gladys 7200 IN TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
-_smtp._tls.gladys.example.org _smtp._tls.gladys 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
-_smtp-tlsrpt.gladys.example.org _smtp-tlsrpt.gladys 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+_adsp._domainkey.gladys.example.org _adsp._domainkey.gladys 7200 IN TXT dkim=all
+_dmarc.gladys.example.org _dmarc.gladys 7200 IN TXT v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s
+_report.gladys.example.org _report.gladys 7200 IN TXT r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;
+_smtp._tls.gladys.example.org _smtp._tls.gladys 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
+_smtp-tlsrpt.gladys.example.org _smtp-tlsrpt.gladys 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
fred.example.org fred 7200 IN MX 10 mx.example.org.
fred.example.org fred 7200 IN A 192.0.2.93
fred.example.org fred 7200 IN AAAA 2001:db8::48:4558:5345:5256
-fred.example.org fred 7200 IN TXT "v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
-d201911._domainkey.fred.example.org d201911._domainkey.fred 7200 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz" "P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"
-d201911e2._domainkey.fred.example.org d201911e2._domainkey.fred 7200 IN TXT "v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A="
-d202003._domainkey.fred.example.org d202003._domainkey.fred 7200 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj" "c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"
-d202003e2._domainkey.fred.example.org d202003e2._domainkey.fred 7200 IN TXT "v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw="
-_adsp._domainkey.fred.example.org _adsp._domainkey.fred 7200 IN TXT "dkim=all"
-_dmarc.fred.example.org _dmarc.fred 7200 IN TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
-_report.fred.example.org _report.fred 7200 IN TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
-_smtp._tls.fred.example.org _smtp._tls.fred 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
-_smtp-tlsrpt.fred.example.org _smtp-tlsrpt.fred 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+fred.example.org fred 7200 IN TXT v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all
+d201911._domainkey.fred.example.org d201911._domainkey.fred 7200 IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/TlzP2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB
+d201911e2._domainkey.fred.example.org d201911e2._domainkey.fred 7200 IN TXT v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A=
+d202003._domainkey.fred.example.org d202003._domainkey.fred 7200 IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYjc0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB
+d202003e2._domainkey.fred.example.org d202003e2._domainkey.fred 7200 IN TXT v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw=
+_adsp._domainkey.fred.example.org _adsp._domainkey.fred 7200 IN TXT dkim=all
+_dmarc.fred.example.org _dmarc.fred 7200 IN TXT v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s
+_report.fred.example.org _report.fred 7200 IN TXT r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;
+_smtp._tls.fred.example.org _smtp._tls.fred 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
+_smtp-tlsrpt.fred.example.org _smtp-tlsrpt.fred 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
mailtest.example.org mailtest 7200 IN MX 10 mx.example.org.
-d201911._domainkey.mailtest.example.org d201911._domainkey.mailtest 7200 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn" "0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"
-d201911e2._domainkey.mailtest.example.org d201911e2._domainkey.mailtest 7200 IN TXT "v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y="
-d202003._domainkey.mailtest.example.org d202003._domainkey.mailtest 7200 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN" "aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"
-d202003e2._domainkey.mailtest.example.org d202003e2._domainkey.mailtest 7200 IN TXT "v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc="
-_adsp._domainkey.mailtest.example.org _adsp._domainkey.mailtest 7200 IN TXT "dkim=all"
-_dmarc.mailtest.example.org _dmarc.mailtest 7200 IN TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
-_report.mailtest.example.org _report.mailtest 7200 IN TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
-_smtp._tls.mailtest.example.org _smtp._tls.mailtest 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
-_smtp-tlsrpt.mailtest.example.org _smtp-tlsrpt.mailtest 7200 IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
+d201911._domainkey.mailtest.example.org d201911._domainkey.mailtest 7200 IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB
+d201911e2._domainkey.mailtest.example.org d201911e2._domainkey.mailtest 7200 IN TXT v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y=
+d202003._domainkey.mailtest.example.org d202003._domainkey.mailtest 7200 IN TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KNaS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB
+d202003e2._domainkey.mailtest.example.org d202003e2._domainkey.mailtest 7200 IN TXT v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc=
+_adsp._domainkey.mailtest.example.org _adsp._domainkey.mailtest 7200 IN TXT dkim=all
+_dmarc.mailtest.example.org _dmarc.mailtest 7200 IN TXT v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s
+_report.mailtest.example.org _report.mailtest 7200 IN TXT r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;
+_smtp._tls.mailtest.example.org _smtp._tls.mailtest 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
+_smtp-tlsrpt.mailtest.example.org _smtp-tlsrpt.mailtest 7200 IN TXT v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org
_pgpkey-http._tcp.sks.example.org _pgpkey-http._tcp.sks 7200 IN SRV 0 0 0 .
_pgpkey-https._tcp.sks.example.org _pgpkey-https._tcp.sks 7200 IN SRV 0 0 0 .
_hkp._tcp.sks.example.org _hkp._tcp.sks 7200 IN SRV 0 0 0 .
@@ -341,7 +341,7 @@ khard.example.org khard 7200 IN NS ns-cloud-d2.googledomains.com.
khard.example.org khard 7200 IN NS ns-cloud-d3.googledomains.com.
khard.example.org khard 7200 IN NS ns-cloud-d4.googledomains.com.
realhost.example.org realhost 7200 IN MX 0 .
-realhost.example.org realhost 7200 IN TXT "v=spf1 -all"
+realhost.example.org realhost 7200 IN TXT v=spf1 -all
_25._tcp.realhost.example.org _25._tcp.realhost 7200 IN TLSA 3 0 0 0000000000000000000000000000000000000000000000000000000000000000
_fedcba9876543210fedcba9876543210.go.example.org _fedcba9876543210fedcba9876543210.go 7200 IN CNAME _45678901234abcdef45678901234abcd.ggedgsdned.acm-validations.aws.
opqrstuvwxyz.example.org opqrstuvwxyz 7200 IN CNAME gv-abcdefghijklmn.dv.googlehosted.com.
diff --git a/commands/test_data/example.org.zone.zone b/commands/test_data/example.org.zone.zone
index 0c48481199..384b8979d6 100644
--- a/commands/test_data/example.org.zone.zone
+++ b/commands/test_data/example.org.zone.zone
@@ -32,9 +32,9 @@ special.test._report._dmarc IN TXT "v=DMARC1"
xn--2j5b.xn--9t4b11yi5a._report._dmarc IN TXT "v=DMARC1"
xn--qck5b9a5eml3bze.xn--zckzah._report._dmarc IN TXT "v=DMARC1"
_adsp._domainkey IN TXT "dkim=all"
-d201911._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks" "6cVGxXBZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"
+d201911._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4SmyE5Tz5/wPL8cb2AKuHnlFeLMOhAl1UX/NYaeDCKMWoBPTgZRT0jonKLmV2UscHdodXu5ZsLr/NAuLCp7HmPLReLz7kxKncP6ppveKxc1aq5SPTKeWe77p6BptlahHc35eiXsZRpTsEzrbEOainy1IWEd+w9p1gWbrSutwE22z0i4V88nQ9UBa1ks6cVGxX" "BZFovWC+i28aGs6Lc7cSfHG5+Mrg3ud5X4evYXTGFMPpunMcCsXrqmS5a+5gRSEMZhngha/cHjLwaJnWzKaywNWF5XOsCjL94QkS0joB7lnGOHMNSZBCcu542Y3Ht3SgHhlpkF9mIbIRfpzA9IoSQIDAQAB"
d201911e2._domainkey IN TXT "v=DKIM1; k=ed25519; p=GBt2k2L39KUb39fg5brOppXDHXvISy0+ECGgPld/bIo="
-d202003._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jo" "pv0d4dR6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"
+d202003._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/1tQvOEs7xtKNm7PbPgY4hQjwHVvqqkDb0+TeqZHYRSczQ3c0LFJrIDFiPIdwQe/7AuKrxvATSh/uXKZ3EP4ouMgROPZnUxVXENeetJj+pc3nfGwTKUBTTTth+SO74gdIWsntjvAfduzosC4ZkxbDwZ9c253qXARGvGu+LB/iAeq0ngEbm5fU13+Jopv0d4d" "R6oGe9GvMEnGGLZzNrxWl1BPe2x5JZ5/X/3fW8vJx3OgRB5N6fqbAJ6HZ9kcbikDH4lPPl9RIoprFk7mmwno/nXLQYGhPobmqq8wLkDiXEkWtYa5lzujz3XI3Zkk8ZIOGvdbVVfAttT0IVPnYkOhQIDAQAB"
d202003e2._domainkey IN TXT "v=DKIM1; k=ed25519; p=DQI5d9sNMrr0SLDoAi071IFOyKnlbR29hAQdqVQecQg="
_kerberos IN TXT "EXAMPLE.ORG"
_le-amazon-tlsa IN TLSA 2 0 1 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4
@@ -124,7 +124,7 @@ _acme-challenge.conference 15 IN CNAME _acme-challenge.conference.chat-acme.d.ex
_xmpp-server._tcp.conference IN SRV 10 2 5269 chat.example.org.
IN SRV 10 2 5269 xmpp-s2s.example.org.
dict IN CNAME services.example.org.
-dns-moreinfo IN TXT "Fred Bloggs, TZ=America/New_York" "Chat-Service-X: @handle1" "Chat-Service-Y: federated-handle@example.org"
+dns-moreinfo IN TXT "Fred Bloggs, TZ=America/New_YorkChat-Service-X: @handle1Chat-Service-Y: federated-handle@example.org"
field IN NS ns1.example.org.
IN NS ns2.example.org.
finger IN CNAME barbican.example.org.
@@ -136,9 +136,9 @@ fred IN A 192.0.2.93
IN TXT "v=spf1 ip4:192.0.2.25 ip6:2001:db8::1:25 mx include:_spf.example.com ~all"
_dmarc.fred IN TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
_adsp._domainkey.fred IN TXT "dkim=all"
-d201911._domainkey.fred IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/Tlz" "P2eenyiFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"
+d201911._domainkey.fred IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8/OMUa3PnWh9LqXFVwlAgYDdTtbq3zTtTOSBmJq5yWauzXYcUuSmhW7CsV0QQlacCsQgJlwg9Nl1vO1TosAj5EKUCLTeSqjlWrM7KXKPx8FT71Q9H9wXX4MHUyGrqHFo0OPzcmtHwqcd8AD6MIvJHSRoAfiPPBp8Euc0wGnJZdGS75Hk+wA3MQ2/TlzP2eeny" "iFyqmUTAGOYsGC/tREsWPiegR/OVxNGlzTY6quHsuVK7UYtIyFnYx9PGWdl3b3p7VjQ5V0Rp+2CLtVrCuS6Zs+/3NhZdM7mdD0a9Jgxakwa1le5YmB5lHTGF7T8quy6TlKe9lMUIRNjqTHfSFz/MwIDAQAB"
d201911e2._domainkey.fred IN TXT "v=DKIM1; k=ed25519; p=rQNsV9YcPJn/WYI1EDLjNbN/VuX1Hqq/oe4htbnhv+A="
-d202003._domainkey.fred IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYj" "c0h+FMDpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"
+d202003._domainkey.fred IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnx7tnRxAnE/poIRbVb2i+f1uQCXWnBHzHurgEyZX0CmGaiJuCbr8SWOW2PoXq9YX8gIv2TS3uzwGv/4yA2yX9Z9zar1LeWUfGgMWLdCol9xfmWrI+6MUzxuwhw/mXwzigbI4bHoakh3ez/i3J9KPS85GfrOODqA1emR13f2pG8EzAcje+rwW2PtYjc0h+FM" "DpeLuPYyYszFbNlrkVUneesxnoz+o4x/s6P14ZoRqz5CR7u6G02HwnNaHads5Eto6FYYErUUTtFmgWuYabHxgLVGRdRQs6B5OBYT/3L2q/lAgmEgdy/QL+c0Psfj99/XQmO8fcM0scBzw2ukQzcUwIDAQAB"
d202003e2._domainkey.fred IN TXT "v=DKIM1; k=ed25519; p=0DAPp/IRLYFI/Z4YSgJRi4gr7xcu1/EfJ5mjVn10aAw="
_report.fred IN TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
_smtp-tlsrpt.fred IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
@@ -247,9 +247,9 @@ kpeople IN AAAA 2001:db8::48:4558:6b70:706c
mailtest IN MX 10 mx.example.org.
_dmarc.mailtest IN TXT "v=DMARC1; p=none; sp=none; rua=mailto:dmarc-notify@example.org; ruf=mailto:dmarc-notify@example.org; adkim=s"
_adsp._domainkey.mailtest IN TXT "dkim=all"
-d201911._domainkey.mailtest IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn" "0XTY6XYgug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"
+d201911._domainkey.mailtest IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo9xHnjHyhm1weA6FjOqM8LKVsklFt26HXWoe/0XCdmBG4i/UzQ7RiSgWO4kv7anPK6qf6rtL1xYsHufaRXG8yLsZxz+BbUP99eZvxZX78tMg4cGf+yU6uFxulCbOzsMy+8Cc3bbQTtIWYjyWBwnHdRRrCkQxjZ5KAd+x7ZB5qzqg2/eLJ7fCuNsr/xn0XTY6X" "Ygug95e3h4CEW3Y+bkG81AMeJmT/hoVTcXvT/Gm6ZOUmx6faQWIHSW7qOR3VS6S75HOuclEUk0gt9r7OQHKl01sXh8g02SHRk8SUMEoNVayqplYZTFFF01Z192m7enmpp+St+HHUIT6jW/CAMCO3wIDAQAB"
d201911e2._domainkey.mailtest IN TXT "v=DKIM1; k=ed25519; p=afulDDnhaTzdqKQN0jtWV04eOhAcyBk3NCyVheOf53Y="
-d202003._domainkey.mailtest IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KN" "aS1okNRRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"
+d202003._domainkey.mailtest IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs2BTVZaVLvL3qZBPaF7tRR0SdOKe+hjcpQ5fqO48lEuYiyTb6lkn8DPjDK11gTN3au0Bm+y8KC7ITKSJosuJXytxt3wqc61Pwtmb/Cy7GzmOF1AuegydB3/88VbgHT5DZucHrh6+ValZk4Trkx+/1K26Uo+h2KL2n/Ldb1y91ATHujp8DqxAOhiZ7KNaS1okN" "RRB4/14jPufAbeiN8/iBPiY5Hl80KHmpjM+7vvjb5jiecZ1ZrVDj7eTES4pmVh2v1c106mZLieoqDPYaf/HVbCM4E4n1B6kjbboSOpANADIcqXxGJQ7Be7/Sk9f7KwRusrsMHXmBHgm4wPmwGVZ3QIDAQAB"
d202003e2._domainkey.mailtest IN TXT "v=DKIM1; k=ed25519; p=iqwH/hhozFdeo1xnuldr8KUi7O7g+DzmC+f0SYMKVDc="
_report.mailtest IN TXT "r=abuse-reports@example.org; rf=ARF; re=postmaster@example.org;"
_smtp-tlsrpt.mailtest IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-reports@example.org"
diff --git a/commands/test_data/simple.com.zone.djs b/commands/test_data/simple.com.zone.djs
index f230f45ab8..ada1d4f7cb 100644
--- a/commands/test_data/simple.com.zone.djs
+++ b/commands/test_data/simple.com.zone.djs
@@ -1,5 +1,6 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("simple.com", REG_CHANGEME
, DnsProvider(DSP_BIND)
//, SOA("@", "ns3.serverfault.com.", "sysadmin.stackoverflow.com.", 2020022300, 3600, 600, 604800, 1440)
@@ -22,3 +23,4 @@ D("simple.com", REG_CHANGEME
, CNAME("info", "stackoverflow.mktoweb.com.")
, SRV("_sip._tcp", 10, 60, 5060, "bigbox.example.com.")
)
+
diff --git a/commands/test_data/simple.com.zone.js b/commands/test_data/simple.com.zone.js
index 9f17b4545e..e32e83de13 100644
--- a/commands/test_data/simple.com.zone.js
+++ b/commands/test_data/simple.com.zone.js
@@ -1,5 +1,6 @@
var DSP_BIND = NewDnsProvider("bind", "BIND");
var REG_CHANGEME = NewRegistrar("none");
+
D("simple.com", REG_CHANGEME,
DnsProvider(DSP_BIND),
//SOA("@", "ns3.serverfault.com.", "sysadmin.stackoverflow.com.", 2020022300, 3600, 600, 604800, 1440),
@@ -20,5 +21,6 @@ D("simple.com", REG_CHANGEME,
TXT("m1._domainkey.dev-email", "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIBezZ2Gc+/3PghWk+YOE6T9HdwgUTMTR0Fne2i51MNN9Qs7AqDitVdG/949iDbI2fPNZSnKtOcnlLYwvve9MhMAMI1nZ26ILhgaBJi2BMZQpGFlO4ucuo/Uj4DPZ5Ge/NZHCX0CRhAhR5sRmL2OffNcFXFrymzUuz4KzI/NyUiwIDAQAB"),
CNAME("email", "mkto-sj280138.com."),
CNAME("info", "stackoverflow.mktoweb.com."),
- SRV("_sip._tcp", 10, 60, 5060, "bigbox.example.com.")
-)
+ SRV("_sip._tcp", 10, 60, 5060, "bigbox.example.com."),
+);
+
diff --git a/commands/test_data/simple.com.zone.tsv b/commands/test_data/simple.com.zone.tsv
index 7e478f4342..b9e95d03d8 100644
--- a/commands/test_data/simple.com.zone.tsv
+++ b/commands/test_data/simple.com.zone.tsv
@@ -8,12 +8,12 @@ simple.com @ 300 IN MX 5 alt1.aspmx.l.google.com.
simple.com @ 300 IN MX 5 alt2.aspmx.l.google.com.
simple.com @ 300 IN MX 10 alt3.aspmx.l.google.com.
simple.com @ 300 IN MX 10 alt4.aspmx.l.google.com.
-simple.com @ 300 IN TXT "google-site-verification=O54a_pYHGr4EB8iLoGFgX8OTZ1DkP1KWnOLpx0YCazI"
-simple.com @ 300 IN TXT "v=spf1 mx include:mktomail.com ~all"
-m1._domainkey.simple.com m1._domainkey 300 IN TXT "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZfEV2C82eJ4OA3Mslz4C6msjYYalg1eUcHeJQ//QM1hOZSvn4qz+hSKGi7jwNDqsZNzM8vCt2+XzdDYL3JddwUEhoDsIsZsJW0qzIVVLLWCg6TLNS3FpVyjc171o94dpoHFekfswWDoEwFQ03Woq2jchYWBrbUf7MMcdEj/EQqwIDAQAB"
+simple.com @ 300 IN TXT google-site-verification=O54a_pYHGr4EB8iLoGFgX8OTZ1DkP1KWnOLpx0YCazI
+simple.com @ 300 IN TXT v=spf1 mx include:mktomail.com ~all
+m1._domainkey.simple.com m1._domainkey 300 IN TXT v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZfEV2C82eJ4OA3Mslz4C6msjYYalg1eUcHeJQ//QM1hOZSvn4qz+hSKGi7jwNDqsZNzM8vCt2+XzdDYL3JddwUEhoDsIsZsJW0qzIVVLLWCg6TLNS3FpVyjc171o94dpoHFekfswWDoEwFQ03Woq2jchYWBrbUf7MMcdEj/EQqwIDAQAB
dev.simple.com dev 300 IN CNAME stackoverflowsandbox2.mktoweb.com.
dev-email.simple.com dev-email 300 IN CNAME mkto-sj310056.com.
-m1._domainkey.dev-email.simple.com m1._domainkey.dev-email 300 IN TXT "v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIBezZ2Gc+/3PghWk+YOE6T9HdwgUTMTR0Fne2i51MNN9Qs7AqDitVdG/949iDbI2fPNZSnKtOcnlLYwvve9MhMAMI1nZ26ILhgaBJi2BMZQpGFlO4ucuo/Uj4DPZ5Ge/NZHCX0CRhAhR5sRmL2OffNcFXFrymzUuz4KzI/NyUiwIDAQAB"
+m1._domainkey.dev-email.simple.com m1._domainkey.dev-email 300 IN TXT v=DKIM1;k=rsa;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIBezZ2Gc+/3PghWk+YOE6T9HdwgUTMTR0Fne2i51MNN9Qs7AqDitVdG/949iDbI2fPNZSnKtOcnlLYwvve9MhMAMI1nZ26ILhgaBJi2BMZQpGFlO4ucuo/Uj4DPZ5Ge/NZHCX0CRhAhR5sRmL2OffNcFXFrymzUuz4KzI/NyUiwIDAQAB
email.simple.com email 300 IN CNAME mkto-sj280138.com.
info.simple.com info 300 IN CNAME stackoverflow.mktoweb.com.
_sip._tcp.simple.com _sip._tcp 300 IN SRV 10 60 5060 bigbox.example.com.
diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts
index 0fac62a7f9..fdb6a9dd50 100644
--- a/commands/types/dnscontrol.d.ts
+++ b/commands/types/dnscontrol.d.ts
@@ -49,9 +49,9 @@ type Duration =
* > 2. Make sure DNSControl only uses verified configuration if you want to use `FETCH`. For example, an attacker can send Pull Requests to your config repo, and have your CI test malicious configurations and make arbitrary HTTP requests. Therefore, `FETCH` must be explicitly enabled with flag `--allow-fetch` on DNSControl invocation.
*
* ```javascript
- * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), [
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* A("@", "1.2.3.4"),
- * ]);
+ * );
*
* FETCH("https://example.com", {
* // All three options below are optional
@@ -110,6 +110,7 @@ interface ResponseHeaders {
declare function require(name: `${string}.json`): any;
+declare function require(name: `${string}.json5`): any;
declare function require(name: string): true;
/**
@@ -180,7 +181,7 @@ declare const DISABLE_REPEATED_DOMAIN_CHECK: RecordModifier;
/**
* A adds an A record To a domain. The name should be the relative label for the record. Use `@` for the domain apex.
*
- * The address should be an ip address, either a string, or a numeric value obtained via [IP](../global/IP.md).
+ * The address should be an ip address, either a string, or a numeric value obtained via [IP](../top-level-functions/IP.md).
*
* Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata.
*
@@ -189,7 +190,7 @@ declare const DISABLE_REPEATED_DOMAIN_CHECK: RecordModifier;
* A("@", "1.2.3.4"),
* A("foo", "2.3.4.5"),
* A("test.foo", IP("1.2.3.4"), TTL(5000)),
- * A("*", "1.2.3.4", {foo: 42})
+ * A("*", "1.2.3.4", {foo: 42}),
* );
* ```
*
@@ -211,7 +212,7 @@ declare function A(name: string, address: string | number, ...modifiers: RecordM
* AAAA("@", addrV6),
* AAAA("foo", addrV6),
* AAAA("test.foo", addrV6, TTL(5000)),
- * AAAA("*", addrV6, {foo: 42})
+ * AAAA("*", addrV6, {foo: 42}),
* );
* ```
*
@@ -247,24 +248,24 @@ declare function AKAMAICDN(name: string, target: string, ...modifiers: RecordMod
declare function ALIAS(name: string, target: string, ...modifiers: RecordModifier[]): DomainModifier;
/**
- * AUTODNSSEC_OFF tells the provider to disable AutoDNSSEC. It takes no
+ * `AUTODNSSEC_OFF` tells the provider to disable AutoDNSSEC. It takes no
* parameters.
*
- * See `AUTODNSSEC_ON` for further details.
+ * See [`AUTODNSSEC_ON`](AUTODNSSEC_ON.md) for further details.
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/autodnssec_off
*/
declare const AUTODNSSEC_OFF: DomainModifier;
/**
- * AUTODNSSEC_ON tells the provider to enable AutoDNSSEC.
+ * `AUTODNSSEC_ON` tells the provider to **enable** AutoDNSSEC.
*
- * AUTODNSSEC_OFF tells the provider to disable AutoDNSSEC.
+ * [`AUTODNSSEC_OFF`](AUTODNSSEC_OFF.md) tells the provider to **disable** AutoDNSSEC.
*
* AutoDNSSEC is a feature where a DNS provider can automatically manage
* DNSSEC for a domain. Not all providers support this.
*
- * At this time, AUTODNSSEC_ON takes no parameters. There is no ability
+ * At this time, `AUTODNSSEC_ON` takes no parameters. There is no ability
* to tune what the DNS provider sets, no algorithm choice. We simply
* ask that they follow their defaults when enabling a no-fuss DNSSEC
* data model.
@@ -275,12 +276,12 @@ declare const AUTODNSSEC_OFF: DomainModifier;
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* AUTODNSSEC_ON, // Enable AutoDNSSEC.
- * A("@", "10.1.1.1")
+ * A("@", "10.1.1.1"),
* );
*
* D("insecure.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* AUTODNSSEC_OFF, // Disable AutoDNSSEC.
- * A("@", "10.2.2.2")
+ * A("@", "10.2.2.2"),
* );
* ```
*
@@ -359,11 +360,11 @@ declare function AZURE_ALIAS(name: string, type: "A" | "AAAA" | "CNAME", target:
* CAA("@", "issuewild", ";"),
* // Report all violation to test@example.com. If CA does not support
* // this record then refuse to issue any certificate
- * CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL)
+ * CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL),
* );
* ```
*
- * DNSControl contains a [`CAA_BUILDER`](../record/CAA_BUILDER.md) which can be used to simply create `CAA()` records for your domains. Instead of creating each CAA record individually, you can simply configure your report mail address, the authorized certificate authorities and the builder cares about the rest.
+ * DNSControl contains a [`CAA_BUILDER`](CAA_BUILDER.md) which can be used to simply create `CAA()` records for your domains. Instead of creating each CAA record individually, you can simply configure your report mail address, the authorized certificate authorities and the builder cares about the rest.
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/caa
*/
@@ -371,55 +372,117 @@ declare function CAA(name: string, tag: "issue" | "issuewild" | "iodef", value:
/**
* DNSControl contains a `CAA_BUILDER` which can be used to simply create
- * [`CAA()`](../domain/CAA.md) records for your domains. Instead of creating each [`CAA()`](../domain/CAA.md) record
+ * [`CAA()`](../domain-modifiers/CAA.md) records for your domains. Instead of creating each [`CAA()`](../domain-modifiers/CAA.md) record
* individually, you can simply configure your report mail address, the
* authorized certificate authorities and the builder cares about the rest.
*
* ## Example
*
- * For example you can use:
+ * ### Simple example
*
* ```javascript
- * CAA_BUILDER({
- * label: "@",
- * iodef: "mailto:test@example.com",
- * iodef_critical: true,
- * issue: [
- * "letsencrypt.org",
- * "comodoca.com",
- * ],
- * issuewild: "none",
- * })
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * CAA_BUILDER({
+ * label: "@",
+ * iodef: "mailto:test@example.com",
+ * iodef_critical: true,
+ * issue: [
+ * "letsencrypt.org",
+ * "comodoca.com",
+ * ],
+ * issuewild: "none",
+ * }),
+ * );
* ```
*
- * The parameters are:
+ * `CAA_BUILDER()` builds multiple records:
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL),
+ * CAA("@", "issue", "letsencrypt.org"),
+ * CAA("@", "issue", "comodoca.com"),
+ * CAA("@", "issuewild", ";"),
+ * );
+ * ```
+ *
+ * which in turns yield the following records:
+ *
+ * ```text
+ * @ 300 IN CAA 128 iodef "mailto:test@example.com"
+ * @ 300 IN CAA 0 issue "letsencrypt.org"
+ * @ 300 IN CAA 0 issue "comodoca.com"
+ * @ 300 IN CAA 0 issuewild ";"
+ * ```
+ *
+ * ### Example with CAA_CRITICAL flag on all records
+ *
+ * The same example can be enriched with CAA_CRITICAL on all records:
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * CAA_BUILDER({
+ * label: "@",
+ * iodef: "mailto:test@example.com",
+ * iodef_critical: true,
+ * issue: [
+ * "letsencrypt.org",
+ * "comodoca.com",
+ * ],
+ * issue_critical: true,
+ * issuewild: "none",
+ * issuewild_critical: true,
+ * }),
+ * );
+ * ```
+ *
+ * `CAA_BUILDER()` then builds (the same) multiple records - all with CAA_CRITICAL flag set:
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL),
+ * CAA("@", "issue", "letsencrypt.org", CAA_CRITICAL),
+ * CAA("@", "issue", "comodoca.com", CAA_CRITICAL),
+ * CAA("@", "issuewild", ";", CAA_CRITICAL),
+ * );
+ * ```
+ *
+ * which in turns yield the following records:
+ *
+ * ```text
+ * @ 300 IN CAA 128 iodef "mailto:test@example.com"
+ * @ 300 IN CAA 128 issue "letsencrypt.org"
+ * @ 300 IN CAA 128 issue "comodoca.com"
+ * @ 300 IN CAA 128 issuewild ";"
+ * ```
+ *
+ * ### Parameters
*
* * `label:` The label of the CAA record. (Optional. Default: `"@"`)
* * `iodef:` Report all violation to configured mail address.
* * `iodef_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`)
* * `issue:` An array of CAs which are allowed to issue certificates. (Use `"none"` to refuse all CAs)
+ * * `issue_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`)
* * `issuewild:` An array of CAs which are allowed to issue wildcard certificates. (Can be simply `"none"` to refuse issuing wildcard certificates for all CAs)
- *
- * `CAA_BUILDER()` returns multiple records (when configured as example above):
- *
- * ```javascript
- * CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL)
- * CAA("@", "issue", "letsencrypt.org")
- * CAA("@", "issue", "comodoca.com")
- * CAA("@", "issuewild", ";")
- * ```
+ * * `issuewild_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`)
+ * * `ttl:` Input for `TTL` method (optional)
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/caa_builder
*/
-declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critical?: boolean; issue: string[]; issuewild: string }): DomainModifier;
+declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critical?: boolean; issue: string[]; issue_critical?: boolean; issuewild: string[]; issuewild_critical?: boolean; ttl?: Duration }): DomainModifier;
/**
+ * WARNING: Cloudflare is removing this feature and replacing it with a new
+ * feature called "Dynamic Single Redirect". DNSControl will automatically
+ * generate "Dynamic Single Redirects" for a limited number of use cases. See
+ * [`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
+ *
* `CF_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page Rules) to
* generate a HTTP 301 permanent redirect.
*
* If _any_ `CF_REDIRECT` or [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) functions are used then
* `dnscontrol` will manage _all_ "Forwarding URL" type Page Rules for the domain.
- * Page Rule types other than "Forwarding URLâ will be left alone.
+ * Page Rule types other than "Forwarding URL" will be left alone.
*
* WARNING: Cloudflare does not currently fully document the Page Rules API and
* this interface is not extensively tested. Take precautions such as making
@@ -445,6 +508,41 @@ declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critic
declare function CF_REDIRECT(source: string, destination: string, ...modifiers: RecordModifier[]): DomainModifier;
/**
+ * `CF_SINGLE_REDIRECT` is a Cloudflare-specific feature for creating HTTP 301
+ * (permanent) or 302 (temporary) redirects.
+ *
+ * This feature manages dynamic "Single Redirects". (Single Redirects can be
+ * static or dynamic but DNSControl only maintains dynamic redirects).
+ *
+ * Cloudflare documentation:
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * CF_SINGLE_REDIRECT("name", 301, "when", "then"),
+ * CF_SINGLE_REDIRECT('redirect www.example.com', 301, 'http.host eq "www.example.com"', 'concat("https://otherplace.com", http.request.uri.path)'),
+ * CF_SINGLE_REDIRECT('redirect yyy.example.com', 301, 'http.host eq "yyy.example.com"', 'concat("https://survey.stackoverflow.co", "")'),
+ * );
+ * ```
+ *
+ * The fields are:
+ *
+ * * name: The name (basically a comment, but it must be unique)
+ * * code: Either 301 (permanent) or 302 (temporary) redirects. May be a number or string.
+ * * when: What Cloudflare sometimes calls the "rule expression".
+ * * then: The replacement expression.
+ *
+ * NOTE: The features [`CF_REDIRECT`](CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) generate `CF_SINGLE_REDIRECT` if enabled in [`CLOUDFLAREAPI`](../../provider/cloudflareapi.md).
+ *
+ * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/service-provider-specific/cloudflare-dns/cf_single_redirect
+ */
+declare function CF_SINGLE_REDIRECT(name: string, code: number, when: string, then: string, ...modifiers: RecordModifier[]): DomainModifier;
+
+/**
+ * WARNING: Cloudflare is removing this feature and replacing it with a new
+ * feature called "Dynamic Single Redirect". DNSControl will automatically
+ * generate "Dynamic Single Redirects" for a limited number of use cases. See
+ * [`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
+ *
* `CF_TEMP_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page
* Rules) to generate a HTTP 302 temporary redirect.
*
@@ -522,11 +620,11 @@ declare function CNAME(name: string, target: string, ...modifiers: RecordModifie
/**
* `D` adds a new Domain for DNSControl to manage. The first two arguments are required: the domain name (fully qualified `example.com` without a trailing dot), and the
* name of the registrar (as previously declared with [NewRegistrar](NewRegistrar.md)). Any number of additional arguments may be included to add DNS Providers with [DNSProvider](NewDnsProvider.md),
- * add records with [A](../domain/A.md), [CNAME](../domain/CNAME.md), and so forth, or add metadata.
+ * add records with [A](../domain-modifiers/A.md), [CNAME](../domain-modifiers/CNAME.md), and so forth, or add metadata.
*
* Modifier arguments are processed according to type as follows:
*
- * - A function argument will be called with the domain object as it's only argument. Most of the [built-in modifier functions](https://docs.dnscontrol.org/language-reference/domain-modifiers) return such functions.
+ * - A function argument will be called with the domain object as it's only argument. Most of the [built-in modifier functions](https://docs.dnscontrol.org/language-reference/domain-modifiers-modifiers) return such functions.
* - An object argument will be merged into the domain's metadata collection.
* - An array argument will have all of it's members evaluated recursively. This allows you to combine multiple common records or modifiers into a variable that can
* be used like a macro in multiple domains.
@@ -535,7 +633,7 @@ declare function CNAME(name: string, target: string, ...modifiers: RecordModifie
* // simple domain
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* A("@","1.2.3.4"),
- * CNAME("test", "foo.example2.com.")
+ * CNAME("test", "foo.example2.com."),
* );
*
* // "macro" for records that can be mixed into any zone
@@ -550,7 +648,7 @@ declare function CNAME(name: string, target: string, ...modifiers: RecordModifie
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* A("@","1.2.3.4"),
* CNAME("test", "foo.example2.com."),
- * GOOGLE_APPS_DOMAIN_MX
+ * GOOGLE_APPS_DOMAIN_MX,
* );
* ```
*
@@ -570,15 +668,15 @@ declare function CNAME(name: string, target: string, ...modifiers: RecordModifie
* var DNS_OUTSIDE = NewDnsProvider("bind");
*
* D("example.com!inside", REG_THIRDPARTY, DnsProvider(DNS_INSIDE),
- * A("www", "10.10.10.10")
+ * A("www", "10.10.10.10"),
* );
*
* D("example.com!outside", REG_THIRDPARTY, DnsProvider(DNS_OUTSIDE),
- * A("www", "20.20.20.20")
+ * A("www", "20.20.20.20"),
* );
*
* D_EXTEND("example.com!inside",
- * A("internal", "10.99.99.99")
+ * A("internal", "10.99.99.99"),
* );
* ```
*
@@ -612,18 +710,18 @@ declare function D(name: string, registrar: string, ...modifiers: DomainModifier
*
* ## Example
*
- * We want to create backup zone files for all domains, but not actually register them. Also create a [`DefaultTTL`](../domain/DefaultTTL.md).
+ * We want to create backup zone files for all domains, but not actually register them. Also create a [`DefaultTTL`](../domain-modifiers/DefaultTTL.md).
* The domain `example.com` will have the defaults set.
*
* ```javascript
* var COMMON = NewDnsProvider("foo");
* DEFAULTS(
* DnsProvider(COMMON, 0),
- * DefaultTTL("1d")
+ * DefaultTTL("1d"),
* );
*
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * A("@","1.2.3.4")
+ * A("@","1.2.3.4"),
* );
* ```
*
@@ -634,7 +732,7 @@ declare function D(name: string, registrar: string, ...modifiers: DomainModifier
* DEFAULTS();
*
* D("example2.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * A("@","1.2.3.4")
+ * A("@","1.2.3.4"),
* );
* ```
*
@@ -642,6 +740,21 @@ declare function D(name: string, registrar: string, ...modifiers: DomainModifier
*/
declare function DEFAULTS(...modifiers: DomainModifier[]): void;
+/**
+ * DHCID adds a DHCID record to the domain.
+ *
+ * Digest should be a string.
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * DHCID("example.com", "ABCDEFG"),
+ * );
+ * ```
+ *
+ * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/dhcid
+ */
+declare function DHCID(name: string, digest: string, ...modifiers: RecordModifier[]): DomainModifier;
+
/**
* `DISABLE_IGNORE_SAFETY_CHECK()` disables the safety check. Normally it is an
* error to insert records that match an `IGNORE()` pattern. This disables that
@@ -650,7 +763,7 @@ declare function DEFAULTS(...modifiers: DomainModifier[]): void;
* It replaces the per-record `IGNORE_NAME_DISABLE_SAFETY_CHECK()` which is
* deprecated as of DNSControl v4.0.0.0.
*
- * See [`IGNORE()`](../domain/IGNORE.md) for more information.
+ * See [`IGNORE()`](../domain-modifiers/IGNORE.md) for more information.
*
* ## Syntax
*
@@ -676,12 +789,14 @@ declare const DISABLE_IGNORE_SAFETY_CHECK: DomainModifier;
* ### Simple example
*
* ```javascript
- * DMARC_BUILDER({
- * policy: "reject",
- * ruf: [
- * "mailto:mailauth-reports@example.com",
- * ],
- * })
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * DMARC_BUILDER({
+ * policy: "reject",
+ * ruf: [
+ * "mailto:mailauth-reports@example.com",
+ * ],
+ * }),
+ * );
* ```
*
* This yield the following record:
@@ -693,36 +808,40 @@ declare const DISABLE_IGNORE_SAFETY_CHECK: DomainModifier;
* ### Advanced example
*
* ```javascript
- * DMARC_BUILDER({
- * policy: "reject",
- * subdomainPolicy: "quarantine",
- * percent: 50,
- * alignmentSPF: "r",
- * alignmentDKIM: "strict",
- * rua: [
- * "mailto:mailauth-reports@example.com",
- * "https://dmarc.example.com/submit",
- * ],
- * ruf: [
- * "mailto:mailauth-reports@example.com",
- * ],
- * failureOptions: "1",
- * reportInterval: "1h",
- * });
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * DMARC_BUILDER({
+ * policy: "reject",
+ * subdomainPolicy: "quarantine",
+ * percent: 50,
+ * alignmentSPF: "r",
+ * alignmentDKIM: "strict",
+ * rua: [
+ * "mailto:mailauth-reports@example.com",
+ * "https://dmarc.example.com/submit",
+ * ],
+ * ruf: [
+ * "mailto:mailauth-reports@example.com",
+ * ],
+ * failureOptions: "1",
+ * reportInterval: "1h",
+ * }),
+ * );
* ```
*
* ```javascript
- * DMARC_BUILDER({
- * label: "insecure",
- * policy: "none",
- * ruf: [
- * "mailto:mailauth-reports@example.com",
- * ],
- * failureOptions: {
- * SPF: false,
- * DKIM: true,
- * },
- * });
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * DMARC_BUILDER({
+ * label: "insecure",
+ * policy: "none",
+ * ruf: [
+ * "mailto:mailauth-reports@example.com",
+ * ],
+ * failureOptions: {
+ * SPF: false,
+ * DKIM: true,
+ * },
+ * }),
+ * );
* ```
*
* This yields the following records:
@@ -757,6 +876,42 @@ declare const DISABLE_IGNORE_SAFETY_CHECK: DomainModifier;
*/
declare function DMARC_BUILDER(opts: { label?: string; version?: string; policy: 'none' | 'quarantine' | 'reject'; subdomainPolicy?: 'none' | 'quarantine' | 'reject'; alignmentSPF?: 'strict' | 's' | 'relaxed' | 'r'; alignmentDKIM?: 'strict' | 's' | 'relaxed' | 'r'; percent?: number; rua?: string[]; ruf?: string[]; failureOptions?: { SPF: boolean, DKIM: boolean } | string; failureFormat?: string; reportInterval?: Duration; ttl?: Duration }): DomainModifier;
+/**
+ * DNAME adds a DNAME record to the domain.
+ *
+ * Target should be a string.
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * DNAME("sub", "example.net."),
+ * );
+ * ```
+ *
+ * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/dname
+ */
+declare function DNAME(name: string, target: string, ...modifiers: RecordModifier[]): DomainModifier;
+
+/**
+ * DNSKEY adds a DNSKEY record to the domain.
+ *
+ * Flags should be a number.
+ *
+ * Protocol should be a number.
+ *
+ * Algorithm must be a number.
+ *
+ * Public key must be a string.
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * DNSKEY("@", 257, 3, 13, "AABBCCDD"),
+ * );
+ * ```
+ *
+ * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/dnskey
+ */
+declare function DNSKEY(name: string, flags: number, protocol: number, algorithm: number, publicKey: string, ...modifiers: RecordModifier[]): DomainModifier;
+
/**
* `DOMAIN_ELSEWHERE()` is a helper macro that lets you easily indicate that
* a domain's zones are managed elsewhere. That is, it permits you easily delegate
@@ -779,11 +934,11 @@ declare function DMARC_BUILDER(opts: { label?: string; version?: string; policy:
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* NO_PURGE,
* NAMESERVER("ns1.foo.com"),
- * NAMESERVER("ns2.foo.com")
+ * NAMESERVER("ns2.foo.com"),
* );
* ```
*
- * NOTE: The [`NO_PURGE`](../domain/NO_PURGE.md) is used out of abundance of caution but since no
+ * NOTE: The [`NO_PURGE`](../domain-modifiers/NO_PURGE.md) is used out of abundance of caution but since no
* `DnsProvider()` statements exist, no updates would be performed.
*
* @see https://docs.dnscontrol.org/language-reference/top-level-functions/domain_elsewhere
@@ -814,11 +969,11 @@ declare function DOMAIN_ELSEWHERE(name: string, registrar: string, nameserver_na
* ```javascript
* D("example.com", REG_NAMEDOTCOM,
* NO_PURGE,
- * DnsProvider(DSP_AZURE)
+ * DnsProvider(DSP_AZURE),
* );
* ```
*
- * NOTE: The [`NO_PURGE`](../domain/NO_PURGE.md) is used to prevent DNSControl from changing the records.
+ * NOTE: The [`NO_PURGE`](../domain-modifiers/NO_PURGE.md) is used to prevent DNSControl from changing the records.
*
* @see https://docs.dnscontrol.org/language-reference/top-level-functions/domain_elsewhere_auto
*/
@@ -837,7 +992,7 @@ declare function DOMAIN_ELSEWHERE_AUTO(name: string, domain: string, registrar:
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * DS("example.com", 2371, 13, 2, "ABCDEF")
+ * DS("example.com", 2371, 13, 2, "ABCDEF"),
* );
* ```
*
@@ -867,31 +1022,32 @@ declare function DS(name: string, keytag: number, algorithm: number, digesttype:
* not `domain.tld`.
*
* Some operators only act on an apex domain (e.g.
- * [`CF_REDIRECT`](../domain/CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](../domain/CF_TEMP_REDIRECT.md)). Using them
+ * [`CF_SINGLE_REDIRECT`](../domain-modifiers/CF_SINGLE_REDIRECT.md),
+ * [`CF_REDIRECT`](../domain-modifiers/CF_REDIRECT.md), and [`CF_TEMP_REDIRECT`](../domain-modifiers/CF_TEMP_REDIRECT.md)). Using them
* in a `D_EXTEND` subdomain may not be what you expect.
*
* ```javascript
* D("domain.tld", REG_MY_PROVIDER, DnsProvider(DNS),
* A("@", "127.0.0.1"), // domain.tld
* A("www", "127.0.0.2"), // www.domain.tld
- * CNAME("a", "b") // a.domain.tld -> b.domain.tld
+ * CNAME("a", "b"), // a.domain.tld -> b.domain.tld
* );
* D_EXTEND("domain.tld",
* A("aaa", "127.0.0.3"), // aaa.domain.tld
- * CNAME("c", "d") // c.domain.tld -> d.domain.tld
+ * CNAME("c", "d"), // c.domain.tld -> d.domain.tld
* );
* D_EXTEND("sub.domain.tld",
* A("bbb", "127.0.0.4"), // bbb.sub.domain.tld
* A("ccc", "127.0.0.5"), // ccc.sub.domain.tld
- * CNAME("e", "f") // e.sub.domain.tld -> f.sub.domain.tld
+ * CNAME("e", "f"), // e.sub.domain.tld -> f.sub.domain.tld
* );
* D_EXTEND("sub.sub.domain.tld",
* A("ddd", "127.0.0.6"), // ddd.sub.sub.domain.tld
- * CNAME("g", "h") // g.sub.sub.domain.tld -> h.sub.sub.domain.tld
+ * CNAME("g", "h"), // g.sub.sub.domain.tld -> h.sub.sub.domain.tld
* );
* D_EXTEND("sub.domain.tld",
* A("@", "127.0.0.7"), // sub.domain.tld
- * CNAME("i", "j") // i.sub.domain.tld -> j.sub.domain.tld
+ * CNAME("i", "j"), // i.sub.domain.tld -> j.sub.domain.tld
* );
* ```
*
@@ -929,20 +1085,20 @@ declare function DS(name: string, keytag: number, algorithm: number, digesttype:
declare function D_EXTEND(name: string, ...modifiers: DomainModifier[]): void;
/**
- * DefaultTTL sets the TTL for all subsequent records following it in a domain that do not explicitly set one with [`TTL`](../record/TTL.md). If neither `DefaultTTL` or `TTL` exist for a record,
- * the record will inherit the DNSControl global internal default of 300 seconds. See also [`DEFAULTS`](../global/DEFAULTS.md) to override the internal defaults.
+ * DefaultTTL sets the TTL for all subsequent records following it in a domain that do not explicitly set one with [`TTL`](../record-modifiers/TTL.md). If neither `DefaultTTL` or `TTL` exist for a record,
+ * the record will inherit the DNSControl global internal default of 300 seconds. See also [`DEFAULTS`](../top-level-functions/DEFAULTS.md) to override the internal defaults.
*
- * NS records are currently a special case, and do not inherit from `DefaultTTL`. See [`NAMESERVER_TTL`](../domain/NAMESERVER_TTL.md) to set a default TTL for all NS records.
+ * NS records are currently a special case, and do not inherit from `DefaultTTL`. See [`NAMESERVER_TTL`](../domain-modifiers/NAMESERVER_TTL.md) to set a default TTL for all NS records.
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* DefaultTTL("4h"),
* A("@","1.2.3.4"), // uses default
- * A("foo", "2.3.4.5", TTL(600)) // overrides default
+ * A("foo", "2.3.4.5", TTL(600)), // overrides default
* );
* ```
*
- * The DefaultTTL duration is the same format as [`TTL`](../record/TTL.md), an integer number of seconds
+ * The DefaultTTL duration is the same format as [`TTL`](../record-modifiers/TTL.md), an integer number of seconds
* or a string with a unit such as `"4d"`.
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/defaultttl
@@ -951,7 +1107,7 @@ declare function DefaultTTL(ttl: Duration): DomainModifier;
/**
* DnsProvider indicates that the specified provider should be used to manage
- * records for this domain. The name must match the name used with [NewDnsProvider](../global/NewDnsProvider.md).
+ * records for this domain. The name must match the name used with [NewDnsProvider](../top-level-functions/NewDnsProvider.md).
*
* The nsCount parameter determines how the nameservers will be managed from this provider.
*
@@ -980,6 +1136,55 @@ declare function DnsProvider(name: string, nsCount?: number): DomainModifier;
*/
declare function FRAME(name: string, target: string, ...modifiers: RecordModifier[]): DomainModifier;
+/**
+ * `HASH` hashes `value` using the hashing algorithm given in `algorithm`
+ * (accepted values `SHA1`, `SHA256`, and `SHA512`) and returns the hex encoded
+ * hash value.
+ *
+ * example `HASH("SHA1", "abc")` returns `a9993e364706816aba3e25717850c26c9cd0d89d`.
+ *
+ * `HASH()`'s primary use case is for managing [catalog zones](https://datatracker.ietf.org/doc/html/rfc9432):
+ *
+ * > a method for automatic DNS zone provisioning among DNS primary and secondary name
+ * > servers by storing and transferring the catalog of zones to be provisioned as one
+ * > or more regular DNS zones.
+ *
+ * Here's an example of a catalog zone:
+ *
+ * ```javascript
+ * foo_name_suffix = HASH("SHA1", "foo.name") + ".zones"
+ * D("catalog.example"
+ * [...]
+ * , TXT("version", "2")
+ * , PTR(foo_name_suffix, "foo.name.")
+ * , A("primaries.ext." + foo_name_suffix, "192.168.1.1")
+ * )
+ * ```
+ *
+ * @see https://docs.dnscontrol.org/language-reference/top-level-functions/hash
+ */
+declare function HASH(algorithm: "SHA1" | "SHA256" | "SHA512", value: string): string;
+
+/**
+ * HTTPS adds an HTTPS record to a domain. The name should be the relative label for the record. Use `@` for the domain apex. The HTTPS record is a special form of the SVCB resource record.
+ *
+ * The priority must be a positive number, the address should be an ip address, either a string, or a numeric value obtained via [IP](../top-level-functions/IP.md).
+ *
+ * The params may be configured to specify the `alpn`, `ipv4hint`, `ipv6hint`, `ech` or `port` setting. Several params may be joined by a space. Not existing params may be specified as an empty string `""`
+ *
+ * Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata.
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * HTTPS("@", 1, ".", "ipv4hint=123.123.123.123 alpn=h3,h2 port=443"),
+ * HTTPS("@", 1, "test.com", ""),
+ * );
+ * ```
+ *
+ * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/https
+ */
+declare function HTTPS(name: string, priority: number, target: string, params: string, ...modifiers: RecordModifier[]): DomainModifier;
+
/**
* `IGNORE()` makes it possible for DNSControl to share management of a domain
* with an external system. The parameters of `IGNORE()` indicate which records
@@ -997,20 +1202,22 @@ declare function FRAME(name: string, target: string, ...modifiers: RecordModifie
*
* Technically `IGNORE_NAME` is a promise that DNSControl will not modify or
* delete existing records that match particular patterns. It is like
- * [`NO_PURGE`](../domain/NO_PURGE.md) that matches only specific records.
+ * [`NO_PURGE`](../domain-modifiers/NO_PURGE.md) that matches only specific records.
*
* Including a record that is ignored is considered an error and may have
* undefined behavior. This safety check can be disabled using the
- * [`DISABLE_IGNORE_SAFETY_CHECK`](../domain/DISABLE_IGNORE_SAFETY_CHECK.md) feature.
+ * [`DISABLE_IGNORE_SAFETY_CHECK`](../domain-modifiers/DISABLE_IGNORE_SAFETY_CHECK.md) feature.
*
* ## Syntax
*
* The `IGNORE()` function can be used with up to 3 parameters:
*
* ```javascript
- * IGNORE(labelSpec, typeSpec, targetSpec):
- * IGNORE(labelSpec, typeSpec):
- * IGNORE(labelSpec):
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * IGNORE(labelSpec, typeSpec, targetSpec),
+ * IGNORE(labelSpec, typeSpec),
+ * IGNORE(labelSpec),
+ * );
* ```
*
* * `labelSpec` is a glob that matches the DNS label. For example `"foo"` or `"foo*"`. `"*"` matches all labels, as does the empty string (`""`).
@@ -1046,7 +1253,7 @@ declare function FRAME(name: string, target: string, ...modifiers: RecordModifie
* IGNORE("bar", "A,MX"), // ignore only A and MX records for name bar
* IGNORE("*", "*", "dev-*"), // Ignore targets with a `dev-` prefix
* IGNORE("*", "A", "1\.2\.3\."), // Ignore targets in the 1.2.3.0/24 CIDR block
- * END);
+ * );
* ```
*
* Ignore Let's Encrypt (ACME) validation records:
@@ -1055,7 +1262,7 @@ declare function FRAME(name: string, target: string, ...modifiers: RecordModifie
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("_acme-challenge", "TXT"),
* IGNORE("_acme-challenge.**", "TXT"),
- * END);
+ * );
* ```
*
* Ignore DNS records typically inserted by Microsoft ActiveDirectory:
@@ -1078,7 +1285,7 @@ declare function FRAME(name: string, target: string, ...modifiers: RecordModifie
* IGNORE("domaindnszones.**", "A"),
* IGNORE("forestdnszones", "A"),
* IGNORE("forestdnszones.**", "A"),
- * END);
+ * );
* ```
*
* ## Detailed examples
@@ -1099,7 +1306,7 @@ declare function FRAME(name: string, target: string, ...modifiers: RecordModifie
* CNAME("cfull", "www.plts.org."),
* CNAME("cfull2", "www.bar.plts.org."),
* CNAME("cfull3", "bar.www.plts.org."),
- * END);
+ * );
*
* D_EXTEND("more.example.com",
* A("foo", "1.1.1.1"),
@@ -1108,87 +1315,134 @@ declare function FRAME(name: string, target: string, ...modifiers: RecordModifie
* CNAME("mfull", "www.plts.org."),
* CNAME("mfull2", "www.bar.plts.org."),
* CNAME("mfull3", "bar.www.plts.org."),
- * END);
+ * );
* ```
*
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("@", "", ""),
- * // Would match:
- * // foo.example.com. A 1.1.1.1
- * // foo.more.example.com. A 1.1.1.1
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * `foo.example.com. A 1.1.1.1`
+ * * `foo.more.example.com. A 1.1.1.1`
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("example.com.", "", ""),
- * // Would match:
- * // nothing
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * nothing
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("foo", "", ""),
- * // Would match:
- * // foo.example.com. A 1.1.1.1
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * `foo.example.com. A 1.1.1.1`
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("foo.**", "", ""),
- * // Would match:
- * // foo.more.example.com. A 1.1.1.1
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * `foo.more.example.com. A 1.1.1.1`
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("www", "", ""),
- * // Would match:
+ * );
* // www.example.com. A 174.136.107.196
* ```
*
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("www.*", "", ""),
- * // Would match:
+ * );
* // nothing
* ```
*
+ * **Would match**:
+ *
+ * * nothing
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("www.example.com", "", ""),
- * // Would match:
+ * );
* // nothing
* ```
*
+ * **Would match**:
+ *
+ * * nothing
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("www.example.com.", "", ""),
- * // Would match:
- * // none
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * none
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* //IGNORE("", "", "1.1.1.*"),
- * // Would match:
- * // foo.example.com. A 1.1.1.1
- * // foo.more.example.com. A 1.1.1.1
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * `foo.example.com. A 1.1.1.1`
+ * * `foo.more.example.com. A 1.1.1.1`
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* //IGNORE("", "", "www"),
- * // Would match:
- * // none
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * none
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("", "", "*bar*"),
- * // Would match:
- * // cfull2.example.com. CNAME www.bar.plts.org.
- * // cfull3.example.com. CNAME bar.www.plts.org.
- * // mfull2.more.example.com. CNAME www.bar.plts.org.
- * // mfull3.more.example.com. CNAME bar.www.plts.org.
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * `cfull2.example.com. CNAME www.bar.plts.org.`
+ * * `cfull3.example.com. CNAME bar.www.plts.org.`
+ * * `mfull2.more.example.com. CNAME www.bar.plts.org.`
+ * * `mfull3.more.example.com. CNAME bar.www.plts.org.`
+ *
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* IGNORE("", "", "bar.**"),
- * // Would match:
- * // cfull3.example.com. CNAME bar.www.plts.org.
- * // mfull3.more.example.com. CNAME bar.www.plts.org.
+ * );
* ```
*
+ * **Would match**:
+ *
+ * * `cfull3.example.com. CNAME bar.www.plts.org.`
+ * * `mfull3.more.example.com. CNAME bar.www.plts.org.`
+ *
* ## Conflict handling
*
* It is considered as an error for a `dnsconfig.js` to both ignore and insert the
@@ -1224,8 +1478,10 @@ declare function FRAME(name: string, target: string, ...modifiers: RecordModifie
* instead.
*
* ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* // THIS NO LONGER WORKS! Use DISABLE_IGNORE_SAFETY_CHECK instead. See above.
* TXT("myhost", "mytext", IGNORE_NAME_DISABLE_SAFETY_CHECK),
+ * );
* ```
*
* ## Caveats
@@ -1265,12 +1521,12 @@ declare function IGNORE_TARGET(pattern: string, rType: string): DomainModifier;
*
* ```javascript
* D("example.com!external", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * A("test", "8.8.8.8")
+ * A("test", "8.8.8.8"),
* );
*
* D("example.com!internal", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* INCLUDE("example.com!external"),
- * A("home", "127.0.0.1")
+ * A("home", "127.0.0.1"),
* );
* ```
*
@@ -1303,7 +1559,7 @@ declare function INCLUDE(domain: string): DomainModifier;
declare function IP(ip: string): number;
/**
- * The parameter number types are as follows:
+ * The parameter number types ingested are as follows:
*
* ```
* name: string
@@ -1314,7 +1570,7 @@ declare function IP(ip: string): number;
* deg2: uint32
* min2: uint32
* sec2: float32
- * altitude: uint32
+ * altitude: float32
* size: float32
* horizontal_precision: float32
* vertical_precision: float32
@@ -1360,10 +1616,10 @@ declare function IP(ip: string): number;
* One must supply the `LOC()` js helper all parameters. If that seems like too
* much work, see also helper functions:
*
- * * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - build a `LOC` by supplying only **d**ecimal **d**egrees.
- * * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - build a `LOC` by supplying only **d**ecimal **d**egrees.
+ * * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
*
* ## Format ##
*
@@ -1371,23 +1627,28 @@ declare function IP(ip: string): number;
*
* `degrees,minutes,seconds,[NnSs],deg,min,sec,[EeWw],altitude,size,horizontal_precision,vertical_precision`
*
+ * where:
+ * altitude: [-100000.00 .. 42849672.95] BY .01 (altitude in meters)
+ * size, horizontal_precision, vertical_precision: [0 .. 90000000.00] (size/precision in meters)
+ *
+ * values outside of the above ranges are gated to within the ranges.
+ *
* ## Examples ##
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* // LOC "subdomain", d1, m1, s1, "[NnSs]", d2, m2, s2, "[EeWw]", alt, siz, hp, vp)
* //42 21 54 N 71 06 18 W -24m 30m
- * , LOC("@", 42, 21, 54, "N", 71, 6, 18, "W", -24, 30, 0, 0)
+ * LOC("@", 42, 21, 54, "N", 71, 6, 18, "W", -24.01, 30, 0, 0),
* //42 21 43.952 N 71 5 6.344 W -24m 1m 200m 10m
- * , LOC("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24, 1, 200, 10)
+ * LOC("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24.33, 1, 200, 10),
* //52 14 05 N 00 08 50 E 10m
- * , LOC("b", 52, 14, 5, "N", 0, 8, 50, "E", 10, 0, 0, 0)
+ * LOC("b", 52, 14, 5, "N", 0, 8, 50, "E", 10, 0, 0, 0),
* //32 7 19 S 116 2 25 E 10m
- * , LOC("c", 32, 7, 19, "S",116, 2, 25, "E", 10, 0, 0, 0)
+ * LOC("c", 32, 7, 19, "S",116, 2, 25, "E", 10, 0, 0, 0),
* //42 21 28.764 N 71 00 51.617 W -44m 2000m
- * , LOC("d", 42, 21, 28.764, "N", 71, 0, 51.617, "W", -44, 2000, 0, 0)
+ * LOC("d", 42, 21, 28.764, "N", 71, 0, 51.617, "W", -44, 2000, 0, 0),
* );
- *
* ```
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/loc
@@ -1403,9 +1664,9 @@ declare function LOC(deg1: number, min1: number, sec1: number, deg2: number, min
* - alt (float32, optional)
* - ttl (optional)
*
- * A helper to build [`LOC`](../domain/LOC.md) records. Supply four parameters instead of 12.
+ * A helper to build [`LOC`](LOC.md) records. Supply four parameters instead of 12.
*
- * Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
+ * Internally assumes some defaults for [`LOC`](LOC.md) records.
*
* The cartesian coordinates are decimal degrees, like you typically find in e.g. Google Maps.
*
@@ -1419,35 +1680,34 @@ declare function LOC(deg1: number, min1: number, sec1: number, deg2: number, min
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * LOC_BUILDER_DD({
+ * LOC_BUILDER_DD({
* label: "big-ben",
* x: 51.50084265331501,
* y: -0.12462541415599787,
* alt: 6,
- * })
- * , LOC_BUILDER_DD({
+ * }),
+ * LOC_BUILDER_DD({
* label: "white-house",
* x: 38.89775977858357,
* y: -77.03655125982903,
* alt: 19,
- * })
- * , LOC_BUILDER_DD({
+ * }),
+ * LOC_BUILDER_DD({
* label: "white-house-ttl",
* x: 38.89775977858357,
* y: -77.03655125982903,
* alt: 19,
* ttl: "5m",
- * })
+ * }),
* );
- *
* ```
*
* Part of the series:
- * * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/loc_builder_dd
*/
@@ -1461,9 +1721,9 @@ declare function LOC_BUILDER_DD(opts: { label?: string; x: number; y: number; al
* - alt (float32, optional)
* - ttl (optional)
*
- * A helper to build [`LOC`](../domain/LOC.md) records. Supply three parameters instead of 12.
+ * A helper to build [`LOC`](LOC.md) records. Supply three parameters instead of 12.
*
- * Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
+ * Internally assumes some defaults for [`LOC`](LOC.md) records.
*
* Accepts a string with decimal minutes (DMM) coordinates in the form: 25.24°S 153.15°E
*
@@ -1479,17 +1739,16 @@ declare function LOC_BUILDER_DD(opts: { label?: string; x: number; y: number; al
* label: "tasmania",
* str: "42°S 147°E",
* alt: 3,
- * })
+ * }),
* );
- *
* ```
*
* Part of the series:
- * * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/loc_builder_dmm_str
*/
@@ -1503,9 +1762,9 @@ declare function LOC_BUILDER_DMM_STR(opts: { label?: string; str: string; alt?:
* - alt (float32, optional)
* - ttl (optional)
*
- * A helper to build [`LOC`](../domain/LOC.md) records. Supply three parameters instead of 12.
+ * A helper to build [`LOC`](LOC.md) records. Supply three parameters instead of 12.
*
- * Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
+ * Internally assumes some defaults for [`LOC`](LOC.md) records.
*
* Accepts a string with degrees, minutes, and seconds (DMS) coordinates in the form: 41°24'12.2"N 2°10'26.5"E
*
@@ -1522,17 +1781,16 @@ declare function LOC_BUILDER_DMM_STR(opts: { label?: string; str: string; alt?:
* str: "33°51â˛31âŗS 151°12â˛51âŗE",
* alt: 4,
* ttl: "5m",
- * })
+ * }),
* );
- *
* ```
*
* Part of the series:
- * * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/loc_builder_dms_str
*/
@@ -1546,41 +1804,40 @@ declare function LOC_BUILDER_DMS_STR(opts: { label?: string; str: string; alt?:
* - alt (float32, optional)
* - ttl (optional)
*
- * A helper to build [`LOC`](../domain/LOC.md) records. Supply three parameters instead of 12.
+ * A helper to build [`LOC`](LOC.md) records. Supply three parameters instead of 12.
*
- * Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
+ * Internally assumes some defaults for [`LOC`](LOC.md) records.
*
* Accepts a string and tries all `LOC_BUILDER_DM*_STR({})` methods:
- * * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * , LOC_BUILDER_STR({
+ * LOC_BUILDER_STR({
* label: "old-faithful",
* str: "44.46046°N 110.82815°W",
* alt: 2240,
- * })
- * , LOC_BUILDER_STR({
+ * }),
+ * LOC_BUILDER_STR({
* label: "ribblehead-viaduct",
* str: "54.210436°N 2.370231°W",
* alt: 300,
- * })
- * , LOC_BUILDER_STR({
+ * }),
+ * LOC_BUILDER_STR({
* label: "guinness-brewery",
* str: "53°20â˛40âŗN 6°17â˛20âŗW",
* alt: 300,
- * })
+ * }),
* );
- *
* ```
*
* Part of the series:
- * * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/loc_builder_str
*/
@@ -1590,16 +1847,18 @@ declare function LOC_BUILDER_STR(opts: { label?: string; str: string; alt?: numb
* DNSControl offers a `M365_BUILDER` which can be used to simply set up Microsoft 365 for a domain in an opinionated way.
*
* It defaults to a setup without support for legacy Skype for Business applications.
- * It doesn't set up SPF or DMARC. See [`SPF_BUILDER`](/language-reference/record-modifiers/dmarc_builder) and [`DMARC_BUILDER`](/language-reference/record-modifiers/spf_builder).
+ * It doesn't set up SPF or DMARC. See [`SPF_BUILDER`](SPF_BUILDER.md) and [`DMARC_BUILDER`](DMARC_BUILDER.md).
*
* ## Example
*
* ### Simple example
*
* ```javascript
- * M365_BUILDER({
- * initialDomain: "example.onmicrosoft.com",
- * });
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * M365_BUILDER("example.com", {
+ * initialDomain: "example.onmicrosoft.com",
+ * }),
+ * );
* ```
*
* This sets up `MX` records, Autodiscover, and DKIM.
@@ -1607,15 +1866,17 @@ declare function LOC_BUILDER_STR(opts: { label?: string; str: string; alt?: numb
* ### Advanced example
*
* ```javascript
- * M365_BUILDER({
- * label: "test",
- * mx: false,
- * autodiscover: false,
- * dkim: false,
- * mdm: true,
- * domainGUID: "test-example-com", // Can be automatically derived in this case, if example.com is the context.
- * initialDomain: "example.onmicrosoft.com",
- * });
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * M365_BUILDER("example.com", {
+ * label: "test",
+ * mx: false,
+ * autodiscover: false,
+ * dkim: false,
+ * mdm: true,
+ * domainGUID: "test-example-com", // Can be automatically derived in this case, if example.com is the context.
+ * initialDomain: "example.onmicrosoft.com",
+ * }),
+ * );
* ```
*
* This sets up Mobile Device Management only.
@@ -1645,7 +1906,7 @@ declare function M365_BUILDER(opts: { label?: string; mx?: boolean; autodiscover
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* MX("@", 5, "mail"), // mx example.com -> mail.example.com
- * MX("sub", 10, "mail.foo.com.")
+ * MX("sub", 10, "mail.foo.com."),
* );
* ```
*
@@ -1654,20 +1915,21 @@ declare function M365_BUILDER(opts: { label?: string; mx?: boolean; autodiscover
declare function MX(name: string, priority: number, target: string, ...modifiers: RecordModifier[]): DomainModifier;
/**
- * `NAMESERVER()` instructs DNSControl to inform the domain"s registrar where to find this zone.
+ * `NAMESERVER()` instructs DNSControl to inform the domain's registrar where to find this zone.
* For some registrars this will also add NS records to the zone itself.
*
* This takes exactly one argument: the name of the nameserver. It must end with
* a "." if it is a FQDN, just like all targets.
*
* This is different than the [`NS()`](NS.md) function, which inserts NS records
- * in the current zone and accepts a label. [`NS()`](NS.md) is useful for downward
+ * in the current zone and accepts a label. [`NS()`](NS.md) is for downward
* delegations. `NAMESERVER()` is for informing upstream delegations.
*
* For more information, refer to [this page](../../nameservers.md).
*
* ```javascript
- * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * D("example.com", REG_MY_PROVIDER,
+ * DnsProvider(DSP_MY_PROVIDER),
* DnsProvider(route53, 0),
* // Replace the nameservers:
* NAMESERVER("ns1.myserver.com."),
@@ -1686,7 +1948,7 @@ declare function MX(name: string, priority: number, target: string, ...modifiers
* Nameservers are one of the least
* understood parts of DNS, so a little extra explanation is required.
*
- * * [`NS()`](NS.md) lets you add an NS record to a zone, just like [`A()`](A.md) adds an A
+ * * [`NS()`](NS.md) adds an NS record to a zone, just like [`A()`](A.md) adds an A
* record to the zone. This is generally used to delegate a subzone.
*
* * The `NAMESERVER()` directive speaks to the Registrar about how the parent should delegate the zone.
@@ -1714,7 +1976,7 @@ declare function MX(name: string, priority: number, target: string, ...modifiers
* If dnsconfig.js has zero `NAMESERVER()` commands for a domain, it will
* use the API to remove all non-default nameservers.
*
- * If dnsconfig.js has 1 or more `NAMESERVER()` commands for a domain, it
+ * If `dnsconfig.js` has 1 or more `NAMESERVER()` commands for a domain, it
* will use the API to add those nameservers (unless, of course,
* they already exist).
*
@@ -1727,7 +1989,7 @@ declare function MX(name: string, priority: number, target: string, ...modifiers
* var REG_THIRDPARTY = NewRegistrar("ThirdParty");
* D("example.com", REG_THIRDPARTY,
* ...
- * )
+ * );
* ```
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/nameserver
@@ -1737,12 +1999,12 @@ declare function NAMESERVER(name: string, ...modifiers: RecordModifier[]): Domai
/**
* NAMESERVER_TTL sets the TTL on the domain apex NS RRs defined by [`NAMESERVER`](NAMESERVER.md).
*
- * The value can be an integer or a string. See [`TTL`](../record/TTL.md) for examples.
+ * The value can be an integer or a string. See [`TTL`](../record-modifiers/TTL.md) for examples.
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* NAMESERVER_TTL("2d"),
- * NAMESERVER("ns")
+ * NAMESERVER("ns"),
* );
* ```
*
@@ -1755,11 +2017,11 @@ declare function NAMESERVER(name: string, ...modifiers: RecordModifier[]): Domai
* NAMESERVER("ns1.provider.com."), //inherits NAMESERVER_TTL
* NAMESERVER("ns2.provider.com."), //inherits NAMESERVER_TTL
* A("@","1.2.3.4"), // inherits DefaultTTL
- * A("foo", "2.3.4.5", TTL(600)) // overrides DefaultTTL for this record only
+ * A("foo", "2.3.4.5", TTL(600)), // overrides DefaultTTL for this record only
* );
* ```
*
- * To apply a default TTL to all other record types, see [`DefaultTTL`](../domain/DefaultTTL.md)
+ * To apply a default TTL to all other record types, see [`DefaultTTL`](../domain-modifiers/DefaultTTL.md)
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/nameserver_ttl
*/
@@ -1907,7 +2169,7 @@ declare function NAMESERVER_TTL(ttl: Duration): DomainModifier;
* NAPTR("7", 10, 10, "u", "E2U+SIP", "!^.*$!sip:jane@example.com!", "."),
* NAPTR("8", 10, 10, "u", "E2U+SIP", "!^.*$!sip:mike@example.com!", "."),
* NAPTR("9", 10, 10, "u", "E2U+SIP", "!^.*$!sip:linda@example.com!", "."),
- * NAPTR("0", 10, 10, "u", "E2U+SIP", "!^.*$!sip:fax@example.com!", ".")
+ * NAPTR("0", 10, 10, "u", "E2U+SIP", "!^.*$!sip:fax@example.com!", "."),
* );
* ```
*
@@ -1916,7 +2178,7 @@ declare function NAMESERVER_TTL(ttl: Duration): DomainModifier;
* D("4.3.2.1.5.5.5.0.0.8.1.e164.arpa.", REG_MY_PROVIDER, DnsProvider(R53),
* NAPTR("@", 100, 50, "u", "E2U+SIP", "!^.*$!sip:customer-service@example.com!", "."),
* NAPTR("@", 101, 50, "u", "E2U+email", "!^.*$!mailto:information@example.com!", "."),
- * NAPTR("@", 101, 50, "u", "smtp+E2U", "!^.*$!mailto:information@example.com!", ".")
+ * NAPTR("@", 101, 50, "u", "smtp+E2U", "!^.*$!mailto:information@example.com!", "."),
* );
* ```
*
@@ -1968,7 +2230,7 @@ declare function NAPTR(subdomain: string, order: number, preference: number, ter
* By setting `NO_PURGE` on a domain, this tells DNSControl not to delete the
* records found in the domain.
*
- * It is similar to [`IGNORE`](domain/IGNORE.md) but more general.
+ * It is similar to [`IGNORE`](IGNORE.md) but more general.
*
* The original reason for `NO_PURGE` was that a legacy system was adopting
* DNSControl. Previously the domain was managed via Microsoft DNS Server's GUI.
@@ -1983,13 +2245,13 @@ declare function NAPTR(subdomain: string, order: number, preference: number, ter
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), NO_PURGE,
- * A("foo","1.2.3.4")
+ * A("foo","1.2.3.4"),
* );
* ```
*
* The main caveat of `NO_PURGE` is that intentionally deleting records becomes
* more difficult. Suppose a `NO_PURGE` zone has an record such as A("ken",
- * "1.2.3.4"). Removing the record from dnsconfig.js will not delete "ken" from
+ * "1.2.3.4"). Removing the record from `dnsconfig.js` will not delete "ken" from
* the domain. DNSControl has no way of knowing the record was deleted from the
* file The DNS record must be removed manually. Users of `NO_PURGE` are prone
* to finding themselves with an accumulation of orphaned DNS records. That's easy
@@ -2004,8 +2266,8 @@ declare function NAPTR(subdomain: string, order: number, preference: number, ter
*
* ## See also
*
- * * [`PURGE`](domain/PURGE.md) is the default, thus this command is a no-op
- * * [`IGNORE`](domain/IGNORE.md) is similar to `NO_PURGE` but is more selective
+ * * [`PURGE`](PURGE.md) is the default, thus this command is a no-op
+ * * [`IGNORE`](IGNORE.md) is similar to `NO_PURGE` but is more selective
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/no_purge
*/
@@ -2033,7 +2295,26 @@ declare const NO_PURGE: DomainModifier;
declare function NS(name: string, target: string, ...modifiers: RecordModifier[]): DomainModifier;
/**
- * Documentation needed.
+ * `NS1_URLFWD` is an NS1-specific feature that maps to NS1's URLFWD record, which creates HTTP 301 (permanent) or 302 (temporary) redirects.
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * NS1_URLFWD("urlfwd", "/ http://example.com 302 2 0")
+ * );
+ * ```
+ *
+ * The fields are:
+ * * name: the record name
+ * * target: a complex field containing the following, space separated:
+ * * from - the path to match
+ * * to - the url to redirect to
+ * * redirectType - (0 - masking, 301, 302)
+ * * pathForwardingMode - (0 - All, 1 - Capture, 2 - None)
+ * * queryForwardingMode - (0 - disabled, 1 - enabled)
+ *
+ * WARNING: According to NS1, this type of record is deprecated and in the process
+ * of being replaced by the premium-only `REDIRECT` record type. While still able to be
+ * configured through the API, as suggested by NS1, please try not to use it, going forward.
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/service-provider-specific/ns1/ns1_urlfwd
*/
@@ -2059,7 +2340,7 @@ declare function NS1_URLFWD(name: string, target: string, ...modifiers: RecordMo
* var DNS_MYAWS = NewDnsProvider("myaws", "ROUTE53");
*
* D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- * A("@","1.2.3.4")
+ * A("@","1.2.3.4"),
* );
* ```
*
@@ -2070,7 +2351,7 @@ declare function NS1_URLFWD(name: string, target: string, ...modifiers: RecordMo
* var DNS_MYAWS = NewDnsProvider("myaws");
*
* D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- * A("@","1.2.3.4")
+ * A("@","1.2.3.4"),
* );
* ```
*
@@ -2098,7 +2379,7 @@ declare function NewDnsProvider(name: string, type?: string, meta?: object): str
* var DNS_MYAWS = NewDnsProvider("myaws", "ROUTE53");
*
* D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- * A("@","1.2.3.4")
+ * A("@","1.2.3.4"),
* );
* ```
*
@@ -2109,7 +2390,7 @@ declare function NewDnsProvider(name: string, type?: string, meta?: object): str
* var DNS_MYAWS = NewDnsProvider("myaws");
*
* D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- * A("@","1.2.3.4")
+ * A("@","1.2.3.4"),
* );
* ```
*
@@ -2128,6 +2409,27 @@ declare function NewRegistrar(name: string, type?: string, meta?: object): strin
*/
declare function PANIC(message: string): never;
+/**
+ * `PORKBUN_URLFWD` is a Porkbun-specific feature that maps to Porkbun's URL forwarding feature, which creates HTTP 301 (permanent) or 302 (temporary) redirects.
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * PORKBUN_URLFWD("urlfwd1", "http://example.com"),
+ * PORKBUN_URLFWD("urlfwd2", "http://example.org", {type: "permanent", includePath: "yes", wildcard: "no"})
+ * );
+ * ```
+ *
+ * The fields are:
+ * * name: the record name
+ * * target: where you'd like to forward the domain to
+ * * type: valid types are: `temporary` (302 / 307) or `permanent` (301), default to `temporary`
+ * * includePath: whether to include the URI path in the redirection. Valid options are `yes` or `no`, default to `no`
+ * * wildcard: forward all subdomains of the domain. Valid options are `yes` or `no`, default to `yes`
+ *
+ * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/service-provider-specific//porkbun_urlfwd
+ */
+declare function PORKBUN_URLFWD(name: string, target: string, ...modifiers: RecordModifier[]): DomainModifier;
+
/**
* PTR adds a PTR record to the domain.
*
@@ -2136,7 +2438,7 @@ declare function PANIC(message: string): never;
*
* Target should be a string representing the FQDN of a host. Like all FQDNs in DNSControl, it must end with a `.`.
*
- * **Magic Mode:**
+ * # Magic Mode
*
* PTR records are complex and typos are common. Therefore DNSControl
* enables features to save labor and
@@ -2167,12 +2469,12 @@ declare function PANIC(message: string): never;
* of `REV("1.2.3.4")` is `4.3.2.1.in-addr.arpa.`, which means the following
* are all equivalent:
*
- * * `PTR(REV("1.2.3.4"), `
- * * `PTR("4.3.2.1.in-addr.arpa."), `
- * * `PTR("4.3",` // Assuming the domain is `2.1.in-addr.arpa`
+ * * `PTR(REV("1.2.3.4", ...`
+ * * `PTR("4.3.2.1.in-addr.arpa.", ...`
+ * * `PTR("4.3", ...` // Assuming the domain is `2.1.in-addr.arpa`
*
* All magic is RFC2317-aware. We use the first format listed in the
- * RFC for both [`REV()`](../global/REV.md) and `PTR()`. The format is
+ * RFC for both [`REV()`](../top-level-functions/REV.md) and `PTR()`. The format is
* `FIRST/MASK.C.B.A.in-addr.arpa` where `FIRST` is the first IP address
* of the zone, `MASK` is the netmask of the zone (25-31 inclusive),
* and A, B, C are the first 3 octets of the IP address. For example
@@ -2204,9 +2506,33 @@ declare function PANIC(message: string): never;
* );
* ```
*
- * In the future we plan on adding a flag to [`A()`](A.md) which will insert
- * the correct PTR() record if the appropriate `.arpa` domain has been
- * defined.
+ * # Automatic forward and reverse lookups
+ *
+ * DNSControl does not automatically generate forward and reverse lookups. However
+ * it is possible to write a macro that does this by using the
+ * [`D_EXTEND()`](../global/D_EXTEND.md)
+ * function to insert `A` and `PTR` records into previously-defined domains.
+ *
+ * ```javascript
+ * function FORWARD_AND_REVERSE(ipaddr, fqdn) {
+ * D_EXTEND(dom,
+ * A(fqdn, ipaddr)
+ * );
+ * D_EXTEND(REV(ipaddr),
+ * PTR(ipaddr, fqdn)
+ * );
+ * }
+ *
+ * D("example.com", REGISTRAR, DnsProvider(DSP_NONE),
+ * ...,
+ * );
+ * D(REV("10.20.30.0/24"), REGISTRAR, DnsProvider(DSP_NONE),
+ * ...,
+ * );
+ *
+ * FORWARD_AND_REVERSE("10.20.30.77", "foo.example.com.");
+ * FORWARD_AND_REVERSE("10.20.30.99", "bar.example.com.");
+ * ```
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/ptr
*/
@@ -2269,7 +2595,7 @@ declare const PURGE: DomainModifier;
* * _S3 bucket_ (configured as website): specify the domain name of the Amazon S3 website endpoint in which you configured the bucket (for instance s3-website-us-east-2.amazonaws.com). For the available values refer to the [Amazon S3 Website Endpoints](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region).
* * _Another Route53 record_: specify the value of the name of another record in the same hosted zone.
*
- * For all the target type, excluding 'another record', you have to specify the `Zone ID` of the target. This is done by using the [`R53_ZONE`](../record/R53_ZONE.md) record modifier.
+ * For all the target type, excluding 'another record', you have to specify the `Zone ID` of the target. This is done by using the [`R53_ZONE`](../record-modifiers/R53_ZONE.md) record modifier.
*
* The zone id can be found depending on the target type:
*
@@ -2279,11 +2605,13 @@ declare const PURGE: DomainModifier;
* * _S3 bucket_ (configured as website): specify the hosted zone ID for the region that you created the bucket in. You can find it in [the List of regions and hosted Zone IDs](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region)
* * _Another Route 53 record_: you can either specify the correct zone id or do not specify anything and DNSControl will figure out the right zone id. (Note: Route53 alias can't reference a record in a different zone).
*
+ * Target health evaluation can be enabled with the [`R53_EVALUATE_TARGET_HEALTH`](../record-modifiers/R53\_EVALUATE\_TARGET\_HEALTH.md) record modifier.
+ *
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider("ROUTE53"),
* R53_ALIAS("foo", "A", "bar"), // record in same zone
* R53_ALIAS("foo", "A", "bar", R53_ZONE("Z35SXDOTRQ7X7K")), // record in same zone, zone specified
- * R53_ALIAS("foo", "A", "blahblah.elasticloadbalancing.us-west-1.amazonaws.com.", R53_ZONE("Z368ELLRRE2KJ0")), // a classic ELB in us-west-1
+ * R53_ALIAS("foo", "A", "blahblah.elasticloadbalancing.us-west-1.amazonaws.com.", R53_ZONE("Z368ELLRRE2KJ0"), R53_EVALUATE_TARGET_HEALTH(true)), // a classic ELB in us-west-1 with target health evaluation enabled
* R53_ALIAS("foo", "A", "blahblah.elasticbeanstalk.us-west-2.amazonaws.com.", R53_ZONE("Z38NKT9BP95V3O")), // an Elastic Beanstalk environment in us-west-2
* R53_ALIAS("foo", "A", "blahblah-bucket.s3-website-us-west-1.amazonaws.com.", R53_ZONE("Z2F56UZL2M1ACD")), // a website S3 Bucket in us-west-1
* );
@@ -2291,14 +2619,21 @@ declare const PURGE: DomainModifier;
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/service-provider-specific/amazon-route-53/r53_alias
*/
-declare function R53_ALIAS(name: string, target: string, zone_idModifier: DomainModifier & RecordModifier): DomainModifier;
+declare function R53_ALIAS(name: string, target: string, zone_idModifier: DomainModifier & RecordModifier, evaluatetargethealthModifier: RecordModifier): DomainModifier;
+
+/**
+ * `R53_EVALUATE_TARGET_HEALTH` lets you enable target health evaluation for a [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) record. Omitting `R53_EVALUATE_TARGET_HEALTH()` from `R53_ALIAS()` set the behavior to false.
+ *
+ * @see https://docs.dnscontrol.org/language-reference/record-modifiers/service-provider-specific/amazon-route-53/r53_evaluate_target_health
+ */
+declare function R53_EVALUATE_TARGET_HEALTH(enabled: boolean): RecordModifier;
/**
- * `R53_ZONE` lets you specify the AWS Zone ID for an entire domain ([`D()`](../global/D.md)) or a specific [`R53_ALIAS()`](../domain/R53_ALIAS.md) record.
+ * `R53_ZONE` lets you specify the AWS Zone ID for an entire domain ([`D()`](../top-level-functions/D.md)) or a specific [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) record.
*
- * When used with [`D()`](../global/D.md), it sets the zone id of the domain. This can be used to differentiate between split horizon domains in public and private zones. See this [example](../../providers/route53.md#split-horizon) in the [Amazon Route 53 provider page](../../providers/route53.md).
+ * When used with [`D()`](../top-level-functions/D.md), it sets the zone id of the domain. This can be used to differentiate between split horizon domains in public and private zones. See this [example](../../provider/route53.md#split-horizon) in the [Amazon Route 53 provider page](../../provider/route53.md).
*
- * When used with [`R53_ALIAS()`](../domain/R53_ALIAS.md) it sets the required Route53 hosted zone id in a R53_ALIAS record. See [`R53_ALIAS()`](../domain/R53_ALIAS.md) documentation for details.
+ * When used with [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) it sets the required Route53 hosted zone id in a R53_ALIAS record. See [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) documentation for details.
*
* @see https://docs.dnscontrol.org/language-reference/record-modifiers/service-provider-specific/amazon-route-53/r53_zone
*/
@@ -2308,53 +2643,115 @@ declare function R53_ZONE(zone_id: string): DomainModifier & RecordModifier;
* `REV` returns the reverse lookup domain for an IP network. For
* example `REV("1.2.3.0/24")` returns `3.2.1.in-addr.arpa.` and
* `REV("2001:db8:302::/48")` returns `2.0.3.0.8.b.d.0.1.0.0.2.ip6.arpa.`.
- * This is used in [`D()`](D.md) functions to create reverse DNS lookup zones.
*
- * This is a convenience function. You could specify `D("3.2.1.in-addr.arpa",
- * ...` if you like to do things manually but why would you risk making
- * typos?
+ * `REV()` is commonly used with the [`D()`](D.md) functions to create reverse DNS lookup zones.
*
- * `REV` complies with RFC2317, "Classless in-addr.arpa delegation"
- * for netmasks of size /25 through /31.
- * While the RFC permits any format, we abide by the recommended format:
- * `FIRST/MASK.C.B.A.in-addr.arpa` where `FIRST` is the first IP address
- * of the zone, `MASK` is the netmask of the zone (25-31 inclusive),
- * and A, B, C are the first 3 octets of the IP address. For example
- * `172.20.18.130/27` is located in a zone named
- * `128/27.18.20.172.in-addr.arpa`
+ * These two are equivalent:
*
- * If the address does not include a "/" then `REV` assumes /32 for IPv4 addresses
+ * ```javascript
+ * D("3.2.1.in-addr.arpa", ...
+ * ```
+ *
+ * ```javascript
+ * D(REV("1.2.3.0/24", ...
+ * ```
+ *
+ * The latter is easier to type and less error-prone.
+ *
+ * If the address does not include a "/" then `REV()` assumes /32 for IPv4 addresses
* and /128 for IPv6 addresses.
*
- * Note that the lower bits (the ones outside the netmask) must be zeros. They are not
- * zeroed out automatically. Thus, `REV("1.2.3.4/24")` is an error. This is done
- * to catch typos.
+ * # RFC compliance
+ *
+ * `REV()` implements both RFC 2317 and the newer RFC 4183. The `REVCOMPAT()`
+ * function selects which mode is used. If `REVCOMPAT()` is not called, a default
+ * is selected for you. The default will change to RFC 4183 in DNSControl v5.0.
+ *
+ * See [`REVCOMPAT()`](REVCOMPAT.md) for details.
+ *
+ * # Host bits
+ *
+ * v4.x:
+ * The host bits (the ones outside the netmask) must be zeros. They are not zeroed
+ * out automatically. Thus, `REV("1.2.3.4/24")` is an error.
+ *
+ * v5.0 and later:
+ * The host bits (the ones outside the netmask) are ignored. Thus
+ * `REV("1.2.3.4/24")` and `REV("1.2.3.0/24")` are equivalent.
+ *
+ * # Examples
+ *
+ * Here's an example reverse lookup domain:
*
* ```javascript
* D(REV("1.2.3.0/24"), REGISTRAR, DnsProvider(BIND),
* PTR("1", "foo.example.com."),
* PTR("2", "bar.example.com."),
* PTR("3", "baz.example.com."),
- * // These take advantage of DNSControl's ability to generate the right name:
+ * // If the first parameter is an IP address, DNSControl automatically calls REV() for you.
* PTR("1.2.3.10", "ten.example.com."),
* );
*
* D(REV("2001:db8:302::/48"), REGISTRAR, DnsProvider(BIND),
* PTR("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", "foo.example.com."), // 2001:db8:302::1
- * // These take advantage of DNSControl's ability to generate the right name:
+ * // If the first parameter is an IP address, DNSControl automatically calls REV() for you.
* PTR("2001:db8:302::2", "two.example.com."), // 2.0.0...
* PTR("2001:db8:302::3", "three.example.com."), // 3.0.0...
* );
* ```
*
- * In the future we plan on adding a flag to [`A()`](../domain/A.md)which will insert
- * the correct PTR() record in the appropriate `D(REV())` domain (i.e. `.arpa` domain) has been
- * defined.
+ * # Automatic forward and reverse record generation
+ *
+ * DNSControl does not automatically generate forward and reverse lookups. However
+ * it is possible to write a macro that does this. See
+ * [`PTR()`](../domain/PTR.md) for an example.
*
* @see https://docs.dnscontrol.org/language-reference/top-level-functions/rev
*/
declare function REV(address: string): string;
+/**
+ * `REVCOMPAT()` controls which RFC the [`REV()`](REV.md) function adheres to.
+ *
+ * Include one of these two commands near the top `dnsconfig.js` (at the global level):
+ *
+ * ```javascript
+ * REVCOMPAT("rfc2317"); // RFC 2117: Compatible with old files.
+ * REVCOMPAT("rfc4183"); // RFC 4183: Adopt the newer standard.
+ * ```
+ *
+ * `REVCOMPAT()` is global for all of `dnsconfig.js`. It must appear before any
+ * use of `REV()`; If not, behavior is undefined.
+ *
+ * # RFC 4183 vs RFC 2317
+ *
+ * RFC 2317 and RFC 4183 are two different ways to implement reverse lookups for
+ * CIDR blocks that are not on 8-bit boundaries (/24, /16, /8).
+ *
+ * Originally DNSControl implemented the older standard, which only specifies what
+ * to do for /8, /16, /24 - /32. Using `REV()` for /9-17 and /17-23 CIDRs was an
+ * error.
+ *
+ * v4 defaults to RFC 2317. In v5.0 the default will change to RFC 4183.
+ * `REVCOMPAT()` is provided for those that wish to retain the old behavior.
+ *
+ * For more information, see [Opinion #9](../../opinions.md#opinion-9-rfc-4183-is-better-than-rfc-2317).
+ *
+ * # Transition plan
+ *
+ * What's the default behavior if `REVCOMPAT()` is not used?
+ *
+ * | Version | /9 to /15 and /17 to /23 | /25 to 32 | Warnings |
+ * |---------|--------------------------|-----------|----------------------------|
+ * | v4 | RFC 4183 | RFC 2317 | Only if /25 - /32 are used |
+ * | v5 | RFC 4183 | RFC 4183 | none |
+ *
+ * No warnings are generated if the `REVCOMPAT()` function is used.
+ *
+ * @see https://docs.dnscontrol.org/language-reference/top-level-functions/revcompat
+ */
+declare function REVCOMPAT(rfc: string): string;
+
/**
* `SOA` adds an `SOA` record to a domain. The name should be `@`. ns and mbox are strings. The other fields are unsigned 32-bit ints.
*
@@ -2373,7 +2770,7 @@ declare function REV(address: string): string;
* * Most providers automatically generate SOA records. They will ignore any `SOA()` statements.
* * The mbox field should not be set to a real email address unless you love spam and hate your privacy.
*
- * There is more info about `SOA` in the documentation for the [BIND provider](../../providers/bind.md).
+ * There is more info about `SOA` in the documentation for the [BIND provider](../../provider/bind.md).
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/soa
*/
@@ -2395,8 +2792,8 @@ declare function SOA(name: string, ns: string, mbox: string, refresh: number, re
*
* ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * TXT("v=spf1 ip4:198.252.206.0/24 ip4:192.111.0.0/24 include:_spf.google.com include:mailgun.org include:spf-basic.fogcreek.com include:mail.zendesk.com include:servers.mcsv.net include:sendgrid.net include:450622.spf05.hubspotemail.net ~all")
- * )
+ * TXT("v=spf1 ip4:198.252.206.0/24 ip4:192.111.0.0/24 include:_spf.google.com include:mailgun.org include:spf-basic.fogcreek.com include:mail.zendesk.com include:servers.mcsv.net include:sendgrid.net include:450622.spf05.hubspotemail.net ~all"),
+ * );
* ```
*
* This has a few problems:
@@ -2652,11 +3049,11 @@ declare function SOA(name: string, ns: string, mbox: string, refresh: number, re
* });
*
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * SPF_MYSETTINGS
+ * SPF_MYSETTINGS,
* );
*
* D("example2.tld", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- * SPF_MYSETTINGS
+ * SPF_MYSETTINGS,
* );
* ```
*
@@ -2706,13 +3103,34 @@ declare function SRV(name: string, priority: number, weight: number, port: numbe
* `value` is the fingerprint as a string.
*
* ```javascript
- * SSHFP("@", 1, 1, "00yourAmazingFingerprint00"),
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * SSHFP("@", 1, 1, "00yourAmazingFingerprint00"),
+ * );
* ```
*
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/sshfp
*/
declare function SSHFP(name: string, algorithm: 0 | 1 | 2 | 3 | 4, type: 0 | 1 | 2, value: string, ...modifiers: RecordModifier[]): DomainModifier;
+/**
+ * SVCB adds an SVCB record to a domain. The name should be the relative label for the record. Use `@` for the domain apex.
+ *
+ * The priority must be a positive number, the address should be an ip address, either a string, or a numeric value obtained via [IP](../top-level-functions/IP.md).
+ *
+ * The params may be configured to specify the `alpn`, `ipv4hint`, `ipv6hint`, `ech` or `port` setting. Several params may be joined by a space. Not existing params may be specified as an empty string `""`
+ *
+ * Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata.
+ *
+ * ```javascript
+ * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ * SVCB("@", 1, ".", "ipv4hint=123.123.123.123 alpn=h3,h2 port=443"),
+ * );
+ * ```
+ *
+ * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/svcb
+ */
+declare function SVCB(name: string, priority: number, target: string, params: string, ...modifiers: RecordModifier[]): DomainModifier;
+
/**
* `TLSA` adds a `TLSA` record to a domain. The name should be the relative label for the record.
*
@@ -2733,7 +3151,7 @@ declare function TLSA(name: string, usage: number, selector: number, type: numbe
/**
* TTL sets the TTL for a single record only. This will take precedence
- * over the domain's [DefaultTTL](../domain/DefaultTTL.md) if supplied.
+ * over the domain's [DefaultTTL](../domain-modifiers/DefaultTTL.md) if supplied.
*
* The value can be:
*
@@ -2784,7 +3202,7 @@ declare function TTL(ttl: Duration): RecordModifier;
* TXT("multiple", ["one", "two", "three"]), // Multiple strings
* TXT("quoted", "any "quotes" and escapes? ugh; no worries!"),
* TXT("_domainkey", "t=y; o=-;"), // Escapes are done for you automatically.
- * TXT("long", "X".repeat(300)) // Long strings are split automatically.
+ * TXT("long", "X".repeat(300)), // Long strings are split automatically.
* );
* ```
*
@@ -2837,7 +3255,7 @@ declare function TTL(ttl: Duration): RecordModifier;
*
* #### How can you tell if a provider will support a particular `TXT()` record?
*
- * Include the `TXT()` record in a [`D()`](../global/D.md) as usual, along
+ * Include the `TXT()` record in a [`D()`](../top-level-functions/D.md) as usual, along
* with the `DnsProvider()` for that provider. Run `dnscontrol check` to
* see if any errors are produced. The check command does not talk to
* the provider's API, thus permitting you to do this without having an
diff --git a/commands/types/fetch.d.ts b/commands/types/fetch.d.ts
index bf34ddab7a..11dff1a277 100644
--- a/commands/types/fetch.d.ts
+++ b/commands/types/fetch.d.ts
@@ -1,5 +1,5 @@
/**
- * @dnscontrol-auto-doc-comment functions/global/FETCH.md
+ * @dnscontrol-auto-doc-comment language-reference/top-level-functions/FETCH.md
*/
declare function FETCH(
url: string,
diff --git a/commands/types/others.d.ts b/commands/types/others.d.ts
index 31e6aa0d7e..185ede344f 100644
--- a/commands/types/others.d.ts
+++ b/commands/types/others.d.ts
@@ -1,4 +1,5 @@
declare function require(name: `${string}.json`): any;
+declare function require(name: `${string}.json5`): any;
declare function require(name: string): true;
/**
diff --git a/commands/writeTypes.go b/commands/writeTypes.go
index 3a8adedcd1..ef0a6100fb 100644
--- a/commands/writeTypes.go
+++ b/commands/writeTypes.go
@@ -4,10 +4,14 @@ import (
_ "embed" // Required by go:embed
"os"
- versionInfo "github.com/StackExchange/dnscontrol/v4/pkg/version"
"github.com/urfave/cli/v2"
)
+// GoReleaser: version
+var (
+ version = "dev"
+)
+
var _ = cmd(catUtils, func() *cli.Command {
var args TypesArgs
return &cli.Command{
@@ -50,11 +54,8 @@ func WriteTypes(args TypesArgs) error {
file.WriteString("// This file was automatically generated by DNSControl. Do not edit it directly.\n")
file.WriteString("// To update it, run `dnscontrol write-types`.\n\n")
- file.WriteString("// DNSControl version: " + versionInfo.Banner() + "\n")
+ file.WriteString("// " + version + "\n")
file.WriteString(dtsContent)
- if err != nil {
- return err
- }
print("Successfully wrote " + args.DTSFile + "\n")
return nil
diff --git a/commands/zonecache.go b/commands/zonecache.go
new file mode 100644
index 0000000000..0cfb70874f
--- /dev/null
+++ b/commands/zonecache.go
@@ -0,0 +1,28 @@
+package commands
+
+import "github.com/StackExchange/dnscontrol/v4/providers"
+
+// NewZoneCache creates a zoneCache.
+func NewZoneCache() *zoneCache {
+ return &zoneCache{}
+}
+
+func (zc *zoneCache) zoneList(name string, lister providers.ZoneLister) (*[]string, error) {
+ zc.Lock()
+ defer zc.Unlock()
+
+ if zc.cache == nil {
+ zc.cache = map[string]*[]string{}
+ }
+
+ if v, ok := zc.cache[name]; ok {
+ return v, nil
+ }
+
+ zones, err := lister.ListZones()
+ if err != nil {
+ return nil, err
+ }
+ zc.cache[name] = &zones
+ return &zones, nil
+}
diff --git a/dnscontrol.nuspec b/dnscontrol.nuspec
deleted file mode 100644
index 015533866c..0000000000
--- a/dnscontrol.nuspec
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- dnscontrol
- 0.0.0
- DnsControl
- Stack Overflow
- https://github.com/stackexchange/dnscontrol
- 2020
- https://github.com/StackExchange/dnscontrol/blob/master/LICENSE
- true
- https://github.com/stackexchange/dnscontrol
- https://docs.dnscontrol.org/
- dns
- Synchronize your DNS to multiple providers from a simple DSL
- This package simply installs the dnscontrol tool on your system
-
-
-
-
-
-
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index 1aa9bff608..18db051591 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -37,7 +37,7 @@
diff --git a/documentation/.gitbook.yaml b/documentation/.gitbook.yaml
new file mode 100644
index 0000000000..983e3097d9
--- /dev/null
+++ b/documentation/.gitbook.yaml
@@ -0,0 +1,52 @@
+redirects:
+ service-providers/providers: providers.md
+ service-providers/providers/akamaiedgedns: provider/akamaiedgedns.md
+ service-providers/providers/autodns: provider/autodns.md
+ service-providers/providers/axfrddns: provider/axfrddns.md
+ service-providers/providers/azure_dns: provider/azure_dns.md
+ service-providers/providers/azure_private_dns: provider/azure_private_dns.md
+ service-providers/providers/bind: provider/bind.md
+ service-providers/providers/bunny_dns: provider/bunny_dns.md
+ service-providers/providers/cloudflareapi: provider/cloudflareapi.md
+ service-providers/providers/cloudns: provider/cloudns.md
+ service-providers/providers/cscglobal: provider/cscglobal.md
+ service-providers/providers/desec: provider/desec.md
+ service-providers/providers/digitalocean: provider/digitalocean.md
+ service-providers/providers/dnsimple: provider/dnsimple.md
+ service-providers/providers/dnsmadeeasy: provider/dnsmadeeasy.md
+ service-providers/providers/dnsoverhttps: provider/dnsoverhttps.md
+ service-providers/providers/domainnameshop: provider/domainnameshop.md
+ service-providers/providers/dynadot: provider/dynadot.md
+ service-providers/providers/easyname: provider/easyname.md
+ service-providers/providers/exoscale: provider/exoscale.md
+ service-providers/providers/gandi_v5: provider/gandi_v5.md
+ service-providers/providers/gcloud: provider/gcloud.md
+ service-providers/providers/gcore: provider/gcore.md
+ service-providers/providers/hedns: provider/hedns.md
+ service-providers/providers/hetzner: provider/hetzner.md
+ service-providers/providers/hexonet: provider/hexonet.md
+ service-providers/providers/hostingde: provider/hostingde.md
+ service-providers/providers/internetbs: provider/internetbs.md
+ service-providers/providers/inwx: provider/inwx.md
+ service-providers/providers/linode: provider/linode.md
+ service-providers/providers/loopia: provider/loopia.md
+ service-providers/providers/luadns: provider/luadns.md
+ service-providers/providers/msdns: provider/msdns.md
+ service-providers/providers/mythicbeasts: provider/mythicbeasts.md
+ service-providers/providers/namecheap: provider/namecheap.md
+ service-providers/providers/namedotcom: provider/namedotcom.md
+ service-providers/providers/netcup: provider/netcup.md
+ service-providers/providers/netlify: provider/netlify.md
+ service-providers/providers/ns1: provider/ns1.md
+ service-providers/providers/opensrs: provider/opensrs.md
+ service-providers/providers/oracle: provider/oracle.md
+ service-providers/providers/ovh: provider/ovh.md
+ service-providers/providers/packetframe: provider/packetframe.md
+ service-providers/providers/porkbun: provider/porkbun.md
+ service-providers/providers/powerdns: provider/powerdns.md
+ service-providers/providers/realtimeregister: provider/realtimeregister.md
+ service-providers/providers/route53: provider/route53.md
+ service-providers/providers/rwth: provider/rwth.md
+ service-providers/providers/softlayer: provider/softlayer.md
+ service-providers/providers/transip: provider/transip.md
+ service-providers/providers/vultr: provider/vultr.md
diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md
index fd4ee01242..a65e5b34eb 100644
--- a/documentation/SUMMARY.md
+++ b/documentation/SUMMARY.md
@@ -8,144 +8,166 @@
* [Examples](examples.md)
* [Migrating zones to DNSControl](migrating.md)
* [TypeScript autocomplete and type checking](typescript.md)
-* [Disabling Colors](colors.md)
+* [Providers](providers.md)
## Language Reference
* [JavaScript DSL](js.md)
* Top Level Functions
- * [D](functions/global/D.md)
- * [DEFAULTS](functions/global/DEFAULTS.md)
- * [DOMAIN_ELSEWHERE](functions/global/DOMAIN_ELSEWHERE.md)
- * [DOMAIN_ELSEWHERE_AUTO](functions/global/DOMAIN_ELSEWHERE_AUTO.md)
- * [D_EXTEND](functions/global/D_EXTEND.md)
- * [FETCH](functions/global/FETCH.md)
- * [IP](functions/global/IP.md)
- * [NewDnsProvider](functions/global/NewDnsProvider.md)
- * [NewRegistrar](functions/global/NewRegistrar.md)
- * [PANIC](functions/global/PANIC.md)
- * [REV](functions/global/REV.md)
- * [getConfiguredDomains](functions/global/getConfiguredDomains.md)
- * [require](functions/global/require.md)
- * [require_glob](functions/global/require_glob.md)
+ * [D](language-reference/top-level-functions/D.md)
+ * [DEFAULTS](language-reference/top-level-functions/DEFAULTS.md)
+ * [DOMAIN_ELSEWHERE](language-reference/top-level-functions/DOMAIN_ELSEWHERE.md)
+ * [DOMAIN_ELSEWHERE_AUTO](language-reference/top-level-functions/DOMAIN_ELSEWHERE_AUTO.md)
+ * [D_EXTEND](language-reference/top-level-functions/D_EXTEND.md)
+ * [FETCH](language-reference/top-level-functions/FETCH.md)
+ * [HASH](language-reference/top-level-functions/HASH.md)
+ * [IP](language-reference/top-level-functions/IP.md)
+ * [NewDnsProvider](language-reference/top-level-functions/NewDnsProvider.md)
+ * [NewRegistrar](language-reference/top-level-functions/NewRegistrar.md)
+ * [PANIC](language-reference/top-level-functions/PANIC.md)
+ * [REV](language-reference/top-level-functions/REV.md)
+ * [REVCOMPAT](language-reference/top-level-functions/REVCOMPAT.md)
+ * [getConfiguredDomains](language-reference/top-level-functions/getConfiguredDomains.md)
+ * [require](language-reference/top-level-functions/require.md)
+ * [require_glob](language-reference/top-level-functions/require_glob.md)
* Domain Modifiers
- * [A](functions/domain/A.md)
- * [AAAA](functions/domain/AAAA.md)
- * [ALIAS](functions/domain/ALIAS.md)
- * [AUTODNSSEC_OFF](functions/domain/AUTODNSSEC_OFF.md)
- * [AUTODNSSEC_ON](functions/domain/AUTODNSSEC_ON.md)
- * [CAA](functions/domain/CAA.md)
- * [CAA_BUILDER](functions/domain/CAA_BUILDER.md)
- * [CNAME](functions/domain/CNAME.md)
- * [DISABLE_IGNORE_SAFETY_CHECK](functions/domain/DISABLE_IGNORE_SAFETY_CHECK.md)
- * [DMARC_BUILDER](functions/domain/DMARC_BUILDER.md)
- * [DS](functions/domain/DS.md)
- * [DefaultTTL](functions/domain/DefaultTTL.md)
- * [DnsProvider](functions/domain/DnsProvider.md)
- * [FRAME](functions/domain/FRAME.md)
- * [IGNORE](functions/domain/IGNORE.md)
- * [IGNORE_NAME](functions/domain/IGNORE_NAME.md)
- * [IGNORE_TARGET](functions/domain/IGNORE_TARGET.md)
- * [IMPORT_TRANSFORM](functions/domain/IMPORT_TRANSFORM.md)
- * [INCLUDE](functions/domain/INCLUDE.md)
- * [LOC](functions/domain/LOC.md)
- * [LOC_BUILDER_DD](functions/domain/LOC_BUILDER_DD.md)
- * [LOC_BUILDER_DMM_STR](functions/domain/LOC_BUILDER_DMM_STR.md)
- * [LOC_BUILDER_DMS_STR](functions/domain/LOC_BUILDER_DMS_STR.md)
- * [LOC_BUILDER_STR](functions/domain/LOC_BUILDER_STR.md)
- * [M365_BUILDER](functions/domain/M365_BUILDER.md)
- * [MX](functions/domain/MX.md)
- * [NAMESERVER](functions/domain/NAMESERVER.md)
- * [NAMESERVER_TTL](functions/domain/NAMESERVER_TTL.md)
- * [NAPTR](functions/domain/NAPTR.md)
- * [NO_PURGE](functions/domain/NO_PURGE.md)
- * [NS](functions/domain/NS.md)
- * [PTR](functions/domain/PTR.md)
- * [PURGE](functions/domain/PURGE.md)
- * [SOA](functions/domain/SOA.md)
- * [SPF_BUILDER](functions/domain/SPF_BUILDER.md)
- * [SRV](functions/domain/SRV.md)
- * [SSHFP](functions/domain/SSHFP.md)
- * [TLSA](functions/domain/TLSA.md)
- * [TXT](functions/domain/TXT.md)
- * [URL](functions/domain/URL.md)
- * [URL301](functions/domain/URL301.md)
+ * [A](language-reference/domain-modifiers/A.md)
+ * [AAAA](language-reference/domain-modifiers/AAAA.md)
+ * [ALIAS](language-reference/domain-modifiers/ALIAS.md)
+ * [AUTODNSSEC_OFF](language-reference/domain-modifiers/AUTODNSSEC_OFF.md)
+ * [AUTODNSSEC_ON](language-reference/domain-modifiers/AUTODNSSEC_ON.md)
+ * [CAA](language-reference/domain-modifiers/CAA.md)
+ * [CAA_BUILDER](language-reference/domain-modifiers/CAA_BUILDER.md)
+ * [CNAME](language-reference/domain-modifiers/CNAME.md)
+ * [DHCID](language-reference/domain-modifiers/DHCID.md)
+ * [DNAME](language-reference/domain-modifiers/DNAME.md)
+ * [DNSKEY](language-reference/domain-modifiers/DNSKEY.md)
+ * [DISABLE_IGNORE_SAFETY_CHECK](language-reference/domain-modifiers/DISABLE_IGNORE_SAFETY_CHECK.md)
+ * [DMARC_BUILDER](language-reference/domain-modifiers/DMARC_BUILDER.md)
+ * [DS](language-reference/domain-modifiers/DS.md)
+ * [DefaultTTL](language-reference/domain-modifiers/DefaultTTL.md)
+ * [DnsProvider](language-reference/domain-modifiers/DnsProvider.md)
+ * [FRAME](language-reference/domain-modifiers/FRAME.md)
+ * [HTTPS](language-reference/domain-modifiers/HTTPS.md)
+ * [IGNORE](language-reference/domain-modifiers/IGNORE.md)
+ * [IGNORE_NAME](language-reference/domain-modifiers/IGNORE_NAME.md)
+ * [IGNORE_TARGET](language-reference/domain-modifiers/IGNORE_TARGET.md)
+ * [IMPORT_TRANSFORM](language-reference/domain-modifiers/IMPORT_TRANSFORM.md)
+ * [IMPORT_TRANSFORM_STRIP](language-reference/domain-modifiers/IMPORT_TRANSFORM_STRIP.md)
+ * [INCLUDE](language-reference/domain-modifiers/INCLUDE.md)
+ * [LOC](language-reference/domain-modifiers/LOC.md)
+ * [LOC_BUILDER_DD](language-reference/domain-modifiers/LOC_BUILDER_DD.md)
+ * [LOC_BUILDER_DMM_STR](language-reference/domain-modifiers/LOC_BUILDER_DMM_STR.md)
+ * [LOC_BUILDER_DMS_STR](language-reference/domain-modifiers/LOC_BUILDER_DMS_STR.md)
+ * [LOC_BUILDER_STR](language-reference/domain-modifiers/LOC_BUILDER_STR.md)
+ * [M365_BUILDER](language-reference/domain-modifiers/M365_BUILDER.md)
+ * [MX](language-reference/domain-modifiers/MX.md)
+ * [NAMESERVER](language-reference/domain-modifiers/NAMESERVER.md)
+ * [NAMESERVER_TTL](language-reference/domain-modifiers/NAMESERVER_TTL.md)
+ * [NAPTR](language-reference/domain-modifiers/NAPTR.md)
+ * [NO_PURGE](language-reference/domain-modifiers/NO_PURGE.md)
+ * [NS](language-reference/domain-modifiers/NS.md)
+ * [PTR](language-reference/domain-modifiers/PTR.md)
+ * [PURGE](language-reference/domain-modifiers/PURGE.md)
+ * [SOA](language-reference/domain-modifiers/SOA.md)
+ * [SPF_BUILDER](language-reference/domain-modifiers/SPF_BUILDER.md)
+ * [SRV](language-reference/domain-modifiers/SRV.md)
+ * [SSHFP](language-reference/domain-modifiers/SSHFP.md)
+ * [SVCB](language-reference/domain-modifiers/SVCB.md)
+ * [TLSA](language-reference/domain-modifiers/TLSA.md)
+ * [TXT](language-reference/domain-modifiers/TXT.md)
+ * [URL](language-reference/domain-modifiers/URL.md)
+ * [URL301](language-reference/domain-modifiers/URL301.md)
* Service Provider specific
* Akamai Edge Dns
- * [AKAMAICDN](functions/domain/AKAMAICDN.md)
+ * [AKAMAICDN](language-reference/domain-modifiers/AKAMAICDN.md)
* Amazon Route 53
- * [R53_ALIAS](functions/domain/R53_ALIAS.md)
+ * [R53_ALIAS](language-reference/domain-modifiers/R53_ALIAS.md)
* Azure DNS
- * [AZURE_ALIAS](functions/domain/AZURE_ALIAS.md)
+ * [AZURE_ALIAS](language-reference/domain-modifiers/AZURE_ALIAS.md)
* Cloudflare DNS
- * [CF_REDIRECT](functions/domain/CF_REDIRECT.md)
- * [CF_TEMP_REDIRECT](functions/domain/CF_TEMP_REDIRECT.md)
- * [CF_WORKER_ROUTE](functions/domain/CF_WORKER_ROUTE.md)
+ * [CF_REDIRECT](language-reference/domain-modifiers/CF_REDIRECT.md)
+ * [CF_SINGLE_REDIRECT](language-reference/domain-modifiers/CF_SINGLE_REDIRECT.md)
+ * [CF_TEMP_REDIRECT](language-reference/domain-modifiers/CF_TEMP_REDIRECT.md)
+ * [CF_WORKER_ROUTE](language-reference/domain-modifiers/CF_WORKER_ROUTE.md)
* ClouDNS
- * [CLOUDNS_WR](functions/domain/CLOUDNS_WR.md)
+ * [CLOUDNS_WR](language-reference/domain-modifiers/CLOUDNS_WR.md)
* NS1
- * [NS1_URLFWD](functions/domain/NS1_URLFWD.md)
+ * [NS1_URLFWD](language-reference/domain-modifiers/NS1_URLFWD.md)
* Record Modifiers
- * [TTL](functions/record/TTL.md)
+ * [TTL](language-reference/record-modifiers/TTL.md)
* Service Provider specific
* Amazon Route 53
- * [R53_ZONE](functions/record/R53_ZONE.md)
+ * [R53_ZONE](language-reference/record-modifiers/R53_ZONE.md)
+ * [R53_EVALUATE_TARGET_HEALTH](language-reference/record-modifiers/R53\_EVALUATE\_TARGET\_HEALTH.md)
* [Why CNAME/MX/NS targets require a "dot"](why-the-dot.md)
-## Service Providers
+## Provider
-* [Providers](providers.md)
- * [Akamai Edge DNS](providers/akamaiedgedns.md)
- * [Amazon Route 53](providers/route53.md)
- * [AutoDNS](providers/autodns.md)
- * [AXFR+DDNS](providers/axfrddns.md)
- * [Azure DNS](providers/azure_dns.md)
- * [BIND](providers/bind.md)
- * [Cloudflare](providers/cloudflareapi.md)
- * [ClouDNS](providers/cloudns.md)
- * [CSC Global](providers/cscglobal.md)
- * [deSEC](providers/desec.md)
- * [DigitalOcean](providers/digitalocean.md)
- * [DNS Made Simple](providers/dnsmadeeasy.md)
- * [DNSimple](providers/dnsimple.md)
- * [DNS-over-HTTPS](providers/dnsoverhttps.md)
- * [DOMAINNAMESHOP](providers/domainnameshop.md)
- * [easyname](providers/easyname.md)
- * [Gandi_v5](providers/gandi_v5.md)
- * [Gcore](providers/gcore.md)
- * [Google Cloud DNS](providers/gcloud.md)
- * [Hetzner DNS Console](providers/hetzner.md)
- * [HEXONET](providers/hexonet.md)
- * [hosting.de](providers/hostingde.md)
- * [Hurricane Electric DNS](providers/hedns.md)
- * [Internet.bs](providers/internetbs.md)
- * [INWX](providers/inwx.md)
- * [Linode](providers/linode.md)
- * [Loopia](providers/loopia.md)
- * [LuaDNS](providers/luadns.md)
- * [Microsoft DNS Server on Microsoft Windows Server](providers/msdns.md)
- * [Mythic Beasts](providers/mythicbeasts.md)
- * [Namecheap](providers/namecheap.md)
- * [Name.com](providers/namedotcom.md)
- * [Netcup](providers/netcup.md)
- * [Netlify](providers/netlify.md)
- * [NS1](providers/ns1.md)
- * [Oracle Cloud](providers/oracle.md)
- * [OVH](providers/ovh.md)
- * [Packetframe](providers/packetframe.md)
- * [Porkbun](providers/porkbun.md)
- * [PowerDNS](providers/powerdns.md)
- * [RWTH DNS-Admin](providers/rwth.md)
- * [SoftLayer DNS](providers/softlayer.md)
- * [TransIP](providers/transip.md)
- * [Vultr](providers/vultr.md)
+* [Akamai Edge DNS](provider/akamaiedgedns.md)
+* [Amazon Route 53](provider/route53.md)
+* [AutoDNS](provider/autodns.md)
+* [AXFR+DDNS](provider/axfrddns.md)
+* [Azure DNS](provider/azure_dns.md)
+* [Azure Private DNS](provider/azure_private_dns.md)
+* [BIND](provider/bind.md)
+* [Bunny DNS](provider/bunny\_dns.md)
+* [CentralNic Reseller (fka RRPproxy)](provider/cnr.md)
+* [Cloudflare](provider/cloudflareapi.md)
+* [ClouDNS](provider/cloudns.md)
+* [CSC Global](provider/cscglobal.md)
+* [deSEC](provider/desec.md)
+* [DigitalOcean](provider/digitalocean.md)
+* [DNS Made Easy](provider/dnsmadeeasy.md)
+* [DNSimple](provider/dnsimple.md)
+* [DNS-over-HTTPS](provider/dnsoverhttps.md)
+* [DOMAINNAMESHOP](provider/domainnameshop.md)
+* [Dynadot](provider/dynadot.md)
+* [easyname](provider/easyname.md)
+* [Exoscale](provider/exoscale.md)
+* [Gandi_v5](provider/gandi_v5.md)
+* [Gcore](provider/gcore.md)
+* [Google Cloud DNS](provider/gcloud.md)
+* [Hetzner DNS Console](provider/hetzner.md)
+* [HEXONET](provider/hexonet.md)
+* [hosting.de](provider/hostingde.md)
+* [Huawei Cloud DNS](provider/huaweicloud.md)
+* [Hurricane Electric DNS](provider/hedns.md)
+* [Internet.bs](provider/internetbs.md)
+* [INWX](provider/inwx.md)
+* [Linode](provider/linode.md)
+* [Loopia](provider/loopia.md)
+* [LuaDNS](provider/luadns.md)
+* [Microsoft DNS Server on Microsoft Windows Server](provider/msdns.md)
+* [Mythic Beasts](provider/mythicbeasts.md)
+* [Namecheap](provider/namecheap.md)
+* [Name.com](provider/namedotcom.md)
+* [Netcup](provider/netcup.md)
+* [Netlify](provider/netlify.md)
+* [NS1](provider/ns1.md)
+* [OpenSRS](provider/opensrs.md)
+* [Oracle Cloud](provider/oracle.md)
+* [OVH](provider/ovh.md)
+* [Packetframe](provider/packetframe.md)
+* [Porkbun](provider/porkbun.md)
+* [PowerDNS](provider/powerdns.md)
+* [Realtime Register](provider/realtimeregister.md)
+* [RWTH DNS-Admin](provider/rwth.md)
+* [Sakura Cloud](provider/sakuracloud.md)
+* [SoftLayer DNS](provider/softlayer.md)
+* [TransIP](provider/transip.md)
+* [Vultr](provider/vultr.md)
## Commands
-* [creds.json](creds-json.md)
+* [preview/push](preview-push.md)
* [check-creds](check-creds.md)
-* [get-certs](get-certs.md)
* [get-zones](get-zones.md)
+* [get-certs](get-certs.md)
+* [fmt](fmt.md)
+* [creds.json](creds-json.md)
+* [Global Flag](globalflags.md)
+* [Disabling Colors](colors.md)
## Advanced features
@@ -164,6 +186,7 @@
* [Writing new DNS providers](writing-providers.md)
* [Creating new DNS Resource Types (rtypes)](adding-new-rtypes.md)
* [Integration Tests](integration-tests.md)
+* [Test a branch](test-a-branch.md)
* [Unit Testing DNS Data](unittests.md)
* [Bug Triage Process](bug-triage.md)
* [Bring-Your-Own-Secrets for automated testing](byo-secrets.md)
diff --git a/documentation/adding-new-rtypes.md b/documentation/adding-new-rtypes.md
index 264bef43cc..acf054e035 100644
--- a/documentation/adding-new-rtypes.md
+++ b/documentation/adding-new-rtypes.md
@@ -16,7 +16,7 @@ Our general philosophy is:
- Anywhere we have a special case for a particular Rtype, we use a `switch` statement and have a `case` for every single record type, usually with a `default:` case that calls `panic()`. This way developers adding a new record type will quickly find where they need to add code (the panic will tell them where). Before we did this, missing implementation code would go unnoticed for months.
- Keep things alphabetical. If you are adding your record type to a case statement, function library, or whatever, please list it alphabetically along with the others when possible.
-Step 2 requires `stringer`.
+Step 2 requires `stringer`.
```shell
go install golang.org/x/tools/cmd/stringer@latest
```
@@ -73,15 +73,15 @@ popd
```
- Add this feature to the feature matrix in `dnscontrol/build/generate/featureMatrix.go`. Add it to the variable `matrix` maintaining alphabetical ordering, which should look like this:
-
+
{% code title="dnscontrol/build/generate/featureMatrix.go" %}
```diff
func matrixData() *FeatureMatrix {
const (
...
- DomainModifierCaa = "[`CAA`](functions/domain/CAA.md)"
- + DomainModifierFoo = "[`FOO`](functions/domain/FOO.md)"
- DomainModifierLoc = "[`LOC`](functions/domain/LOC.md)"
+ DomainModifierCaa = "[`CAA`](language-reference/domain-modifiers/CAA.md)"
+ + DomainModifierFoo = "[`FOO`](language-reference/domain-modifiers/FOO.md)"
+ DomainModifierLoc = "[`LOC`](language-reference/domain-modifiers/LOC.md)"
...
)
matrix := &FeatureMatrix{
@@ -98,7 +98,7 @@ popd
{% endcode %}
then add it later in the file with a `setCapability()` statement, which should look like this:
-
+
{% code title="dnscontrol/build/generate/featureMatrix.go" %}
```diff
...
@@ -153,7 +153,7 @@ example we removed `providers.CanUseCAA` from the
Add a function to `pkg/js/helpers.js` for the new record type. This
is the JavaScript file that defines `dnsconfig.js`'s functions like
-[`A()`](functions/domain/A.md) and [`MX()`](functions/domain/MX.md). Look at the definition of `A`, `MX` and `CAA` for good
+[`A()`](language-reference/domain-modifiers/A.md) and [`MX()`](language-reference/domain-modifiers/MX.md). Look at the definition of `A`, `MX` and `CAA` for good
examples to use as a base.
Please add the function alphabetically with the others. Also, please run
@@ -252,7 +252,7 @@ in the source code.
To run the integration test with the BIND provider:
```shell
-cd integrationTest/
+cd integrationTest # NOTE: Not needed if already in that subdirectory
go test -v -verbose -provider BIND
```
@@ -275,6 +275,7 @@ For example, this will run the tests on Amazon AWS Route53:
export R53_DOMAIN=dnscontroltest-r53.com # Use a test domain.
export R53_KEY_ID=CHANGE_TO_THE_ID
export R53_KEY='CHANGE_TO_THE_KEY'
+cd integrationTest # NOTE: Not needed if already in that subdirectory
go test -v -verbose -provider ROUTE53
```
@@ -291,7 +292,7 @@ tests, please ask!
## Step 8: Write documentation
-Add a new Markdown file to `documentation/functions/domain`. Copy an existing file (`CNAME.md` is a good example). The section between the lines of `---` is called the front matter and it has the following keys:
+Add a new Markdown file to `documentation/language-reference/domain-modifiers`. Copy an existing file (`CNAME.md` is a good example). The section between the lines of `---` is called the front matter and it has the following keys:
- `name`: The name of the record. This should match the file name and the name of the record in `helpers.js`.
- `parameters`: A list of parameter names, in order. Feel free to use spaces in the name if necessary. Your last parameter should be `modifiers...` to allow arbitrary modifiers like `TTL` to be applied to your record.
@@ -306,24 +307,34 @@ Add the new file `FOO.md` to the documentation table of contents [`documentation
...
* Domain Modifiers
...
- * [DnsProvider](functions/domain/DnsProvider.md)
-+ * [FOO](functions/domain/FOO.md)
- * [FRAME](functions/domain/FRAME.md)
+ * [DnsProvider](language-reference/domain-modifiers/DnsProvider.md)
++ * [FOO](language-reference/domain-modifiers/FOO.md)
+ * [FRAME](language-reference/domain-modifiers/FRAME.md)
...
* Service Provider specific
...
* ClouDNS
- * [CLOUDNS_WR](functions/domain/CLOUDNS_WR.md)
+ * [CLOUDNS_WR](language-reference/domain-modifiers/CLOUDNS_WR.md)
+ * ASDF
-+ * [ASDF_NINJA](function/domain/ASDF_NINJA.md)
++ * [ASDF_NINJA](language-reference/domain-modifiers/ASDF_NINJA.md)
* NS1
- * [NS1_URLFWD](functions/domain/NS1_URLFWD.md)
+ * [NS1_URLFWD](language-reference/domain-modifiers/NS1_URLFWD.md)
...
* Record Modifiers
...
- * [DMARC_BUILDER](functions/record/DMARC_BUILDER.md)
-+ * [FOO_HELPER](functions/record/FOO_HELPER.md)
- * [SPF_BUILDER](functions/record/SPF_BUILDER.md)
+ * [DMARC_BUILDER](language-reference/domain-modifiers/DMARC_BUILDER.md)
++ * [FOO_HELPER](language-reference/record-modifiers/FOO_HELPER.md)
+ * [SPF_BUILDER](language-reference/domain-modifiers/SPF_BUILDER.md)
...
```
{% endcode %}
+
+## Step 9: "go generate"
+
+Re-generate the documentation:
+
+```shell
+go generate ./...
+```
+
+This will regenerate things like the table of which providers have which features and the `dnscontrol.d.ts` file.
diff --git a/documentation/assets/providers/cloudflareapi/example-permissions-configuration.png b/documentation/assets/providers/cloudflareapi/example-permissions-configuration.png
index cabb6076ab..a145f6091e 100644
Binary files a/documentation/assets/providers/cloudflareapi/example-permissions-configuration.png and b/documentation/assets/providers/cloudflareapi/example-permissions-configuration.png differ
diff --git a/documentation/assets/styleguide-doc/pull-request-preview.webp b/documentation/assets/styleguide-doc/pull-request-preview.webp
new file mode 100644
index 0000000000..587fe889c4
Binary files /dev/null and b/documentation/assets/styleguide-doc/pull-request-preview.webp differ
diff --git a/documentation/bug-triage.md b/documentation/bug-triage.md
index 4eae829cc7..35102dd0d8 100644
--- a/documentation/bug-triage.md
+++ b/documentation/bug-triage.md
@@ -39,7 +39,7 @@ Message to requester:
Thank you for requesting this provider!
I've tagged this issue as a provider-request. It will (soon) be listed as a "requested provider" on the provider list web page:
-https://docs.dnscontrol.org/service-providers/providers
+https://docs.dnscontrol.org/provider
I will now close the issue. I know that's a bit confusing, but it will remain on the "requested provider" list.
diff --git a/documentation/byo-secrets.md b/documentation/byo-secrets.md
index 7b2e41ed57..add16af3e8 100644
--- a/documentation/byo-secrets.md
+++ b/documentation/byo-secrets.md
@@ -1,23 +1,17 @@
# Bring-Your-Own-Secrets for automated testing
Goal: Enable automated integration testing without accidentally
-leaking our API keys and other secrets; at the same time permit anyone
-to automate their own tests without having to share their API keys and
-secrets.
+leaking credentials (API keys and other secrets); at the same time permit everyone
+to automate their own tests without having to share their credentials.
+
+The instructions in this document will enable automated tests to run in these situations:
* PR from a project member:
- * Automated tests run for a long list of providers. All officially supported
- providers have automated tests, plus a few others too.
-* PR from an external person
- * Automated tests run for a short list of providers. Any test that
- requires secrets are skipped in the fork. They will run after the fact though
- once the PR has been merged to into the `master` branch of StackExchange/dnscontrol.
-* PR from an external person that wants automated tests for their
- provider.
- * They can set up secrets in their own GitHub account for any tests
- they'd like to automate without sharing their secrets.
- * Note: These tests can always be run outside of GitHub at the
- command line.
+ * All officially supported providers plus many others too.
+* PR from an external people:
+ * Automated tests run for providers that don't require secrets, which is currently only `BIND`.
+* PR on a fork of DNSControl:
+ * The forker can set up secrets in their fork and only those providers with secrets will be tested. They can "set it and forget it" and all their future PRs will receive all the benefits of automated testing.
# Background: How GitHub Actions protects secrets
@@ -47,43 +41,54 @@ gets its secrets from TomOnTime's secrets.
Our automated integration tests leverages this info to have tests
only run if they have access to the secrets they will need.
-# How it works
+# Which providers are selected for testing?
-Tests are executed if `*_DOMAIN` exists where `*` is the name of the provider. If the value is empty or
+Tests are executed if the env variable`*_DOMAIN` exists where `*` is the name of the provider. If the value is empty or
unset, the test is skipped.
For example, if a provider is called `FANCYDNS`, there must
-be a secret called `FANCYDNS_DOMAIN`.
+be a variable called `FANCYDNS_DOMAIN`.
# Bring your own secrets
-This section describes how to add a provider to the testing system.
+This section describes how to add a provider to the "Actions" part of GitHub.
Step 1: Create a branch
Create a branch as you normally would to submit a PR to the project.
-Step 2: Update `build.yml`
+Step 2: Update `pr_integration_tests.yml`
+
+{% hint style="info" %}
+Edits to `pr_integration_tests.yml` may have already been done for you.
+{% endhint %}
-In this branch, edit `.github/workflows/build.yml`:
+Edit `.github/workflows/pr_integration_tests.yml`
-1. In the `integration-test-providers` section, the name of the provider.
+1. Add the provider to the `PROVIDERS` list.
+
+* Add the name of the provider to the PROVIDERS list.
+* Please keep this list sorted alphabetically.
-Add your provider's name (alphabetically).
The line looks something like:
-{% code title=".github/workflows/build.yml" %}
-```
- PROVIDERS: "['BIND','HEXONET','AZURE_DNS','CLOUDFLAREAPI','GCLOUD','NAMEDOTCOM','ROUTE53','CLOUDNS','DIGITALOCEAN','GANDI_V5','HEDNS','INWX','NS1','POWERDNS','TRANSIP']"
+{% code title=".github/workflows/pr_integration_tests.yml" %}
+```yaml
+ env:
+ PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','HUAWEICLOUD','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']"
+ ENV_CONTEXT: ${{ toJson(env) }}
```
{% endcode %}
2. Add your providers `_DOMAIN` env variable:
-Add it to the `env` section of `integrtests-diff2`.
+* Add it to the `env` section of `integration-tests`.
+* Please keep this list sorted alphabetically.
+
+To find this section, search for `PROVIDER SECRET LIST`.
For example, the entry for BIND looks like:
-{% code title=".github/workflows/build.yml" %}
+{% code title=".github/workflows/pr_integration_tests.yml" %}
```
BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
```
@@ -91,26 +96,81 @@ For example, the entry for BIND looks like:
3. Add your providers other ENV variables:
-If there are other env variables (for example, for an API key), add that as a "secret".
+Every provider requires different variables set to perform the integration tests. The list of such variables is in `integrationTest/providers.json`.
+
+You've already added `*_DOMAIN` to `pr_integration_tests.yml`. Now we're going to add the remaining ones.
+
+To find this section, search for `PROVIDER SECRET LIST`.
For example, the entry for CLOUDFLAREAPI looks like this:
-{% code title=".github/workflows/build.yml" %}
+{% code title=".github/workflows/pr_integration_tests.yml" %}
```
CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
```
{% endcode %}
-Step 3. Submit this PR like any other.
+Step 3. Add the secrets to the repo.
+
+The `*_DOMAIN` variable is stored as a "variable" while the others are stored as "secrets".
+
+1. Go to Settings -> Secrets and variables -> Actions.
+
+2. On the "Variables" tab, add `*_DOMAIN` with the name of a test domain. This domain must already exist in the account. The DNS records of the domain will be deleted, so please use a test domain or other disposable domain.
+
+{% hint style="info" %}
+For the main project, **variables** are added here: [https://github.com/StackExchange/dnscontrol/settings/variables/actions](https://github.com/StackExchange/dnscontrol/settings/variables/actions)
+{% endhint %}
+
+3. On the "Secrets" tab, add the other env variables.
+
+{% hint style="info" %}
+For the main project, **secrets** are added here: [https://github.com/StackExchange/dnscontrol/settings/secrets/actions](https://github.com/StackExchange/dnscontrol/settings/secrets/actions)
+{% endhint %}
+
+If you have forked the project, add these to the settings of that fork.
+
+Step 4. Submit this PR like any other.
+
+GitHub Actions should kick and and run the tests.
+
+The tests will fail if a secret is wrong or missing. It may take a few iterations to get everything working because... computers.
+
+# Donate secrets to the project
+
+The DNSControl project would like to have all providers automatically tested.
+However, we can't fund purchasing domains or maintaining credentials at every
+provider. Instead we depend on volunteers to maintain (and pay for) such
+accounts.
+
+We recommend the domain be named `dnscontroltest-PROVIDER.com` (or similar)
+where PROVIDER is replaced by the name of your provider or an abbreviation. For
+example `dnscontroltest-r53.com` and `dnscontroltest-gcloud.com`.
+
+When possible, use an OTE or free domain. Don't spend money if you don't have
+to. This isn't just to be thrifty! It avoids renewals and other hassles too.
+You'd be surprised at how many providers (such as Google and Azure) permit DNS
+zones to be created in your account without registering them.
+
+For actual DNS domains, please select the "private registration" option if it
+is available. Otherwise you will get spam phones calls and emails. The phone
+calls will make you wish you didn't own a phone.
+
+{% hint style="danger" %}
+Some rules:
+
+* The account/credentials should only access the test domain. Don't send your company's actual credentials and trust us to only touch the test domain. (this hasn't happened yet, thankfully!)
+* Renew the domain in a timely manner. This may be monitoring an email inbox you don't normally monitor.
+* Don't do anything that will get you in trouble with your employer, like charging it to your employer without permission. (this hasn't happend yet either, thankfully!)
+{% endhint %}
+
+Now that we've covered all that...
+Create a new Github issue with a subject "Add PROVIDER to automated tests" where "PROVIDER" is the name of the provider. DO NOT SEND THE CREDENTIALS IN THE GITHUB ISSUE. Write that you understand the above rules and would like to volunteer to maintain the credentials and account.
-# Caveats
+To securely send the credentials to the project, use this link: [https://transfer.secretoverflow.com/u/tlimoncelli](https://transfer.secretoverflow.com/u/tlimoncelli)
-Sadly there is no locking to prevent two PRs from running the same
-test on the same domain at the same time. When that happens, both PRs
-running the tests fail. In the future we hope to add some locking.
+You'll hear back within a week.
-Also, maintaining a fork requires keeping it up to date. That's a bit
-more Git knowledge than I can describe here. (I'm not a Git expert by
-any stretch of the imagination!)
+Thank you for contributing credentials. The more providers we can test automatically with each PR, the better. It "shifts left" finding bugs and API changes and makes less work for everyone.
diff --git a/documentation/ci-cd-gitlab.md b/documentation/ci-cd-gitlab.md
index d34f756cbe..e42d822d6a 100644
--- a/documentation/ci-cd-gitlab.md
+++ b/documentation/ci-cd-gitlab.md
@@ -25,7 +25,7 @@ D("cafferata.dev",
TXT("spf", [
"v=spf1",
"-all"
- ].join(" "))
+ ].join(" ")),
);
```
{% endcode %}
diff --git a/documentation/cli-variables.md b/documentation/cli-variables.md
index b996d36b91..2b4324bce0 100644
--- a/documentation/cli-variables.md
+++ b/documentation/cli-variables.md
@@ -60,7 +60,7 @@ D("example.com", REG_NAMECOM, DnsProvider(DNS_NAMECOM), DnsProvider(DNS_BIND),
A("sitea", host01, TTL(1800)),
A("siteb", host01, TTL(1800)),
A("sitec", host02, TTL(1800)),
- A("sited", host02, TTL(1800))
+ A("sited", host02, TTL(1800)),
);
```
{% endcode %}
@@ -95,7 +95,7 @@ if (emergency) {
D_EXTEND("example.com",
CNAME("a", "a.othersite"),
CNAME("b", "b.othersite"),
- CNAME("c", "c.othersite")
+ CNAME("c", "c.othersite"),
);
} else {
@@ -104,7 +104,7 @@ if (emergency) {
D_EXTEND("example.com",
A("a", "10.10.10.10"),
A("b", "10.10.10.11"),
- A("c", "10.10.10.12")
+ A("c", "10.10.10.12"),
);
}
diff --git a/documentation/code-tricks.md b/documentation/code-tricks.md
index ac383943fe..30ed409c1b 100644
--- a/documentation/code-tricks.md
+++ b/documentation/code-tricks.md
@@ -4,10 +4,25 @@ Problem: It is difficult to get CAA and other records exactly right.
Solution: Use a "builder" to construct it for you.
-* [CAA Builder](functions/record/CAA_BUILDER.md)
-* [DMARC Builder](functions/record/DMARC_BUILDER.md)
-* [M365_BUILDER](functions/record/M365_BUILDER.md)
-* [SPF Optimizer](functions/record/SPF_BUILDER.md)
+* [CAA_BUILDER](language-reference/domain-modifiers/CAA_BUILDER.md)
+* [DMARC_BUILDER](language-reference/domain-modifiers/DMARC_BUILDER.md)
+* [M365_BUILDER](language-reference/domain-modifiers/M365_BUILDER.md)
+* [SPF_BUILDER](language-reference/domain-modifiers/SPF_BUILDER.md)
+
+# Trailing commas
+
+You might encounter `D()` statements in code examples that include `END` at the end, such as:
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ A("test", "1.2.3.4"),
+END);
+```
+{% endcode %}
+
+As of [DNSControl v4.15.0](https://github.com/StackExchange/dnscontrol/releases/tag/v4.15.0), the `END` statements are no longer necessary.
+These were originally included for historical reasons that are now irrelevant. You can safely remove them from your configurations.
# Repeat records in many domains (macros)
@@ -63,13 +78,13 @@ records.
Solution 1: Use a macro.
-```
+```javascript
function PARKED_R53(name) {
D(name, REG_NAMECOM, DnsProvider(DSP_MY_PROVIDER),
A("@", "10.2.3.4"),
CNAME("www", "@"),
SPF_NONE, //deters spammers from using the domain in From: lines.
- END);
+ );
}
PARKED_R53("example1.tld");
@@ -92,7 +107,7 @@ _.each(
D(d, REG_NAMECOM, DnsProvider(DSP_MY_PROVIDER),
A("@", "10.2.3.4"),
CNAME("www", "@"),
- END);
+ );
}
);
```
@@ -157,5 +172,5 @@ domain exists, who requested it, any associated ticket numbers, and so
on.
We also comment the individual parts of a record. Look at the [SPF
-Optimizer](functions/record/SPF_BUILDER.md) example. Each part of
+Optimizer](language-reference/domain-modifiers/SPF_BUILDER.md) example. Each part of
the SPF record has a comment.
diff --git a/documentation/colors.md b/documentation/colors.md
index 2247329c90..4daafdb5dd 100644
--- a/documentation/colors.md
+++ b/documentation/colors.md
@@ -15,6 +15,8 @@ codes.
In order to do so, a global `--no-colors` command option is provided, which when
set `--no-colors=true`, will disable colors globally.
+Alternatively, a `NO_COLOR` environment variable set to any non-empty string will disable color output.
+
## (Force) Enable colors
If color support is not correctly detected, providing `--no-colors=false` would
diff --git a/documentation/creds-json.md b/documentation/creds-json.md
index 19f6e6ef7b..318fe9213a 100644
--- a/documentation/creds-json.md
+++ b/documentation/creds-json.md
@@ -178,9 +178,8 @@ The `--creds` flag allows you to specify a different file name.
* Do not end the filename with `.yaml` or `.yml` as some day we hope to support YAML.
* Rather than specifying a file, you can specify a program or shell command to be run. The output of the program/command must be valid JSON and will be read the same way.
* If the name begins with `!`, the remainder of the name is taken to be a shell command or program to be run.
- * If the name is a file that is executable (chmod `+x` bit), it is taken as the command to be run.
+ * If the name is a file that is executable (chmod `+x` bit), it is taken as the command to be run (Linux/MacOS only).
* Exceptions: The `x` bit is not checked if the filename ends with `.yaml`, `.yml` or `.json`.
- * Windows: Executing an external script isn't supported. There's no code that prevents it from trying, but it isn't supported.
### Example commands
@@ -198,14 +197,14 @@ dnscontrol preview --creds /some/absolute/path/creds.sh
Following commands would execute a shell command:
```shell
-dnscontrol preview --creds "!op inject -i creds.json.tpl"
+dnscontrol preview --creds '!op inject -i creds.json.tpl'
```
This example requires the [1Password command-line tool](https://developer.1password.com/docs/cli/)
but works with any shell command that returns a properly formatted `creds.json`.
In this case, the 1Password CLI is used to inject the secrets from
a 1Password vault, rather than storing them in environment variables.
-An example of a template file containing Linode and Cloudflare API credentials is available here: [creds.json](https://github.com/StackExchange/dnscontrol/blob/master/documentation/assets/1password/creds.json).
+An example of a template file containing Linode and Cloudflare API credentials is available here: [creds.json](https://github.com/StackExchange/dnscontrol/blob/main/documentation/assets/1password/creds.json).
{% code title="creds.json" %}
```json
@@ -226,13 +225,14 @@ An example of a template file containing Linode and Cloudflare API credentials i
```
{% endcode %}
-## Don't store secrets in a Git repo!
+## Don't store creds.json in a Git repo!
-Do NOT store secrets in a Git repository. That is not secure. For example,
-storing the example `cloudflare_tal` is insecure because anyone with access to
-your Git repository or the history will know your apiuser is `REDACTED`.
-Removing secrets accidentally stored in Git is very difficult. You'll probably
-give up and re-create the repo and lose all history.
+Do NOT store `creds.json` (or any secrets!) in a Git repository. That is not secure.
-Instead, use environment variables as in the `hexonet` example above. Use
+For example, storing the creds.json at the top of this document would be horribly insecure.
+Anyone with access to your Git repository *or the history* will know your apiuser is `REDACTED`.
+Removing secrets accidentally stored in Git is very difficult because you'll need to rewrite
+the repo history.
+
+A better way is to use environment variables as in the `hexonet` example above. Use
secure means to distribute the names and values of the environment variables.
diff --git a/documentation/examples.md b/documentation/examples.md
index b2f1db50b7..b8f440108c 100644
--- a/documentation/examples.md
+++ b/documentation/examples.md
@@ -1,4 +1,4 @@
-## Typical DNS Records ##
+## Typical DNS Records
{% code title="dnsconfig.js" %}
```javascript
@@ -12,13 +12,13 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
MX("mail", 20, "mailqueue"),
TXT("the", "message"),
NS("department2", "ns1.dnsexample.com."), // use different nameservers
- NS("department2", "ns2.dnsexample.com.") // for department2.example.com
-)
+ NS("department2", "ns2.dnsexample.com."), // for department2.example.com
+);
```
{% endcode %}
-## Set TTLs ##
+## Set TTLs
{% code title="dnsconfig.js" %}
```javascript
var mailTTL = TTL("1h");
@@ -31,12 +31,12 @@ D("example.com", REG_MY_PROVIDER,
MX("@", 10, "4.3.2.1", mailTTL), // set TTL
A("@", "1.2.3.4", TTL("10m")), // individual record
- CNAME("mail", "mx01") // TTL of 5m, as defined per DefaultTTL()
+ CNAME("mail", "mx01"), // TTL of 5m, as defined per DefaultTTL()
);
```
{% endcode %}
-## Variables for common IP Addresses ##
+## Variables for common IP Addresses
{% code title="dnsconfig.js" %}
```javascript
var addrA = IP("1.2.3.4")
@@ -46,12 +46,12 @@ var DSP_R53 = NewDnsProvider("route53_user1");
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_R53),
A("@", addrA), // 1.2.3.4
A("www", addrA + 1), // 1.2.3.5
-)
+);
```
{% endcode %}
{% hint style="info" %}
-**NOTE**: The [`IP()`](functions/global/IP.md) function doesn't currently support IPv6 (PRs welcome!). IPv6 addresses are strings.
+**NOTE**: The [`IP()`](language-reference/top-level-functions/IP.md) function doesn't currently support IPv6 (PRs welcome!). IPv6 addresses are strings.
{% endhint %}
{% code title="dnsconfig.js" %}
```javascript
@@ -59,7 +59,7 @@ var addrAAAA = "0:0:0:0:0:0:0:0";
```
{% endcode %}
-## Variables to swap active Data Center ##
+## Variables to swap active Data Center
{% code title="dnsconfig.js" %}
```javascript
var DSP_R53 = NewDnsProvider("route53_user1");
@@ -72,11 +72,11 @@ var activeDC = dcA;
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_R53),
A("@", activeDC + 5), // fixed address based on activeDC
-)
+);
```
{% endcode %}
-## Macro for repeated records ##
+## Macro for repeated records
{% code title="dnsconfig.js" %}
```javascript
var GOOGLE_APPS_MX_RECORDS = [
@@ -99,12 +99,12 @@ var GOOGLE_APPS_CNAME_RECORDS = [
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_R53),
GOOGLE_APPS_MX_RECORDS,
GOOGLE_APPS_CNAME_RECORDS,
- A("@", "1.2.3.4")
-)
+ A("@", "1.2.3.4"),
+);
```
{% endcode %}
-## Use SPF_BUILDER to add comments to SPF records ##
+## Use SPF_BUILDER to add comments to SPF records
{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
@@ -127,20 +127,20 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
```
{% endcode %}
-## Set default records modifiers ##
+## Set default records modifiers
{% code title="dnsconfig.js" %}
```javascript
DEFAULTS(
NAMESERVER_TTL("24h"),
DefaultTTL("12h"),
- CF_PROXY_DEFAULT_OFF
+ CF_PROXY_DEFAULT_OFF,
);
```
{% endcode %}
# Advanced Examples #
-## Dual DNS Providers ##
+## Dual DNS Providers
{% code title="dnsconfig.js" %}
```javascript
@@ -148,24 +148,24 @@ var DSP_R53 = NewDnsProvider("route53_user1");
var DSP_GCLOUD = NewDnsProvider("gcloud_admin");
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_R53), DnsProvider(DSP_GCLOUD),
- A("@", "1.2.3.4")
-)
+ A("@", "1.2.3.4"),
+);
// above zone uses 8 NS records total (4 from each provider dynamically gathered)
// below zone will only take 2 from each for a total of 4. May be better for performance reasons.
D("example2.com", REG_MY_PROVIDER, DnsProvider(DSP_R53, 2), DnsProvider(DSP_GCLOUD ,2),
- A("@", "1.2.3.4")
-)
+ A("@", "1.2.3.4"),
+);
// or set a Provider as a non-authoritative backup (don"t register its nameservers)
D("example3.com", REG_MY_PROVIDER, DnsProvider(DSP_R53), DnsProvider(DSP_GCLOUD, 0),
- A("@", "1.2.3.4")
-)
+ A("@", "1.2.3.4"),
+);
```
{% endcode %}
-## Automate Fastmail DKIM records ##
+## Automate Fastmail DKIM records
In this example we need a macro that can dynamically change for each domain.
@@ -189,7 +189,7 @@ var FASTMAIL_DKIM = function(the_domain){
return [
CNAME("fm1._domainkey", "fm1." + the_domain + ".dkim.fmhosted.com."),
CNAME("fm2._domainkey", "fm2." + the_domain + ".dkim.fmhosted.com."),
- CNAME("fm3._domainkey", "fm3." + the_domain + ".dkim.fmhosted.com.")
+ CNAME("fm3._domainkey", "fm3." + the_domain + ".dkim.fmhosted.com."),
]
}
```
@@ -203,8 +203,8 @@ var DSP_R53_MAIN = NewDnsProvider("r53_main");
D("example.com", REG_NONE, DnsProvider(DSP_R53_MAIN),
FASTMAIL_MX,
- FASTMAIL_DKIM("example.com")
-)
+ FASTMAIL_DKIM("example.com"),
+);
```
{% endcode %}
diff --git a/documentation/fmt.md b/documentation/fmt.md
new file mode 100644
index 0000000000..1c7f5dcdc2
--- /dev/null
+++ b/documentation/fmt.md
@@ -0,0 +1,50 @@
+# fmt
+
+This is a stand-alone utility to pretty-format your `dnsconfig.js` configuration file.
+
+```shell
+NAME:
+ dnscontrol fmt - [BETA] Format and prettify a given file
+
+USAGE:
+ dnscontrol fmt [command options] [arguments...]
+
+CATEGORY:
+ utility
+
+OPTIONS:
+ --input value, -i value Input file (default: "dnsconfig.js")
+ --output value, -o value Output file
+ --help, -h show help
+```
+
+## Examples
+
+By default the output goes to stdout:
+
+```shell
+dnscontrol fmt >new-dnsconfig.js
+```
+
+You can also redirect the output via the `-o` option:
+
+```shell
+dnscontrol fmt -o new-dnsconfig.js
+```
+
+The **safest** method involves making a backup first:
+
+```shell
+cp dnsconfig.js dnsconfig.js.BACKUP
+dnscontrol fmt -i dnsconfig.js.BACKUP -o dnsconfig.js
+```
+
+The **riskiest** method depends on the fact that DNSControl currently processes
+the `-o` file after the input file is completely read. It makes no backups.
+This is useful if Git is your backup mechanism.
+
+```shell
+git commit -m'backup dnsconfig.js' dnsconfig.js
+dnscontrol fmt -o dnsconfig.js
+git diff -- dnsconfig.js
+```
diff --git a/documentation/functions/domain/AUTODNSSEC_OFF.md b/documentation/functions/domain/AUTODNSSEC_OFF.md
deleted file mode 100644
index c2e91fe5cc..0000000000
--- a/documentation/functions/domain/AUTODNSSEC_OFF.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: AUTODNSSEC_OFF
----
-
-AUTODNSSEC_OFF tells the provider to disable AutoDNSSEC. It takes no
-parameters.
-
-See `AUTODNSSEC_ON` for further details.
diff --git a/documentation/functions/domain/CAA_BUILDER.md b/documentation/functions/domain/CAA_BUILDER.md
deleted file mode 100644
index 920910e06e..0000000000
--- a/documentation/functions/domain/CAA_BUILDER.md
+++ /dev/null
@@ -1,59 +0,0 @@
----
-name: CAA_BUILDER
-parameters:
- - label
- - iodef
- - iodef_critical
- - issue
- - issuewild
-parameters_object: true
-parameter_types:
- label: string?
- iodef: string
- iodef_critical: boolean?
- issue: string[]
- issuewild: string
----
-
-DNSControl contains a `CAA_BUILDER` which can be used to simply create
-[`CAA()`](../domain/CAA.md) records for your domains. Instead of creating each [`CAA()`](../domain/CAA.md) record
-individually, you can simply configure your report mail address, the
-authorized certificate authorities and the builder cares about the rest.
-
-## Example
-
-For example you can use:
-
-{% code title="dnsconfig.js" %}
-```javascript
-CAA_BUILDER({
- label: "@",
- iodef: "mailto:test@example.com",
- iodef_critical: true,
- issue: [
- "letsencrypt.org",
- "comodoca.com",
- ],
- issuewild: "none",
-})
-```
-{% endcode %}
-
-The parameters are:
-
-* `label:` The label of the CAA record. (Optional. Default: `"@"`)
-* `iodef:` Report all violation to configured mail address.
-* `iodef_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`)
-* `issue:` An array of CAs which are allowed to issue certificates. (Use `"none"` to refuse all CAs)
-* `issuewild:` An array of CAs which are allowed to issue wildcard certificates. (Can be simply `"none"` to refuse issuing wildcard certificates for all CAs)
-
-`CAA_BUILDER()` returns multiple records (when configured as example above):
-
-{% code title="dnsconfig.js" %}
-```javascript
-CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL)
-CAA("@", "issue", "letsencrypt.org")
-CAA("@", "issue", "comodoca.com")
-CAA("@", "issuewild", ";")
-```
-{% endcode %}
diff --git a/documentation/functions/domain/LOC_BUILDER_STR.md b/documentation/functions/domain/LOC_BUILDER_STR.md
deleted file mode 100644
index 368f2bc906..0000000000
--- a/documentation/functions/domain/LOC_BUILDER_STR.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-name: LOC_BUILDER_STR
-parameters:
- - label
- - str
- - alt
- - ttl
-parameters_object: true
-parameter_types:
- label: string?
- str: string
- alt: number?
- ttl: Duration?
----
-
-`LOC_BUILDER_STR({})` actually takes an object with the following: properties.
-
- - label (optional, defaults to `@`)
- - str (string)
- - alt (float32, optional)
- - ttl (optional)
-
-A helper to build [`LOC`](../domain/LOC.md) records. Supply three parameters instead of 12.
-
-Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
-
-
-Accepts a string and tries all `LOC_BUILDER_DM*_STR({})` methods:
- * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
-
-
-{% code title="dnsconfig.js" %}
-```javascript
-D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- , LOC_BUILDER_STR({
- label: "old-faithful",
- str: "44.46046°N 110.82815°W",
- alt: 2240,
- })
- , LOC_BUILDER_STR({
- label: "ribblehead-viaduct",
- str: "54.210436°N 2.370231°W",
- alt: 300,
- })
- , LOC_BUILDER_STR({
- label: "guinness-brewery",
- str: "53°20â˛40âŗN 6°17â˛20âŗW",
- alt: 300,
- })
-);
-
-```
-{% endcode %}
-
-
-Part of the series:
- * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
diff --git a/documentation/functions/domain/NS1_URLFWD.md b/documentation/functions/domain/NS1_URLFWD.md
deleted file mode 100644
index f097e59f5a..0000000000
--- a/documentation/functions/domain/NS1_URLFWD.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-name: NS1_URLFWD
-parameters:
- - name
- - target
- - modifiers...
-provider: NS1
-parameter_types:
- name: string
- target: string
- "modifiers...": RecordModifier[]
----
-
-{% hint style="info" %}
-Documentation needed.
-{% endhint %}
diff --git a/documentation/functions/global/REV.md b/documentation/functions/global/REV.md
deleted file mode 100644
index 5bc12c201a..0000000000
--- a/documentation/functions/global/REV.md
+++ /dev/null
@@ -1,56 +0,0 @@
----
-name: REV
-parameters:
- - address
-parameter_types:
- address: string
-ts_return: string
----
-
-`REV` returns the reverse lookup domain for an IP network. For
-example `REV("1.2.3.0/24")` returns `3.2.1.in-addr.arpa.` and
-`REV("2001:db8:302::/48")` returns `2.0.3.0.8.b.d.0.1.0.0.2.ip6.arpa.`.
-This is used in [`D()`](D.md) functions to create reverse DNS lookup zones.
-
-This is a convenience function. You could specify `D("3.2.1.in-addr.arpa",
-...` if you like to do things manually but why would you risk making
-typos?
-
-`REV` complies with RFC2317, "Classless in-addr.arpa delegation"
-for netmasks of size /25 through /31.
-While the RFC permits any format, we abide by the recommended format:
-`FIRST/MASK.C.B.A.in-addr.arpa` where `FIRST` is the first IP address
-of the zone, `MASK` is the netmask of the zone (25-31 inclusive),
-and A, B, C are the first 3 octets of the IP address. For example
-`172.20.18.130/27` is located in a zone named
-`128/27.18.20.172.in-addr.arpa`
-
-If the address does not include a "/" then `REV` assumes /32 for IPv4 addresses
-and /128 for IPv6 addresses.
-
-Note that the lower bits (the ones outside the netmask) must be zeros. They are not
-zeroed out automatically. Thus, `REV("1.2.3.4/24")` is an error. This is done
-to catch typos.
-
-{% code title="dnsconfig.js" %}
-```javascript
-D(REV("1.2.3.0/24"), REGISTRAR, DnsProvider(BIND),
- PTR("1", "foo.example.com."),
- PTR("2", "bar.example.com."),
- PTR("3", "baz.example.com."),
- // These take advantage of DNSControl's ability to generate the right name:
- PTR("1.2.3.10", "ten.example.com."),
-);
-
-D(REV("2001:db8:302::/48"), REGISTRAR, DnsProvider(BIND),
- PTR("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", "foo.example.com."), // 2001:db8:302::1
- // These take advantage of DNSControl's ability to generate the right name:
- PTR("2001:db8:302::2", "two.example.com."), // 2.0.0...
- PTR("2001:db8:302::3", "three.example.com."), // 3.0.0...
-);
-```
-{% endcode %}
-
-In the future we plan on adding a flag to [`A()`](../domain/A.md)which will insert
-the correct PTR() record in the appropriate `D(REV())` domain (i.e. `.arpa` domain) has been
-defined.
diff --git a/documentation/functions/record/R53_ZONE.md b/documentation/functions/record/R53_ZONE.md
deleted file mode 100644
index 8fa6a09aaa..0000000000
--- a/documentation/functions/record/R53_ZONE.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: R53_ZONE
-parameters:
- - zone_id
-parameter_types:
- zone_id: string
-ts_return: DomainModifier & RecordModifier
-provider: ROUTE53
----
-
-`R53_ZONE` lets you specify the AWS Zone ID for an entire domain ([`D()`](../global/D.md)) or a specific [`R53_ALIAS()`](../domain/R53_ALIAS.md) record.
-
-When used with [`D()`](../global/D.md), it sets the zone id of the domain. This can be used to differentiate between split horizon domains in public and private zones. See this [example](../../providers/route53.md#split-horizon) in the [Amazon Route 53 provider page](../../providers/route53.md).
-
-When used with [`R53_ALIAS()`](../domain/R53_ALIAS.md) it sets the required Route53 hosted zone id in a R53_ALIAS record. See [`R53_ALIAS()`](../domain/R53_ALIAS.md) documentation for details.
diff --git a/documentation/get-certs.md b/documentation/get-certs.md
index 774b574b88..f6696cb287 100644
--- a/documentation/get-certs.md
+++ b/documentation/get-certs.md
@@ -1,8 +1,11 @@
# *Let's Encrypt* Certificate generation
{% hint style="warning" %}
-**WARNING**: This feature
-is frozen and will be removed in early 2023. The "get-certs" command (renews certs via Let's Encrypt) has no maintainer. There are other projects that do a better job. If you don't use this feature, please do not start. If you do use this feature, please plan on migrating to something else. See discussion in [#1400](https://github.com/StackExchange/dnscontrol/issues/1400)
+**WARNING**: This feature is frozen and will be removed without notice between now and July 2025.
+It has been unsupported since December 2022.
+This feature has no maintainer. There are other projects that do a better job.
+If you do use this feature, please migrate to something else ASAP.
+See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400)
{% endhint %}
DNSControl will generate/renew Let's Encrypt certificates using DNS
diff --git a/documentation/get-zones.md b/documentation/get-zones.md
index 223cd10292..da0ee5ce21 100644
--- a/documentation/get-zones.md
+++ b/documentation/get-zones.md
@@ -1,4 +1,4 @@
-# get-zones (was "convertzone")
+# get-zones
DNSControl has a stand-alone utility that will contact a provider,
download the records of one or more zones, and output them to a file
@@ -192,4 +192,4 @@ provider.)
Implementing the `ListZones` function also activates the `check-creds`
subcommand for that provider. Please add to the provider documentation
a list of error messages that people might see if the credentials are
-invalid. See `documentation/providers/gcloud.md` for examples.
+invalid. See `documentation/provider/gcloud.md` for examples.
diff --git a/documentation/getting-started.md b/documentation/getting-started.md
index 9235b3c537..675a3f52b9 100644
--- a/documentation/getting-started.md
+++ b/documentation/getting-started.md
@@ -5,6 +5,11 @@
Choose one of the following installation methods:
+1. [Homebrew](#homebrew)
+2. [Docker](#docker)
+3. [GitHub binaries](#binaries)
+4. [GitHub source](#source)
+
### Homebrew
On macOS (or Linux) you can install it using [Homebrew](https://brew.sh).
@@ -13,14 +18,6 @@ On macOS (or Linux) you can install it using [Homebrew](https://brew.sh).
brew install dnscontrol
```
-### MacPorts
-
-Alternatively on macOS you can install it using [MacPorts](https://www.macports.org).
-
-```shell
-sudo port install dnscontrol
-```
-
### Docker
You can use DNSControl locally using the Docker image from [Docker hub](https://hub.docker.com/r/stackexchange/dnscontrol/) or [GitHub Container Registry](https://github.com/stackexchange/dnscontrol/pkgs/container/dnscontrol) and the command below.
@@ -54,6 +51,23 @@ git clone https://github.com/StackExchange/dnscontrol
If these don't work, more info is in [#805](https://github.com/StackExchange/dnscontrol/issues/805).
+## 1.1. Shell Completion
+
+Shell completion is available for `zsh` and `bash`.
+
+### zsh
+
+Add `eval "$(dnscontrol shell-completion zsh)"` to your `~/.zshrc` file.
+
+This requires completion to be enabled in zsh. A good tutorial for this is available at
+[The Valuable Dev](https://thevaluable.dev/zsh-completion-guide-examples/) [[archived](https://web.archive.org/web/20231015083946/https://thevaluable.dev/zsh-completion-guide-examples/)].
+
+### bash
+
+Add `eval "$(dnscontrol shell-completion bash)"` to your `~/.bashrc` file.
+
+This requires the `bash-completion` package to be installed. See [scop/bash-completion](https://github.com/scop/bash-completion/) for instructions.
+
## 2. Create a place for the config files
Create a directory where you'll store your configuration files.
@@ -72,7 +86,7 @@ use BIND for DNS service, it is useful for testing.
domains, and so on.
Start your `dnsconfig.js` file by downloading
-[dnsconfig.js](https://github.com/StackExchange/dnscontrol/blob/master/documentation/assets/getting-started/dnsconfig.js)
+[dnsconfig.js](https://github.com/StackExchange/dnscontrol/blob/main/documentation/assets/getting-started/dnsconfig.js)
and renaming it.
The file looks like:
@@ -83,7 +97,7 @@ var REG_NONE = NewRegistrar("none");
var DNS_BIND = NewDnsProvider("bind");
D("example.com", REG_NONE, DnsProvider(DNS_BIND),
- A("@", "1.2.3.4")
+ A("@", "1.2.3.4"),
);
```
{% endcode %}
@@ -138,7 +152,7 @@ It is only needed if any providers require credentials (API keys,
usernames, passwords, etc.).
Start your `creds.json` file by downloading
-[creds.json](https://github.com/StackExchange/dnscontrol/blob/master/documentation/assets/getting-started/creds.json)
+[creds.json](https://github.com/StackExchange/dnscontrol/blob/main/documentation/assets/getting-started/creds.json)
and renaming it.
The file looks like:
@@ -281,7 +295,7 @@ $TTL 300
```
You can change the "DEFAULT_NOT_SET" text by following the documentation
-for the [BIND provider](providers/bind.md) to set
+for the [BIND provider](provider/bind.md) to set
the "master" and "mbox" settings. Try that now.
diff --git a/documentation/globalflags.md b/documentation/globalflags.md
new file mode 100644
index 0000000000..67831fa579
--- /dev/null
+++ b/documentation/globalflags.md
@@ -0,0 +1,42 @@
+# Global Flags
+
+These flags are global. They affect all subcommands.
+
+```text
+ --debug, -v Enable detailed logging (default: false)
+ --allow-fetch Enable JS fetch(), dangerous on untrusted code! (default: false)
+ --disableordering Disables update reordering (default: false)
+ --no-colors Disable colors (default: false)
+ --help, -h show help
+```
+
+They must appear before the subcommand.
+
+**Right**
+
+{% hint style="success" %}
+```shell
+dnscontrol --no-colors preview
+```
+{% endhint %}
+
+**Wrong**
+
+{% hint style="danger" %}
+```shell
+dnscontrol preview --no-colors
+```
+{% endhint %}
+
+* `-debug`
+ * Enable debug output. (The `-v` alias is the original name for this flag. That alias will go away eventually.)
+
+
+* `--allow-fetch`
+ * Enable the `fetch()` function in `dnsconfig.js` (or equivalent). It is disabled by default because it can be used for nefarious purposes. It is dangerous on untrusted code! Enable it only if you trust all the people editing dnsconfig.js.
+
+* `--disableordering`
+ * Disables update reordering. Normally DNSControl re-orders the updates done by `push`. This is usually only used to work around bugs in the reordering code.
+
+* `--no-colors`
+ * Disable colors. See [Disabling Colors](colors.md) for details.
diff --git a/documentation/index.md b/documentation/index.md
index 58b4a8ad79..34bc897d3a 100644
--- a/documentation/index.md
+++ b/documentation/index.md
@@ -15,10 +15,10 @@ Take advantage of the advanced features. Use macros and variables for easier upd
* Super extensible! Plug-in architecture makes adding new DNS providers and Registrars easy!
* Eliminate vendor lock-in. Switch DNS providers easily, any time, with full fidelity.
* Reduce points of failure: Easily maintain dual DNS providers and easily drop one that is down.
-* Supports 35+ [DNS Providers](providers.md) including [BIND](providers/bind.md), [AWS Route 53](providers/route53.md), [Google DNS](providers/gcloud.md), and [name.com](providers/namedotcom.md).
+* Supports 35+ [DNS Providers](providers.md) including [BIND](provider/bind.md), [AWS Route 53](provider/route53.md), [Google DNS](provider/gcloud.md), and [name.com](provider/namedotcom.md).
* [Apply CI/CD principles](ci-cd-gitlab.md) to DNS: Unit-tests, system-tests, automated deployment.
* All the benefits of Git (or any VCS) for your DNS zone data. View history. Accept PRs.
-* Optimize DNS with [SPF optimizer](functions/record/SPF_BUILDER.md). Detect too many lookups. Flatten includes.
+* Optimize DNS with [SPF optimizer](language-reference/domain-modifiers/SPF_BUILDER.md). Detect too many lookups. Flatten includes.
* Runs on Linux, Windows, Mac, or any operating system supported by Go.
* Enable/disable Cloudflare proxying (the "orange cloud" button) directly from your DNSControl files.
* [Assign an IP address to a constant](examples.md#variables-for-common-ip-addresses) and use the variable name throughout the configuration. Need to change the IP address globally? Just change the variable and "recompile".
diff --git a/documentation/integration-tests.md b/documentation/integration-tests.md
index dd068edaaf..14cd604b7e 100644
--- a/documentation/integration-tests.md
+++ b/documentation/integration-tests.md
@@ -12,8 +12,9 @@ For each step, it will run the config once and expect changes. It will run it ag
## Running a test
-1. Define all environment variables expected for the provider you wish to run. I setup a local `.env` file with the appropriate values and use [zoo](https://github.com/jsonmaur/zoo) to run my commands.
-2. run `go test -v -provider $NAME` where $NAME is the name of the provider you wish to run.
+1. The integration tests need a test domain to run on. All the records of this domain will be deleted!
+2. Define all environment variables expected for the provider you wish to run.
+3. run `cd integrationTest && go test -v -provider $NAME` where $NAME is the name of the provider you wish to run.
Example:
@@ -34,11 +35,36 @@ export ROUTE53_DOMAIN="testdomain.tld"
```
```shell
+cd integrationTest # NOTE: Not needed if already in that subdirectory
go test -v -verbose -provider ROUTE53
```
+The `-start` and `-end` flags allow you to run just a portion of the tests.
+
+```shell
+go test -v -verbose -provider ROUTE53 -start 16
+go test -v -verbose -provider ROUTE53 -end 5
+go test -v -verbose -provider ROUTE53 -start 16 -end 20
+```
+
+The `start` and `end` flags are both inclusive (i.e. `-start 16 -end 20` will run `[16, 17, 18, 19, 20]`).
+
+For some providers it may be necessary to increase the test timeout using `-test`. The default is 10 minutes. `0` is "no limit". Typical Go durations work too (`1h` for 1 hour, etc).
+
+```shell
+go test -timeout 0 -v -verbose -provider CLOUDNS
+```
+
+FYI: The order of the flags matters. Flags native to the Go testing suite (`-timeout` and `-v`) must come before flags that are part of the DNSControl integration tests (`-verbose`, `-provider`). Yeah, that sucks and is confusing.
+
+The actual tests are in the file `integrationTest/integration_test.go`. The
+tests are in a little language which can be used to describe just about any
+interaction with the API. Look for the comment `START HERE` or the line
+`func makeTests` for instructions.
+
+
{% hint style="warning" %}
-**WARNING**: The records in the test domain will be deleted. Only use
+**WARNING**: THE RECORDS IN THE TEST DOMAIN WILL BE DELETED. Only use
a domain that is not used in production. Some providers have a way
to run tests on domains that aren't registered (often a test
environment or a side-effect of the company not being a registrar).
diff --git a/documentation/json-reports.md b/documentation/json-reports.md
index e6efa79288..64224606ac 100644
--- a/documentation/json-reports.md
+++ b/documentation/json-reports.md
@@ -1,10 +1,21 @@
# JSON Reports
-DNSControl has build in functionality to generate a machine-parseable report after pushing changes. This report is JSON formated and contains the zonename, the provider or registrar name and the amount of performed changes.
+DNSControl can generate a machine-parseable report of changes.
-## Usage
+The report is JSON formated and contains the zonename, the provider or
+registrar name, and the number of changes.
-To enable the report option you must use the `push` operation in combination with the `--report ` option. This generates the json file.
+To generate the report, add the `--report ` option to a preview or
+push command (this includes `preview`, `ppreview`, `push`,
+`ppush`).
+
+
+The report lists the changes that would be (preview) or are (push) attempted,
+whether they are successful or not.
+
+If a fatal error happens during the run, no report is generated.
+
+## Sample output
{% code title="report.json" %}
```json
diff --git a/documentation/functions/domain/A.md b/documentation/language-reference/domain-modifiers/A.md
similarity index 89%
rename from documentation/functions/domain/A.md
rename to documentation/language-reference/domain-modifiers/A.md
index 8c1dbd1f3d..fdc8b266f2 100644
--- a/documentation/functions/domain/A.md
+++ b/documentation/language-reference/domain-modifiers/A.md
@@ -12,7 +12,7 @@ parameter_types:
A adds an A record To a domain. The name should be the relative label for the record. Use `@` for the domain apex.
-The address should be an ip address, either a string, or a numeric value obtained via [IP](../global/IP.md).
+The address should be an ip address, either a string, or a numeric value obtained via [IP](../top-level-functions/IP.md).
Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata.
@@ -22,7 +22,7 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
A("@", "1.2.3.4"),
A("foo", "2.3.4.5"),
A("test.foo", IP("1.2.3.4"), TTL(5000)),
- A("*", "1.2.3.4", {foo: 42})
+ A("*", "1.2.3.4", {foo: 42}),
);
```
{% endcode %}
diff --git a/documentation/functions/domain/AAAA.md b/documentation/language-reference/domain-modifiers/AAAA.md
similarity index 96%
rename from documentation/functions/domain/AAAA.md
rename to documentation/language-reference/domain-modifiers/AAAA.md
index b1d2f7a665..361290956f 100644
--- a/documentation/functions/domain/AAAA.md
+++ b/documentation/language-reference/domain-modifiers/AAAA.md
@@ -24,7 +24,7 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
AAAA("@", addrV6),
AAAA("foo", addrV6),
AAAA("test.foo", addrV6, TTL(5000)),
- AAAA("*", addrV6, {foo: 42})
+ AAAA("*", addrV6, {foo: 42}),
);
```
{% endcode %}
diff --git a/documentation/functions/domain/AKAMAICDN.md b/documentation/language-reference/domain-modifiers/AKAMAICDN.md
similarity index 100%
rename from documentation/functions/domain/AKAMAICDN.md
rename to documentation/language-reference/domain-modifiers/AKAMAICDN.md
diff --git a/documentation/functions/domain/ALIAS.md b/documentation/language-reference/domain-modifiers/ALIAS.md
similarity index 100%
rename from documentation/functions/domain/ALIAS.md
rename to documentation/language-reference/domain-modifiers/ALIAS.md
diff --git a/documentation/language-reference/domain-modifiers/AUTODNSSEC_OFF.md b/documentation/language-reference/domain-modifiers/AUTODNSSEC_OFF.md
new file mode 100644
index 0000000000..1c46770425
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/AUTODNSSEC_OFF.md
@@ -0,0 +1,8 @@
+---
+name: AUTODNSSEC_OFF
+---
+
+`AUTODNSSEC_OFF` tells the provider to disable AutoDNSSEC. It takes no
+parameters.
+
+See [`AUTODNSSEC_ON`](AUTODNSSEC_ON.md) for further details.
diff --git a/documentation/functions/domain/AUTODNSSEC_ON.md b/documentation/language-reference/domain-modifiers/AUTODNSSEC_ON.md
similarity index 76%
rename from documentation/functions/domain/AUTODNSSEC_ON.md
rename to documentation/language-reference/domain-modifiers/AUTODNSSEC_ON.md
index 7164cf7d80..df04516ab9 100644
--- a/documentation/functions/domain/AUTODNSSEC_ON.md
+++ b/documentation/language-reference/domain-modifiers/AUTODNSSEC_ON.md
@@ -2,14 +2,14 @@
name: AUTODNSSEC_ON
---
-AUTODNSSEC_ON tells the provider to enable AutoDNSSEC.
+`AUTODNSSEC_ON` tells the provider to **enable** AutoDNSSEC.
-AUTODNSSEC_OFF tells the provider to disable AutoDNSSEC.
+[`AUTODNSSEC_OFF`](AUTODNSSEC_OFF.md) tells the provider to **disable** AutoDNSSEC.
AutoDNSSEC is a feature where a DNS provider can automatically manage
DNSSEC for a domain. Not all providers support this.
-At this time, AUTODNSSEC_ON takes no parameters. There is no ability
+At this time, `AUTODNSSEC_ON` takes no parameters. There is no ability
to tune what the DNS provider sets, no algorithm choice. We simply
ask that they follow their defaults when enabling a no-fuss DNSSEC
data model.
@@ -23,12 +23,12 @@ correct syntax is `AUTODNSSEC_ON` not `AUTODNSSEC_ON()`
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
AUTODNSSEC_ON, // Enable AutoDNSSEC.
- A("@", "10.1.1.1")
+ A("@", "10.1.1.1"),
);
D("insecure.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
AUTODNSSEC_OFF, // Disable AutoDNSSEC.
- A("@", "10.2.2.2")
+ A("@", "10.2.2.2"),
);
```
{% endcode %}
diff --git a/documentation/functions/domain/AZURE_ALIAS.md b/documentation/language-reference/domain-modifiers/AZURE_ALIAS.md
similarity index 100%
rename from documentation/functions/domain/AZURE_ALIAS.md
rename to documentation/language-reference/domain-modifiers/AZURE_ALIAS.md
diff --git a/documentation/functions/domain/CAA.md b/documentation/language-reference/domain-modifiers/CAA.md
similarity index 77%
rename from documentation/functions/domain/CAA.md
rename to documentation/language-reference/domain-modifiers/CAA.md
index bcea062022..22428afb1d 100644
--- a/documentation/functions/domain/CAA.md
+++ b/documentation/language-reference/domain-modifiers/CAA.md
@@ -33,9 +33,9 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
CAA("@", "issuewild", ";"),
// Report all violation to test@example.com. If CA does not support
// this record then refuse to issue any certificate
- CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL)
+ CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL),
);
```
{% endcode %}
-DNSControl contains a [`CAA_BUILDER`](../record/CAA_BUILDER.md) which can be used to simply create `CAA()` records for your domains. Instead of creating each CAA record individually, you can simply configure your report mail address, the authorized certificate authorities and the builder cares about the rest.
+DNSControl contains a [`CAA_BUILDER`](CAA_BUILDER.md) which can be used to simply create `CAA()` records for your domains. Instead of creating each CAA record individually, you can simply configure your report mail address, the authorized certificate authorities and the builder cares about the rest.
diff --git a/documentation/language-reference/domain-modifiers/CAA_BUILDER.md b/documentation/language-reference/domain-modifiers/CAA_BUILDER.md
new file mode 100644
index 0000000000..a7d4b5b780
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/CAA_BUILDER.md
@@ -0,0 +1,126 @@
+---
+name: CAA_BUILDER
+parameters:
+ - label
+ - iodef
+ - iodef_critical
+ - issue
+ - issue_critical
+ - issuewild
+ - issuewild_critical
+ - ttl
+parameters_object: true
+parameter_types:
+ label: string?
+ iodef: string
+ iodef_critical: boolean?
+ issue: string[]
+ issue_critical: boolean?
+ issuewild: string[]
+ issuewild_critical: boolean?
+ ttl: Duration?
+---
+
+DNSControl contains a `CAA_BUILDER` which can be used to simply create
+[`CAA()`](../domain-modifiers/CAA.md) records for your domains. Instead of creating each [`CAA()`](../domain-modifiers/CAA.md) record
+individually, you can simply configure your report mail address, the
+authorized certificate authorities and the builder cares about the rest.
+
+## Example
+
+### Simple example
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ CAA_BUILDER({
+ label: "@",
+ iodef: "mailto:test@example.com",
+ iodef_critical: true,
+ issue: [
+ "letsencrypt.org",
+ "comodoca.com",
+ ],
+ issuewild: "none",
+ }),
+);
+```
+{% endcode %}
+
+`CAA_BUILDER()` builds multiple records:
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL),
+ CAA("@", "issue", "letsencrypt.org"),
+ CAA("@", "issue", "comodoca.com"),
+ CAA("@", "issuewild", ";"),
+);
+```
+{% endcode %}
+
+which in turns yield the following records:
+
+```text
+@ 300 IN CAA 128 iodef "mailto:test@example.com"
+@ 300 IN CAA 0 issue "letsencrypt.org"
+@ 300 IN CAA 0 issue "comodoca.com"
+@ 300 IN CAA 0 issuewild ";"
+```
+
+### Example with CAA_CRITICAL flag on all records
+
+The same example can be enriched with CAA_CRITICAL on all records:
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ CAA_BUILDER({
+ label: "@",
+ iodef: "mailto:test@example.com",
+ iodef_critical: true,
+ issue: [
+ "letsencrypt.org",
+ "comodoca.com",
+ ],
+ issue_critical: true,
+ issuewild: "none",
+ issuewild_critical: true,
+ }),
+);
+```
+{% endcode %}
+
+`CAA_BUILDER()` then builds (the same) multiple records - all with CAA_CRITICAL flag set:
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ CAA("@", "iodef", "mailto:test@example.com", CAA_CRITICAL),
+ CAA("@", "issue", "letsencrypt.org", CAA_CRITICAL),
+ CAA("@", "issue", "comodoca.com", CAA_CRITICAL),
+ CAA("@", "issuewild", ";", CAA_CRITICAL),
+);
+```
+{% endcode %}
+
+which in turns yield the following records:
+
+```text
+@ 300 IN CAA 128 iodef "mailto:test@example.com"
+@ 300 IN CAA 128 issue "letsencrypt.org"
+@ 300 IN CAA 128 issue "comodoca.com"
+@ 300 IN CAA 128 issuewild ";"
+```
+
+### Parameters
+
+* `label:` The label of the CAA record. (Optional. Default: `"@"`)
+* `iodef:` Report all violation to configured mail address.
+* `iodef_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`)
+* `issue:` An array of CAs which are allowed to issue certificates. (Use `"none"` to refuse all CAs)
+* `issue_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`)
+* `issuewild:` An array of CAs which are allowed to issue wildcard certificates. (Can be simply `"none"` to refuse issuing wildcard certificates for all CAs)
+* `issuewild_critical:` This can be `true` or `false`. If enabled and CA does not support this record, then certificate issue will be refused. (Optional. Default: `false`)
+* `ttl:` Input for `TTL` method (optional)
diff --git a/documentation/functions/domain/CF_REDIRECT.md b/documentation/language-reference/domain-modifiers/CF_REDIRECT.md
similarity index 79%
rename from documentation/functions/domain/CF_REDIRECT.md
rename to documentation/language-reference/domain-modifiers/CF_REDIRECT.md
index 75bb84f671..dedf9c37e5 100644
--- a/documentation/functions/domain/CF_REDIRECT.md
+++ b/documentation/language-reference/domain-modifiers/CF_REDIRECT.md
@@ -11,12 +11,19 @@ parameter_types:
"modifiers...": RecordModifier[]
---
+{% hint style="warning" %}
+WARNING: Cloudflare is removing this feature and replacing it with a new
+feature called "Dynamic Single Redirect". DNSControl will automatically
+generate "Dynamic Single Redirects" for a limited number of use cases. See
+[`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
+{% endhint %}
+
`CF_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page Rules) to
generate a HTTP 301 permanent redirect.
If _any_ `CF_REDIRECT` or [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) functions are used then
`dnscontrol` will manage _all_ "Forwarding URL" type Page Rules for the domain.
-Page Rule types other than "Forwarding URLâ will be left alone.
+Page Rule types other than "Forwarding URL" will be left alone.
{% hint style="warning" %}
**WARNING**: Cloudflare does not currently fully document the Page Rules API and
diff --git a/documentation/language-reference/domain-modifiers/CF_SINGLE_REDIRECT.md b/documentation/language-reference/domain-modifiers/CF_SINGLE_REDIRECT.md
new file mode 100644
index 0000000000..718cac8b75
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/CF_SINGLE_REDIRECT.md
@@ -0,0 +1,45 @@
+---
+name: CF_SINGLE_REDIRECT
+parameters:
+ - name
+ - code
+ - when
+ - then
+ - modifiers...
+provider: CLOUDFLAREAPI
+parameter_types:
+ name: string
+ code: number
+ when: string
+ then: string
+ "modifiers...": RecordModifier[]
+---
+
+`CF_SINGLE_REDIRECT` is a Cloudflare-specific feature for creating HTTP 301
+(permanent) or 302 (temporary) redirects.
+
+This feature manages dynamic "Single Redirects". (Single Redirects can be
+static or dynamic but DNSControl only maintains dynamic redirects).
+
+Cloudflare documentation:
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ CF_SINGLE_REDIRECT("name", 301, "when", "then"),
+ CF_SINGLE_REDIRECT('redirect www.example.com', 301, 'http.host eq "www.example.com"', 'concat("https://otherplace.com", http.request.uri.path)'),
+ CF_SINGLE_REDIRECT('redirect yyy.example.com', 301, 'http.host eq "yyy.example.com"', 'concat("https://survey.stackoverflow.co", "")'),
+);
+```
+{% endcode %}
+
+The fields are:
+
+* name: The name (basically a comment, but it must be unique)
+* code: Either 301 (permanent) or 302 (temporary) redirects. May be a number or string.
+* when: What Cloudflare sometimes calls the "rule expression".
+* then: The replacement expression.
+
+{% hint style="info" %}
+**NOTE**: The features [`CF_REDIRECT`](CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) generate `CF_SINGLE_REDIRECT` if enabled in [`CLOUDFLAREAPI`](../../provider/cloudflareapi.md).
+{% endhint %}
diff --git a/documentation/functions/domain/CF_TEMP_REDIRECT.md b/documentation/language-reference/domain-modifiers/CF_TEMP_REDIRECT.md
similarity index 78%
rename from documentation/functions/domain/CF_TEMP_REDIRECT.md
rename to documentation/language-reference/domain-modifiers/CF_TEMP_REDIRECT.md
index 7543ebd54b..fca2a8a146 100644
--- a/documentation/functions/domain/CF_TEMP_REDIRECT.md
+++ b/documentation/language-reference/domain-modifiers/CF_TEMP_REDIRECT.md
@@ -11,6 +11,13 @@ parameter_types:
"modifiers...": RecordModifier[]
---
+{% hint style="warning" %}
+**WARNING**: Cloudflare is removing this feature and replacing it with a new
+feature called "Dynamic Single Redirect". DNSControl will automatically
+generate "Dynamic Single Redirects" for a limited number of use cases. See
+[`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
+{% endhint %}
+
`CF_TEMP_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page
Rules) to generate a HTTP 302 temporary redirect.
diff --git a/documentation/functions/domain/CF_WORKER_ROUTE.md b/documentation/language-reference/domain-modifiers/CF_WORKER_ROUTE.md
similarity index 100%
rename from documentation/functions/domain/CF_WORKER_ROUTE.md
rename to documentation/language-reference/domain-modifiers/CF_WORKER_ROUTE.md
diff --git a/documentation/functions/domain/CLOUDNS_WR.md b/documentation/language-reference/domain-modifiers/CLOUDNS_WR.md
similarity index 100%
rename from documentation/functions/domain/CLOUDNS_WR.md
rename to documentation/language-reference/domain-modifiers/CLOUDNS_WR.md
diff --git a/documentation/functions/domain/CNAME.md b/documentation/language-reference/domain-modifiers/CNAME.md
similarity index 100%
rename from documentation/functions/domain/CNAME.md
rename to documentation/language-reference/domain-modifiers/CNAME.md
diff --git a/documentation/language-reference/domain-modifiers/DHCID.md b/documentation/language-reference/domain-modifiers/DHCID.md
new file mode 100644
index 0000000000..0c6eefbb95
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/DHCID.md
@@ -0,0 +1,23 @@
+---
+name: DHCID
+parameters:
+ - name
+ - digest
+ - modifiers...
+parameter_types:
+ name: string
+ digest: string
+ "modifiers...": RecordModifier[]
+---
+
+DHCID adds a DHCID record to the domain.
+
+Digest should be a string.
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ DHCID("example.com", "ABCDEFG"),
+);
+```
+{% endcode %}
diff --git a/documentation/functions/domain/DISABLE_IGNORE_SAFETY_CHECK.md b/documentation/language-reference/domain-modifiers/DISABLE_IGNORE_SAFETY_CHECK.md
similarity index 88%
rename from documentation/functions/domain/DISABLE_IGNORE_SAFETY_CHECK.md
rename to documentation/language-reference/domain-modifiers/DISABLE_IGNORE_SAFETY_CHECK.md
index 4954b6b153..ecb77dff3d 100644
--- a/documentation/functions/domain/DISABLE_IGNORE_SAFETY_CHECK.md
+++ b/documentation/language-reference/domain-modifiers/DISABLE_IGNORE_SAFETY_CHECK.md
@@ -9,7 +9,7 @@ safety check for the entire domain.
It replaces the per-record `IGNORE_NAME_DISABLE_SAFETY_CHECK()` which is
deprecated as of DNSControl v4.0.0.0.
-See [`IGNORE()`](../domain/IGNORE.md) for more information.
+See [`IGNORE()`](../domain-modifiers/IGNORE.md) for more information.
## Syntax
diff --git a/documentation/functions/domain/DMARC_BUILDER.md b/documentation/language-reference/domain-modifiers/DMARC_BUILDER.md
similarity index 76%
rename from documentation/functions/domain/DMARC_BUILDER.md
rename to documentation/language-reference/domain-modifiers/DMARC_BUILDER.md
index 0a237c40ec..50f085c5e8 100644
--- a/documentation/functions/domain/DMARC_BUILDER.md
+++ b/documentation/language-reference/domain-modifiers/DMARC_BUILDER.md
@@ -41,12 +41,14 @@ DMARC policies for your domains.
{% code title="dnsconfig.js" %}
```javascript
-DMARC_BUILDER({
- policy: "reject",
- ruf: [
- "mailto:mailauth-reports@example.com",
- ],
-})
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ DMARC_BUILDER({
+ policy: "reject",
+ ruf: [
+ "mailto:mailauth-reports@example.com",
+ ],
+ }),
+);
```
{% endcode %}
@@ -60,38 +62,42 @@ This yield the following record:
{% code title="dnsconfig.js" %}
```javascript
-DMARC_BUILDER({
- policy: "reject",
- subdomainPolicy: "quarantine",
- percent: 50,
- alignmentSPF: "r",
- alignmentDKIM: "strict",
- rua: [
- "mailto:mailauth-reports@example.com",
- "https://dmarc.example.com/submit",
- ],
- ruf: [
- "mailto:mailauth-reports@example.com",
- ],
- failureOptions: "1",
- reportInterval: "1h",
-});
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ DMARC_BUILDER({
+ policy: "reject",
+ subdomainPolicy: "quarantine",
+ percent: 50,
+ alignmentSPF: "r",
+ alignmentDKIM: "strict",
+ rua: [
+ "mailto:mailauth-reports@example.com",
+ "https://dmarc.example.com/submit",
+ ],
+ ruf: [
+ "mailto:mailauth-reports@example.com",
+ ],
+ failureOptions: "1",
+ reportInterval: "1h",
+ }),
+);
```
{% endcode %}
{% code title="dnsconfig.js" %}
```javascript
-DMARC_BUILDER({
- label: "insecure",
- policy: "none",
- ruf: [
- "mailto:mailauth-reports@example.com",
- ],
- failureOptions: {
- SPF: false,
- DKIM: true,
- },
-});
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ DMARC_BUILDER({
+ label: "insecure",
+ policy: "none",
+ ruf: [
+ "mailto:mailauth-reports@example.com",
+ ],
+ failureOptions: {
+ SPF: false,
+ DKIM: true,
+ },
+ }),
+);
```
{% endcode %}
@@ -102,7 +108,6 @@ This yields the following records:
insecure IN TXT "v=DMARC1; p=none; ruf=mailto:mailauth-reports@example.com; fo=d"
```
-
### Parameters
* `label:` The DNS label for the DMARC record (`_dmarc` prefix is added, default: `"@"`)
diff --git a/documentation/language-reference/domain-modifiers/DNAME.md b/documentation/language-reference/domain-modifiers/DNAME.md
new file mode 100644
index 0000000000..85cc14dde2
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/DNAME.md
@@ -0,0 +1,23 @@
+---
+name: DNAME
+parameters:
+ - name
+ - target
+ - modifiers...
+parameter_types:
+ name: string
+ target: string
+ "modifiers...": RecordModifier[]
+---
+
+DNAME adds a DNAME record to the domain.
+
+Target should be a string.
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ DNAME("sub", "example.net."),
+);
+```
+{% endcode %}
diff --git a/documentation/language-reference/domain-modifiers/DNSKEY.md b/documentation/language-reference/domain-modifiers/DNSKEY.md
new file mode 100644
index 0000000000..7c2f305366
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/DNSKEY.md
@@ -0,0 +1,35 @@
+---
+name: DNSKEY
+parameters:
+ - name
+ - flags
+ - protocol
+ - algorithm
+ - publicKey
+ - modifiers...
+parameter_types:
+ name: string
+ flags: number
+ protocol: number
+ algorithm: number
+ publicKey: string
+ "modifiers...": RecordModifier[]
+---
+
+DNSKEY adds a DNSKEY record to the domain.
+
+Flags should be a number.
+
+Protocol should be a number.
+
+Algorithm must be a number.
+
+Public key must be a string.
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ DNSKEY("@", 257, 3, 13, "AABBCCDD"),
+);
+```
+{% endcode %}
diff --git a/documentation/functions/domain/DS.md b/documentation/language-reference/domain-modifiers/DS.md
similarity index 92%
rename from documentation/functions/domain/DS.md
rename to documentation/language-reference/domain-modifiers/DS.md
index ae9cc25f02..529888c959 100644
--- a/documentation/functions/domain/DS.md
+++ b/documentation/language-reference/domain-modifiers/DS.md
@@ -29,7 +29,7 @@ Digest must be a string.
{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- DS("example.com", 2371, 13, 2, "ABCDEF")
+ DS("example.com", 2371, 13, 2, "ABCDEF"),
);
```
{% endcode %}
diff --git a/documentation/functions/domain/DefaultTTL.md b/documentation/language-reference/domain-modifiers/DefaultTTL.md
similarity index 54%
rename from documentation/functions/domain/DefaultTTL.md
rename to documentation/language-reference/domain-modifiers/DefaultTTL.md
index 7df9b7b3a7..e5db9f2ae9 100644
--- a/documentation/functions/domain/DefaultTTL.md
+++ b/documentation/language-reference/domain-modifiers/DefaultTTL.md
@@ -6,10 +6,10 @@ parameter_types:
ttl: Duration
---
-DefaultTTL sets the TTL for all subsequent records following it in a domain that do not explicitly set one with [`TTL`](../record/TTL.md). If neither `DefaultTTL` or `TTL` exist for a record,
-the record will inherit the DNSControl global internal default of 300 seconds. See also [`DEFAULTS`](../global/DEFAULTS.md) to override the internal defaults.
+DefaultTTL sets the TTL for all subsequent records following it in a domain that do not explicitly set one with [`TTL`](../record-modifiers/TTL.md). If neither `DefaultTTL` or `TTL` exist for a record,
+the record will inherit the DNSControl global internal default of 300 seconds. See also [`DEFAULTS`](../top-level-functions/DEFAULTS.md) to override the internal defaults.
-NS records are currently a special case, and do not inherit from `DefaultTTL`. See [`NAMESERVER_TTL`](../domain/NAMESERVER_TTL.md) to set a default TTL for all NS records.
+NS records are currently a special case, and do not inherit from `DefaultTTL`. See [`NAMESERVER_TTL`](../domain-modifiers/NAMESERVER_TTL.md) to set a default TTL for all NS records.
{% code title="dnsconfig.js" %}
@@ -17,10 +17,10 @@ NS records are currently a special case, and do not inherit from `DefaultTTL`. S
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
DefaultTTL("4h"),
A("@","1.2.3.4"), // uses default
- A("foo", "2.3.4.5", TTL(600)) // overrides default
+ A("foo", "2.3.4.5", TTL(600)), // overrides default
);
```
{% endcode %}
-The DefaultTTL duration is the same format as [`TTL`](../record/TTL.md), an integer number of seconds
+The DefaultTTL duration is the same format as [`TTL`](../record-modifiers/TTL.md), an integer number of seconds
or a string with a unit such as `"4d"`.
diff --git a/documentation/functions/domain/DnsProvider.md b/documentation/language-reference/domain-modifiers/DnsProvider.md
similarity index 94%
rename from documentation/functions/domain/DnsProvider.md
rename to documentation/language-reference/domain-modifiers/DnsProvider.md
index ccefe44b88..075d12b0df 100644
--- a/documentation/functions/domain/DnsProvider.md
+++ b/documentation/language-reference/domain-modifiers/DnsProvider.md
@@ -9,7 +9,7 @@ parameter_types:
---
DnsProvider indicates that the specified provider should be used to manage
-records for this domain. The name must match the name used with [NewDnsProvider](../global/NewDnsProvider.md).
+records for this domain. The name must match the name used with [NewDnsProvider](../top-level-functions/NewDnsProvider.md).
The nsCount parameter determines how the nameservers will be managed from this provider.
diff --git a/documentation/functions/domain/FRAME.md b/documentation/language-reference/domain-modifiers/FRAME.md
similarity index 100%
rename from documentation/functions/domain/FRAME.md
rename to documentation/language-reference/domain-modifiers/FRAME.md
diff --git a/documentation/language-reference/domain-modifiers/HTTPS.md b/documentation/language-reference/domain-modifiers/HTTPS.md
new file mode 100644
index 0000000000..12d98ec95d
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/HTTPS.md
@@ -0,0 +1,32 @@
+---
+name: HTTPS
+parameters:
+ - name
+ - priority
+ - target
+ - params
+ - modifiers...
+parameter_types:
+ name: string
+ priority: number
+ target: string
+ params: string
+ "modifiers...": RecordModifier[]
+---
+
+HTTPS adds an HTTPS record to a domain. The name should be the relative label for the record. Use `@` for the domain apex. The HTTPS record is a special form of the SVCB resource record.
+
+The priority must be a positive number, the address should be an ip address, either a string, or a numeric value obtained via [IP](../top-level-functions/IP.md).
+
+The params may be configured to specify the `alpn`, `ipv4hint`, `ipv6hint`, `ech` or `port` setting. Several params may be joined by a space. Not existing params may be specified as an empty string `""`
+
+Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata.
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ HTTPS("@", 1, ".", "ipv4hint=123.123.123.123 alpn=h3,h2 port=443"),
+ HTTPS("@", 1, "test.com", ""),
+);
+```
+{% endcode %}
diff --git a/documentation/functions/domain/IGNORE.md b/documentation/language-reference/domain-modifiers/IGNORE.md
similarity index 81%
rename from documentation/functions/domain/IGNORE.md
rename to documentation/language-reference/domain-modifiers/IGNORE.md
index e840a904c2..8be59e5148 100644
--- a/documentation/functions/domain/IGNORE.md
+++ b/documentation/language-reference/domain-modifiers/IGNORE.md
@@ -26,11 +26,11 @@ records.
Technically `IGNORE_NAME` is a promise that DNSControl will not modify or
delete existing records that match particular patterns. It is like
-[`NO_PURGE`](../domain/NO_PURGE.md) that matches only specific records.
+[`NO_PURGE`](../domain-modifiers/NO_PURGE.md) that matches only specific records.
Including a record that is ignored is considered an error and may have
undefined behavior. This safety check can be disabled using the
-[`DISABLE_IGNORE_SAFETY_CHECK`](../domain/DISABLE_IGNORE_SAFETY_CHECK.md) feature.
+[`DISABLE_IGNORE_SAFETY_CHECK`](../domain-modifiers/DISABLE_IGNORE_SAFETY_CHECK.md) feature.
## Syntax
@@ -38,14 +38,16 @@ The `IGNORE()` function can be used with up to 3 parameters:
{% code %}
```javascript
-IGNORE(labelSpec, typeSpec, targetSpec):
-IGNORE(labelSpec, typeSpec):
-IGNORE(labelSpec):
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ IGNORE(labelSpec, typeSpec, targetSpec),
+ IGNORE(labelSpec, typeSpec),
+ IGNORE(labelSpec),
+);
```
{% endcode %}
* `labelSpec` is a glob that matches the DNS label. For example `"foo"` or `"foo*"`. `"*"` matches all labels, as does the empty string (`""`).
-* `typeSpec` is a comma-separated list of DNS types. For example `"A"` matches DNS A records, `"A,CNAME"` matches both A and CNAME records. `"*"` matches any DNS type, as does the empty string (`""`).
+* `typeSpec` is a comma-separated list of DNS types. For example `"A"` matches DNS A records, `"A,CNAME"` matches both A and CNAME records. `"*"` matches any DNS type, as does the empty string (`""`).
* `targetSpec` is a glob that matches the DNS target. For example `"foo"` or `"foo*"`. `"*"` matches all targets, as does the empty string (`""`).
`typeSpec` and `targetSpec` default to `"*"` if they are omitted.
@@ -78,7 +80,7 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("bar", "A,MX"), // ignore only A and MX records for name bar
IGNORE("*", "*", "dev-*"), // Ignore targets with a `dev-` prefix
IGNORE("*", "A", "1\.2\.3\."), // Ignore targets in the 1.2.3.0/24 CIDR block
-END);
+);
```
{% endcode %}
@@ -89,7 +91,7 @@ Ignore Let's Encrypt (ACME) validation records:
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("_acme-challenge", "TXT"),
IGNORE("_acme-challenge.**", "TXT"),
-END);
+);
```
{% endcode %}
@@ -114,7 +116,7 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("domaindnszones.**", "A"),
IGNORE("forestdnszones", "A"),
IGNORE("forestdnszones.**", "A"),
-END);
+);
```
{% endcode %}
@@ -137,7 +139,7 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
CNAME("cfull", "www.plts.org."),
CNAME("cfull2", "www.bar.plts.org."),
CNAME("cfull3", "bar.www.plts.org."),
-END);
+);
D_EXTEND("more.example.com",
A("foo", "1.1.1.1"),
@@ -146,112 +148,159 @@ D_EXTEND("more.example.com",
CNAME("mfull", "www.plts.org."),
CNAME("mfull2", "www.bar.plts.org."),
CNAME("mfull3", "bar.www.plts.org."),
-END);
+);
```
{% endcode %}
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("@", "", ""),
- // Would match:
- // foo.example.com. A 1.1.1.1
- // foo.more.example.com. A 1.1.1.1
+);
```
{% endcode %}
+**Would match**:
+
+* `foo.example.com. A 1.1.1.1`
+* `foo.more.example.com. A 1.1.1.1`
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("example.com.", "", ""),
- // Would match:
- // nothing
+);
```
{% endcode %}
+**Would match**:
+
+* nothing
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("foo", "", ""),
- // Would match:
- // foo.example.com. A 1.1.1.1
+);
```
{% endcode %}
+**Would match**:
+
+* `foo.example.com. A 1.1.1.1`
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("foo.**", "", ""),
- // Would match:
- // foo.more.example.com. A 1.1.1.1
+);
```
{% endcode %}
+**Would match**:
+
+* `foo.more.example.com. A 1.1.1.1`
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("www", "", ""),
- // Would match:
+);
// www.example.com. A 174.136.107.196
```
{% endcode %}
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("www.*", "", ""),
- // Would match:
+);
// nothing
```
{% endcode %}
+**Would match**:
+
+* nothing
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("www.example.com", "", ""),
- // Would match:
+);
// nothing
```
{% endcode %}
+**Would match**:
+
+* nothing
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("www.example.com.", "", ""),
- // Would match:
- // none
+);
```
{% endcode %}
+**Would match**:
+
+* none
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
//IGNORE("", "", "1.1.1.*"),
- // Would match:
- // foo.example.com. A 1.1.1.1
- // foo.more.example.com. A 1.1.1.1
+);
```
{% endcode %}
+**Would match**:
+
+* `foo.example.com. A 1.1.1.1`
+* `foo.more.example.com. A 1.1.1.1`
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
//IGNORE("", "", "www"),
- // Would match:
- // none
+);
```
{% endcode %}
+**Would match**:
+
+* none
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("", "", "*bar*"),
- // Would match:
- // cfull2.example.com. CNAME www.bar.plts.org.
- // cfull3.example.com. CNAME bar.www.plts.org.
- // mfull2.more.example.com. CNAME www.bar.plts.org.
- // mfull3.more.example.com. CNAME bar.www.plts.org.
+);
```
{% endcode %}
+**Would match**:
+
+* `cfull2.example.com. CNAME www.bar.plts.org.`
+* `cfull3.example.com. CNAME bar.www.plts.org.`
+* `mfull2.more.example.com. CNAME www.bar.plts.org.`
+* `mfull3.more.example.com. CNAME bar.www.plts.org.`
+
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
IGNORE("", "", "bar.**"),
- // Would match:
- // cfull3.example.com. CNAME bar.www.plts.org.
- // mfull3.more.example.com. CNAME bar.www.plts.org.
+);
```
{% endcode %}
+**Would match**:
+
+* `cfull3.example.com. CNAME bar.www.plts.org.`
+* `mfull3.more.example.com. CNAME bar.www.plts.org.`
+
## Conflict handling
It is considered as an error for a `dnsconfig.js` to both ignore and insert the
@@ -294,8 +343,10 @@ instead.
{% code %}
```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
// THIS NO LONGER WORKS! Use DISABLE_IGNORE_SAFETY_CHECK instead. See above.
TXT("myhost", "mytext", IGNORE_NAME_DISABLE_SAFETY_CHECK),
+);
```
{% endcode %}
diff --git a/documentation/functions/domain/IGNORE_NAME.md b/documentation/language-reference/domain-modifiers/IGNORE_NAME.md
similarity index 100%
rename from documentation/functions/domain/IGNORE_NAME.md
rename to documentation/language-reference/domain-modifiers/IGNORE_NAME.md
diff --git a/documentation/functions/domain/IGNORE_TARGET.md b/documentation/language-reference/domain-modifiers/IGNORE_TARGET.md
similarity index 100%
rename from documentation/functions/domain/IGNORE_TARGET.md
rename to documentation/language-reference/domain-modifiers/IGNORE_TARGET.md
diff --git a/documentation/functions/domain/IMPORT_TRANSFORM.md b/documentation/language-reference/domain-modifiers/IMPORT_TRANSFORM.md
similarity index 55%
rename from documentation/functions/domain/IMPORT_TRANSFORM.md
rename to documentation/language-reference/domain-modifiers/IMPORT_TRANSFORM.md
index 3476bfacd2..0c137ccd82 100644
--- a/documentation/functions/domain/IMPORT_TRANSFORM.md
+++ b/documentation/language-reference/domain-modifiers/IMPORT_TRANSFORM.md
@@ -3,6 +3,7 @@ name: IMPORT_TRANSFORM
parameters:
- transform table
- domain
+ - ttl
- modifiers...
ts_ignore: true
---
@@ -11,9 +12,14 @@ ts_ignore: true
Don't use this feature. It was added for a very specific situation at Stack Overflow.
{% endhint %}
-`IMPORT_TRANSFORM` adds to the domain all the records from another
+`IMPORT_TRANSFORM` adds to the domain the records from another
domain, after making certain transformations and resetting the TTL.
+Not all records are copied and transformed:
+* The record must be record type A or CNAME.
+* Records are skipped if they have a metadata key `import_transform_skip` (any non-null value).
+* Records are skipped if a record of the same label+type already exist in the destination domain.
+
Example:
Suppose foo.com is a regular domain. bar.com is a regular domain,
@@ -45,27 +51,50 @@ Here's how you'd implement this in DNSControl:
{% code title="dnsconfig.js" %}
```javascript
-var TRANSFORM_INT = [
+// Transforms that use newBase:
+var TRANSFORM_CF = [
// RANGE_START, RANGE_END, NEW_BASE
{ low: "1.2.3.10", high: "1.2.3.20", newBase: "123.123.123.100" }, // .10 to .20 rewritten as 123.123.123.100+IP
{ low: "2.4.6.80", high: "2.4.6.90", newBase: "123.123.123.200" }, // Another rule, just to show that you can have many.
]
+// Transforms that use newIP
+var TRANSFORM_FSTLY = [
+ // RANGE_START, RANGE_END, NEW_BASE
+ { low: "1.2.3.10", high: "1.2.3.10", newIP: "123.123.123.100" }, // .10 is rewritten as 123.123.123.100
+ { low: "2.4.6.20", high: "2.4.6.20", newIP: "123.123.123.200" }, // .20 is rewritten as 123.123.123.200
+]
D("foo.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- A("one","1.2.3.1")
- A("two","1.2.3.2")
- A("three","1.2.3.13")
- A("four","1.2.3.14")
+ A("one","1.2.3.1"),
+ A("two","1.2.3.2"),
+ A("three","1.2.3.13"),
+ A("four","1.2.3.14"),
);
D("bar.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- A("www","123.123.123.123")
- IMPORT_TRANSFORM(TRANSFORM_INT, "foo.com", 300),
+ A("www","123.123.123.123"),
+ IMPORT_TRANSFORM(TRANSFORM_CF, "foo.com", 300),
+);
+
+D("baz.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ A("www","123.123.123.123"),
+ IMPORT_TRANSFORM(TRANSFORM_FSTLY, "foo.com", 300),
);
```
{% endcode %}
-Transform rules are: RANGE_START, RANGE_END, NEW_BASE. NEW_BASE may be:
+Transform rules are: `RANGE_START`, `RANGE_END`, `NEW_BASE` or `NEW_IP`.
+
+Only one of `newBase` and `newBase` may be specified. If both are non-null the behavior is undefined.
+
+`NEW_BASE` works as follows:
* An IP address. Rebase the IP address on this IP address. Extract the host part of the /24 and add it to the "new base" address.
* A list of IP addresses. For each A record, inject an A record for each item in the list: `newBase: ["1.2.3.100", "2.4.6.8.100"]` would produce 2 records for each A record.
+* For CNAMEs, the `.internal` is appended to the end of the target.
+
+`NEW_IP` works as follows:
+
+* An IP address. Change the IP address of the A record to this IP address. If there are multiple A records at this label, only one A record is generated.
+* A list of IP addresses. Not supported.
+* For CNAMEs, the `.internal` is appended to the end of the target. (same as `NEW_BASE`)
diff --git a/documentation/language-reference/domain-modifiers/IMPORT_TRANSFORM_STRIP.md b/documentation/language-reference/domain-modifiers/IMPORT_TRANSFORM_STRIP.md
new file mode 100644
index 0000000000..68572f2952
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/IMPORT_TRANSFORM_STRIP.md
@@ -0,0 +1,29 @@
+---
+name: IMPORT_TRANSFORM_STRIP
+parameters:
+ - transform table
+ - domain
+ - ttl
+ - suffixstrip
+ - modifiers...
+ts_ignore: true
+---
+
+{% hint style="warning" %}
+Don't use this feature. It was added for a very specific situation at Stack Overflow.
+{% endhint %}
+
+`IMPORT_TRANSFORM_STRIP` is the same as `IMPORT_TRANSFORM` with an additional parameter: `suffixstrip`.
+
+When `IMPORT_TRANSFORM_STRIP` is generating the label for new records, it
+checks the label. If the label ends with `.` + `suffixstrip`, that suffix is removed.
+If the label does not end with `suffixstrip`, an error is returned.
+
+For CNAMEs, the `suffixstrip` is stripped from the beginning (prefix) of the target domain.
+
+For example, if the domain is `com.extra` and the label is `foo.com`,
+`IMPORT_TRANSFORM` would generate a label `foo.com.com.extra`.
+`IMPORT_TRANSFORM_STRIP(... , 'com')` would generate
+the label `foo.com.extra` instead.
+
+In the case of a CNAME, if the target is `foo.com.`, the new target would be `foo.com.extra`.
diff --git a/documentation/functions/domain/INCLUDE.md b/documentation/language-reference/domain-modifiers/INCLUDE.md
similarity index 88%
rename from documentation/functions/domain/INCLUDE.md
rename to documentation/language-reference/domain-modifiers/INCLUDE.md
index 64944668e1..1449adf3e1 100644
--- a/documentation/functions/domain/INCLUDE.md
+++ b/documentation/language-reference/domain-modifiers/INCLUDE.md
@@ -12,12 +12,12 @@ Includes all records from a given domain
{% code title="dnsconfig.js" %}
```javascript
D("example.com!external", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- A("test", "8.8.8.8")
+ A("test", "8.8.8.8"),
);
D("example.com!internal", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
INCLUDE("example.com!external"),
- A("home", "127.0.0.1")
+ A("home", "127.0.0.1"),
);
```
{% endcode %}
diff --git a/documentation/functions/domain/LOC.md b/documentation/language-reference/domain-modifiers/LOC.md
similarity index 69%
rename from documentation/functions/domain/LOC.md
rename to documentation/language-reference/domain-modifiers/LOC.md
index 829eb127f2..ecdf4ade9d 100644
--- a/documentation/functions/domain/LOC.md
+++ b/documentation/language-reference/domain-modifiers/LOC.md
@@ -26,7 +26,7 @@ parameter_types:
vertical_precision: number
---
-The parameter number types are as follows:
+The parameter number types ingested are as follows:
```
name: string
@@ -37,7 +37,7 @@ sec1: float32
deg2: uint32
min2: uint32
sec2: float32
-altitude: uint32
+altitude: float32
size: float32
horizontal_precision: float32
vertical_precision: float32
@@ -86,17 +86,22 @@ the LOC record type will supply defaults where values were absent on DNS import.
One must supply the `LOC()` js helper all parameters. If that seems like too
much work, see also helper functions:
- * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - build a `LOC` by supplying only **d**ecimal **d**egrees.
- * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - build a `LOC` by supplying only **d**ecimal **d**egrees.
+ * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
## Format ##
-The coordinate format for `LOC()` is:
+The coordinate format for `LOC()` is:
`degrees,minutes,seconds,[NnSs],deg,min,sec,[EeWw],altitude,size,horizontal_precision,vertical_precision`
+where:
+ altitude: [-100000.00 .. 42849672.95] BY .01 (altitude in meters)
+ size, horizontal_precision, vertical_precision: [0 .. 90000000.00] (size/precision in meters)
+
+values outside of the above ranges are gated to within the ranges.
## Examples ##
@@ -105,16 +110,15 @@ The coordinate format for `LOC()` is:
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
// LOC "subdomain", d1, m1, s1, "[NnSs]", d2, m2, s2, "[EeWw]", alt, siz, hp, vp)
//42 21 54 N 71 06 18 W -24m 30m
- , LOC("@", 42, 21, 54, "N", 71, 6, 18, "W", -24, 30, 0, 0)
+ LOC("@", 42, 21, 54, "N", 71, 6, 18, "W", -24.01, 30, 0, 0),
//42 21 43.952 N 71 5 6.344 W -24m 1m 200m 10m
- , LOC("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24, 1, 200, 10)
+ LOC("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24.33, 1, 200, 10),
//52 14 05 N 00 08 50 E 10m
- , LOC("b", 52, 14, 5, "N", 0, 8, 50, "E", 10, 0, 0, 0)
+ LOC("b", 52, 14, 5, "N", 0, 8, 50, "E", 10, 0, 0, 0),
//32 7 19 S 116 2 25 E 10m
- , LOC("c", 32, 7, 19, "S",116, 2, 25, "E", 10, 0, 0, 0)
+ LOC("c", 32, 7, 19, "S",116, 2, 25, "E", 10, 0, 0, 0),
//42 21 28.764 N 71 00 51.617 W -44m 2000m
- , LOC("d", 42, 21, 28.764, "N", 71, 0, 51.617, "W", -44, 2000, 0, 0)
+ LOC("d", 42, 21, 28.764, "N", 71, 0, 51.617, "W", -44, 2000, 0, 0),
);
-
```
{% endcode %}
diff --git a/documentation/functions/domain/LOC_BUILDER_DD.md b/documentation/language-reference/domain-modifiers/LOC_BUILDER_DD.md
similarity index 58%
rename from documentation/functions/domain/LOC_BUILDER_DD.md
rename to documentation/language-reference/domain-modifiers/LOC_BUILDER_DD.md
index 619ba675e9..513c09c6ff 100644
--- a/documentation/functions/domain/LOC_BUILDER_DD.md
+++ b/documentation/language-reference/domain-modifiers/LOC_BUILDER_DD.md
@@ -23,9 +23,9 @@ parameter_types:
- alt (float32, optional)
- ttl (optional)
-A helper to build [`LOC`](../domain/LOC.md) records. Supply four parameters instead of 12.
+A helper to build [`LOC`](LOC.md) records. Supply four parameters instead of 12.
-Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
+Internally assumes some defaults for [`LOC`](LOC.md) records.
The cartesian coordinates are decimal degrees, like you typically find in e.g. Google Maps.
@@ -42,34 +42,32 @@ The White House:
{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- LOC_BUILDER_DD({
+ LOC_BUILDER_DD({
label: "big-ben",
x: 51.50084265331501,
y: -0.12462541415599787,
alt: 6,
- })
- , LOC_BUILDER_DD({
+ }),
+ LOC_BUILDER_DD({
label: "white-house",
x: 38.89775977858357,
y: -77.03655125982903,
alt: 19,
- })
- , LOC_BUILDER_DD({
+ }),
+ LOC_BUILDER_DD({
label: "white-house-ttl",
x: 38.89775977858357,
y: -77.03655125982903,
alt: 19,
ttl: "5m",
- })
+ }),
);
-
```
{% endcode %}
-
Part of the series:
- * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
diff --git a/documentation/functions/domain/LOC_BUILDER_DMM_STR.md b/documentation/language-reference/domain-modifiers/LOC_BUILDER_DMM_STR.md
similarity index 55%
rename from documentation/functions/domain/LOC_BUILDER_DMM_STR.md
rename to documentation/language-reference/domain-modifiers/LOC_BUILDER_DMM_STR.md
index 99e71bcd68..3be208bc53 100644
--- a/documentation/functions/domain/LOC_BUILDER_DMM_STR.md
+++ b/documentation/language-reference/domain-modifiers/LOC_BUILDER_DMM_STR.md
@@ -20,9 +20,9 @@ parameter_types:
- alt (float32, optional)
- ttl (optional)
-A helper to build [`LOC`](../domain/LOC.md) records. Supply three parameters instead of 12.
+A helper to build [`LOC`](LOC.md) records. Supply three parameters instead of 12.
-Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
+Internally assumes some defaults for [`LOC`](LOC.md) records.
Accepts a string with decimal minutes (DMM) coordinates in the form: 25.24°S 153.15°E
@@ -40,16 +40,14 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
label: "tasmania",
str: "42°S 147°E",
alt: 3,
- })
+ }),
);
-
```
{% endcode %}
-
Part of the series:
- * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
diff --git a/documentation/functions/domain/LOC_BUILDER_DMS_STR.md b/documentation/language-reference/domain-modifiers/LOC_BUILDER_DMS_STR.md
similarity index 58%
rename from documentation/functions/domain/LOC_BUILDER_DMS_STR.md
rename to documentation/language-reference/domain-modifiers/LOC_BUILDER_DMS_STR.md
index 8a772bc5f3..208242c64d 100644
--- a/documentation/functions/domain/LOC_BUILDER_DMS_STR.md
+++ b/documentation/language-reference/domain-modifiers/LOC_BUILDER_DMS_STR.md
@@ -20,9 +20,9 @@ parameter_types:
- alt (float32, optional)
- ttl (optional)
-A helper to build [`LOC`](../domain/LOC.md) records. Supply three parameters instead of 12.
+A helper to build [`LOC`](LOC.md) records. Supply three parameters instead of 12.
-Internally assumes some defaults for [`LOC`](../domain/LOC.md) records.
+Internally assumes some defaults for [`LOC`](LOC.md) records.
Accepts a string with degrees, minutes, and seconds (DMS) coordinates in the form: 41°24'12.2"N 2°10'26.5"E
@@ -41,16 +41,14 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
str: "33°51â˛31âŗS 151°12â˛51âŗE",
alt: 4,
ttl: "5m",
- })
+ }),
);
-
```
{% endcode %}
-
Part of the series:
- * [`LOC()`](../domain/LOC.md) - build a `LOC` by supplying all 12 parameters
- * [`LOC_BUILDER_DD({})`](../record/LOC_BUILDER_DD.md) - accepts cartesian x, y
- * [`LOC_BUILDER_DMS_STR({})`](../record/LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
- * [`LOC_BUILDER_DMM_STR({})`](../record/LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
- * [`LOC_BUILDER_STR({})`](../record/LOC_BUILDER_STR.md) - tries the cooordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
+ * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
diff --git a/documentation/language-reference/domain-modifiers/LOC_BUILDER_STR.md b/documentation/language-reference/domain-modifiers/LOC_BUILDER_STR.md
new file mode 100644
index 0000000000..c7058c74e7
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/LOC_BUILDER_STR.md
@@ -0,0 +1,60 @@
+---
+name: LOC_BUILDER_STR
+parameters:
+ - label
+ - str
+ - alt
+ - ttl
+parameters_object: true
+parameter_types:
+ label: string?
+ str: string
+ alt: number?
+ ttl: Duration?
+---
+
+`LOC_BUILDER_STR({})` actually takes an object with the following: properties.
+
+ - label (optional, defaults to `@`)
+ - str (string)
+ - alt (float32, optional)
+ - ttl (optional)
+
+A helper to build [`LOC`](LOC.md) records. Supply three parameters instead of 12.
+
+Internally assumes some defaults for [`LOC`](LOC.md) records.
+
+
+Accepts a string and tries all `LOC_BUILDER_DM*_STR({})` methods:
+ * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ LOC_BUILDER_STR({
+ label: "old-faithful",
+ str: "44.46046°N 110.82815°W",
+ alt: 2240,
+ }),
+ LOC_BUILDER_STR({
+ label: "ribblehead-viaduct",
+ str: "54.210436°N 2.370231°W",
+ alt: 300,
+ }),
+ LOC_BUILDER_STR({
+ label: "guinness-brewery",
+ str: "53°20â˛40âŗN 6°17â˛20âŗW",
+ alt: 300,
+ }),
+);
+```
+{% endcode %}
+
+Part of the series:
+ * [`LOC()`](LOC.md) - build a `LOC` by supplying all 12 parameters
+ * [`LOC_BUILDER_DD({})`](LOC_BUILDER_DD.md) - accepts cartesian x, y
+ * [`LOC_BUILDER_DMS_STR({})`](LOC_BUILDER_DMS_STR.md) - accepts DMS 33°51â˛31âŗS 151°12â˛51âŗE
+ * [`LOC_BUILDER_DMM_STR({})`](LOC_BUILDER_DMM_STR.md) - accepts DMM 25.24°S 153.15°E
+ * [`LOC_BUILDER_STR({})`](LOC_BUILDER_STR.md) - tries the coordinate string in all `LOC_BUILDER_DM*_STR()` functions until one works
diff --git a/documentation/functions/domain/M365_BUILDER.md b/documentation/language-reference/domain-modifiers/M365_BUILDER.md
similarity index 70%
rename from documentation/functions/domain/M365_BUILDER.md
rename to documentation/language-reference/domain-modifiers/M365_BUILDER.md
index dd58e48ab0..bddf611843 100644
--- a/documentation/functions/domain/M365_BUILDER.md
+++ b/documentation/language-reference/domain-modifiers/M365_BUILDER.md
@@ -24,7 +24,7 @@ parameter_types:
DNSControl offers a `M365_BUILDER` which can be used to simply set up Microsoft 365 for a domain in an opinionated way.
It defaults to a setup without support for legacy Skype for Business applications.
-It doesn't set up SPF or DMARC. See [`SPF_BUILDER`](/language-reference/record-modifiers/dmarc_builder) and [`DMARC_BUILDER`](/language-reference/record-modifiers/spf_builder).
+It doesn't set up SPF or DMARC. See [`SPF_BUILDER`](SPF_BUILDER.md) and [`DMARC_BUILDER`](DMARC_BUILDER.md).
## Example
@@ -32,9 +32,11 @@ It doesn't set up SPF or DMARC. See [`SPF_BUILDER`](/language-reference/record-m
{% code title="dnsconfig.js" %}
```javascript
-M365_BUILDER({
- initialDomain: "example.onmicrosoft.com",
-});
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ M365_BUILDER("example.com", {
+ initialDomain: "example.onmicrosoft.com",
+ }),
+);
```
{% endcode %}
@@ -44,15 +46,17 @@ This sets up `MX` records, Autodiscover, and DKIM.
{% code title="dnsconfig.js" %}
```javascript
-M365_BUILDER({
- label: "test",
- mx: false,
- autodiscover: false,
- dkim: false,
- mdm: true,
- domainGUID: "test-example-com", // Can be automatically derived in this case, if example.com is the context.
- initialDomain: "example.onmicrosoft.com",
-});
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ M365_BUILDER("example.com", {
+ label: "test",
+ mx: false,
+ autodiscover: false,
+ dkim: false,
+ mdm: true,
+ domainGUID: "test-example-com", // Can be automatically derived in this case, if example.com is the context.
+ initialDomain: "example.onmicrosoft.com",
+ }),
+);
```
{% endcode %}
diff --git a/documentation/functions/domain/MX.md b/documentation/language-reference/domain-modifiers/MX.md
similarity index 95%
rename from documentation/functions/domain/MX.md
rename to documentation/language-reference/domain-modifiers/MX.md
index e5ba4b27d5..67adcada40 100644
--- a/documentation/functions/domain/MX.md
+++ b/documentation/language-reference/domain-modifiers/MX.md
@@ -22,7 +22,7 @@ Target should be a string representing the MX target. If it is a single label we
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
MX("@", 5, "mail"), // mx example.com -> mail.example.com
- MX("sub", 10, "mail.foo.com.")
+ MX("sub", 10, "mail.foo.com."),
);
```
{% endcode %}
diff --git a/documentation/functions/domain/NAMESERVER.md b/documentation/language-reference/domain-modifiers/NAMESERVER.md
similarity index 88%
rename from documentation/functions/domain/NAMESERVER.md
rename to documentation/language-reference/domain-modifiers/NAMESERVER.md
index d404ec86af..5538766db0 100644
--- a/documentation/functions/domain/NAMESERVER.md
+++ b/documentation/language-reference/domain-modifiers/NAMESERVER.md
@@ -8,21 +8,22 @@ parameter_types:
"modifiers...": RecordModifier[]
---
-`NAMESERVER()` instructs DNSControl to inform the domain"s registrar where to find this zone.
+`NAMESERVER()` instructs DNSControl to inform the domain's registrar where to find this zone.
For some registrars this will also add NS records to the zone itself.
This takes exactly one argument: the name of the nameserver. It must end with
a "." if it is a FQDN, just like all targets.
This is different than the [`NS()`](NS.md) function, which inserts NS records
-in the current zone and accepts a label. [`NS()`](NS.md) is useful for downward
+in the current zone and accepts a label. [`NS()`](NS.md) is for downward
delegations. `NAMESERVER()` is for informing upstream delegations.
For more information, refer to [this page](../../nameservers.md).
{% code title="dnsconfig.js" %}
```javascript
-D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+D("example.com", REG_MY_PROVIDER,
+ DnsProvider(DSP_MY_PROVIDER),
DnsProvider(route53, 0),
// Replace the nameservers:
NAMESERVER("ns1.myserver.com."),
@@ -43,7 +44,7 @@ D("example2.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
Nameservers are one of the least
understood parts of DNS, so a little extra explanation is required.
-* [`NS()`](NS.md) lets you add an NS record to a zone, just like [`A()`](A.md) adds an A
+* [`NS()`](NS.md) adds an NS record to a zone, just like [`A()`](A.md) adds an A
record to the zone. This is generally used to delegate a subzone.
* The `NAMESERVER()` directive speaks to the Registrar about how the parent should delegate the zone.
@@ -71,7 +72,7 @@ manually.
If dnsconfig.js has zero `NAMESERVER()` commands for a domain, it will
use the API to remove all non-default nameservers.
-If dnsconfig.js has 1 or more `NAMESERVER()` commands for a domain, it
+If `dnsconfig.js` has 1 or more `NAMESERVER()` commands for a domain, it
will use the API to add those nameservers (unless, of course,
they already exist).
@@ -85,6 +86,6 @@ It looks like this:
var REG_THIRDPARTY = NewRegistrar("ThirdParty");
D("example.com", REG_THIRDPARTY,
...
-)
+);
```
{% endcode %}
diff --git a/documentation/functions/domain/NAMESERVER_TTL.md b/documentation/language-reference/domain-modifiers/NAMESERVER_TTL.md
similarity index 79%
rename from documentation/functions/domain/NAMESERVER_TTL.md
rename to documentation/language-reference/domain-modifiers/NAMESERVER_TTL.md
index f2d40cdb8c..54a3094472 100644
--- a/documentation/functions/domain/NAMESERVER_TTL.md
+++ b/documentation/language-reference/domain-modifiers/NAMESERVER_TTL.md
@@ -10,13 +10,13 @@ parameter_types:
NAMESERVER_TTL sets the TTL on the domain apex NS RRs defined by [`NAMESERVER`](NAMESERVER.md).
-The value can be an integer or a string. See [`TTL`](../record/TTL.md) for examples.
+The value can be an integer or a string. See [`TTL`](../record-modifiers/TTL.md) for examples.
{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
NAMESERVER_TTL("2d"),
- NAMESERVER("ns")
+ NAMESERVER("ns"),
);
```
{% endcode %}
@@ -31,9 +31,9 @@ D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
NAMESERVER("ns1.provider.com."), //inherits NAMESERVER_TTL
NAMESERVER("ns2.provider.com."), //inherits NAMESERVER_TTL
A("@","1.2.3.4"), // inherits DefaultTTL
- A("foo", "2.3.4.5", TTL(600)) // overrides DefaultTTL for this record only
+ A("foo", "2.3.4.5", TTL(600)), // overrides DefaultTTL for this record only
);
```
{% endcode %}
-To apply a default TTL to all other record types, see [`DefaultTTL`](../domain/DefaultTTL.md)
+To apply a default TTL to all other record types, see [`DefaultTTL`](../domain-modifiers/DefaultTTL.md)
diff --git a/documentation/functions/domain/NAPTR.md b/documentation/language-reference/domain-modifiers/NAPTR.md
similarity index 99%
rename from documentation/functions/domain/NAPTR.md
rename to documentation/language-reference/domain-modifiers/NAPTR.md
index afb8473938..739c86e1a9 100644
--- a/documentation/functions/domain/NAPTR.md
+++ b/documentation/language-reference/domain-modifiers/NAPTR.md
@@ -166,7 +166,7 @@ D("3.2.1.5.5.5.0.0.8.1.e164.arpa.", REG_MY_PROVIDER, DnsProvider(R53),
NAPTR("7", 10, 10, "u", "E2U+SIP", "!^.*$!sip:jane@example.com!", "."),
NAPTR("8", 10, 10, "u", "E2U+SIP", "!^.*$!sip:mike@example.com!", "."),
NAPTR("9", 10, 10, "u", "E2U+SIP", "!^.*$!sip:linda@example.com!", "."),
- NAPTR("0", 10, 10, "u", "E2U+SIP", "!^.*$!sip:fax@example.com!", ".")
+ NAPTR("0", 10, 10, "u", "E2U+SIP", "!^.*$!sip:fax@example.com!", "."),
);
```
{% endcode %}
@@ -177,7 +177,7 @@ Single e164 zone
D("4.3.2.1.5.5.5.0.0.8.1.e164.arpa.", REG_MY_PROVIDER, DnsProvider(R53),
NAPTR("@", 100, 50, "u", "E2U+SIP", "!^.*$!sip:customer-service@example.com!", "."),
NAPTR("@", 101, 50, "u", "E2U+email", "!^.*$!mailto:information@example.com!", "."),
- NAPTR("@", 101, 50, "u", "smtp+E2U", "!^.*$!mailto:information@example.com!", ".")
+ NAPTR("@", 101, 50, "u", "smtp+E2U", "!^.*$!mailto:information@example.com!", "."),
);
```
{% endcode %}
diff --git a/documentation/functions/domain/NO_PURGE.md b/documentation/language-reference/domain-modifiers/NO_PURGE.md
similarity index 85%
rename from documentation/functions/domain/NO_PURGE.md
rename to documentation/language-reference/domain-modifiers/NO_PURGE.md
index b4b2a1795f..664a29da3b 100644
--- a/documentation/functions/domain/NO_PURGE.md
+++ b/documentation/language-reference/domain-modifiers/NO_PURGE.md
@@ -12,7 +12,7 @@ other system.
By setting `NO_PURGE` on a domain, this tells DNSControl not to delete the
records found in the domain.
-It is similar to [`IGNORE`](domain/IGNORE.md) but more general.
+It is similar to [`IGNORE`](IGNORE.md) but more general.
The original reason for `NO_PURGE` was that a legacy system was adopting
DNSControl. Previously the domain was managed via Microsoft DNS Server's GUI.
@@ -28,14 +28,14 @@ in place.
{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), NO_PURGE,
- A("foo","1.2.3.4")
+ A("foo","1.2.3.4"),
);
```
{% endcode %}
The main caveat of `NO_PURGE` is that intentionally deleting records becomes
more difficult. Suppose a `NO_PURGE` zone has an record such as A("ken",
-"1.2.3.4"). Removing the record from dnsconfig.js will not delete "ken" from
+"1.2.3.4"). Removing the record from `dnsconfig.js` will not delete "ken" from
the domain. DNSControl has no way of knowing the record was deleted from the
file The DNS record must be removed manually. Users of `NO_PURGE` are prone
to finding themselves with an accumulation of orphaned DNS records. That's easy
@@ -50,5 +50,5 @@ With introduction of `diff2` algorithm (enabled by default in v4.0.0),
## See also
-* [`PURGE`](domain/PURGE.md) is the default, thus this command is a no-op
-* [`IGNORE`](domain/IGNORE.md) is similar to `NO_PURGE` but is more selective
+* [`PURGE`](PURGE.md) is the default, thus this command is a no-op
+* [`IGNORE`](IGNORE.md) is similar to `NO_PURGE` but is more selective
diff --git a/documentation/functions/domain/NS.md b/documentation/language-reference/domain-modifiers/NS.md
similarity index 100%
rename from documentation/functions/domain/NS.md
rename to documentation/language-reference/domain-modifiers/NS.md
diff --git a/documentation/language-reference/domain-modifiers/NS1_URLFWD.md b/documentation/language-reference/domain-modifiers/NS1_URLFWD.md
new file mode 100644
index 0000000000..626a009383
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/NS1_URLFWD.md
@@ -0,0 +1,38 @@
+---
+name: NS1_URLFWD
+parameters:
+ - name
+ - target
+ - modifiers...
+provider: NS1
+parameter_types:
+ name: string
+ target: string
+ "modifiers...": RecordModifier[]
+---
+
+`NS1_URLFWD` is an NS1-specific feature that maps to NS1's URLFWD record, which creates HTTP 301 (permanent) or 302 (temporary) redirects.
+
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ NS1_URLFWD("urlfwd", "/ http://example.com 302 2 0")
+);
+```
+{% endcode %}
+
+The fields are:
+* name: the record name
+* target: a complex field containing the following, space separated:
+ * from - the path to match
+ * to - the url to redirect to
+ * redirectType - (0 - masking, 301, 302)
+ * pathForwardingMode - (0 - All, 1 - Capture, 2 - None)
+ * queryForwardingMode - (0 - disabled, 1 - enabled)
+
+{% hint style="warning" %}
+**WARNING**: According to NS1, this type of record is deprecated and in the process
+of being replaced by the premium-only `REDIRECT` record type. While still able to be
+configured through the API, as suggested by NS1, please try not to use it, going forward.
+{% endhint %}
diff --git a/documentation/language-reference/domain-modifiers/PORKBUN_URLFWD.md b/documentation/language-reference/domain-modifiers/PORKBUN_URLFWD.md
new file mode 100644
index 0000000000..85ab491d6b
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/PORKBUN_URLFWD.md
@@ -0,0 +1,31 @@
+---
+name: PORKBUN_URLFWD
+parameters:
+ - name
+ - target
+ - modifiers...
+provider: PORKBUN
+parameter_types:
+ name: string
+ target: string
+ "modifiers...": RecordModifier[]
+---
+
+`PORKBUN_URLFWD` is a Porkbun-specific feature that maps to Porkbun's URL forwarding feature, which creates HTTP 301 (permanent) or 302 (temporary) redirects.
+
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ PORKBUN_URLFWD("urlfwd1", "http://example.com"),
+ PORKBUN_URLFWD("urlfwd2", "http://example.org", {type: "permanent", includePath: "yes", wildcard: "no"})
+);
+```
+{% endcode %}
+
+The fields are:
+* name: the record name
+* target: where you'd like to forward the domain to
+* type: valid types are: `temporary` (302 / 307) or `permanent` (301), default to `temporary`
+* includePath: whether to include the URI path in the redirection. Valid options are `yes` or `no`, default to `no`
+* wildcard: forward all subdomains of the domain. Valid options are `yes` or `no`, default to `yes`
diff --git a/documentation/functions/domain/PTR.md b/documentation/language-reference/domain-modifiers/PTR.md
similarity index 78%
rename from documentation/functions/domain/PTR.md
rename to documentation/language-reference/domain-modifiers/PTR.md
index 889920310f..ec51a43a5c 100644
--- a/documentation/functions/domain/PTR.md
+++ b/documentation/language-reference/domain-modifiers/PTR.md
@@ -17,7 +17,7 @@ saving the user from having to reverse the IP address manually.
Target should be a string representing the FQDN of a host. Like all FQDNs in DNSControl, it must end with a `.`.
-**Magic Mode:**
+# Magic Mode
PTR records are complex and typos are common. Therefore DNSControl
enables features to save labor and
@@ -48,12 +48,12 @@ then the name will be replaced with `4.3`. Note that the output
of `REV("1.2.3.4")` is `4.3.2.1.in-addr.arpa.`, which means the following
are all equivalent:
-* `PTR(REV("1.2.3.4"), `
-* `PTR("4.3.2.1.in-addr.arpa."), `
-* `PTR("4.3",` // Assuming the domain is `2.1.in-addr.arpa`
+* `PTR(REV("1.2.3.4", ...`
+* `PTR("4.3.2.1.in-addr.arpa.", ...`
+* `PTR("4.3", ...` // Assuming the domain is `2.1.in-addr.arpa`
All magic is RFC2317-aware. We use the first format listed in the
-RFC for both [`REV()`](../global/REV.md) and `PTR()`. The format is
+RFC for both [`REV()`](../top-level-functions/REV.md) and `PTR()`. The format is
`FIRST/MASK.C.B.A.in-addr.arpa` where `FIRST` is the first IP address
of the zone, `MASK` is the netmask of the zone (25-31 inclusive),
and A, B, C are the first 3 octets of the IP address. For example
@@ -91,6 +91,32 @@ D(REV("2001:db8:302::/48"), REGISTRAR, DnsProvider(BIND),
```
{% endcode %}
-In the future we plan on adding a flag to [`A()`](A.md) which will insert
-the correct PTR() record if the appropriate `.arpa` domain has been
-defined.
+# Automatic forward and reverse lookups
+
+DNSControl does not automatically generate forward and reverse lookups. However
+it is possible to write a macro that does this by using the
+[`D_EXTEND()`](../global/D_EXTEND.md)
+function to insert `A` and `PTR` records into previously-defined domains.
+
+{% code title="dnsconfig.js" %}
+```javascript
+function FORWARD_AND_REVERSE(ipaddr, fqdn) {
+ D_EXTEND(dom,
+ A(fqdn, ipaddr)
+ );
+ D_EXTEND(REV(ipaddr),
+ PTR(ipaddr, fqdn)
+ );
+}
+
+D("example.com", REGISTRAR, DnsProvider(DSP_NONE),
+ ...,
+);
+D(REV("10.20.30.0/24"), REGISTRAR, DnsProvider(DSP_NONE),
+ ...,
+);
+
+FORWARD_AND_REVERSE("10.20.30.77", "foo.example.com.");
+FORWARD_AND_REVERSE("10.20.30.99", "bar.example.com.");
+```
+{% endcode %}
diff --git a/documentation/functions/domain/PURGE.md b/documentation/language-reference/domain-modifiers/PURGE.md
similarity index 100%
rename from documentation/functions/domain/PURGE.md
rename to documentation/language-reference/domain-modifiers/PURGE.md
diff --git a/documentation/functions/domain/R53_ALIAS.md b/documentation/language-reference/domain-modifiers/R53_ALIAS.md
similarity index 91%
rename from documentation/functions/domain/R53_ALIAS.md
rename to documentation/language-reference/domain-modifiers/R53_ALIAS.md
index 3754a22c39..c7f4b0d0c0 100644
--- a/documentation/functions/domain/R53_ALIAS.md
+++ b/documentation/language-reference/domain-modifiers/R53_ALIAS.md
@@ -4,10 +4,12 @@ parameters:
- name
- target
- ZONE_ID modifier
+ - EvaluateTargetHealth modifier
parameter_types:
name: string
target: string
ZONE_ID modifier: DomainModifier & RecordModifier
+ EvaluateTargetHealth modifier: RecordModifier
provider: ROUTE53
---
@@ -27,7 +29,7 @@ The Target can be any of:
* _S3 bucket_ (configured as website): specify the domain name of the Amazon S3 website endpoint in which you configured the bucket (for instance s3-website-us-east-2.amazonaws.com). For the available values refer to the [Amazon S3 Website Endpoints](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region).
* _Another Route53 record_: specify the value of the name of another record in the same hosted zone.
-For all the target type, excluding 'another record', you have to specify the `Zone ID` of the target. This is done by using the [`R53_ZONE`](../record/R53_ZONE.md) record modifier.
+For all the target type, excluding 'another record', you have to specify the `Zone ID` of the target. This is done by using the [`R53_ZONE`](../record-modifiers/R53_ZONE.md) record modifier.
The zone id can be found depending on the target type:
@@ -37,12 +39,14 @@ The zone id can be found depending on the target type:
* _S3 bucket_ (configured as website): specify the hosted zone ID for the region that you created the bucket in. You can find it in [the List of regions and hosted Zone IDs](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region)
* _Another Route 53 record_: you can either specify the correct zone id or do not specify anything and DNSControl will figure out the right zone id. (Note: Route53 alias can't reference a record in a different zone).
+Target health evaluation can be enabled with the [`R53_EVALUATE_TARGET_HEALTH`](../record-modifiers/R53\_EVALUATE\_TARGET\_HEALTH.md) record modifier.
+
{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider("ROUTE53"),
R53_ALIAS("foo", "A", "bar"), // record in same zone
R53_ALIAS("foo", "A", "bar", R53_ZONE("Z35SXDOTRQ7X7K")), // record in same zone, zone specified
- R53_ALIAS("foo", "A", "blahblah.elasticloadbalancing.us-west-1.amazonaws.com.", R53_ZONE("Z368ELLRRE2KJ0")), // a classic ELB in us-west-1
+ R53_ALIAS("foo", "A", "blahblah.elasticloadbalancing.us-west-1.amazonaws.com.", R53_ZONE("Z368ELLRRE2KJ0"), R53_EVALUATE_TARGET_HEALTH(true)), // a classic ELB in us-west-1 with target health evaluation enabled
R53_ALIAS("foo", "A", "blahblah.elasticbeanstalk.us-west-2.amazonaws.com.", R53_ZONE("Z38NKT9BP95V3O")), // an Elastic Beanstalk environment in us-west-2
R53_ALIAS("foo", "A", "blahblah-bucket.s3-website-us-west-1.amazonaws.com.", R53_ZONE("Z2F56UZL2M1ACD")), // a website S3 Bucket in us-west-1
);
diff --git a/documentation/functions/domain/SOA.md b/documentation/language-reference/domain-modifiers/SOA.md
similarity index 96%
rename from documentation/functions/domain/SOA.md
rename to documentation/language-reference/domain-modifiers/SOA.md
index e14f1b4e06..649bbdf324 100644
--- a/documentation/functions/domain/SOA.md
+++ b/documentation/language-reference/domain-modifiers/SOA.md
@@ -39,4 +39,4 @@ when you are making it easier for spammers how to find you.
* Most providers automatically generate SOA records. They will ignore any `SOA()` statements.
* The mbox field should not be set to a real email address unless you love spam and hate your privacy.
-There is more info about `SOA` in the documentation for the [BIND provider](../../providers/bind.md).
+There is more info about `SOA` in the documentation for the [BIND provider](../../provider/bind.md).
diff --git a/documentation/functions/domain/SPF_BUILDER.md b/documentation/language-reference/domain-modifiers/SPF_BUILDER.md
similarity index 99%
rename from documentation/functions/domain/SPF_BUILDER.md
rename to documentation/language-reference/domain-modifiers/SPF_BUILDER.md
index 2d3ba98787..daea3a41dd 100644
--- a/documentation/functions/domain/SPF_BUILDER.md
+++ b/documentation/language-reference/domain-modifiers/SPF_BUILDER.md
@@ -40,8 +40,8 @@ Here is an example of how SPF settings are normally done:
{% code title="dnsconfig.js" %}
```javascript
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- TXT("v=spf1 ip4:198.252.206.0/24 ip4:192.111.0.0/24 include:_spf.google.com include:mailgun.org include:spf-basic.fogcreek.com include:mail.zendesk.com include:servers.mcsv.net include:sendgrid.net include:450622.spf05.hubspotemail.net ~all")
-)
+ TXT("v=spf1 ip4:198.252.206.0/24 ip4:192.111.0.0/24 include:_spf.google.com include:mailgun.org include:spf-basic.fogcreek.com include:mail.zendesk.com include:servers.mcsv.net include:sendgrid.net include:450622.spf05.hubspotemail.net ~all"),
+);
```
{% endcode %}
@@ -306,11 +306,11 @@ var SPF_MYSETTINGS = SPF_BUILDER({
});
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- SPF_MYSETTINGS
+ SPF_MYSETTINGS,
);
D("example2.tld", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- SPF_MYSETTINGS
+ SPF_MYSETTINGS,
);
```
{% endcode %}
diff --git a/documentation/functions/domain/SRV.md b/documentation/language-reference/domain-modifiers/SRV.md
similarity index 100%
rename from documentation/functions/domain/SRV.md
rename to documentation/language-reference/domain-modifiers/SRV.md
diff --git a/documentation/functions/domain/SSHFP.md b/documentation/language-reference/domain-modifiers/SSHFP.md
similarity index 86%
rename from documentation/functions/domain/SSHFP.md
rename to documentation/language-reference/domain-modifiers/SSHFP.md
index 2774c9f423..6d10226290 100644
--- a/documentation/functions/domain/SSHFP.md
+++ b/documentation/language-reference/domain-modifiers/SSHFP.md
@@ -38,6 +38,8 @@ parameter_types:
{% code title="dnsconfig.js" %}
```javascript
-SSHFP("@", 1, 1, "00yourAmazingFingerprint00"),
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ SSHFP("@", 1, 1, "00yourAmazingFingerprint00"),
+);
```
{% endcode %}
diff --git a/documentation/language-reference/domain-modifiers/SVCB.md b/documentation/language-reference/domain-modifiers/SVCB.md
new file mode 100644
index 0000000000..1aef189ba9
--- /dev/null
+++ b/documentation/language-reference/domain-modifiers/SVCB.md
@@ -0,0 +1,31 @@
+---
+name: SVCB
+parameters:
+ - name
+ - priority
+ - target
+ - params
+ - modifiers...
+parameter_types:
+ name: string
+ priority: number
+ target: string
+ params: string
+ "modifiers...": RecordModifier[]
+---
+
+SVCB adds an SVCB record to a domain. The name should be the relative label for the record. Use `@` for the domain apex.
+
+The priority must be a positive number, the address should be an ip address, either a string, or a numeric value obtained via [IP](../top-level-functions/IP.md).
+
+The params may be configured to specify the `alpn`, `ipv4hint`, `ipv6hint`, `ech` or `port` setting. Several params may be joined by a space. Not existing params may be specified as an empty string `""`
+
+Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/language-reference/record-modifiers) or JSON objects, which will be merged into the record's metadata.
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
+ SVCB("@", 1, ".", "ipv4hint=123.123.123.123 alpn=h3,h2 port=443"),
+);
+```
+{% endcode %}
diff --git a/documentation/functions/domain/TLSA.md b/documentation/language-reference/domain-modifiers/TLSA.md
similarity index 100%
rename from documentation/functions/domain/TLSA.md
rename to documentation/language-reference/domain-modifiers/TLSA.md
diff --git a/documentation/functions/domain/TXT.md b/documentation/language-reference/domain-modifiers/TXT.md
similarity index 96%
rename from documentation/functions/domain/TXT.md
rename to documentation/language-reference/domain-modifiers/TXT.md
index 67b18dc5b0..0b2edb70bd 100644
--- a/documentation/functions/domain/TXT.md
+++ b/documentation/language-reference/domain-modifiers/TXT.md
@@ -30,7 +30,7 @@ Modifiers can be any number of [record modifiers](https://docs.dnscontrol.org/la
TXT("multiple", ["one", "two", "three"]), // Multiple strings
TXT("quoted", "any "quotes" and escapes? ugh; no worries!"),
TXT("_domainkey", "t=y; o=-;"), // Escapes are done for you automatically.
- TXT("long", "X".repeat(300)) // Long strings are split automatically.
+ TXT("long", "X".repeat(300)), // Long strings are split automatically.
);
```
{% endcode %}
@@ -87,7 +87,7 @@ double quotes, back-ticks, or other chars.
#### How can you tell if a provider will support a particular `TXT()` record?
-Include the `TXT()` record in a [`D()`](../global/D.md) as usual, along
+Include the `TXT()` record in a [`D()`](../top-level-functions/D.md) as usual, along
with the `DnsProvider()` for that provider. Run `dnscontrol check` to
see if any errors are produced. The check command does not talk to
the provider's API, thus permitting you to do this without having an
diff --git a/documentation/functions/domain/URL.md b/documentation/language-reference/domain-modifiers/URL.md
similarity index 100%
rename from documentation/functions/domain/URL.md
rename to documentation/language-reference/domain-modifiers/URL.md
diff --git a/documentation/functions/domain/URL301.md b/documentation/language-reference/domain-modifiers/URL301.md
similarity index 100%
rename from documentation/functions/domain/URL301.md
rename to documentation/language-reference/domain-modifiers/URL301.md
diff --git a/documentation/language-reference/record-modifiers/R53_EVALUATE_TARGET_HEALTH.md b/documentation/language-reference/record-modifiers/R53_EVALUATE_TARGET_HEALTH.md
new file mode 100644
index 0000000000..95f28e01a0
--- /dev/null
+++ b/documentation/language-reference/record-modifiers/R53_EVALUATE_TARGET_HEALTH.md
@@ -0,0 +1,11 @@
+---
+name: R53_EVALUATE_TARGET_HEALTH
+parameters:
+ - enabled
+parameter_types:
+ enabled: boolean
+ts_return: RecordModifier
+provider: ROUTE53
+---
+
+`R53_EVALUATE_TARGET_HEALTH` lets you enable target health evaluation for a [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) record. Omitting `R53_EVALUATE_TARGET_HEALTH()` from `R53_ALIAS()` set the behavior to false.
diff --git a/documentation/language-reference/record-modifiers/R53_ZONE.md b/documentation/language-reference/record-modifiers/R53_ZONE.md
new file mode 100644
index 0000000000..482783913b
--- /dev/null
+++ b/documentation/language-reference/record-modifiers/R53_ZONE.md
@@ -0,0 +1,15 @@
+---
+name: R53_ZONE
+parameters:
+ - zone_id
+parameter_types:
+ zone_id: string
+ts_return: DomainModifier & RecordModifier
+provider: ROUTE53
+---
+
+`R53_ZONE` lets you specify the AWS Zone ID for an entire domain ([`D()`](../top-level-functions/D.md)) or a specific [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) record.
+
+When used with [`D()`](../top-level-functions/D.md), it sets the zone id of the domain. This can be used to differentiate between split horizon domains in public and private zones. See this [example](../../provider/route53.md#split-horizon) in the [Amazon Route 53 provider page](../../provider/route53.md).
+
+When used with [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) it sets the required Route53 hosted zone id in a R53_ALIAS record. See [`R53_ALIAS()`](../domain-modifiers/R53_ALIAS.md) documentation for details.
diff --git a/documentation/functions/record/TTL.md b/documentation/language-reference/record-modifiers/TTL.md
similarity index 93%
rename from documentation/functions/record/TTL.md
rename to documentation/language-reference/record-modifiers/TTL.md
index e51699bd20..a76806e0be 100644
--- a/documentation/functions/record/TTL.md
+++ b/documentation/language-reference/record-modifiers/TTL.md
@@ -7,7 +7,7 @@ parameter_types:
---
TTL sets the TTL for a single record only. This will take precedence
-over the domain's [DefaultTTL](../domain/DefaultTTL.md) if supplied.
+over the domain's [DefaultTTL](../domain-modifiers/DefaultTTL.md) if supplied.
The value can be:
diff --git a/documentation/functions/global/D.md b/documentation/language-reference/top-level-functions/D.md
similarity index 90%
rename from documentation/functions/global/D.md
rename to documentation/language-reference/top-level-functions/D.md
index 5df7a190ac..de6c0ccf80 100644
--- a/documentation/functions/global/D.md
+++ b/documentation/language-reference/top-level-functions/D.md
@@ -12,11 +12,11 @@ parameter_types:
`D` adds a new Domain for DNSControl to manage. The first two arguments are required: the domain name (fully qualified `example.com` without a trailing dot), and the
name of the registrar (as previously declared with [NewRegistrar](NewRegistrar.md)). Any number of additional arguments may be included to add DNS Providers with [DNSProvider](NewDnsProvider.md),
-add records with [A](../domain/A.md), [CNAME](../domain/CNAME.md), and so forth, or add metadata.
+add records with [A](../domain-modifiers/A.md), [CNAME](../domain-modifiers/CNAME.md), and so forth, or add metadata.
Modifier arguments are processed according to type as follows:
-- A function argument will be called with the domain object as it's only argument. Most of the [built-in modifier functions](https://docs.dnscontrol.org/language-reference/domain-modifiers) return such functions.
+- A function argument will be called with the domain object as it's only argument. Most of the [built-in modifier functions](https://docs.dnscontrol.org/language-reference/domain-modifiers-modifiers) return such functions.
- An object argument will be merged into the domain's metadata collection.
- An array argument will have all of it's members evaluated recursively. This allows you to combine multiple common records or modifiers into a variable that can
be used like a macro in multiple domains.
@@ -26,7 +26,7 @@ Modifier arguments are processed according to type as follows:
// simple domain
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
A("@","1.2.3.4"),
- CNAME("test", "foo.example2.com.")
+ CNAME("test", "foo.example2.com."),
);
// "macro" for records that can be mixed into any zone
@@ -41,12 +41,11 @@ var GOOGLE_APPS_DOMAIN_MX = [
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
A("@","1.2.3.4"),
CNAME("test", "foo.example2.com."),
- GOOGLE_APPS_DOMAIN_MX
+ GOOGLE_APPS_DOMAIN_MX,
);
```
{% endcode %}
-
# Split Horizon DNS
DNSControl supports Split Horizon DNS. Simply
@@ -64,15 +63,15 @@ var DNS_INSIDE = NewDnsProvider("Cloudflare");
var DNS_OUTSIDE = NewDnsProvider("bind");
D("example.com!inside", REG_THIRDPARTY, DnsProvider(DNS_INSIDE),
- A("www", "10.10.10.10")
+ A("www", "10.10.10.10"),
);
D("example.com!outside", REG_THIRDPARTY, DnsProvider(DNS_OUTSIDE),
- A("www", "20.20.20.20")
+ A("www", "20.20.20.20"),
);
D_EXTEND("example.com!inside",
- A("internal", "10.99.99.99")
+ A("internal", "10.99.99.99"),
);
```
{% endcode %}
diff --git a/documentation/functions/global/DEFAULTS.md b/documentation/language-reference/top-level-functions/DEFAULTS.md
similarity index 86%
rename from documentation/functions/global/DEFAULTS.md
rename to documentation/language-reference/top-level-functions/DEFAULTS.md
index aac34f6316..fb0eae9028 100644
--- a/documentation/functions/global/DEFAULTS.md
+++ b/documentation/language-reference/top-level-functions/DEFAULTS.md
@@ -11,7 +11,7 @@ arguments passed as if they were the first modifiers in the argument list.
## Example
-We want to create backup zone files for all domains, but not actually register them. Also create a [`DefaultTTL`](../domain/DefaultTTL.md).
+We want to create backup zone files for all domains, but not actually register them. Also create a [`DefaultTTL`](../domain-modifiers/DefaultTTL.md).
The domain `example.com` will have the defaults set.
{% code title="dnsconfig.js" %}
@@ -19,11 +19,11 @@ The domain `example.com` will have the defaults set.
var COMMON = NewDnsProvider("foo");
DEFAULTS(
DnsProvider(COMMON, 0),
- DefaultTTL("1d")
+ DefaultTTL("1d"),
);
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- A("@","1.2.3.4")
+ A("@","1.2.3.4"),
);
```
{% endcode %}
@@ -36,7 +36,7 @@ The domain `example2.com` will **not** have the defaults set.
DEFAULTS();
D("example2.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- A("@","1.2.3.4")
+ A("@","1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/functions/global/DOMAIN_ELSEWHERE.md b/documentation/language-reference/top-level-functions/DOMAIN_ELSEWHERE.md
similarity index 90%
rename from documentation/functions/global/DOMAIN_ELSEWHERE.md
rename to documentation/language-reference/top-level-functions/DOMAIN_ELSEWHERE.md
index 2f6d0ed4e5..5dca9adaff 100644
--- a/documentation/functions/global/DOMAIN_ELSEWHERE.md
+++ b/documentation/language-reference/top-level-functions/DOMAIN_ELSEWHERE.md
@@ -34,12 +34,12 @@ DOMAIN_ELSEWHERE("example.com", REG_MY_PROVIDER, ["ns1.foo.com", "ns2.foo.com"])
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
NO_PURGE,
NAMESERVER("ns1.foo.com"),
- NAMESERVER("ns2.foo.com")
+ NAMESERVER("ns2.foo.com"),
);
```
{% endcode %}
{% hint style="info" %}
-**NOTE**: The [`NO_PURGE`](../domain/NO_PURGE.md) is used out of abundance of caution but since no
+**NOTE**: The [`NO_PURGE`](../domain-modifiers/NO_PURGE.md) is used out of abundance of caution but since no
`DnsProvider()` statements exist, no updates would be performed.
{% endhint %}
diff --git a/documentation/functions/global/DOMAIN_ELSEWHERE_AUTO.md b/documentation/language-reference/top-level-functions/DOMAIN_ELSEWHERE_AUTO.md
similarity index 90%
rename from documentation/functions/global/DOMAIN_ELSEWHERE_AUTO.md
rename to documentation/language-reference/top-level-functions/DOMAIN_ELSEWHERE_AUTO.md
index 3c11a7756b..49e4a2ccf4 100644
--- a/documentation/functions/global/DOMAIN_ELSEWHERE_AUTO.md
+++ b/documentation/language-reference/top-level-functions/DOMAIN_ELSEWHERE_AUTO.md
@@ -38,11 +38,11 @@ DOMAIN_ELSEWHERE_AUTO("example.com", REG_NAMEDOTCOM, DSP_AZURE);
```javascript
D("example.com", REG_NAMEDOTCOM,
NO_PURGE,
- DnsProvider(DSP_AZURE)
+ DnsProvider(DSP_AZURE),
);
```
{% endcode %}
{% hint style="info" %}
-**NOTE**: The [`NO_PURGE`](../domain/NO_PURGE.md) is used to prevent DNSControl from changing the records.
+**NOTE**: The [`NO_PURGE`](../domain-modifiers/NO_PURGE.md) is used to prevent DNSControl from changing the records.
{% endhint %}
diff --git a/documentation/functions/global/D_EXTEND.md b/documentation/language-reference/top-level-functions/D_EXTEND.md
similarity index 86%
rename from documentation/functions/global/D_EXTEND.md
rename to documentation/language-reference/top-level-functions/D_EXTEND.md
index 1e4f5e8fab..86c2998a27 100644
--- a/documentation/functions/global/D_EXTEND.md
+++ b/documentation/language-reference/top-level-functions/D_EXTEND.md
@@ -29,7 +29,8 @@ defined as separate domains via separate [`D()`](D.md) statements, then
not `domain.tld`.
Some operators only act on an apex domain (e.g.
-[`CF_REDIRECT`](../domain/CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](../domain/CF_TEMP_REDIRECT.md)). Using them
+[`CF_SINGLE_REDIRECT`](../domain-modifiers/CF_SINGLE_REDIRECT.md),
+[`CF_REDIRECT`](../domain-modifiers/CF_REDIRECT.md), and [`CF_TEMP_REDIRECT`](../domain-modifiers/CF_TEMP_REDIRECT.md)). Using them
in a `D_EXTEND` subdomain may not be what you expect.
{% code title="dnsconfig.js" %}
@@ -37,24 +38,24 @@ in a `D_EXTEND` subdomain may not be what you expect.
D("domain.tld", REG_MY_PROVIDER, DnsProvider(DNS),
A("@", "127.0.0.1"), // domain.tld
A("www", "127.0.0.2"), // www.domain.tld
- CNAME("a", "b") // a.domain.tld -> b.domain.tld
+ CNAME("a", "b"), // a.domain.tld -> b.domain.tld
);
D_EXTEND("domain.tld",
A("aaa", "127.0.0.3"), // aaa.domain.tld
- CNAME("c", "d") // c.domain.tld -> d.domain.tld
+ CNAME("c", "d"), // c.domain.tld -> d.domain.tld
);
D_EXTEND("sub.domain.tld",
A("bbb", "127.0.0.4"), // bbb.sub.domain.tld
A("ccc", "127.0.0.5"), // ccc.sub.domain.tld
- CNAME("e", "f") // e.sub.domain.tld -> f.sub.domain.tld
+ CNAME("e", "f"), // e.sub.domain.tld -> f.sub.domain.tld
);
D_EXTEND("sub.sub.domain.tld",
A("ddd", "127.0.0.6"), // ddd.sub.sub.domain.tld
- CNAME("g", "h") // g.sub.sub.domain.tld -> h.sub.sub.domain.tld
+ CNAME("g", "h"), // g.sub.sub.domain.tld -> h.sub.sub.domain.tld
);
D_EXTEND("sub.domain.tld",
A("@", "127.0.0.7"), // sub.domain.tld
- CNAME("i", "j") // i.sub.domain.tld -> j.sub.domain.tld
+ CNAME("i", "j"), // i.sub.domain.tld -> j.sub.domain.tld
);
```
{% endcode %}
diff --git a/documentation/functions/global/FETCH.md b/documentation/language-reference/top-level-functions/FETCH.md
similarity index 95%
rename from documentation/functions/global/FETCH.md
rename to documentation/language-reference/top-level-functions/FETCH.md
index eab3e82ae7..bebd11ef5f 100644
--- a/documentation/functions/global/FETCH.md
+++ b/documentation/language-reference/top-level-functions/FETCH.md
@@ -22,9 +22,9 @@ Otherwise the syntax of `FETCH` is the same as `fetch`.
{% code title="dnsconfig.js" %}
```javascript
-D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), [
+D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
A("@", "1.2.3.4"),
-]);
+);
FETCH("https://example.com", {
// All three options below are optional
diff --git a/documentation/language-reference/top-level-functions/HASH.md b/documentation/language-reference/top-level-functions/HASH.md
new file mode 100644
index 0000000000..61d1fd91a1
--- /dev/null
+++ b/documentation/language-reference/top-level-functions/HASH.md
@@ -0,0 +1,36 @@
+---
+name: HASH
+parameters:
+ - algorithm
+ - value
+parameter_types:
+ algorithm: '"SHA1" | "SHA256" | "SHA512"'
+ value: string
+ts_return: string
+---
+
+`HASH` hashes `value` using the hashing algorithm given in `algorithm`
+(accepted values `SHA1`, `SHA256`, and `SHA512`) and returns the hex encoded
+hash value.
+
+example `HASH("SHA1", "abc")` returns `a9993e364706816aba3e25717850c26c9cd0d89d`.
+
+`HASH()`'s primary use case is for managing [catalog zones](https://datatracker.ietf.org/doc/html/rfc9432):
+
+> a method for automatic DNS zone provisioning among DNS primary and secondary name
+> servers by storing and transferring the catalog of zones to be provisioned as one
+> or more regular DNS zones.
+
+Here's an example of a catalog zone:
+
+{% code title="dnsconfig.js" %}
+```javascript
+foo_name_suffix = HASH("SHA1", "foo.name") + ".zones"
+D("catalog.example"
+ [...]
+ , TXT("version", "2")
+ , PTR(foo_name_suffix, "foo.name.")
+ , A("primaries.ext." + foo_name_suffix, "192.168.1.1")
+)
+```
+{% endcode %}
diff --git a/documentation/functions/global/IP.md b/documentation/language-reference/top-level-functions/IP.md
similarity index 100%
rename from documentation/functions/global/IP.md
rename to documentation/language-reference/top-level-functions/IP.md
diff --git a/documentation/functions/global/NewDnsProvider.md b/documentation/language-reference/top-level-functions/NewDnsProvider.md
similarity index 97%
rename from documentation/functions/global/NewDnsProvider.md
rename to documentation/language-reference/top-level-functions/NewDnsProvider.md
index 4c9f394842..fe333b767b 100644
--- a/documentation/functions/global/NewDnsProvider.md
+++ b/documentation/language-reference/top-level-functions/NewDnsProvider.md
@@ -31,7 +31,7 @@ var REG_MYNDC = NewRegistrar("mynamedotcom", "NAMEDOTCOM");
var DNS_MYAWS = NewDnsProvider("myaws", "ROUTE53");
D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- A("@","1.2.3.4")
+ A("@","1.2.3.4"),
);
```
{% endcode %}
@@ -44,7 +44,7 @@ var REG_MYNDC = NewRegistrar("mynamedotcom");
var DNS_MYAWS = NewDnsProvider("myaws");
D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- A("@","1.2.3.4")
+ A("@","1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/functions/global/NewRegistrar.md b/documentation/language-reference/top-level-functions/NewRegistrar.md
similarity index 97%
rename from documentation/functions/global/NewRegistrar.md
rename to documentation/language-reference/top-level-functions/NewRegistrar.md
index 1f0a897036..ce3d6566f4 100644
--- a/documentation/functions/global/NewRegistrar.md
+++ b/documentation/language-reference/top-level-functions/NewRegistrar.md
@@ -31,7 +31,7 @@ var REG_MYNDC = NewRegistrar("mynamedotcom", "NAMEDOTCOM");
var DNS_MYAWS = NewDnsProvider("myaws", "ROUTE53");
D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- A("@","1.2.3.4")
+ A("@","1.2.3.4"),
);
```
{% endcode %}
@@ -44,7 +44,7 @@ var REG_MYNDC = NewRegistrar("mynamedotcom");
var DNS_MYAWS = NewDnsProvider("myaws");
D("example.com", REG_MYNDC, DnsProvider(DNS_MYAWS),
- A("@","1.2.3.4")
+ A("@","1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/functions/global/PANIC.md b/documentation/language-reference/top-level-functions/PANIC.md
similarity index 100%
rename from documentation/functions/global/PANIC.md
rename to documentation/language-reference/top-level-functions/PANIC.md
diff --git a/documentation/language-reference/top-level-functions/REV.md b/documentation/language-reference/top-level-functions/REV.md
new file mode 100644
index 0000000000..a8a77dd44a
--- /dev/null
+++ b/documentation/language-reference/top-level-functions/REV.md
@@ -0,0 +1,81 @@
+---
+name: REV
+parameters:
+ - address
+parameter_types:
+ address: string
+ts_return: string
+---
+
+`REV` returns the reverse lookup domain for an IP network. For
+example `REV("1.2.3.0/24")` returns `3.2.1.in-addr.arpa.` and
+`REV("2001:db8:302::/48")` returns `2.0.3.0.8.b.d.0.1.0.0.2.ip6.arpa.`.
+
+`REV()` is commonly used with the [`D()`](D.md) functions to create reverse DNS lookup zones.
+
+These two are equivalent:
+
+{% code title="dnsconfig.js" %}
+```javascript
+D("3.2.1.in-addr.arpa", ...
+```
+{% endcode %}
+
+{% code title="dnsconfig.js" %}
+```javascript
+D(REV("1.2.3.0/24", ...
+```
+{% endcode %}
+
+The latter is easier to type and less error-prone.
+
+If the address does not include a "/" then `REV()` assumes /32 for IPv4 addresses
+and /128 for IPv6 addresses.
+
+# RFC compliance
+
+`REV()` implements both RFC 2317 and the newer RFC 4183. The `REVCOMPAT()`
+function selects which mode is used. If `REVCOMPAT()` is not called, a default
+is selected for you. The default will change to RFC 4183 in DNSControl v5.0.
+
+See [`REVCOMPAT()`](REVCOMPAT.md) for details.
+
+
+# Host bits
+
+v4.x:
+The host bits (the ones outside the netmask) must be zeros. They are not zeroed
+out automatically. Thus, `REV("1.2.3.4/24")` is an error.
+
+v5.0 and later:
+The host bits (the ones outside the netmask) are ignored. Thus
+`REV("1.2.3.4/24")` and `REV("1.2.3.0/24")` are equivalent.
+
+# Examples
+
+Here's an example reverse lookup domain:
+
+{% code title="dnsconfig.js" %}
+```javascript
+D(REV("1.2.3.0/24"), REGISTRAR, DnsProvider(BIND),
+ PTR("1", "foo.example.com."),
+ PTR("2", "bar.example.com."),
+ PTR("3", "baz.example.com."),
+ // If the first parameter is an IP address, DNSControl automatically calls REV() for you.
+ PTR("1.2.3.10", "ten.example.com."),
+);
+
+D(REV("2001:db8:302::/48"), REGISTRAR, DnsProvider(BIND),
+ PTR("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", "foo.example.com."), // 2001:db8:302::1
+ // If the first parameter is an IP address, DNSControl automatically calls REV() for you.
+ PTR("2001:db8:302::2", "two.example.com."), // 2.0.0...
+ PTR("2001:db8:302::3", "three.example.com."), // 3.0.0...
+);
+```
+{% endcode %}
+
+# Automatic forward and reverse record generation
+
+DNSControl does not automatically generate forward and reverse lookups. However
+it is possible to write a macro that does this. See
+[`PTR()`](../domain/PTR.md) for an example.
diff --git a/documentation/language-reference/top-level-functions/REVCOMPAT.md b/documentation/language-reference/top-level-functions/REVCOMPAT.md
new file mode 100644
index 0000000000..69a32404cf
--- /dev/null
+++ b/documentation/language-reference/top-level-functions/REVCOMPAT.md
@@ -0,0 +1,47 @@
+---
+name: REVCOMPAT
+parameters:
+ - rfc
+parameter_types:
+ rfc: string
+ts_return: string
+---
+
+`REVCOMPAT()` controls which RFC the [`REV()`](REV.md) function adheres to.
+
+Include one of these two commands near the top `dnsconfig.js` (at the global level):
+
+{% code title="dnsconfig.js" %}
+```javascript
+REVCOMPAT("rfc2317"); // RFC 2117: Compatible with old files.
+REVCOMPAT("rfc4183"); // RFC 4183: Adopt the newer standard.
+```
+{% endcode %}
+
+`REVCOMPAT()` is global for all of `dnsconfig.js`. It must appear before any
+use of `REV()`; If not, behavior is undefined.
+
+# RFC 4183 vs RFC 2317
+
+RFC 2317 and RFC 4183 are two different ways to implement reverse lookups for
+CIDR blocks that are not on 8-bit boundaries (/24, /16, /8).
+
+Originally DNSControl implemented the older standard, which only specifies what
+to do for /8, /16, /24 - /32. Using `REV()` for /9-17 and /17-23 CIDRs was an
+error.
+
+v4 defaults to RFC 2317. In v5.0 the default will change to RFC 4183.
+`REVCOMPAT()` is provided for those that wish to retain the old behavior.
+
+For more information, see [Opinion #9](../../opinions.md#opinion-9-rfc-4183-is-better-than-rfc-2317).
+
+# Transition plan
+
+What's the default behavior if `REVCOMPAT()` is not used?
+
+| Version | /9 to /15 and /17 to /23 | /25 to 32 | Warnings |
+|---------|--------------------------|-----------|----------------------------|
+| v4 | RFC 4183 | RFC 2317 | Only if /25 - /32 are used |
+| v5 | RFC 4183 | RFC 4183 | none |
+
+No warnings are generated if the `REVCOMPAT()` function is used.
diff --git a/documentation/functions/global/getConfiguredDomains.md b/documentation/language-reference/top-level-functions/getConfiguredDomains.md
similarity index 100%
rename from documentation/functions/global/getConfiguredDomains.md
rename to documentation/language-reference/top-level-functions/getConfiguredDomains.md
diff --git a/documentation/functions/global/require.md b/documentation/language-reference/top-level-functions/require.md
similarity index 82%
rename from documentation/functions/global/require.md
rename to documentation/language-reference/top-level-functions/require.md
index 7e7482e245..c6919e68a2 100644
--- a/documentation/functions/global/require.md
+++ b/documentation/language-reference/top-level-functions/require.md
@@ -5,14 +5,14 @@ parameters:
ts_ignore: true
---
-`require(...)` loads the specified JavaScript or JSON file, allowing
+`require(...)` loads the specified JavaScript, JSON, or JSON5 file, allowing
to split your configuration across multiple files.
A better name for this function might be "include".
If the supplied `path` string ends with `.js`, the file is interpreted
as JavaScript code, almost as though its contents had been included in
-the currently-executing file. If the path string ends with `.json`,
+the currently-executing file. If the path string ends with `.json` or `.json5` (case insensitive),
`require()` returns the `JSON.parse()` of the file's contents.
If the path string begins with a `./`, it is interpreted relative to
@@ -45,7 +45,7 @@ Here's a more complex example:
require("kubernetes/clusters.js");
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
- IncludeKubernetes()
+ IncludeKubernetes(),
);
```
{% endcode %}
@@ -81,7 +81,7 @@ function includeK8Sdev() {
```
{% endcode %}
-### Example 3: JSON
+### Example 3: JSON and JSON5
Requiring JSON files initializes variables:
@@ -89,8 +89,11 @@ Requiring JSON files initializes variables:
```javascript
var domains = require("./domain-ip-map.json")
+var REG_MY_PROVIDER = NewRegistrar("none");
+var DSP_MY_DNSSERVER = NewDnsProvider("none");
+
for (var domain in domains) {
- D(domain, REG_MY_PROVIDER, PROVIDER,
+ D(domain, REG_MY_PROVIDER, DSP_MY_DNSSERVER,
A("@", domains[domain])
);
}
@@ -106,6 +109,11 @@ for (var domain in domains) {
```
{% endcode %}
+JSON5 works the same way, but the filename ends in `.json5`. (Note: JSON5
+features are supported whether the filename ends with `.json` or `.json5`.
+However please don't rely on JSON5 features in a `.json` file as this may
+change some day.)
+
# Notes
`require()` is *much* closer to PHP's `include()` function than it
diff --git a/documentation/functions/global/require_glob.md b/documentation/language-reference/top-level-functions/require_glob.md
similarity index 100%
rename from documentation/functions/global/require_glob.md
rename to documentation/language-reference/top-level-functions/require_glob.md
diff --git a/documentation/markdown-examples/code/dnsconfig-code-example-with-filename.md b/documentation/markdown-examples/code/dnsconfig-code-example-with-filename.md
index 7f926d83d6..3875b1897b 100644
--- a/documentation/markdown-examples/code/dnsconfig-code-example-with-filename.md
+++ b/documentation/markdown-examples/code/dnsconfig-code-example-with-filename.md
@@ -4,7 +4,7 @@ var REG_NONE = NewRegistrar("none");
var DNS_BIND = NewDnsProvider("bind");
D("example.com", REG_NONE, DnsProvider(DNS_BIND),
- A("@", "1.2.3.4")
+ A("@", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/nameservers.md b/documentation/nameservers.md
index cd56c6e9ee..13cc627f0a 100644
--- a/documentation/nameservers.md
+++ b/documentation/nameservers.md
@@ -45,7 +45,7 @@ Simplicity.
```javascript
D("example.com", REG_NAMECOM,
DnsProvider(DNS_NAMECOM),
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -64,7 +64,7 @@ you want to use a high-performance DNS server.
```javascript
D("example.com", REG_NAMECOM,
DnsProvider(DNS_AWS),
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -85,7 +85,7 @@ updating the zone's records (most likely at a different provider).
```javascript
D("example.com", REG_THIRDPARTY,
DnsProvider(DNS_NAMECOM),
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -130,7 +130,7 @@ D("example.com", REG_NAMECOM,
DnsProvider(DNS_CLOUDFLARE, 0), // Set the DNS provider but ignore the nameservers it suggests (0 == take none of the names it reports)
NAMESERVER("kim.ns.cloudflare.com."),
NAMESERVER("walt.ns.cloudflare.com."),
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -149,7 +149,7 @@ Usually only to correct a bug or misconfiguration elsewhere.
D("example.com", REG_NAMECOM,
DnsProvider(DNS_NAMECOM),
NAMESERVER("ns1.myexample.com"),
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -173,7 +173,7 @@ D("example.com", REG_NAMECOM,
DnsProvider(DNS_NAMECOM), // Our real DNS server
DnsProvider(DNS_CLOUDFLARE, 0), // Quietly send a copy of the zone here.
DnsProvider(DNS_BIND, 0), // And here too!
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -203,7 +203,7 @@ More info: https://www.dns-oarc.net/files/workshop-201203/OARC-workshop-London-2
D("example.com", REG_NAMECOM,
DnsProvider(DNS_AWS, 2), // Take 2 nameservers from AWS
DnsProvider(DNS_GOOGLE, 2), // Take 2 nameservers from GCP
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -232,7 +232,7 @@ this is the output of DNSControl, not the input.
D("example.com", REG_NAMECOM,
DnsProvider(DNS_NAMECOM),
DnsProvider(DNS_BIND, 0), // Don't activate any nameservers related to BIND.
- A("@", "10.2.3.4")
+ A("@", "10.2.3.4"),
);
```
{% endcode %}
@@ -248,7 +248,7 @@ a notified if the delegation diverges.
Why?
Sometimes you just want to know if something changes!
-See the [DNS-over-HTTPS Provider](providers/dnsoverhttps.md) documentation for more info.
+See the [DNS-over-HTTPS Provider](provider/dnsoverhttps.md) documentation for more info.
{% code title="dnsconfig.js" %}
```javascript
diff --git a/documentation/notifications.md b/documentation/notifications.md
index 081e1dc3a4..cef59b33bb 100644
--- a/documentation/notifications.md
+++ b/documentation/notifications.md
@@ -1,26 +1,22 @@
# Notifications
-DNSControl has build in support for notifications when changes are made. This allows you to post messages in team chat, or send emails when dns changes are made.
-
-Notifications are written in the [notifications package](https://github.com/StackExchange/dnscontrol/tree/master/pkg/notifications), and is a really simple interface to implement if you want to add
-new types or destinations.
+DNSControl's "notifications" feature will log `push` changes to other services in real time. Typically this is used to automatically announce DNS changes in a team chatroom. The functionality is implemented using the open source [Shoutrrr](https://github.com/containrrr/shoutrrr) library, which knows how to communicate to many different systems. Some additional services are provided natively, see the [notifications package](https://github.com/StackExchange/dnscontrol/tree/main/pkg/notifications).
## Configuration
-Notifications are set up in your credentials JSON file. They will use the `notifications` key to look for keys or configuration needed for various notification types.
+Notifications are configured in the `creds.json` file, since they often contain API keys or other secrets. The `notifications` key lists the notification service and options.
{% code title="creds.json" %}
```json
- "r53": {
- ...
- },
- "gcloud": {
- ...
- } ,
+{
+ "r53": {},
+ "gcloud": {},
"notifications": {
- "slack_url": "https://api.slack.com/apps/0XXX0X0XX0/incoming-webhooks",
- "teams_url": "https://outlook.office.com/webhook/00000000-0000-0000-0000-000000000000@00000000-0000-0000-0000-000000000000/IncomingWebhook/00000000000000000000000000000000/00000000-0000-0000-0000-000000000000"
+ "slack_url": "https://api.slack.com/apps/0XXX0X0XX0/incoming-webhooks",
+ "teams_url": "https://outlook.office.com/webhook/00000000-0000-0000-0000-000000000000@00000000-0000-0000-0000-000000000000/IncomingWebhook/00000000000000000000000000000000/00000000-0000-0000-0000-000000000000",
+ "shoutrrr_url": "discover://token@id"
}
+}
```
{% endcode %}
@@ -28,7 +24,7 @@ Notifications are set up in your credentials JSON file. They will use the `notif
If you want to send a notification, add the `--notify` flag to the `dnscontrol preview` or `dnscontrol push` commands.
-Below is an example where we add [the A record](functions/domain/A.md) `foo` and display the notification output.
+Below is an example where we add [the A record](language-reference/domain-modifiers/A.md) `foo` and display the notification output.
{% code title="dnsconfig.js" %}
```diff
@@ -66,25 +62,62 @@ dnscontrol push --notify
Successfully ran correction for **example.com[my_provider]** - CREATE foo.example.com A 1.2.3.4 ttl=86400
```
-## Notification types
+## Notification services
+
+### Shoutrrr
+
+DNSControl supports various notification methods via Shoutrrr, including email (SMTP), Discord, Pushover, and many others. For detailed setup instructions, click on the desired service:
+
+* [Bark](https://containrrr.dev/shoutrrr/latest/services/bark/)
+* [Discord](https://containrrr.dev/shoutrrr/latest/services/discord/)
+* [Email](https://containrrr.dev/shoutrrr/latest/services/email/)
+* [Google Chat](https://containrrr.dev/shoutrrr/latest/services/googlechat/)
+* [Gotify](https://containrrr.dev/shoutrrr/latest/services/gotify/)
+* [IFTTT](https://containrrr.dev/shoutrrr/latest/services/ifttt/)
+* [Join](https://containrrr.dev/shoutrrr/latest/services/join/)
+* [Matrix](https://containrrr.dev/shoutrrr/latest/services/matrix/)
+* [Mattermost](https://containrrr.dev/shoutrrr/latest/services/mattermost/)
+* [Ntfy](https://containrrr.dev/shoutrrr/latest/services/ntfy/)
+* [OpsGenie](https://containrrr.dev/shoutrrr/latest/services/opsgenie/)
+* [Pushbullet](https://containrrr.dev/shoutrrr/latest/services/pushbullet/)
+* [Pushover](https://containrrr.dev/shoutrrr/latest/services/pushover/)
+* [Rocketchat](https://containrrr.dev/shoutrrr/latest/services/rocketchat/)
+* [Slack](https://containrrr.dev/shoutrrr/latest/services/slack/)
+* [Teams](https://containrrr.dev/shoutrrr/latest/services/teams/)
+* [Telegram](https://containrrr.dev/shoutrrr/latest/services/telegram/)
+* [Zulip Chat](https://containrrr.dev/shoutrrr/latest/services/zulip/)
+
+The above list is accurate as of 2024-Dec. The compete list and all configuration details are in the [Shoutrrr documentation](https://containrrr.dev/shoutrrr/latest/services/overview/).
+
+Configure `shoutrrr_url` with the Shoutrrr URL to be notified.
+
+{% code title="creds.json" %}
+```json
+{
+ "notifications": {
+ "shoutrrr_url": "discover://token@id"
+ }
+}
+```
+{% endcode %}
### Slack/Mattermost
-If you want to use the Slack integration, you need to create a webhook in Slack.
+To use the Slack integration, you need to create a webhook in Slack.
Please see the [Slack documentation](https://api.slack.com/messaging/webhooks) or the [Mattermost documentation](https://docs.mattermost.com/developer/webhooks-incoming.html)
Configure `slack_url` to this webhook. Mattermost works as well, as they share the same api,
### Microsoft Teams
-If you want to use the Teams integration, you need to create a webhook in Teams.
+To use the Teams integration, you need to create a webhook in Teams.
Please see the [Teams documentation](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#add-an-incoming-webhook-to-a-teams-channel)
Configure `teams_url` to this webhook.
### Telegram
-If you want to use the [Telegram](https://telegram.org/) integration, you need to create a Telegram bot and obtain a Bot Token, as well as a Chat ID. Get a Bot Token by contacting [@BotFather](https://telegram.me/botfather), and a Chat ID by contacting [@myidbot](https://telegram.me/myidbot).
+To use the [Telegram](https://telegram.org/) integration, you need to create a Telegram bot and obtain a Bot Token, as well as a Chat ID. Get a Bot Token by contacting [@BotFather](https://telegram.me/botfather), and a Chat ID by contacting [@myidbot](https://telegram.me/myidbot).
Configure `telegram_bot_token` and `telegram_chat_id` to these values.
@@ -93,13 +126,3 @@ Configure `telegram_bot_token` and `telegram_chat_id` to these values.
This is Stack Overflow's built in chat system. This is probably not useful for most people.
Configure `bonfire_url` to be the full url including room and api key.
-
-## Future work
-
-Yes, this seems pretty limited right now in what it can do. We didn't want to add a bunch of notification types if nobody was going to use them. The good news is, it should
-be really simple to add more. We gladly welcome any PRs with new notification destinations. Some easy possibilities:
-
-- Email
-- Generic Webhooks
-
-Please update this documentation if you add anything.
diff --git a/documentation/opinions.md b/documentation/opinions.md
index 71256a2ef8..6c3aa825d4 100644
--- a/documentation/opinions.md
+++ b/documentation/opinions.md
@@ -37,17 +37,17 @@ coworkers who aren't DNS experts.
Things your coworkers should not have to know:
-Your coworkers should not have to know obscure DNS technical
+- Your coworkers should not have to know obscure DNS technical
knowledge. That's your job.
-Your coworkers should not have to know what happens in ambiguous
+- Your coworkers should not have to know what happens in ambiguous
situations. That's your job.
-Your coworkers should be able to submit PRs to `dnsconfig.js` for you
+- Your coworkers should be able to submit PRs to `dnsconfig.js` for you
to approve; preferably via a CI system that does rudimentary checks
before you even have to see the PR.
-Your coworkers should be able to figure out the language without
+- Your coworkers should be able to figure out the language without
much training. The system should block them from doing dangerous
things (even if they are technically legal).
@@ -90,7 +90,7 @@ Some examples:
* SPF records are stated in the most verbose way; DNSControl optimizes it for you in a safe, opt-in way.
-# Opinion #6 If it is ambiguous in DNS, it is forbidden in DNSControl
+# Opinion #6: If it is ambiguous in DNS, it is forbidden in DNSControl
When there is ambiguity an expert knows what the system will do.
Your coworkers should not be expected to be experts. (See [Opinion #2](#opinion-2-non-experts-should-be-able-to-safely-make-dns-changes)).
@@ -124,10 +124,10 @@ Therefore, we require all CNAME, MX, and NS targets to be FQDNs (they must
end with a "."), or to be a shortname (no dots at all). Everything
else is ambiguous and therefore an error.
-# Opinion #7 Hostnames don't have underscores
+# Opinion #7: Hostnames don't have underscores
DNSControl prints warnings if a hostname includes an underscore
-(`_`) because underscores are not permitted in hostnames.
+(`_`) because underscores are not permitted in hostnames.
We want to prevent a naive user from including an underscore
when they meant to use a hyphen (`-`).
@@ -150,3 +150,55 @@ Therefore we print a warning if a label has an underscore in it,
unless the rtype is SRV, TLSA, TXT, or if the name starts with
certain prefixes such as `_dmarc`. We're always willing to
[add more exceptions](https://github.com/StackExchange/dnscontrol/pull/453/files).
+
+# Opinion #8: TXT Records are one long string
+
+* TXT records are a single string with a length of 0 to 65,280 bytes
+ (the maximum possible TXT record size).
+
+It is the provider's responsibility to split, join, quote, parse,
+encode, or decoded the string as needed by the provider's API. This
+should be invisible to the user.
+
+The user may represent the string any way that JavaScript permits
+strings to be represented (usually double-quotes). For backwards
+compatibility they may also provide a list of strings which will be
+concatenated.
+
+You may be wondering: Isn't a TXT record really a series of 255-octet
+segments? Yes, TXT record's wire-format is a series of strings, each
+no longer than 255-octets. However that kind of detail should be
+hidden from users. The user should represent the string they want and
+DNSControl should magically do the right thing behind the scenes. The
+same with quoting and escaping required by APIs.
+
+You may be wondering: Are there any higher-level applications which
+ascribe semantic value to the TXT string boundaries? I believe that
+the answer is "no". My proof is not based on reading RFCs, but
+instead based on (a) observing that I've never seen a DNS provider's
+control panel let you specify the boundaries, (b) I've never seen a
+FAQ or reddit post asking how to specify those boundaries. Therefore,
+there is no need for this. I also assert that there will be no such
+need in the future.
+
+
+# Opinion #9: RFC 4183 is better than RFC 2317
+
+There is no standard for how to do reverse lookup zones (in-addr.arpa)
+for CIDR blocks that are not /8, /16, or /24. There are only
+recommendations.
+
+RFC 2317 is a good recommendation, but it only covers /25 to /32.
+It also uses `/` in zone names, which many DNS providers do not
+support.
+
+RFC 4183 covers /8 through /32 and uses hyphens, which are supported
+universally.
+
+Originally DNSControl implemented RFC 2317.
+
+In v5.0 we will adopt RFC 4183 as the default. A new function,
+[REVCOMPAT()](language-reference/top-level-functions/REVCOMPAT.md), will be provided to enable backwards compatibility.
+v4.x users can use the function to adopt the new behavior early.
+
+See [REVCOMPAT()](language-reference/top-level-functions/REVCOMPAT.md) for details.
diff --git a/documentation/preview-push.md b/documentation/preview-push.md
new file mode 100644
index 0000000000..ff0697baed
--- /dev/null
+++ b/documentation/preview-push.md
@@ -0,0 +1,129 @@
+# preview/push
+
+`preview` reads the dnsconfig.js file (or equivalent), determines what changes are to be made, and
+prints them. `push` is the same but executes the changes.
+
+```shell
+NAME:
+ dnscontrol preview - read live configuration and identify changes to be made, without applying them
+
+USAGE:
+ dnscontrol preview [command options] [arguments...]
+
+CATEGORY:
+ main
+
+OPTIONS:
+ --config value File containing dns config in javascript DSL (default: "dnsconfig.js")
+ --dev Use helpers.js from disk instead of embedded copy (default: false)
+ --variable value, -v value [ --variable value, -v value ] Add variable that is passed to JS
+ --ir value Read IR (json) directly from this file. Do not process DSL at all
+ --creds value Provider credentials JSON file (or !program to execute program that outputs json) (default: "creds.json")
+ --providers value Providers to enable (comma separated list); default is all. Can exclude individual providers from default by adding '"_exclude_from_defaults": "true"' to the credentials file for a provider
+ --domains value Comma separated list of domain names to include
+ --notify set to true to send notifications to configured destinations (default: false)
+ --expect-no-changes set to true for non-zero return code if there are changes (default: false)
+ --no-populate Use this flag to not auto-create non-existing zones at the provider (default: false)
+ --full Add headings, providers names, notifications of no changes, etc (default: false)
+ --bindserial value Force BIND serial numbers to this value (for reproducibility) (default: 0)
+ --report value Generate a JSON-formatted report of the number of changes.
+ --help, -h show help
+```
+
+* `--config name`
+ * Specifies the name of the main configuration file, normally
+`dnsconfig.js`.
+
+* `--creds name`
+ * Specifies the name of the credentials file, normally `creds.json`.
+ Typically the file is read. If the executable bit is set, the file is
+ executed and the output is used as the configuration. See
+ [creds.json][creds-json.md] for details.
+
+* `--providers name,name2`
+ * Specifies a comma-separated list of providers to
+ enable. The default is all providers. A provider can opt out of being in the
+ default list by `"_exclude_from_defaults": "true"` to the credentials entry for
+ that provider. In that case, the provider will only be activated if it is
+ included in `--providers`.
+
+* `--domains value`
+ * Specifies a comma-separated list of domains to include.
+ Typically all domains are included in `preview`/`push`. Wildcards are not
+ permitted except `*` at the start of the entry. For example, `--domains
+ example.com,*.in-addr.arpa` would include `example.com` plus all reverse lookup
+ domains.
+
+* `--v foo=bar`
+ * Sets the variable `foo` to the value `bar` prior to
+ interpreting the configuration file. Multiple `-v` options can be used.
+
+* `--notify`
+ * Enables sending notifications to the destinations configured in `creds.json`.
+
+* `--dev`
+ * Developer mode. Normally `helpers.js` is embedded in the dnscontrol
+ executable. With this flag, the local file `helpers.js` is read instead.
+
+* `--expect-no-changes`
+ * If set, a non-zero exit code is returned if there are
+ changes. Normally DNSControl sets the exit code based on whether or not there
+ were protocol errors or other reasons the program can not continue. With this
+ flag set, the exit code indicates if any changes were required. This is
+ typically used with `preview` to allow scripts to determine if changes would
+ happen if `push` was used. For example, one might want to run `dnscontrol
+ preview --expect-no-changes` daily to determine if changes have been made to
+ a domain outside of DNSControl.
+
+* `--no-populate`
+ * Do not auto-create non-existing zones at the provider.
+ Normally non-existent zones are automatically created at a provider (unless the
+ provider does not implement zone creation). This flag disables that feature.
+
+* `--full`
+ * Add headings, providers names, notifications of no changes, etc. to
+ the output. Normally the output of `preview`/`push` is extremely brief. This
+ makes the output more verbose. Useful for debugging.
+
+* `--bindserial value`
+ * Force BIND serial numbers to this value. Normally the
+ BIND provider generates SOA serial numbers automatically. This flag forces the
+ serial number generator to output the value specified for all domains. This is
+ generally used for reproducibility in testing pipelines.
+
+* `--cmode value`
+ * Concurrency mode. See below.
+
+* `--report name`
+ * Write a machine-parseable report of
+ corrections to the file named `name`. If no name is specified, no
+ report is generated. See [JSON Reports](json-reports.md)
+
+## cmode
+
+The `preview`/`push` commands begin with a data-gathering phase that collects current configuration
+from providers and zones. This collection can be done sequentially or concurrently. Concurrently is significantly faster. However since concurrent mode is newer, not all providers have been tested and certified as being compatible with this mode. Therefore the `--cmode` flag can be used to control concurrency.
+
+The `--cmode` value may be one of the following:
+
+* `legacy` -- Use the older, sequential code. All data is gathered sequentially. This option and the related code will removed in release v4.16 (or later). Please test `--cmode concurrent` and [report any bugs](https://github.com/StackExchange/dnscontrol/issues) ASAP.
+* `concurrent` -- Gathering is done either sequentially or concurrently depending on whether the provider is marked as having been tested to run concurrently.
+* `none` -- All providers are run sequentially. This is the safest mode. It can be used if a concurrency bug is discovered. While this is logically the same as `legacy`, it is implemented using the newer concurrent code, with concurrency disabled.
+* `all` -- This is unsafe. It runs all providers concurrently, even the ones that have not be validated to run concurrently. It is generally only used for demonstrating bugs.
+
+The default value of `--cmode` will change over time:
+
+* v4.14: `--cmode legacy`
+* v4.15: `--cmode concurrent`
+* v4.16 or later (target 1-Jan-2025): The `--cmode legacy` option will be removed, along with the old serial code.
+
+## ppreview/ppush
+
+{% hint style="warning" %}
+These commands will go away in v4.16 or later. Starting in v4.14, please use
+`preview`/`push` with `--cmode concurrent` instead.
+{% endhint %}
+
+The `ppreview`/`ppush` subcommands are a preview of a future feature where zone
+data is gathered concurrently. The commands will go away when
+they replace the existing `preview`/`push` commands.
\ No newline at end of file
diff --git a/documentation/providers/akamaiedgedns.md b/documentation/provider/akamaiedgedns.md
similarity index 99%
rename from documentation/providers/akamaiedgedns.md
rename to documentation/provider/akamaiedgedns.md
index 8f9e827932..c611e04bf7 100644
--- a/documentation/providers/akamaiedgedns.md
+++ b/documentation/provider/akamaiedgedns.md
@@ -71,7 +71,7 @@ D("example.com", REG_NONE, DnsProvider(DSP_AKAMAIEDGEDNS),
NAMESERVER_TTL(86400),
AUTODNSSEC_ON,
AKAMAICDN("@", "www.preconfigured.edgesuite.net", TTL(20)),
- A("foo", "1.2.3.4")
+ A("foo", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/autodns.md b/documentation/provider/autodns.md
similarity index 96%
rename from documentation/providers/autodns.md
rename to documentation/provider/autodns.md
index d804111cab..0b3de21b67 100644
--- a/documentation/providers/autodns.md
+++ b/documentation/provider/autodns.md
@@ -28,7 +28,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_AUTODNS = NewDnsProvider("autodns");
D("example.com", REG_NONE, DnsProvider(DSP_AUTODNS),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/axfrddns.md b/documentation/provider/axfrddns.md
similarity index 93%
rename from documentation/providers/axfrddns.md
rename to documentation/provider/axfrddns.md
index ea78fbeb2e..ca182e276f 100644
--- a/documentation/providers/axfrddns.md
+++ b/documentation/provider/axfrddns.md
@@ -98,7 +98,7 @@ var DSP_AXFRDDNS = NewDnsProvider("axfrddns", {
"ns4.example.com."
]
}
-}
+)
```
{% endcode %}
@@ -107,7 +107,7 @@ var DSP_AXFRDDNS = NewDnsProvider("axfrddns", {
{
"axfrddns": {
"TYPE": "AXFRDDNS",
- "nameservers": "ns1.example.com.,ns2.example.com.,ns3.example.com.,ns4.example.com."
+ "nameservers": "ns1.example.com,ns2.example.com,ns3.example.com,ns4.example.com"
}
}
```
@@ -144,6 +144,24 @@ the following error message:
Please consider adding default `nameservers` or an explicit `master` in `creds.json`.
```
+### Transfer/AXFR server
+
+As mentioned above, the AXFR+DDNS provider will send AXFR requests to the
+primary master for the zone. On some networks, the AXFR requests are handled
+by a separate server to DDNS requests. Use the `transfer-server` option in
+`creds.json`. If not specified, it falls back to the primary master.
+
+{% code title="creds.json" %}
+```json
+{
+ "axfrddns": {
+ "TYPE": "AXFRDDNS",
+ "transfer-server": "233.252.0.0"
+ }
+}
+```
+{% endcode %}
+
### Buggy DNS servers regarding CNAME updates
When modifying a CNAME record, or when replacing an A record by a
diff --git a/documentation/providers/azure_dns.md b/documentation/provider/azure_dns.md
similarity index 94%
rename from documentation/providers/azure_dns.md
rename to documentation/provider/azure_dns.md
index 241bc4f6d6..cde9e9c89b 100644
--- a/documentation/providers/azure_dns.md
+++ b/documentation/provider/azure_dns.md
@@ -45,6 +45,8 @@ export AZURE_CLIENT_SECRET=BBBBBBBBB
```
{% endcode %}
+NOTE: The ResourceGroup is case sensitive.
+
## Metadata
This provider does not recognize any special metadata fields unique to Azure DNS.
@@ -57,7 +59,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_AZURE_MAIN = NewDnsProvider("azuredns_main");
D("example.com", REG_NONE, DnsProvider(DSP_AZURE_MAIN),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -68,4 +70,6 @@ DNSControl depends on a standard [Client credentials Authentication](https://doc
## New domains
If a domain does not exist in your Azure account, DNSControl will *not* automatically add it with the `push` command. You can do that either manually via the control panel, or via the command `dnscontrol create-domains` command.
+## Caveats
+The ResourceGroup is case sensitive.
diff --git a/documentation/provider/azure_private_dns.md b/documentation/provider/azure_private_dns.md
new file mode 100644
index 0000000000..4b0e130d9d
--- /dev/null
+++ b/documentation/provider/azure_private_dns.md
@@ -0,0 +1,74 @@
+## Configuration
+
+This provider is for the [Azure Private DNS Service](https://learn.microsoft.com/en-us/azure/dns/private-dns-overview). This provider can only manage Azure Private DNS zones and will not manage public Azure DNS zones. To use this provider, add an entry to `creds.json` with `TYPE` set to `AZURE_PRIVATE_DNS`
+along with the API credentials.
+
+Example:
+
+{% code title="creds.json" %}
+```json
+{
+ "azure_private_dns_main": {
+ "TYPE": "AZURE_PRIVATE_DNS",
+ "SubscriptionID": "AZURE_PRIVATE_SUBSCRIPTION_ID",
+ "ResourceGroup": "AZURE_PRIVATE_RESOURCE_GROUP",
+ "TenantID": "AZURE_PRIVATE_TENANT_ID",
+ "ClientID": "AZURE_PRIVATE_CLIENT_ID",
+ "ClientSecret": "AZURE_PRIVATE_CLIENT_SECRET"
+ }
+}
+```
+{% endcode %}
+
+You can also use environment variables:
+
+```shell
+export AZURE_SUBSCRIPTION_ID=XXXXXXXXX
+export AZURE_RESOURCE_GROUP=YYYYYYYYY
+export AZURE_TENANT_ID=ZZZZZZZZ
+export AZURE_CLIENT_ID=AAAAAAAAA
+export AZURE_CLIENT_SECRET=BBBBBBBBB
+```
+
+{% code title="creds.json" %}
+```json
+{
+ "azure_private_dns_main": {
+ "TYPE": "AZURE_PRIVATE_DNS",
+ "SubscriptionID": "$AZURE_PRIVATE_SUBSCRIPTION_ID",
+ "ResourceGroup": "$AZURE_PRIVATE_RESOURCE_GROUP",
+ "ClientID": "$AZURE_PRIVATE_CLIENT_ID",
+ "TenantID": "$AZURE_PRIVATE_TENANT_ID",
+ "ClientSecret": "$AZURE_PRIVATE_CLIENT_SECRET"
+ }
+}
+```
+{% endcode %}
+
+## Metadata
+This provider does not recognize any special metadata fields unique to Azure Private DNS.
+
+## Usage
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_AZURE_PRIVATE_MAIN = NewDnsProvider("azure_private_dns_main");
+
+D("example.com", REG_NONE, DnsProvider(DSP_AZURE_PRIVATE_MAIN),
+ A("test", "1.2.3.4"),
+);
+```
+{% endcode %}
+
+## Activation
+DNSControl depends on a standard [Client credentials Authentication](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest) with permission to list, create and update private zones.
+
+## New domains
+
+If a domain does not exist in your Azure account, DNSControl will *not* automatically add it with the `push` command. You can do that manually via the control panel.
+
+## Caveats
+
+The ResourceGroup is case sensitive.
diff --git a/documentation/providers/bind.md b/documentation/provider/bind.md
similarity index 95%
rename from documentation/providers/bind.md
rename to documentation/provider/bind.md
index c21c18377b..dd40598cbd 100644
--- a/documentation/providers/bind.md
+++ b/documentation/provider/bind.md
@@ -30,10 +30,10 @@ Example:
## Meta configuration
-This provider accepts some optional metadata in the NewDnsProvider() call.
+This provider accepts some optional metadata in the `NewDnsProvider()` call.
-* `default_soa`: If no SOA record exists in a zone file, one will be created. The values of the new SOA are specified here.
-* `default_ns`: Inject these NS records into the zone.
+* `default_soa`: If no SOA record exists in a zone file, one will be created based on the values specified here. Use `SOA()` to update existing zone files.
+* `default_ns`: Inject these NS records into the zone. Use this when `NS()` is insufficient.
In this example we set the default SOA settings and NS records.
diff --git a/documentation/provider/bunny_dns.md b/documentation/provider/bunny_dns.md
new file mode 100644
index 0000000000..a8c9d5ef2d
--- /dev/null
+++ b/documentation/provider/bunny_dns.md
@@ -0,0 +1,69 @@
+# Configuration
+
+To use this provider, add an entry to `creds.json` with `TYPE` set to `BUNNY_DNS` along with
+your [Bunny API Key](https://dash.bunny.net/account/settings).
+
+Example:
+
+{% code title="creds.json" %}
+```json
+{
+ "bunny_dns": {
+ "TYPE": "BUNNY_DNS",
+ "api_key": "your-bunny-api-key"
+ }
+}
+```
+{% endcode %}
+
+You can also use environment variables:
+
+```shell
+export BUNNY_DNS_API_KEY=XXXXXXXXX
+```
+
+{% code title="creds.json" %}
+```json
+{
+ "bunny_dns": {
+ "TYPE": "BUNNY_DNS",
+ "api_key": "$BUNNY_DNS_API_KEY"
+ }
+}
+```
+{% endcode %}
+
+## Metadata
+
+This provider does not recognize any special metadata fields unique to Bunny DNS.
+
+## Usage
+
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_BUNNY_DNS = NewDnsProvider("bunny_dns");
+
+D("example.com", REG_NONE, DnsProvider(DSP_BUNNY_DNS),
+ A("test", "1.2.3.4"),
+);
+```
+{% endcode %}
+
+# Activation
+
+DNSControl depends on the [Bunny API](https://docs.bunny.net/reference/bunnynet-api-overview) to manage your DNS
+records. You will need to generate an [API key](https://dash.bunny.net/account/settings) to use this provider.
+
+## New domains
+
+If a domain does not exist in your Bunny account, DNSControl will automatically add it with the `push` command.
+
+## Caveats
+
+- Bunny DNS does not support dual-hosting or configuring custom TTLs for NS records on the zone apex.
+- While custom nameservers are properly recognized by this provider, it is currently not possible to configure them.
+- Any custom record types like Script, Redirect, Flatten or Pull Zone are currently not supported by this provider. Such
+ records will be completely ignored by DNSControl and left as-is.
diff --git a/documentation/providers/cloudflareapi.md b/documentation/provider/cloudflareapi.md
similarity index 67%
rename from documentation/providers/cloudflareapi.md
rename to documentation/provider/cloudflareapi.md
index 05001abcc2..48d47a1162 100644
--- a/documentation/providers/cloudflareapi.md
+++ b/documentation/provider/cloudflareapi.md
@@ -63,6 +63,8 @@ DNSControl requires the token to have the following permissions:
* Add: Enable SSL controls (`Zone â SSL and Certificates â Edit`)
* Editing Page Rules?
* Add: Edit Page Rules (`Zone â Page Rules â Edit`)
+* Creating Redirects?
+ * Add: Edit Single Redirect (`Zone â Single Redirect â Edit`)
* Managing Cloudflare Workers? (if `manage_workers`: set to `true` or `CF_WORKER_ROUTE()` is in use.)
* Add: Edit Worker Scripts (`Account â Workers Scripts â Edit`)
* Add: Edit Worker Scripts (`Zone â Workers Routes â Edit`)
@@ -161,7 +163,7 @@ var DSP_CLOUDFLARE = NewDnsProvider("cloudflare");
D("example.com", REG_NONE, DnsProvider(DSP_CLOUDFLARE),
A("www1","1.2.3.11", CF_PROXY_ON), // turn proxy ON.
A("www2","1.2.3.12", CF_PROXY_OFF), // default is OFF, this is a no-op.
- A("www3","1.2.3.13", {"cloudflare_proxy": "on"}) // Old format.
+ A("www3","1.2.3.13", {"cloudflare_proxy": "on"}), // Old format.
);
```
{% endcode %}
@@ -180,7 +182,7 @@ D("example.com", REG_NONE, DnsProvider(DSP_CLOUDFLARE),
A("notproxied", "1.2.3.5"),
A("another", "1.2.3.6", CF_PROXY_ON),
ALIAS("@", "www.example.com.", CF_PROXY_ON),
- CNAME("myalias", "www.example.com.", CF_PROXY_ON)
+ CNAME("myalias", "www.example.com.", CF_PROXY_ON),
);
// Example domain where the CF proxy default is set to "on":
@@ -190,15 +192,140 @@ D("example2.tld", REG_NONE, DnsProvider(DSP_CLOUDFLARE),
A("notproxied", "1.2.3.5", CF_PROXY_OFF),
A("another", "1.2.3.6"),
ALIAS("@", "www.example2.tld."),
- CNAME("myalias", "www.example2.tld.")
+ CNAME("myalias", "www.example2.tld."),
);
```
{% endcode %}
## New domains
If a domain does not exist in your Cloudflare account, DNSControl
-will *not* automatically add it. You'll need to do that via the
-control panel manually or via the `dnscontrol create-domains` command.
+will automatically add it when `dnscontrol push` is executed.
+
+
+## Old-style vs new-style redirects
+
+Old-style redirects uses the [Page Rules](https://developers.cloudflare.com/rules/page-rules/) product feature, which is [going away](https://developers.cloudflare.com/rules/reference/page-rules-migration/). In this mode,
+`CF_REDIRECT` and `CF_TEMP_REDIRECT` functions generate Page Rules.
+
+Enable it using:
+
+```javascript
+var DSP_CLOUDFLARE = NewDnsProvider("cloudflare", {
+ "manage_redirects": true,
+ "transcode_log": "transcode.log",
+});
+```
+
+New redirects uses the [Single Redirects](https://developers.cloudflare.com/rules/url-forwarding/) product feature. In this mode,
+`CF_REDIRECT` and `CF_TEMP_REDIRECT` functions generates Single Redirects.
+
+Enable it using:
+
+```javascript
+var DSP_CLOUDFLARE = NewDnsProvider("cloudflare", {
+ "manage_single_redirects": true
+});
+```
+
+{% hint style="warning" %}
+New-style redirects ("Single Redirect Rules") are a new feature of DNSControl
+as of v4.12.0 and may have bugs. Please test carefully.
+{% endhint %}
+
+### Conversion mode:
+
+DNSControl can convert from old-style redirects (Page Rules) to new-style
+redirect (Single Redirects). To enable this mode, set both `manage_redirects`
+and `manage_single_redirects` to true.
+
+{% hint style="warning" %}
+The conversion process only handles a few, very simple, patterns.
+See `providers/cloudflare/rtypes/cfsingleredirect/convert_test.go` for a list of patterns
+supported. Please file bugs if you find problems. PRs welcome!
+{% endhint %}
+
+In conversion mode, DNSControl takes `CF_REDIRECT`/`CF_TEMP_REDIRECT`
+statements and turns each of them into two records: a Page Rules and an
+equivalent Single Redirects rule.
+
+Cloudflare processes Single Redirects before Page Rules, thus it is safe to
+have both at the same time, and provides an easy way to test the new-style
+rules. If they do not work properly, use the Cloudflare web-based control
+panel to manually delete the new-style rule to expose the old-style rule. (and
+report the bug to DNSControl!)
+
+You'll find the new-style rule in the Cloudflare control panel. It will have
+a very long name that includes the `CF_REDIRECT`/`CF_TEMP_REDIRECT` operands
+plus matcher and replacement expressions.
+
+There is no mechanism to easily delete the old-style rules. Either delete them
+manually using the Cloudflare control panel or wait for Cloudflare to remove
+the old-style Page Rule feature.
+
+Once the conversion is complete, change
+`manage_redirects` to `false` then either delete the old redirects
+via the CloudFlare control panel or wait for Cloudflare to remove support for the old-style feature.
+
+{% hint style="warning" %}
+Cloudflare's announcement says that they will convert old-style redirects (Page Rules) to new-style
+redirect (Single Redirects) but they do not give an exact date for when this will happen. DNSControl
+will probably see these new redirects as foreign and delete them.
+
+Therefore it is probably safer to do the conversion ahead of them.
+
+On the other hand, if you let them do the conversion, their conversion may be more correct
+than DNSControl's. However there's no way for DNSControl to manage them since the automatically-generated name will be different.
+
+If you have suggestions on how to handle this better please file a bug.
+{% endhint %}
+
+### Converting to CF_SINGLE_REDIRECT permanently
+
+DNSControl will help convert `CF_REDIRECT`/`CF_TEMP_REDIRECT` statements into
+`CF_SINGLE_REDIRECT` statements. You might choose to do this if you do not want
+to rely on the automatic translation, or if you want to edit the results of the
+translation.
+
+DNSControl will generate a file of the translated statements if you specify
+a filename using the `transcode_log` meta option.
+
+```javascript
+var DSP_CLOUDFLARE = NewDnsProvider("cloudflare", {
+ "manage_single_redirects": true,
+ "transcode_log": "transcode.log",
+});
+```
+
+After running `dnscontrol preview` the contents will look something like this:
+
+{% code title="transcode.log" %}
+```text
+D("example.com", ...
+ CF_SINGLE_REDIRECT("1,302,https://example.com/*,https://replacement.example.com/$1",
+ 302,
+ 'http.host eq "example.com"',
+ 'concat("https://replacement.example.com", http.request.uri.path)'
+ ),
+ CF_SINGLE_REDIRECT("2,302,https://img.example.com/*,https://replacement.example.com/$1",
+ 302,
+ 'http.host eq "img.example.com"',
+ 'concat("https://replacement.example.com", http.request.uri.path)'
+ ),
+ CF_SINGLE_REDIRECT("3,302,https://i.example.com/*,https://replacement.example.com/$1",
+ 302,
+ 'http.host eq "i.example.com"',
+ 'concat("https://replacement.example.com", http.request.uri.path)'
+ ),
+D("otherdomain.com", ...
+ CF_SINGLE_REDIRECT("1,301,https://one.otherdomain.com/,https://www.google.com/",
+ 301,
+ 'http.host eq "one.otherdomain.com" and http.request.uri.path eq "/"',
+ 'concat("https://www.google.com/", "")'
+ ),
+```
+{% endcode %}
+
+Copying the statements to the proper place in `dnsconfig.js` is manual.
## Redirects
@@ -266,6 +393,7 @@ This flag is intended for use with legacy domains where the integration test cre
have access to read/edit Workers. This flag will eventually go away.
```shell
+cd integrationTest # NOTE: Not needed if already in that subdirectory
go test -v -verbose -provider CLOUDFLAREAPI -cfworkers=false
```
diff --git a/documentation/providers/cloudns.md b/documentation/provider/cloudns.md
similarity index 96%
rename from documentation/providers/cloudns.md
rename to documentation/provider/cloudns.md
index 7536ae33bb..727acea495 100644
--- a/documentation/providers/cloudns.md
+++ b/documentation/provider/cloudns.md
@@ -42,8 +42,8 @@ var DSP_CLOUDNS = NewDnsProvider("cloudns");
D("example.com", REG_NONE, DnsProvider(DSP_CLOUDNS),
CLOUDNS_WR("@", "http://example.com/"),
- CLOUDNS_WR("www", "http://example.com/")
-)
+ CLOUDNS_WR("www", "http://example.com/"),
+);
```
{% endcode %}
@@ -56,7 +56,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_CLOUDNS = NewDnsProvider("cloudns");
D("example.com", REG_NONE, DnsProvider(DSP_CLOUDNS),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/provider/cnr.md b/documentation/provider/cnr.md
new file mode 100644
index 0000000000..a439fd04b1
--- /dev/null
+++ b/documentation/provider/cnr.md
@@ -0,0 +1,112 @@
+CentralNic Reseller (CNR), formerly known as RRPProxy, is a prominent provider of domain registration and DNS solutions. Trusted by individuals, service providers, and registrars around the world, CNR is recognized for its cutting-edge technology, exceptional performance, and reliable uptime.
+
+Our advanced DNS expertise is integral to our offering. With CentralNic Reseller, you benefit from a leading DNS platform that features robust DNS automation, DNSSEC for enhanced security, and PremiumDNS via our Anycast Network. Additionally, our platform supports a comprehensive set of features, as detailed by DNSControl.
+
+This is based on API documents found at [https://kb.centralnicreseller.com/api/api-commands/api-command-reference#cat-dynamicdns](https://kb.centralnicreseller.com/api/api-commands/api-command-reference#cat-dynamicdns)
+
+## Configuration
+
+To use this provider, add an entry to `creds.json` with `TYPE` set to `CNR`
+along with your CentralNic Reseller login data.
+
+Example:
+
+{% code title="creds.json" %}
+```json
+{
+ "CNR": {
+ "TYPE": "CNR",
+ "apilogin": "your-cnr-account-id",
+ "apipassword": "your-cnr-account-password",
+ "apientity": "LIVE", // for the LIVE system; use "OTE" for the OT&E system
+ "debugmode": "0", // set it to "1" to get debug output of the communication with our Backend System API
+ }
+}
+```
+{% endcode %}
+
+Here a working example for our OT&E System:
+
+{% code title="creds.json" %}
+```json
+{
+ "CNR": {
+ "TYPE": "CNR",
+ "apilogin": "YourUserName",
+ "apipassword": "YourPassword",
+ "apientity": "OTE",
+ "debugmode": "0"
+ }
+}
+```
+{% endcode %}
+
+{% hint style="info" %}
+**NOTE**: The above credentials are known to the public.
+{% endhint %}
+
+With the above CentralNic Reseller entry in `creds.json`, you can run the
+integration tests as follows:
+
+```shell
+dnscontrol get-zones --format=nameonly cnr CNR all
+```
+```shell
+# Review the output. Pick one domain and set CNR_DOMAIN.
+export CNR_DOMAIN=yodream.com # Pick a domain name.
+export CNR_ENTITY=OTE
+export CNR_UID=test.user
+export CNR_PW=test.passw0rd
+cd integrationTest # NOTE: Not needed if already in that subdirectory
+go test -v -verbose -provider CNR
+```
+
+## Usage
+
+Here's an example DNS Configuration `dnsconfig.js` using our provider module.
+Even though it shows how you use us as Domain Registrar AND DNS Provider, we don't force you to do that.
+You are free to decide if you want to use both of our provider technology or just one of them.
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_CNR = NewRegistrar("CNR");
+var DSP_CNR = NewDnsProvider("CNR");
+
+// Set Default TTL for all RR to reflect our Backend API Default
+// If you use additional DNS Providers, configure a default TTL
+// per domain using the domain modifier DefaultTTL instead.
+// also check this issue for [NAMESERVER TTL](https://github.com/StackExchange/dnscontrol/issues/176).
+DEFAULTS(
+ {"ns_ttl":"3600"},
+ DefaultTTL(3600)
+);
+
+D("example.com", REG_CNR, DnsProvider(DSP_CNR),
+ NAMESERVER("ns1.rrpproxy.net"),
+ NAMESERVER("ns2.rrpproxy.net"),
+ NAMESERVER("ns3.rrpproxy.net"),
+ NAMESERVER("ns4.rrpproxy.net"),
+ A("elk1", "10.190.234.178"),
+ A("test", "56.123.54.12"),
+);
+```
+{% endcode %}
+
+## Metadata
+
+This provider does not recognize any special metadata fields unique to CentralNic Reseller (CNR).
+
+## get-zones
+
+`dnscontrol get-zones` is implemented for this provider. The list
+includes both basic and premier zones.
+
+## New domains
+
+If a dnszone does not exist in your CNR account, DNSControl will *not* automatically add it with the `dnscontrol push` or `dnscontrol preview` command. You'll need to do that via the control panel manually or using the command `dnscontrol create-domains`.
+This is because it could lead to unwanted costs on customer-side that we want to avoid.
+
+## Debug Mode
+
+As shown in the configuration examples above, this can be activated on demand and it can be used to check the API commands send to our system.
+In general this is thought for our purpose to have an easy way to dive into issues. But if you're interested what's going on, feel free to activate it.
diff --git a/documentation/providers/cscglobal.md b/documentation/provider/cscglobal.md
similarity index 98%
rename from documentation/providers/cscglobal.md
rename to documentation/provider/cscglobal.md
index fef8b025ec..922861f971 100644
--- a/documentation/providers/cscglobal.md
+++ b/documentation/provider/cscglobal.md
@@ -37,7 +37,7 @@ var REG_CSCGLOBAL = NewRegistrar("cscglobal");
var DSP_BIND = NewDnsProvider("bind");
D("example.com", REG_CSCGLOBAL, DnsProvider(DSP_BIND),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/desec.md b/documentation/provider/desec.md
similarity index 64%
rename from documentation/providers/desec.md
rename to documentation/provider/desec.md
index fcd1b5c5a5..fb70afee90 100644
--- a/documentation/providers/desec.md
+++ b/documentation/provider/desec.md
@@ -28,7 +28,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_DESEC = NewDnsProvider("desec");
D("example.com", REG_NONE, DnsProvider(DSP_DESEC),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -36,3 +36,11 @@ D("example.com", REG_NONE, DnsProvider(DSP_DESEC),
## Activation
DNSControl depends on a deSEC account auth token.
This token can be obtained by [logging in via the deSEC API](https://desec.readthedocs.io/en/latest/auth/account.html#log-in).
+
+{% hint style="warning" %}
+deSEC enforces a daily limit of 300 RRset creation/deletion/modification per
+domain. Large changes may have to be done over the course of a few days. The
+integration test suite can not be run in a single session. See
+[https://desec.readthedocs.io/en/latest/rate-limits.html#api-request-throttling](https://desec.readthedocs.io/en/latest/rate-limits.html#api-request-throttling)
+{% endhint %}
+
diff --git a/documentation/providers/digitalocean.md b/documentation/provider/digitalocean.md
similarity index 97%
rename from documentation/providers/digitalocean.md
rename to documentation/provider/digitalocean.md
index d592a7051d..bb83d34f79 100644
--- a/documentation/providers/digitalocean.md
+++ b/documentation/provider/digitalocean.md
@@ -28,7 +28,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_DIGITALOCEAN = NewDnsProvider("mydigitalocean");
D("example.com", REG_NONE, DnsProvider(DSP_DIGITALOCEAN),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/dnsimple.md b/documentation/provider/dnsimple.md
similarity index 72%
rename from documentation/providers/dnsimple.md
rename to documentation/provider/dnsimple.md
index 6582d0139b..0560b404fb 100644
--- a/documentation/providers/dnsimple.md
+++ b/documentation/provider/dnsimple.md
@@ -24,9 +24,11 @@ Examples:
{% endcode %}
## Metadata
+
This provider does not recognize any special metadata fields unique to DNSimple.
## Usage
+
An example configuration:
{% code title="dnsconfig.js" %}
@@ -35,14 +37,26 @@ var REG_DNSIMPLE = NewRegistrar("dnsimple");
var DSP_DNSIMPLE = NewDnsProvider("dnsimple");
D("example.com", REG_DNSIMPLE, DnsProvider(DSP_DNSIMPLE),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
## Activation
+
DNSControl depends on a DNSimple account access token.
## Caveats
-None at this time
+### TXT record length
+
+The DNSimple API supports TXT records of up to 1000 "characters" (assumed to
+be octets, per DNS norms, not Unicode characters in an encoding).
+
+See https://support.dnsimple.com/articles/txt-record/
+
+## Development
+
+### Debugging
+
+Set `DNSIMPLE_DEBUG_HTTP` environment variable to `1` to dump all API calls made by this provider.
diff --git a/documentation/providers/dnsmadeeasy.md b/documentation/provider/dnsmadeeasy.md
similarity index 98%
rename from documentation/providers/dnsmadeeasy.md
rename to documentation/provider/dnsmadeeasy.md
index 5357ccaaea..027b58e156 100644
--- a/documentation/providers/dnsmadeeasy.md
+++ b/documentation/provider/dnsmadeeasy.md
@@ -37,7 +37,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_DNSMADEEASY = NewDnsProvider("dnsmadeeasy");
D("example.com", REG_NONE, DnsProvider(DSP_DNSMADEEASY),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/dnsoverhttps.md b/documentation/provider/dnsoverhttps.md
similarity index 100%
rename from documentation/providers/dnsoverhttps.md
rename to documentation/provider/dnsoverhttps.md
diff --git a/documentation/providers/domainnameshop.md b/documentation/provider/domainnameshop.md
similarity index 97%
rename from documentation/providers/domainnameshop.md
rename to documentation/provider/domainnameshop.md
index abb115f01d..49a589eff0 100644
--- a/documentation/providers/domainnameshop.md
+++ b/documentation/provider/domainnameshop.md
@@ -29,7 +29,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_DOMAINNAMESHOP = NewDnsProvider("mydomainnameshop");
D("example.com", REG_NONE, DnsProvider(DSP_DOMAINNAMESHOP),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/provider/dynadot.md b/documentation/provider/dynadot.md
new file mode 100644
index 0000000000..615f238c6b
--- /dev/null
+++ b/documentation/provider/dynadot.md
@@ -0,0 +1,41 @@
+DNSControl's Dynadot provider supports being a Registrar. Support for being a DNS Provider is not included, but could be added in the future.
+
+## Configuration
+
+To use this provider, add an entry to `creds.json` with `TYPE` set to `DYNADOT`
+along with `key` from the [Dynadot API](https://www.dynadot.com/account/domain/setting/api.html).
+
+Example:
+
+{% code title="creds.json" %}
+```json
+{
+ "dynadot": {
+ "TYPE": "DYNADOT",
+ "key": "API Key",
+ }
+}
+```
+{% endcode %}
+
+## Metadata
+This provider does not recognize any special metadata fields unique to Dynadot.
+
+## Usage
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_DYNADOT = NewRegistrar("dynadot");
+
+DOMAIN_ELSEWHERE("example.com", REG_DYNADOT, [
+ "ns1.example.net.",
+ "ns2.example.net.",
+ "ns3.example.net.",
+]);
+```
+{% endcode %}
+
+## Activation
+
+You must [enable the Dynadot API](https://www.dynadot.com/account/domain/setting/api.html) for your account and whitelist the IP address of the machine that will run DNSControl.
\ No newline at end of file
diff --git a/documentation/providers/easyname.md b/documentation/provider/easyname.md
similarity index 100%
rename from documentation/providers/easyname.md
rename to documentation/provider/easyname.md
diff --git a/documentation/provider/exoscale.md b/documentation/provider/exoscale.md
new file mode 100644
index 0000000000..2f3d2dd5f6
--- /dev/null
+++ b/documentation/provider/exoscale.md
@@ -0,0 +1,19 @@
+## Configuration
+
+To use this provider, add an entry to `creds.json` with `TYPE` set to `EXOSCALE`
+along with your Exoscale credentials.
+
+## Usage
+
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_EXOSCALE = NewDnsProvider("exoscale");
+
+D("example.com", REG_NONE, DnsProvider(DSP_EXOSCALE),
+ A("test", "1.2.3.4"),
+);
+```
+{% endcode %}
diff --git a/documentation/providers/gandi_v5.md b/documentation/provider/gandi_v5.md
similarity index 50%
rename from documentation/providers/gandi_v5.md
rename to documentation/provider/gandi_v5.md
index bf9dc3a264..9253f3a32f 100644
--- a/documentation/providers/gandi_v5.md
+++ b/documentation/provider/gandi_v5.md
@@ -1,19 +1,31 @@
`GANDI_V5` uses the v5 API and can act as a registrar provider
or a DNS provider. It is only able to work with domains
migrated to the new LiveDNS API, which should be all domains.
-API keys are assigned to particular users. Go to User Settings,
-"Manage the user account and security settings", the "Security"
-tab, then regenerate the "Production API key".
* API Documentation: https://api.gandi.net/docs
* API Endpoint: https://api.gandi.net/
+* Sandbox API Documentation: https://api.sandbox.gandi.net/docs/
+* Sandbox API Endpoint: https://api.sandbox.gandi.net/
## Configuration
To use this provider, add an entry to `creds.json` with `TYPE` set to `GANDI_V5`
-along your Gandi.net API key. The [sharing_id](https://api.gandi.net/docs/reference/) is optional.
+along with other settings:
-The `sharing_id` selects between different organizations which your account is
+* (mandatory, string) your Gandi.net access credentials (see below) - one of:
+ * `token`: Personal Access Token (PAT)
+ * `apikey` API Key (deprecated)
+* `apiurl`: (optional, string) the endpoint of the API. When empty or absent the production
+endpoint is used (default) ; you can use it to select the Sandbox API Endpoint instead.
+* `sharing_id`: (optional, string) let you scope to a specific organization. When empty or absent
+calls are not scoped to a specific organization.
+
+When both `token` and `apikey` are defined, the priority is given to `token` which will
+be used for API communication (as if `apikey` was not set).
+See [the Authentication section](#authentication) for details on obtaining these credentials.
+
+
+The [sharing_id](https://api.gandi.net/docs/reference/#Sharing-ID) selects between different organizations which your account is
a member of; to manage domains in multiple organizations, you can use multiple
`creds.json` entries.
@@ -33,13 +45,32 @@ Example:
{
"gandi": {
"TYPE": "GANDI_V5",
- "apikey": "your-gandi-key",
+ "token": "your-gandi-personal-access-token",
"sharing_id": "your-sharing_id"
}
}
```
{% endcode %}
+## Authentication
+
+(Cf [official documentation of the API](https://api.gandi.net/docs/authentication/)
+The **Personal Access Token** (PAT) is configured in the [Account Settings of the
+Gandi Admin application](https://admin.gandi.net/organizations/account/pat), then
+click on "Create a token" button.
+Choose an organisation (if your account happens to have multiple ones).
+Then, choose a name (limited to 42 chars), an expiration date.
+You can choose to limit the scope to a select number of products (domain names).
+Finally, choose the permissions : the needed one is "Manage domain name technical configurations"
+(in French: "GÊrer la configuration technique des domaines"), which automatically
+implies "See and renew domain names" (in French: "Voir et renouveler les domaines").
+You then have only one (1) chance to copy and save the token somewhere.
+
+The **API Key** is the previous (deprecated) mechanism used to do api calls.
+To generate or delete your API key, go to User Settings,
+"Manage the user account and security settings", the "Authentication options"
+tab, then regenerate the "Production API key" under "Developer access"
+
## Metadata
This provider does not recognize any special metadata fields unique to Gandi.
@@ -59,7 +90,7 @@ var REG_GANDI = NewRegistrar("gandi");
var DSP_GANDI = NewDnsProvider("gandi");
D("example.com", REG_GANDI, DnsProvider(DSP_GANDI),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -80,7 +111,7 @@ If a domain does not exist in your Gandi account, DNSControl will *not* automati
Error getting corrections: 401: The server could not verify that you authorized to access the document you requested. Either you supplied the wrong credentials (e.g., bad api key), or your access token has expired
```
-This is the error you'll see if your `apikey` in `creds.json` is wrong or invalid.
+This is the error you'll see if your `token` (or (deprecated) `apikey`) in `creds.json` is wrong or invalid.
#### Domain does not exist in profile
@@ -97,3 +128,12 @@ If a `dnscontrol get-zones --format=nameonly CredId - all` returns nothing,
this is usually because your `creds.json` information is pointing at an empty
organization or no organization. The solution is to set `sharing_id` in
`creds.json`.
+
+
+## Development
+
+### Debugging
+Set `GANDI_V5_DEBUG` environment variable to a [boolean-compatible](https://pkg.go.dev/strconv#ParseBool) value to dump all API calls made by this provider.
+
+### Testing
+Set `apiurl` key to the endpoint url for the sandbox (https://api.sandbox.gandi.net/), along with corresponding `token` (or (deprecated) `apikey`) created in this sandbox environment (Cf https://api.sandbox.gandi.net/docs/sandbox/) to make all API calls against Gandi sandbox environment.
diff --git a/documentation/providers/gcloud.md b/documentation/provider/gcloud.md
similarity index 99%
rename from documentation/providers/gcloud.md
rename to documentation/provider/gcloud.md
index aba14354fe..b53093ebcb 100644
--- a/documentation/providers/gcloud.md
+++ b/documentation/provider/gcloud.md
@@ -65,7 +65,7 @@ var REG_NAMECOM = NewRegistrar("name.com");
var DSP_GCLOUD = NewDnsProvider("gcloud");
D("example.com", REG_NAMECOM, DnsProvider(DSP_GCLOUD),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -108,7 +108,7 @@ var DSP_GCLOUD = NewDnsProvider("gcloud", {
});
D("example.tld", REG_NAMECOM, DnsProvider(DSP_GCLOUD),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/provider/gcore.md b/documentation/provider/gcore.md
new file mode 100644
index 0000000000..a034b163bd
--- /dev/null
+++ b/documentation/provider/gcore.md
@@ -0,0 +1,156 @@
+## Configuration
+
+To use this provider, add an entry to `creds.json` with `TYPE` set to `GCORE`
+along with a Gcore account API token.
+
+Example:
+
+{% code title="creds.json" %}
+```json
+{
+ "gcore": {
+ "TYPE": "GCORE",
+ "api-key": "your-gcore-api-key"
+ }
+}
+```
+{% endcode %}
+
+## Metadata
+This provider supports the following metadata fields specific to Gcore, to support Gcore's GeoDNS and failover features.
+
+All metadata values are a string, instead of a number/int or a boolean value. If you want to set a number or a boolean value, you must specify them in their string forms.
+
+### Record level metadata
+These metadata fields can be set on each individual record:
+
+- `gcore_asn`: Comma separated string of ASNs the record should be served to.
+- `gcore_continents`: Comma separated string of continents the record should be served to. Valid values are: `as,na,an,sa,oc,eu,af`.
+- `gcore_countries`: Comma separated string of countries and regions the record should be served to. Countries and regions are represented by their two-letter code ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)).
+- `gcore_latitude` and `gcore_longitude`: Coordinate of the server's location the record is pointing to. Used for GeoDistance feature.
+- `gcore_notes`: Arbitrary notes for the record.
+- `gcore_weight`: Weight of the record, used in load balancing.
+- `gcore_ip`: Comma separated string of IPs/CIDRs the record should be served to.
+
+### Failover (Healthcheck) metadata
+These metadata fields are shared within the same RRSet (record name and type combo). The failover metadata MUST be set on all the records within the RRSet, and MUST be exactly the same across all records.
+
+- `gcore_failover_protocol`: Protocol to perform healthcheck on the server the record is pointing to. Valid values: `HTTP, TCP, UDP, ICMP`
+- `gcore_failover_port`: The port (as a string) to connect to the server.
+- `gcore_failover_frequency`: How often healthcheck should be performed, in seconds as a string. Valid values: 10-3600
+- `gcore_failover_timeout`: How long healthcheck should wait for server's response, in seconds as a string. Valid values: 1-10
+- `gcore_failover_method`: HTTP method used in the healthcheck. Only applies if `gcore_failover_protocol` is set to `HTTP`.
+- `gcore_failover_command`: Bytes to be sent to the server in the healthcheck. Only applies if `gcore_failover_protocol` is set to `TCP` or `UDP`.
+- `gcore_failover_url`: Relative URL to be requested, e.g. `/`. Only applies if `gcore_failover_protocol` is set to `HTTP`.
+- `gcore_failover_tls`: If SSL/TLS should be used when connecting to origin server. Can be either `true` or `false` as a string.
+- `gcore_failover_regexp`: Regular expression of expected contents in the response.
+- `gcore_failover_http_status_code`: Expected HTTP status code as a string. Only applies if `gcore_failover_protocol` is set to `HTTP`.
+- `gcore_failover_host`: Host field in the HTTP request header. Only applies if `gcore_failover_protocol` is set to `HTTP`.
+
+### Filters
+
+The `gcore_filters` metadata is a semicolon delimited string of several "filter" values.
+
+Each filter is a comma separated string of 2 or 3 fields:
+
+- 2 fields: `type,strict`.
+- 3 fields: `type,strict,limit`.
+
+As for the meaning of the fields:
+
+- `type` is the type of the filter, e.g. `healthcheck`, `geodistance`.
+- `strict` specifies what happens if the filter returns no records. If `strict` is `false`, then all records will be returned. If `strict` is `true`, then no records will be returned.
+- `limit` specifies the maximum number of records to be returned. This is an optional field.
+
+Let's take an example `gcore_filters` value: `healthcheck,false;geodistance,false;first_n,false,2`
+
+This example filter specifies 3 filters: `healthcheck`, `geodistance`, `first_n`. For all three filters, the `strict` field is set to `false`. For `first_n` filter, the max number of records to return is 2.
+
+### But this is too complicated for me! (Generating metadata with GCore itself)
+
+GCore provider will also display the record metadata in the corrections list. If you're overwhelmed by the instructions above, you can do the following instead:
+
+1. Add your records manually on Gcore's DNS control panel, and set all the metadata/filters you need.
+2. Run `dnscontrol preview`. You will see DNSControl wants to delete your records:
+
+ ```
+ - DELETE test.example.com A 1.1.1.1 {"gcore_asn":"1234,2345","gcore_continents":"af,an,as,eu,na,oc,sa","gcore_countries":"cn,us","gcore_filters":"geodistance,false;first_n,false,2","gcore_ip":"1.2.3.4","gcore_latitude":"34.567","gcore_longitude":"12.89","gcore_notes":"test","gcore_weight":"12"} ttl=60
+ ```
+
+ Here DNSControl has generated all the metadata above based on your existing records.
+
+3. Copy the metadata into your `dnsconfig.js` file.
+
+## Usage
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_GCORE = NewDnsProvider("gcore");
+
+D("example.com", REG_NONE, DnsProvider(DSP_GCORE),
+ A("test", "1.2.3.4"),
+);
+```
+{% endcode %}
+
+### Example with metadata
+
+An example configuration with metadata set:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_GCORE = NewDnsProvider("gcore");
+
+D("example.com", REG_NONE, DnsProvider(DSP_GCORE),
+ A('@', '1.1.1.1', TTL('1m'), {
+ gcore_filters: 'geodistance,false;first_n,false,2',
+ gcore_failover_protocol: 'HTTP',
+ gcore_failover_port: '443',
+ gcore_failover_frequency: '30',
+ gcore_failover_timeout: '10',
+ gcore_failover_method: 'GET',
+ gcore_failover_url: '/',
+ gcore_failover_tls: 'true',
+ gcore_failover_regexp: '',
+ gcore_failover_host: 'example.com',
+ gcore_asn: '1234,2345',
+ gcore_continents: 'as,na,an,sa,oc,eu,af',
+ gcore_countries: 'cn,us',
+ gcore_latitude: '12.345',
+ gcore_longitude: '67.890',
+ gcore_notes: 'test',
+ gcore_weight: '12',
+ gcore_ip: '1.2.3.4',
+ }),
+ A('@', '1.1.1.2', TTL('1m'), {
+ gcore_filters: 'geodistance,false;first_n,false,2',
+ gcore_failover_protocol: 'HTTP',
+ gcore_failover_port: '443',
+ gcore_failover_frequency: '30',
+ gcore_failover_timeout: '10',
+ gcore_failover_method: 'GET',
+ gcore_failover_url: '/',
+ gcore_failover_tls: 'true',
+ gcore_failover_regexp: '',
+ gcore_failover_host: 'example.com',
+ gcore_asn: '1234,2345',
+ gcore_continents: 'as,na,an,sa,oc,eu,af',
+ gcore_countries: 'cn,us',
+ gcore_latitude: '12.890',
+ gcore_longitude: '34.567',
+ gcore_notes: 'test',
+ gcore_weight: '34',
+ gcore_ip: '1.2.3.5',
+ }),
+);
+```
+{% endcode %}
+
+## Activation
+
+DNSControl depends on a Gcore account API token.
+
+You can obtain your API token on this page:
diff --git a/documentation/providers/hedns.md b/documentation/provider/hedns.md
similarity index 99%
rename from documentation/providers/hedns.md
rename to documentation/provider/hedns.md
index 6b8be53f9f..3cf294264f 100644
--- a/documentation/providers/hedns.md
+++ b/documentation/provider/hedns.md
@@ -111,7 +111,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_HEDNS = NewDnsProvider("hedns");
D("example.com", REG_NONE, DnsProvider(DSP_HEDNS),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/hetzner.md b/documentation/provider/hetzner.md
similarity index 99%
rename from documentation/providers/hetzner.md
rename to documentation/provider/hetzner.md
index ab1b9bc5f6..0616f878fe 100644
--- a/documentation/providers/hetzner.md
+++ b/documentation/provider/hetzner.md
@@ -31,7 +31,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_HETZNER = NewDnsProvider("hetzner");
D("example.com", REG_NONE, DnsProvider(DSP_HETZNER),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/hexonet.md b/documentation/provider/hexonet.md
similarity index 97%
rename from documentation/providers/hexonet.md
rename to documentation/provider/hexonet.md
index 5fc8539fbe..fe04a57a56 100644
--- a/documentation/providers/hexonet.md
+++ b/documentation/provider/hexonet.md
@@ -57,11 +57,11 @@ dnscontrol get-zones --format=nameonly hexonet HEXONET all
```
```shell
# Review the output. Pick one domain and set HEXONET_DOMAIN.
-cd integrationTest/
export HEXONET_DOMAIN=yodream.com # Pick a domain name.
export HEXONET_ENTITY=OTE
export HEXONET_UID=test.user
export HEXONET_PW=test.passw0rd
+cd integrationTest # NOTE: Not needed if already in that subdirectory
go test -v -verbose -provider HEXONET
```
@@ -91,7 +91,7 @@ D("example.com", REG_HEXONET, DnsProvider(DSP_HEXONET),
NAMESERVER("ns3.ispapi.net"),
NAMESERVER("ns4.ispapi.net"),
A("elk1", "10.190.234.178"),
- A("test", "56.123.54.12")
+ A("test", "56.123.54.12"),
);
```
{% endcode %}
diff --git a/documentation/providers/hostingde.md b/documentation/provider/hostingde.md
similarity index 98%
rename from documentation/providers/hostingde.md
rename to documentation/provider/hostingde.md
index 765368947c..3d96731bba 100644
--- a/documentation/providers/hostingde.md
+++ b/documentation/provider/hostingde.md
@@ -26,7 +26,7 @@ var REG_HOSTINGDE = NewRegistrar("hosting.de");
var DSP_HOSTINGDE = NewDnsProvider("hosting.de");
D("example.com", REG_HOSTINGDE, DnsProvider(DSP_HOSTINGDE),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/provider/huaweicloud.md b/documentation/provider/huaweicloud.md
new file mode 100644
index 0000000000..6804e3329f
--- /dev/null
+++ b/documentation/provider/huaweicloud.md
@@ -0,0 +1,103 @@
+## Configuration
+
+
+This provider is for the [Huawei Cloud DNS](https://www.huaweicloud.com/intl/en-us/product/dns.html)(Public DNS). To use this provider, add an entry to `creds.json` with `TYPE` set to `HUAWEICLOUD`.
+along with the API credentials.
+
+Example:
+
+{% code title="creds.json" %}
+```json
+{
+ "huaweicloud": {
+ "TYPE": "HUAWEICLOUD",
+ "KeyId": "YOUR_ACCESS_KEY_ID",
+ "SecretKey": "YOUR_SECRET_ACCESS_KEY",
+ "Region": "YOUR_SERVICE_REGION"
+ }
+}
+```
+{% endcode %}
+
+## Metadata
+There are some record level metadata available for this provider:
+ * `hw_line` (Line ID, default "default_view") Refer to the [Intelligent Resolution](https://support.huaweicloud.com/intl/en-us/usermanual-dns/dns_usermanual_0041.html) for more information.
+ * Available Line ID refer to [Resolution Lines](https://support.huaweicloud.com/intl/en-us/api-dns/en-us_topic_0085546214.html). Custom Line ID can also be used.
+ * `hw_weight` (0-1000, default "1") Refer to the [Configuring Weighted Routing](https://support.huaweicloud.com/intl/en-us/usermanual-dns/dns_usermanual_0705.html) for more information.
+ * `hw_rrset_key` (default "") User defined key for RRset load balance. This value would be stored in the description field of the RRset.
+
+The following example shows how to use the metadata:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_HWCLOUD = NewDnsProvider("huaweicloud");
+
+D("example.com", REG_NONE, DnsProvider(DSP_HWCLOUD),
+ // this example will create 4 rrsets with the same name "test"
+ A("test", "8.8.8.8"),
+ A("test", "8.8.4.4"),
+ A("test", "9.9.9.9", {hw_weight: "10"}), // Weighted Routing
+ A("test", "149.112.112.112", {hw_weight: "10"}), // Weighted Routing
+ A("test", "223.5.5.5", {hw_line: "CN"}), // GEODNS
+ A("test", "223.6.6.6", {hw_line: "CN", hw_weight: "10"}), // GEODNS with weight
+
+ // this example will create 3 rrsets with the same name "rr-lb"
+ A("rr-lb", "10.0.0.1", {hw_weight: "10", hw_rrset_key: "lb-zone-a"}),
+ A("rr-lb", "10.0.0.2", {hw_weight: "10", hw_rrset_key: "lb-zone-a"}),
+ A("rr-lb", "10.0.1.1", {hw_weight: "10", hw_rrset_key: "lb-zone-b"}),
+ A("rr-lb", "10.0.1.2", {hw_weight: "10", hw_rrset_key: "lb-zone-b"}),
+ A("rr-lb", "10.0.2.2", {hw_weight: "0", hw_rrset_key: "lb-zone-c"}),
+);
+```
+{% endcode %}
+
+## Usage
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_HWCLOUD = NewDnsProvider("huaweicloud");
+
+D("example.com", REG_NONE, DnsProvider(DSP_HWCLOUD),
+ A("test", "1.2.3.4"),
+);
+```
+{% endcode %}
+
+## Activation
+DNSControl depends on a standard [IAM User](https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_02_0003.html) with permission to list, create and update hosted zones.
+
+The `DNS FullAccess` policy will also work, but that provides access to many other areas and violates the "principle of least privilege".
+
+The minimum permissions required are as follows:
+
+```json
+{
+ "Version": "1.1",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "dns:recordset:delete",
+ "dns:recordset:create",
+ "dns:zone:create",
+ "dns:recordset:get",
+ "dns:nameserver:getZoneNameServer",
+ "dns:zone:list",
+ "dns:recordset:update",
+ "dns:recordset:list",
+ "dns:zone:get"
+ ]
+ }
+ ]
+}
+```
+
+To determine the `Region` parameter, refer to the [endpoint page of huaweicloud](https://console-intl.huaweicloud.com/apiexplorer/#/endpoint/DNS). For example, on the international site, the `Region` name `ap-southeast-1` is known to work.
+
+If that doesn't work, log into Huaweicloud's website and open the [API Explorer](https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/debug?api=ListPublicZones), find the `ListPublicZones` API, select a different Region and click Debug to try and find your Region.
+
+## New domains
+If a domain does not exist in your Huawei Cloud account, DNSControl will automatically add it with the `push` command.
diff --git a/documentation/providers/internetbs.md b/documentation/provider/internetbs.md
similarity index 100%
rename from documentation/providers/internetbs.md
rename to documentation/provider/internetbs.md
diff --git a/documentation/providers/inwx.md b/documentation/provider/inwx.md
similarity index 89%
rename from documentation/providers/inwx.md
rename to documentation/provider/inwx.md
index 036a77edea..dcd1279acd 100644
--- a/documentation/providers/inwx.md
+++ b/documentation/provider/inwx.md
@@ -105,13 +105,7 @@ var REG_INWX = NewRegistrar("inwx");
var DSP_CF = NewDnsProvider("cloudflare");
D("example.com", REG_INWX, DnsProvider(DSP_CF),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
-
-{% hint style="info" %}
-**NOTE**: The INWX provider implementation currently only supports up to 2,147,483,647 domains. If you exceed
-this limit, it is expected that DNSControl will fail to recognize some domains. Should you exceed this
-limit, please [open an issue on GitHub](https://github.com/StackExchange/dnscontrol/issues/new/choose).
-{% endhint %}
diff --git a/documentation/providers/linode.md b/documentation/provider/linode.md
similarity index 98%
rename from documentation/providers/linode.md
rename to documentation/provider/linode.md
index c35f213304..090e41f3ae 100644
--- a/documentation/providers/linode.md
+++ b/documentation/provider/linode.md
@@ -28,7 +28,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_LINODE = NewDnsProvider("linode");
D("example.com", REG_NONE, DnsProvider(DSP_LINODE),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/loopia.md b/documentation/provider/loopia.md
similarity index 95%
rename from documentation/providers/loopia.md
rename to documentation/provider/loopia.md
index 034cc49e47..b1616ccea0 100644
--- a/documentation/providers/loopia.md
+++ b/documentation/provider/loopia.md
@@ -1,10 +1,10 @@
Loopia is a đŠ provider of DNS. Using DNSControl hides some of the đŠ.
If you are stuck with Loopia, hopefully this will reduce the pain.
-They provide DNS services, both as a registrar, and a provider.
+They provide DNS services, both as a registrar, and a provider.
They provide support in English and other regional variants (Norwegian, Serbian, Swedish).
-This plugin is based on API documents found at
+This plugin is based on API documents found at
[https://www.loopia.com/api/](https://www.loopia.com/api/)
and by observing API responses. Hat tip to GitHub @hazzeh whose code for the
LEGO Loopia implementation was helpful.
@@ -50,7 +50,7 @@ Example:
"TYPE": "LOOPIA",
"username": "your-loopia-api-account-id@loopiaapi",
"password": "your-loopia-api-account-password",
- "debug": "true" // Set to true for extra debug output. Remove or set to false to prevent extra debug output.
+ "debug": "true" // Set to true for extra debug output. Remove or set to false to prevent extra debug output.
}
}
```
@@ -60,7 +60,7 @@ Example:
* `username` - string - your @loopiaapi created username
* `password` - string - your loopia API password
-* `debug` - string - Set to true for extra debug output. Remove or set to false to prevent extra debug output.
+* `debug` - string - Set to true for extra debug output. Remove or set to false to prevent extra debug output.
* `rate_limit_per` - string - See [Rate Limiting](#rate-limiting) below.
* `region` - string - See [Regions](#regions) below.
* `modify_name_servers` - string - See [Modify Name Servers](#modify-name-servers) below.
@@ -69,7 +69,7 @@ Example:
There is no test endpoint. Fly free, grasshopper.
Turning on debug will show the XML requests and responses, and include the
-username and password from your `creds.json` file. If you want to share these,
+username and password from your `creds.json` file. If you want to share these,
like for a GitHub issue, be sure to redact those from the XML.
### Fetch Apex NS Entries
@@ -114,7 +114,7 @@ This setting defaults to "false" (off).
`creds.json` setting: `region`
-Loopia operate in a few regions. Norway (`no`), Serbia (`rs`), Sweden (`se`).
+Loopia operate in a few regions. Norway (`no`), Serbia (`rs`), Sweden (`se`).
For the parameter `region`, specify one of `no`, `rs`, `se`, or omit, or leave empty for the default `se` Sweden.
@@ -167,7 +167,7 @@ In your `creds.json` for all `LOOPIA` provider entries:
"TYPE": "LOOPIA",
"username": "your-loopia-api-account-id@loopiaapi",
"password": "your-loopia-api-account-password",
- "debug": "true", // Set to true for extra debug output. Remove or set to false to prevent extra debug output.
+ "debug": "true", // Set to true for extra debug output. Remove or set to false to prevent extra debug output.
"rate_limit_per": "Minute"
}
}
@@ -198,7 +198,7 @@ D("example.com", REG_LOOPIA, DnsProvider(DSP_LOOPIA),
//NAMESERVER("ns1.loopia.se."), //default
//NAMESERVER("ns2.loopia.se."), //default
A("elk1", "192.0.2.1"),
- A("test", "192.0.2.2")
+ A("test", "192.0.2.2"),
);
```
{% endcode %}
@@ -217,7 +217,7 @@ This provider does not recognize any special metadata fields unique to LOOPIA.
## get-zones
-`dnscontrol get-zones` is implemented for this provider.
+`dnscontrol get-zones` is implemented for this provider.
## New domains
diff --git a/documentation/providers/luadns.md b/documentation/provider/luadns.md
similarity index 97%
rename from documentation/providers/luadns.md
rename to documentation/provider/luadns.md
index ee6ce7a8d8..8dd8af63f7 100644
--- a/documentation/providers/luadns.md
+++ b/documentation/provider/luadns.md
@@ -29,7 +29,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_LUADNS = NewDnsProvider("luadns");
D("example.com", REG_NONE, DnsProvider(DSP_LUADNS),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/msdns.md b/documentation/provider/msdns.md
similarity index 87%
rename from documentation/providers/msdns.md
rename to documentation/provider/msdns.md
index 31eb419dd2..c7566fcaa1 100644
--- a/documentation/providers/msdns.md
+++ b/documentation/provider/msdns.md
@@ -13,8 +13,6 @@ DNSControl will use `New-PSSession` to execute the commands remotely if
DNS and DNSControl are both updating a zone, there will be
unhappiness. DNSControl will blindly remove the dynamic records
unless precautions such as `IGNORE*` and `NO_PURGE` are in use.
-* This is a new provider and has not been tested extensively,
- especially the `pssession` feature.
# Running on Non-Windows systems
@@ -30,7 +28,6 @@ To use this provider, add an entry to `creds.json` with `TYPE` set to `MSDNS`
along with other settings:
* `dnsserver`: (optional) the name of the Microsoft DNS Server to communicate with.
-* `pssession`: (optional) the name of the PowerShell PSSession host to run commands on.
* `psusername`: (optional) the username to connect to the PowerShell PSSession host.
* `pspassword`: (optional) the password to connect to the PowerShell PSSession host.
@@ -42,7 +39,6 @@ Example:
"msdns": {
"TYPE": "MSDNS",
"dnsserver": "ny-dc01",
- "pssession": "mywindowshost",
"psusername": "mywindowsusername",
"pspassword": "mysupersecurepassword"
}
@@ -58,7 +54,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_MSDNS = NewDnsProvider("msdns");
D("example.com", REG_NONE, DnsProvider(DSP_MSDNS),
- A("test", "1.2.3.4")
-)
+ A("test", "1.2.3.4"),
+);
```
{% endcode %}
diff --git a/documentation/providers/mythicbeasts.md b/documentation/provider/mythicbeasts.md
similarity index 97%
rename from documentation/providers/mythicbeasts.md
rename to documentation/provider/mythicbeasts.md
index f239722ce5..40c65b7fea 100644
--- a/documentation/providers/mythicbeasts.md
+++ b/documentation/provider/mythicbeasts.md
@@ -33,7 +33,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_MYTHIC = NewDnsProvider("mythicbeasts");
D("example.com", REG_NONE, DnsProvider(DSP_MYTHIC),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/namecheap.md b/documentation/provider/namecheap.md
similarity index 95%
rename from documentation/providers/namecheap.md
rename to documentation/provider/namecheap.md
index 3e402422c1..b3c2e1f427 100644
--- a/documentation/providers/namecheap.md
+++ b/documentation/provider/namecheap.md
@@ -51,7 +51,7 @@ var REG_NAMECHEAP = NewRegistrar("namecheap");
var DSP_BIND = NewDnsProvider("bind");
D("example.com", REG_NAMECHEAP, DnsProvider(DSP_BIND),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -67,8 +67,8 @@ var DSP_NAMECHEAP = NewDnsProvider("namecheap");
D("example.com", REG_NAMECHEAP, DnsProvider(DSP_NAMECHEAP),
URL("@", "http://example.com/"),
URL("www", "http://example.com/"),
- URL301("backup", "http://backup.example.com/")
-)
+ URL301("backup", "http://backup.example.com/"),
+);
```
{% endcode %}
diff --git a/documentation/providers/namedotcom.md b/documentation/provider/namedotcom.md
similarity index 98%
rename from documentation/providers/namedotcom.md
rename to documentation/provider/namedotcom.md
index 682df23bd8..949be1bc61 100644
--- a/documentation/providers/namedotcom.md
+++ b/documentation/provider/namedotcom.md
@@ -44,7 +44,7 @@ var REG_NAMECOM = NewRegistrar("name.com");
var DSP_NAMECOM = NewDnsProvider("name.com");
D("example.com", REG_NAMECOM, DnsProvider(DSP_NAMECOM),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -58,7 +58,7 @@ var REG_NAMECOM = NewRegistrar("name.com");
var DSP_R53 = NewDnsProvider("r53");
D("example.com", REG_NAMECOM, DnsProvider(DSP_R53),
- A("test","1.2.3.4")
+ A("test","1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/netcup.md b/documentation/provider/netcup.md
similarity index 96%
rename from documentation/providers/netcup.md
rename to documentation/provider/netcup.md
index cfd22c0b57..1e86606679 100644
--- a/documentation/providers/netcup.md
+++ b/documentation/provider/netcup.md
@@ -27,7 +27,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_NETCUP = NewDnsProvider("netcup");
D("example.com", REG_NONE, DnsProvider(DSP_NETCUP),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/netlify.md b/documentation/provider/netlify.md
similarity index 97%
rename from documentation/providers/netlify.md
rename to documentation/provider/netlify.md
index d9058d0a64..cf0b5c818c 100644
--- a/documentation/providers/netlify.md
+++ b/documentation/provider/netlify.md
@@ -30,7 +30,7 @@ var REG_NETLIFY = NewRegistrar("netlify");
var DSP_NETLIFY = NewDnsProvider("netlify");
D("example.com", REG_NETLIFY, DnsProvider(DSP_NETLIFY),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/ns1.md b/documentation/provider/ns1.md
similarity index 95%
rename from documentation/providers/ns1.md
rename to documentation/provider/ns1.md
index ec6c41416c..7c588a44e5 100644
--- a/documentation/providers/ns1.md
+++ b/documentation/provider/ns1.md
@@ -28,7 +28,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_NS1 = NewDnsProvider("ns1");
D("example.com", REG_NONE, DnsProvider(DSP_NS1),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/provider/opensrs.md b/documentation/provider/opensrs.md
new file mode 100644
index 0000000000..21278b6bdd
--- /dev/null
+++ b/documentation/provider/opensrs.md
@@ -0,0 +1,19 @@
+## Configuration
+
+To use this provider, add an entry to `creds.json` with `TYPE` set to `OpenSRS`
+along with your OpenSRS credentials.
+
+## Usage
+
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_OPENSRS = NewDnsProvider("opensrs");
+
+D("example.com", REG_NONE, DnsProvider(DSP_OPENSRS),
+ A("test", "1.2.3.4"),
+);
+```
+{% endcode %}
diff --git a/documentation/providers/oracle.md b/documentation/provider/oracle.md
similarity index 70%
rename from documentation/providers/oracle.md
rename to documentation/provider/oracle.md
index 25defbd473..abe88dde52 100644
--- a/documentation/providers/oracle.md
+++ b/documentation/provider/oracle.md
@@ -25,9 +25,11 @@ Example:
{% endcode %}
## Metadata
+
This provider does not recognize any special metadata fields unique to Oracle Cloud.
## Usage
+
An example configuration:
{% code title="dnsconfig.js" %}
@@ -38,7 +40,17 @@ var DSP_ORACLE = NewDnsProvider("oracle");
D("example.com", REG_NONE, DnsProvider(DSP_ORACLE),
NAMESERVER_TTL(86400),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
+
+## Notes for developers
+
+Integration does not have the capability to set the TTL set differently when Oracle is the provider being tested.
+You will see an error message behind displayed, such as below, but it can be safely ignored.
+
+```Text
+=== RUN TestDNSProviders/example.co.uk/Clean_Slate:Empty
+WARNING: Oracle Cloud forces TTL=86400 for NS records. Ignoring configured TTL of 300 for ns1.p201.dns.oraclecloud.net.
+```
diff --git a/documentation/providers/ovh.md b/documentation/provider/ovh.md
similarity index 54%
rename from documentation/providers/ovh.md
rename to documentation/provider/ovh.md
index 70d0153ede..33d9b7a47c 100644
--- a/documentation/providers/ovh.md
+++ b/documentation/provider/ovh.md
@@ -1,7 +1,7 @@
## Configuration
To use this provider, add an entry to `creds.json` with `TYPE` set to `OVH`
-along with a OVH app-key, app-secret-key and consumer-key.
+along with a OVH app-key, app-secret-key, consumer-key and optionally endpoint.
Example:
@@ -12,7 +12,8 @@ Example:
"TYPE": "OVH",
"app-key": "your app key",
"app-secret-key": "your app secret key",
- "consumer-key": "your consumer key"
+ "consumer-key": "your consumer key",
+ "endpoint": "eu"
}
}
```
@@ -20,6 +21,13 @@ Example:
See [the Activation section](#activation) for details on obtaining these credentials.
+`endpoint` can take the following values:
+
+* `eu` (the default), for connecting to the OVH European endpoint
+* `ca` for connecting to OVH Canada API endpoint
+* `us` for connecting to the OVH USA API endpoint
+* an url for connecting to a different endpoint than the ones above
+
## Metadata
This provider does not recognize any special metadata fields unique to OVH.
@@ -34,7 +42,7 @@ var REG_OVH = NewRegistrar("ovh");
var DSP_OVH = NewDnsProvider("ovh");
D("example.com", REG_OVH, DnsProvider(DSP_OVH),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -47,7 +55,7 @@ var REG_OVH = NewRegistrar("ovh");
var DSP_R53 = NewDnsProvider("r53");
D("example.com", REG_OVH, DnsProvider(DSP_R53),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -55,10 +63,10 @@ D("example.com", REG_OVH, DnsProvider(DSP_R53),
## Activation
To obtain the OVH keys, one need to register an app at OVH by following the
-[OVH API Getting Started](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/)
+[OVH API Getting Started](https://help.ovhcloud.com/csm/en-gb-api-getting-started-ovhcloud-api?id=kb_article_view&sysparm_article=KB0042784)
-It consist in declaring the app at https://eu.api.ovh.com/createApp/
-which gives the `app-key` and `app-secret-key`.
+It consist in declaring the app at
+which gives the `app-key` and `app-secret-key`. If your domains and zones are located in another region, see below for the correct url to use.
Once done, to obtain the `consumer-key` it is necessary to authorize the just created app
to access the data in a specific account:
@@ -100,7 +108,6 @@ curl -XPOST -H"X-Ovh-Application: " -H "Content-type: application/j
It should return something akin to:
-{% code title="creds.json" %}
```json
{
"validationUrl": "https://eu.api.ovh.com/auth/?credentialToken=",
@@ -108,13 +115,19 @@ It should return something akin to:
"state": "pendingValidation"
}
```
-{% endcode %}
Open the "validationUrl" in a browser and log in with your OVH account. This will link the app with your account,
authorizing it to access your zones and domains.
Do not forget to fill the `consumer-key` of your `creds.json`.
+For accessing the other international endpoints such as US and CA, change the `https://eu.api.ovh.com` used above to one of the following:
+
+* Canada endpoint: `https://ca.api.ovh.com`
+* US endpoint: `https://api.us.ovhcloud.com`
+
+Do not forget to fill the `endpoint` of your `creds.json` if you use an endpoint different than the EU one.
+
## New domains
If a domain does not exist in your OVH account, DNSControl
@@ -123,16 +136,18 @@ control panel manually.
## Dual providers scenario
-OVH now allows to host DNS zone for a domain that is not registered in their registrar (see: https://www.ovh.com/manager/web/#/zone). The following dual providers scenario are supported:
+OVH now allows to host DNS zone for a domain that is not registered in their registrar (see: ). The following dual providers scenario are supported:
| registrar | zone | working? |
|:---------:|:-----------:|:--------:|
-| OVH | other | â |
-| OVH | OVH + other | â |
-| other | OVH | â |
+| OVH | other | â
|
+| OVH | OVH + other | â
|
+| other | OVH | â
|
-## Caveat
+## Caveats
-OVH doesn't allow resetting the zone to the OVH DNS through the API. If for any reasons OVH NS entries were
+* OVH doesn't allow resetting the zone to the OVH DNS through the API. If for any reasons OVH NS entries were
removed the only way to add them back is by using the OVH Control Panel (in the DNS Servers tab, click on the "Reset the
DNS servers" button.
+* There may be a slight delay (1-10 minutes) before your modifications appear in the OVH Control Panel. However it seems that it's only cosmetic - the changes are indeed available at the DNS servers. You can confirm that the changes are taken into account by OVH by choosing "Change in text format", and see in the BIND compatible format that your changes are indeed there. And you can confirm by directly asking the DNS servers (e.g. with `dig`).
+* OVH enforces the [Restrictions on valid hostnames](https://en.wikipedia.org/wiki/Hostname#Syntax). A hostname with an underscore ("_") will cause the following error `FAILURE! OVHcloud API error (status code 400): Client::BadRequest: "Invalid domain name, underscore not allowed"`
diff --git a/documentation/providers/packetframe.md b/documentation/provider/packetframe.md
similarity index 96%
rename from documentation/providers/packetframe.md
rename to documentation/provider/packetframe.md
index 25905ce986..a67e490c20 100644
--- a/documentation/providers/packetframe.md
+++ b/documentation/provider/packetframe.md
@@ -28,7 +28,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_PACKETFRAME = NewDnsProvider("packetframe");
D("example.com", REG_NONE, DnsProvider(DSP_PACKETFRAME),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/porkbun.md b/documentation/provider/porkbun.md
similarity index 96%
rename from documentation/providers/porkbun.md
rename to documentation/provider/porkbun.md
index 676bd942b4..e162842291 100644
--- a/documentation/providers/porkbun.md
+++ b/documentation/provider/porkbun.md
@@ -31,7 +31,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_PORKBUN = NewDnsProvider("porkbun");
D("example.com", REG_NONE, DnsProvider(DSP_PORKBUN),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/powerdns.md b/documentation/provider/powerdns.md
similarity index 98%
rename from documentation/providers/powerdns.md
rename to documentation/provider/powerdns.md
index 87e5ae86ad..f451e9ff94 100644
--- a/documentation/providers/powerdns.md
+++ b/documentation/provider/powerdns.md
@@ -55,7 +55,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_POWERDNS = NewDnsProvider("powerdns");
D("example.com", REG_NONE, DnsProvider(DSP_POWERDNS),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/provider/realtimeregister.md b/documentation/provider/realtimeregister.md
new file mode 100644
index 0000000000..ab41b216e8
--- /dev/null
+++ b/documentation/provider/realtimeregister.md
@@ -0,0 +1,48 @@
+[realtimeregister.com](https://realtimeregister.com) is a domain registrar based in the Netherlands.
+
+## Configuration
+
+To use this provider, add an entry to `creds.json` with `TYPE` set to `REALTIMEREGISTER`
+along with your API-key. Further configuration includes a flag indicating BASIC or PREMIUM DNS-service and a flag
+indicating the use of the sandbox environment
+
+**Example:**
+
+{% code title="creds.json" %}
+```json
+{
+ "realtimeregister": {
+ "TYPE": "REALTIMEREGISTER",
+ "apikey": "abcdefghijklmnopqrstuvwxyz1234567890",
+ "sandbox" : "0",
+ "premium" : "0"
+ }
+}
+```
+{% endcode %}
+
+If sandbox is omitted or set to any other value than "1" the production API will be used.
+If premium is set to "1", you will only be able to update zones using Premium DNS. If it is omitted or set to any other value, you
+will only be able to update zones using Basic DNS.
+
+**Important Notes**:
+* It is recommended to create a 'DNSControl' user in your account settings with limited permissions
+(i.e. VIEW_DNS_ZONE, CREATE_DNS_ZONE, UPDATE_DNS_ZONE, VIEW_DOMAIN, UPDATE_DOMAIN), otherwise anyone with
+access to this `creds.json` file might have *full* access to your RTR account and will be able to transfer or delete your domains.
+
+## Metadata
+This provider does not recognize any special metadata fields unique to Realtime Register.
+
+## Usage
+An example `dnsconfig.js` configuration file
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_RTR = NewRegistrar("realtimeregister");
+var DSP_RTR = NewDnsProvider("realtimeregister");
+
+D("example.com", REG_RTR, DnsProvider(DSP_RTR),
+ A("test", "1.2.3.4"),
+);
+```
+{% endcode %}
diff --git a/documentation/providers/route53.md b/documentation/provider/route53.md
similarity index 97%
rename from documentation/providers/route53.md
rename to documentation/provider/route53.md
index 7f1402c1da..2d0f1925ad 100644
--- a/documentation/providers/route53.md
+++ b/documentation/provider/route53.md
@@ -73,17 +73,17 @@ var REG_NONE = NewRegistrar("none");
var DSP_R53 = NewDnsProvider("r53_main");
D("example.com", REG_NONE, DnsProvider(DSP_R53),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
## Split horizon
-This provider supports split horizons using the [`R53_ZONE()`](../functions/record/R53_ZONE.md) domain function.
+This provider supports split horizons using the [`R53_ZONE()`](../language-reference/record-modifiers/R53_ZONE.md) domain function.
In this example the domain `testzone.net` appears in the same account twice,
-each with different zone IDs specified using [`R53_ZONE()`](../functions/record/R53_ZONE.md).
+each with different zone IDs specified using [`R53_ZONE()`](../language-reference/record-modifiers/R53_ZONE.md).
{% code title="dnsconfig.js" %}
```javascript
diff --git a/documentation/providers/rwth.md b/documentation/provider/rwth.md
similarity index 97%
rename from documentation/providers/rwth.md
rename to documentation/provider/rwth.md
index dc38fcddc6..06ea021695 100644
--- a/documentation/providers/rwth.md
+++ b/documentation/provider/rwth.md
@@ -28,7 +28,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_RWTH = NewDnsProvider("rwth");
D("example.rwth-aachen.de", REG_NONE, DnsProvider(DSP_RWTH),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/provider/sakuracloud.md b/documentation/provider/sakuracloud.md
new file mode 100644
index 0000000000..f7fb2bf1d6
--- /dev/null
+++ b/documentation/provider/sakuracloud.md
@@ -0,0 +1,95 @@
+This is the provider for [Sakura Cloud](https://cloud.sakura.ad.jp/).
+
+## Configuration
+To use this provider, add an entry to `creds.json` with `TYPE` set to `SAKURACLOUD`
+along with API credentials.
+
+Example:
+
+{% code title="creds.json" %}
+```json
+{
+ "sakuracloud": {
+ "TYPE": "SAKURACLOUD",
+ "access_token": "your-access-token",
+ "access_token_secret": "your-access-token-secret"
+ }
+}
+```
+{% endcode %}
+
+The `endpoint` is optional. If omitted, the default endpoint is assumed.
+
+Endpoints are as follows:
+
+* `https://secure.sakura.ad.jp/cloud/zone/is1a/api/cloud/1.1` (Ishikari first Zone)
+* `https://secure.sakura.ad.jp/cloud/zone/is1b/api/cloud/1.1` (Ishikari second Zone)
+* `https://secure.sakura.ad.jp/cloud/zone/tk1a/api/cloud/1.1` (Tokyo first Zone)
+* `https://secure.sakura.ad.jp/cloud/zone/tk1b/api/cloud/1.1` (Tokyo second Zone)
+
+DNS service is independent of zones, so you can use any of these endpoints.
+The default is the Ishikari first Zone.
+
+Alternatively you can also use environment variables.
+
+```shell
+export SAKURACLOUD_ACCESS_TOKEN="your-access-token"
+export SAKURACLOUD_ACCESS_TOKEN_SECRET="your-access-token-secret"
+```
+
+{% code title="creds.json" %}
+```json
+{
+ "sakuracloud": {
+ "TYPE": "SAKURACLOUD",
+ "access_token": "$SAKURACLOUD_ACCESS_TOKEN",
+ "access_token_secret": "$SAKURACLOUD_ACCESS_TOKEN_SECRET"
+ }
+}
+```
+{% endcode %}
+
+## Metadata
+This provider does not recognize any special metadata fields unique to
+Sakura Cloud.
+
+## Usage
+An example configuration:
+
+{% code title="dnsconfig.js" %}
+```javascript
+var REG_NONE = NewRegistrar("none");
+var DSP_SAKURACLOUD = NewDnsProvider("sakuracloud");
+
+D("example.com", REG_NONE, DnsProvider(DSP_SAKURACLOUD),
+ A("test", "192.0.2.1"),
+);
+```
+{% endcode %}
+
+`NAMESERVER` does not need to be set as the name servers for the
+Sakura Cloud provider cannot be changed.
+
+`SOA` cannot be set as SOA record of Sakura Cloud provider cannot be changed.
+
+## Activation
+Sakura Cloud depends on an [API Key](https://manual.sakura.ad.jp/cloud/api/apikey.html).
+
+When creating an API key, select "can modify settings" as "Access level".
+if you plan to create zones, select "can create and delete resources" as
+"Access level".
+None of the options in the "Allow access to other services" field need
+to be checked.
+
+## Caveats
+The limitations of the Sakura Cloud DNS service are described in [the DNS manual](https://manual.sakura.ad.jp/cloud/appliance/dns/index.html), which is written in Japanese.
+
+The limitations not described in that manual are:
+
+* "Null MX", RFC 7505, is not supported.
+* SRV records with a Target of "." are not supported.
+* SRV records with Port "0" are not supported.
+* CAA records with a property value longer than 64 bytes are not allowed.
+* Owner names and RDATA targets containing the following labels are not allowed:
+ * example
+ * exampleN, where N is a numerical character
diff --git a/documentation/providers/softlayer.md b/documentation/provider/softlayer.md
similarity index 97%
rename from documentation/providers/softlayer.md
rename to documentation/provider/softlayer.md
index 337f8e9d9d..9f0cd58c09 100644
--- a/documentation/providers/softlayer.md
+++ b/documentation/provider/softlayer.md
@@ -36,7 +36,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_SOFTLAYER = NewDnsProvider("softlayer");
D("example.com", REG_NONE, DnsProvider(DSP_SOFTLAYER),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -53,7 +53,7 @@ var DSP_SOFTLAYER = NewDnsProvider("softlayer");
D("example.com", REG_NONE, DnsProvider(SOFTLAYER),
NAMESERVER_TTL(86400),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers/transip.md b/documentation/provider/transip.md
similarity index 63%
rename from documentation/providers/transip.md
rename to documentation/provider/transip.md
index 94af314d81..624ace0bc4 100644
--- a/documentation/providers/transip.md
+++ b/documentation/provider/transip.md
@@ -51,7 +51,7 @@ var REG_NONE = NewRegistrar("none");
var DSP_TRANSIP = NewDnsProvider("transip");
D("example.com", REG_NONE, DnsProvider(DSP_TRANSIP),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
@@ -59,3 +59,39 @@ D("example.com", REG_NONE, DnsProvider(DSP_TRANSIP),
## Activation
TransIP depends on a TransIP personal access token.
+
+## Limitations
+
+> "When multiple or none of the current DNS entries matches, the response will be an error with http status code 406." â _[TransIP - REST API - Update single DNS entry](https://api.transip.nl/rest/docs.html#domains-dns-patch)_
+
+This makes it not possible, for example, to update a [`CAA()`](../language-reference/domain-modifiers/CAA.md) record in one update. Instead, the old DNS entry is deleted and the replacement is added. You'll see `[1/2]` and `[2/2]` in the DNSControl output whenever this happens.
+
+### Example with a `CAA_BUILDER()`
+
+{% code title="dnsconfig.js" %}
+```diff
+CAA_BUILDER({
+ label: '@',
+ iodef: 'mailto:info@cafferata.dev',
++ iodef_critical: true,
+ issue: [
+ 'letsencrypt.org',
+ ],
+ issuewild: 'none',
+}),
+```
+{% endcode %}
+
+```shell
+dnscontrol push --domains cafferata.dev
+```
+
+```shell
+******************** Domain: cafferata.dev
+2 corrections (transip)
+#1: [1/2] delete: Âą MODIFY cafferata.dev CAA (0 iodef "mailto:info@cafferata.dev" ttl=86400) -> (128 iodef "mailto:info@cafferata.dev" ttl=86400)
+SUCCESS!
+#2: [2/2] create: Âą MODIFY cafferata.dev CAA (0 iodef "mailto:info@cafferata.dev" ttl=86400) -> (128 iodef "mailto:info@cafferata.dev" ttl=86400)
+SUCCESS!
+Done. 2 corrections.
+```
diff --git a/documentation/providers/vultr.md b/documentation/provider/vultr.md
similarity index 96%
rename from documentation/providers/vultr.md
rename to documentation/provider/vultr.md
index 056f9cf3ab..42a91b0991 100644
--- a/documentation/providers/vultr.md
+++ b/documentation/provider/vultr.md
@@ -29,7 +29,7 @@ An example configuration:
var DSP_VULTR = NewDnsProvider("vultr");
D("example.com", REG_DNSIMPLE, DnsProvider(DSP_VULTR),
- A("test", "1.2.3.4")
+ A("test", "1.2.3.4"),
);
```
{% endcode %}
diff --git a/documentation/providers.md b/documentation/providers.md
index 4f2c37a2ab..23d9862c18 100644
--- a/documentation/providers.md
+++ b/documentation/providers.md
@@ -12,54 +12,61 @@ a provider that supports it, we'd love your contribution to ensure it works corr
If a feature is definitively not supported for whatever reason, we would also like a PR to clarify why it is not supported, and fill in this entire matrix.
-| Provider name | Official Support | DNS Provider | Registrar | [`ALIAS`](functions/domain/ALIAS.md) | [`CAA`](functions/domain/CAA.md) | [`AUTODNSSEC`](functions/domain/AUTODNSSEC_ON.md) | [`LOC`](functions/domain/LOC.md) | [`NAPTR`](functions/domain/NAPTR.md) | [`PTR`](functions/domain/PTR.md) | [`SOA`](functions/domain/SOA.md) | [`SRV`](functions/domain/SRV.md) | [`SSHFP`](functions/domain/SSHFP.md) | [`TLSA`](functions/domain/TLSA.md) | [`DS`](functions/domain/DS.md) | [`DHCID`](functions/domain/DHCID.md) | dual host | create-domains | [`NO_PURGE`](functions/domain/NO_PURGE.md) | get-zones |
-| ------------- | ---------------- | ------------ | --------- | ------------------------------------ | -------------------------------- | ------------------------------------------------- | -------------------------------- | ------------------------------------ | -------------------------------- | -------------------------------- | -------------------------------- | ------------------------------------ | ---------------------------------- | ------------------------------ | ------------------------------------ | --------- | -------------- | ------------------------------------------ | --------- |
-| [`AKAMAIEDGEDNS`](providers/akamaiedgedns.md) | â | â
| â | â | â
| â
| â
| â
| â
| â | â
| â
| â
| â | â | â
| â
| â | â
|
-| [`AUTODNS`](providers/autodns.md) | â | â
| â | â
| â
| â | â | â | â | â | â
| â | â | â | â | â | â | â
| â
|
-| [`AXFRDDNS`](providers/axfrddns.md) | â | â
| â | â | â
| â
| â | â
| â
| â | â
| â
| â
| â | â
| â | â | â | â |
-| [`AZURE_DNS`](providers/azure_dns.md) | â
| â
| â | â | â
| â | â | â | â
| â | â
| â | â | â | â | â
| â
| â
| â
|
-| [`BIND`](providers/bind.md) | â
| â
| â | â | â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â | â
|
-| [`CLOUDFLAREAPI`](providers/cloudflareapi.md) | â
| â
| â | â
| â
| â | â | â
| â
| â | â
| â
| â
| â | â | â | â
| â
| â
|
-| [`CLOUDNS`](providers/cloudns.md) | â | â
| â | â
| â
| â | â | â | â
| â | â
| â
| â
| â | â | â | â
| â
| â
|
-| [`CSCGLOBAL`](providers/cscglobal.md) | â
| â
| â
| â | â
| â | â | â | â | â | â
| â | â | â | â | â | â | â
| â
|
-| [`DESEC`](providers/desec.md) | â | â
| â | â | â
| â
| â | â
| â
| â | â
| â
| â
| â
| â | â | â
| â
| â
|
-| [`DIGITALOCEAN`](providers/digitalocean.md) | â | â
| â | â | â
| â | â | â | â | â | â
| â | â | â | â | â | â
| â
| â
|
-| [`DNSIMPLE`](providers/dnsimple.md) | â | â
| â
| â
| â
| â
| â | â
| â
| â | â
| â
| â | â | â | â | â | â
| â
|
-| [`DNSMADEEASY`](providers/dnsmadeeasy.md) | â | â
| â | â
| â
| â | â | â | â
| â | â
| â | â | â | â | â
| â
| â
| â
|
-| [`DNSOVERHTTPS`](providers/dnsoverhttps.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â
| â |
-| [`DOMAINNAMESHOP`](providers/domainnameshop.md) | â | â
| â | â | â
| â | â | â | â | â | â
| â | â | â | â | â | â | â
| â |
-| [`EASYNAME`](providers/easyname.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â
| â |
-| [`EXOSCALE`](providers/exoscale.md) | â | â
| â | â
| â
| â | â | â | â
| â | â
| â | â | â | â | â | â | â
| â |
-| [`GANDI_V5`](providers/gandi_v5.md) | â | â
| â
| â
| â
| â | â | â | â
| â | â
| â
| â
| â | â | â | â | â | â
|
-| [`GCLOUD`](providers/gcloud.md) | â
| â
| â | â
| â
| â | â | â | â
| â | â
| â
| â
| â | â | â
| â
| â
| â
|
-| [`GCORE`](providers/gcore.md) | â | â
| â | â | â
| â | â | â | â | â | â
| â | â | â | â | â
| â
| â
| â
|
-| [`HEDNS`](providers/hedns.md) | â | â
| â | â
| â
| â | â
| â
| â
| â | â
| â
| â | â | â | â
| â
| â
| â
|
-| [`HETZNER`](providers/hetzner.md) | â | â
| â | â | â
| â | â | â | â | â | â
| â | â
| â
| â | â
| â
| â
| â
|
-| [`HEXONET`](providers/hexonet.md) | â | â
| â
| â | â
| â | â | â | â
| â | â
| â | â
| â | â | â
| â
| â
| â |
-| [`HOSTINGDE`](providers/hostingde.md) | â | â
| â
| â
| â
| â
| â | â | â
| â
| â
| â
| â
| â
| â | â
| â
| â
| â
|
-| [`INTERNETBS`](providers/internetbs.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â
| â |
-| [`INWX`](providers/inwx.md) | â | â
| â
| â | â
| â | â | â
| â
| â | â
| â
| â
| â | â | â
| â
| â
| â
|
-| [`LINODE`](providers/linode.md) | â | â
| â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â
| â
|
-| [`LOOPIA`](providers/loopia.md) | â | â
| â
| â | â
| â | â
| â
| â | â | â
| â
| â
| â | â | â
| â | â
| â
|
-| [`LUADNS`](providers/luadns.md) | â | â
| â | â
| â
| â | â | â | â
| â | â
| â
| â
| â | â | â
| â
| â
| â
|
-| [`MSDNS`](providers/msdns.md) | â
| â
| â | â | â | â | â | â
| â
| â | â
| â | â | â | â | â | â | â
| â
|
-| [`MYTHICBEASTS`](providers/mythicbeasts.md) | â | â
| â | â | â
| â | â | â | â
| â | â
| â
| â
| â | â | â
| â | â
| â
|
-| [`NAMECHEAP`](providers/namecheap.md) | â | â
| â
| â
| â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â
|
-| [`NAMEDOTCOM`](providers/namedotcom.md) | â | â
| â
| â
| â | â | â | â | â | â | â
| â | â | â | â | â
| â | â
| â
|
-| [`NETCUP`](providers/netcup.md) | â | â
| â | â | â
| â | â | â | â | â | â
| â | â | â | â | â | â | â
| â |
-| [`NETLIFY`](providers/netlify.md) | â | â
| â | â
| â
| â | â | â | â | â | â
| â | â | â | â | â | â | â
| â
|
-| [`NS1`](providers/ns1.md) | â | â
| â | â
| â
| â
| â | â
| â
| â | â
| â | â | â
| â | â
| â
| â
| â
|
-| [`OPENSRS`](providers/opensrs.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â
| â |
-| [`ORACLE`](providers/oracle.md) | â | â
| â | â
| â
| â | â | â
| â
| â | â
| â
| â
| â | â | â
| â
| â
| â
|
-| [`OVH`](providers/ovh.md) | â | â
| â
| â | â
| â | â | â | â | â | â
| â
| â
| â | â | â
| â | â
| â
|
-| [`PACKETFRAME`](providers/packetframe.md) | â | â
| â | â | â | â | â | â | â
| â | â
| â | â | â | â | â | â | â
| â |
-| [`PORKBUN`](providers/porkbun.md) | â | â
| â
| â
| â | â | â | â | â | â | â
| â | â
| â | â | â | â | â
| â
|
-| [`POWERDNS`](providers/powerdns.md) | â | â
| â | â
| â
| â
| â | â
| â
| â | â
| â
| â
| â
| â | â
| â
| â
| â
|
-| [`ROUTE53`](providers/route53.md) | â
| â
| â
| â | â
| â | â | â | â
| â | â
| â | â | â | â | â
| â
| â
| â
|
-| [`RWTH`](providers/rwth.md) | â | â
| â | â | â
| â | â | â | â
| â | â
| â
| â | â | â | â | â | â
| â
|
-| [`SOFTLAYER`](providers/softlayer.md) | â | â
| â | â | â | â | â | â | â | â | â
| â | â | â | â | â | â | â
| â |
-| [`TRANSIP`](providers/transip.md) | â | â
| â | â
| â
| â | â | â
| â | â | â
| â
| â
| â | â | â | â | â
| â
|
-| [`VULTR`](providers/vultr.md) | â | â
| â | â | â
| â | â | â | â | â | â
| â
| â | â | â | â | â
| â
| â
|
+| Provider name | Official Support | DNS Provider | Registrar | Concurrency Verified | [`ALIAS`](language-reference/domain-modifiers/ALIAS.md) | [`CAA`](language-reference/domain-modifiers/CAA.md) | [`AUTODNSSEC`](language-reference/domain-modifiers/AUTODNSSEC_ON.md) | [`HTTPS`](language-reference/domain-modifiers/HTTPS.md) | [`LOC`](language-reference/domain-modifiers/LOC.md) | [`NAPTR`](language-reference/domain-modifiers/NAPTR.md) | [`PTR`](language-reference/domain-modifiers/PTR.md) | [`SOA`](language-reference/domain-modifiers/SOA.md) | [`SRV`](language-reference/domain-modifiers/SRV.md) | [`SSHFP`](language-reference/domain-modifiers/SSHFP.md) | [`SVCB`](language-reference/domain-modifiers/SVCB.md) | [`TLSA`](language-reference/domain-modifiers/TLSA.md) | [`DS`](language-reference/domain-modifiers/DS.md) | [`DHCID`](language-reference/domain-modifiers/DHCID.md) | [`DNAME`](language-reference/domain-modifiers/DNAME.md) | [`DNSKEY`](language-reference/domain-modifiers/DNSKEY.md) | dual host | create-domains | get-zones |
+| ------------- | ---------------- | ------------ | --------- | -------------------- | ------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- | --------- | -------------- | --------- |
+| [`AKAMAIEDGEDNS`](provider/akamaiedgedns.md) | â | â
| â | â | â | â
| â
| â | â
| â
| â
| â | â
| â
| â | â
| â | â | â | â | â
| â
| â
|
+| [`AUTODNS`](provider/autodns.md) | â | â
| â | â | â
| â
| â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â | â | â
|
+| [`AXFRDDNS`](provider/axfrddns.md) | â | â
| â | â | â | â
| â
| â
| â
| â
| â
| â | â
| â
| â
| â
| â | â
| â | â | â | â | â |
+| [`AZURE_DNS`](provider/azure_dns.md) | â
| â
| â | â
| â | â
| â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â
| â
| â
|
+| [`AZURE_PRIVATE_DNS`](provider/azure_private_dns.md) | â
| â
| â | â | â | â | â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â
| â
| â
|
+| [`BIND`](provider/bind.md) | â
| â
| â | â
| â | â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
| â
|
+| [`BUNNY_DNS`](provider/bunny_dns.md) | â | â
| â | â | â
| â
| â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â | â
| â
|
+| [`CLOUDFLAREAPI`](provider/cloudflareapi.md) | â
| â
| â | â
| â
| â
| â | â
| â | â
| â
| â | â
| â
| â
| â
| â | â | â | â | â | â
| â
|
+| [`CLOUDNS`](provider/cloudns.md) | â | â
| â | â
| â
| â
| â
| â | â
| â | â
| â | â
| â
| â | â
| â | â | â
| â | â | â
| â
|
+| [`CNR`](provider/cnr.md) | â | â
| â
| â
| â | â
| â | â | â | â
| â
| â | â
| â
| â | â
| â | â | â | â | â
| â
| â
|
+| [`CSCGLOBAL`](provider/cscglobal.md) | â
| â
| â
| â
| â | â
| â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â | â | â
|
+| [`DESEC`](provider/desec.md) | â | â
| â | â
| â | â
| â
| â
| â | â
| â
| â | â
| â
| â
| â
| â
| â | â | â
| â | â
| â
|
+| [`DIGITALOCEAN`](provider/digitalocean.md) | â | â
| â | â
| â | â
| â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â | â
| â
|
+| [`DNSIMPLE`](provider/dnsimple.md) | â | â
| â
| â | â
| â
| â
| â | â | â
| â
| â | â
| â
| â | â | â | â | â | â | â | â | â
|
+| [`DNSMADEEASY`](provider/dnsmadeeasy.md) | â | â
| â | â | â
| â
| â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â
| â
| â
|
+| [`DNSOVERHTTPS`](provider/dnsoverhttps.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â |
+| [`DOMAINNAMESHOP`](provider/domainnameshop.md) | â | â
| â | â | â | â
| â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â | â | â |
+| [`DYNADOT`](provider/dynadot.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â |
+| [`EASYNAME`](provider/easyname.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â |
+| [`EXOSCALE`](provider/exoscale.md) | â | â
| â | â | â
| â
| â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â | â | â |
+| [`GANDI_V5`](provider/gandi_v5.md) | â | â
| â
| â
| â
| â
| â | â | â | â | â
| â | â
| â
| â | â
| â | â | â | â | â | â | â
|
+| [`GCLOUD`](provider/gcloud.md) | â
| â
| â | â
| â
| â
| â | â
| â | â | â
| â | â
| â
| â
| â
| â | â | â | â | â
| â
| â
|
+| [`GCORE`](provider/gcore.md) | â | â
| â | â | â
| â
| â
| â
| â | â | â
| â | â
| â | â
| â | â | â | â | â | â
| â
| â
|
+| [`HEDNS`](provider/hedns.md) | â | â
| â | â | â
| â
| â | â
| â
| â
| â
| â | â
| â
| â
| â | â | â | â | â | â
| â
| â
|
+| [`HETZNER`](provider/hetzner.md) | â | â
| â | â
| â | â
| â | â | â | â | â | â | â
| â | â | â
| â
| â | â | â | â
| â
| â
|
+| [`HEXONET`](provider/hexonet.md) | â | â
| â
| â | â | â
| â | â | â | â | â
| â | â
| â | â | â
| â | â | â | â | â
| â
| â |
+| [`HOSTINGDE`](provider/hostingde.md) | â | â
| â
| â | â
| â
| â
| â | â | â | â
| â
| â
| â
| â | â
| â
| â | â | â | â
| â
| â
|
+| [`HUAWEICLOUD`](provider/huaweicloud.md) | â | â
| â | â | â | â
| â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â
| â
| â
|
+| [`INTERNETBS`](provider/internetbs.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â |
+| [`INWX`](provider/inwx.md) | â | â
| â
| â | â | â
| â | â
| â | â
| â
| â | â
| â
| â
| â
| â | â | â | â | â
| â
| â
|
+| [`LINODE`](provider/linode.md) | â | â
| â | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â
|
+| [`LOOPIA`](provider/loopia.md) | â | â
| â
| â | â | â
| â | â | â
| â
| â | â | â
| â
| â | â
| â | â | â | â | â
| â | â
|
+| [`LUADNS`](provider/luadns.md) | â | â
| â | â | â
| â
| â | â | â | â | â
| â | â
| â
| â | â
| â | â | â | â | â
| â
| â
|
+| [`MSDNS`](provider/msdns.md) | â
| â
| â | â | â | â | â | â | â | â
| â
| â | â
| â | â | â | â | â | â | â | â | â | â
|
+| [`MYTHICBEASTS`](provider/mythicbeasts.md) | â | â
| â | â | â | â
| â | â | â | â | â
| â | â
| â
| â | â
| â | â | â | â | â
| â | â
|
+| [`NAMECHEAP`](provider/namecheap.md) | â | â
| â
| â
| â
| â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â
|
+| [`NAMEDOTCOM`](provider/namedotcom.md) | â | â
| â
| â | â
| â | â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â
| â | â
|
+| [`NETCUP`](provider/netcup.md) | â | â
| â | â | â | â
| â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â | â | â |
+| [`NETLIFY`](provider/netlify.md) | â | â
| â | â
| â
| â
| â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â | â | â
|
+| [`NS1`](provider/ns1.md) | â | â
| â | â
| â
| â
| â
| â
| â | â
| â
| â | â
| â | â
| â
| â
| â
| â
| â | â
| â
| â
|
+| [`OPENSRS`](provider/opensrs.md) | â | â | â
| â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â | â |
+| [`ORACLE`](provider/oracle.md) | â | â
| â | â | â
| â
| â | â | â | â
| â
| â | â
| â
| â | â
| â | â | â | â | â
| â
| â
|
+| [`OVH`](provider/ovh.md) | â | â
| â
| â | â | â
| â | â | â | â | â | â | â
| â
| â | â
| â | â | â | â | â
| â | â
|
+| [`PACKETFRAME`](provider/packetframe.md) | â | â
| â | â | â | â | â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â | â | â |
+| [`PORKBUN`](provider/porkbun.md) | â | â
| â
| â | â
| â
| â | â | â | â | â | â | â
| â | â | â
| â | â | â | â | â | â | â
|
+| [`POWERDNS`](provider/powerdns.md) | â | â
| â | â | â
| â
| â
| â | â | â
| â
| â | â
| â
| â | â
| â
| â
| â | â | â
| â
| â
|
+| [`REALTIMEREGISTER`](provider/realtimeregister.md) | â | â
| â
| â | â
| â
| â
| â | â
| â
| â | â | â
| â
| â | â
| â | â | â | â | â | â
| â
|
+| [`ROUTE53`](provider/route53.md) | â
| â
| â
| â
| â | â
| â | â | â | â | â
| â | â
| â | â | â | â | â | â | â | â
| â
| â
|
+| [`RWTH`](provider/rwth.md) | â | â
| â | â | â | â
| â | â | â | â | â
| â | â
| â
| â | â | â | â | â | â | â | â | â
|
+| [`SAKURACLOUD`](provider/sakuracloud.md) | â | â
| â | â | â
| â
| â | â
| â | â | â
| â | â
| â | â
| â | â | â | â | â | â | â
| â
|
+| [`SOFTLAYER`](provider/softlayer.md) | â | â
| â | â | â | â | â | â | â | â | â | â | â
| â | â | â | â | â | â | â | â | â | â |
+| [`TRANSIP`](provider/transip.md) | â | â
| â | â
| â
| â
| â | â | â | â
| â | â | â
| â
| â | â
| â | â | â | â | â | â | â
|
+| [`VULTR`](provider/vultr.md) | â | â
| â | â | â | â
| â | â | â | â | â | â | â
| â
| â | â | â | â | â | â | â | â
| â
|
### Providers with "official support"
@@ -75,13 +82,13 @@ Providers in this category and their maintainers are:
|Name|Maintainer|
|---|---|
-|[`AZURE_DNS`](providers/azure_dns.md)|@vatsalyagoel|
-|[`BIND`](providers/bind.md)|@tlimoncelli|
-|[`CLOUDFLAREAPI`](providers/cloudflareapi.md)|@tresni|
-|[`CSCGLOBAL`](providers/cscglobal.md)|@mikenz|
-|[`GCLOUD`](providers/gcloud.md)|@riyadhalnur|
-|[`MSDNS`](providers/msdns.md)|@tlimoncelli|
-|[`ROUTE53`](providers/route53.md)|@tresni|
+|[`AZURE_DNS`](provider/azure_dns.md)|@vatsalyagoel|
+|[`BIND`](provider/bind.md)|@tlimoncelli|
+|[`CLOUDFLAREAPI`](provider/cloudflareapi.md)|@tresni|
+|[`CSCGLOBAL`](provider/cscglobal.md)|@mikenz|
+|[`GCLOUD`](provider/gcloud.md)|@riyadhalnur|
+|[`MSDNS`](provider/msdns.md)|@tlimoncelli|
+|[`ROUTE53`](provider/route53.md)|@tresni|
### Providers with "contributor support"
@@ -94,54 +101,61 @@ provided to help community members support their code independently.
Expectations of maintainers:
-* Maintainers are expected to support their provider and/or find a new maintainer.
+* Maintainers are expected to support their provider and/or help find a new maintainer.
* Maintainers should set up test accounts and periodically verify that all tests pass (`pkg/js/parse_tests` and `integrationTest`).
* Contributors are encouraged to add new tests and refine old ones. (Test-driven development is encouraged.)
* Bugs will be referred to the maintainer or their designate.
-* Maintainers must be responsible to bug reports and PRs. If a maintainer is unresponsive for more than 2 months, we will consider disabling the provider. First we will put out a call for new maintainer. If noboby volunteers, the provider will be disabled.
+* Maintainers must be responsible to bug reports and PRs. If a maintainer is unresponsive for more than 2 months, we will consider disabling the provider. First we will put out a call for new maintainer. If nobody volunteers, the provider may be disabled.
+* Tom needs to know your real email address. Please email tlimoncelli at stack over flow dot com so he has it.
Providers in this category and their maintainers are:
|Name|Maintainer|
|---|---|
-|[`AKAMAIEDGEDNS`](providers/akamaiedgedns.md)|@svernick|
-|[`AXFRDDNS`](providers/axfrddns.md)|@hnrgrgr|
-|[`CLOUDFLAREAPI`](providers/cloudflareapi.md)|@tresni|
-|[`CLOUDNS`](providers/CLOUDNS.md)|@pragmaton|
-|[`CSCGLOBAL`](providers/cscglobal.md)|@Air-New-Zealand|
-|[`DESEC`](providers/desec.md)|@D3luxee|
-|[`DIGITALOCEAN`](providers/digitalocean.md)|@Deraen|
-|[`DNSIMPLE`](providers/dnsimple.md)|@onlyhavecans|
-|[`DNSMADEEASY`](providers/dnsmadeeasy.md)|@vojtad|
-|[`DNSOVERHTTPS`](providers/dnsoverhttps.md)|@mikenz|
-|[`DOMAINNAMESHOP`](providers/domainnameshop.md)|@SimenBai|
-|[`EASYNAME`](providers/easyname.md)|@tresni|
-|[`EXOSCALE`](providers/exoscale.md)|@pierre-emmanuelJ|
-|[`GANDI_V5`](providers/gandi_v5.md)|@TomOnTime|
-|[`GCORE`](providers/gcore.md)|@xddxdd|
-|[`HEDNS`](providers/hedns.md)|@rblenkinsopp|
-|[`HETZNER`](providers/hetzner.md)|@das7pad|
-|[`HEXONET`](providers/hexonet.md)|@KaiSchwarz-cnic|
-|[`HOSTINGDE`](providers/hostingde.md)|@membero|
-|[`INTERNETBS`](providers/internetbs.md)|@pragmaton|
-|[`INWX`](providers/inwx.md)|@patschi|
-|[`LINODE`](providers/linode.md)|@koesie10|
-|[`LOOPIA`](providers/loopia.md)|@systemcrash|
-|[`LUADNS`](providers/luadns.md)|@riku22|
-|[`NAMECHEAP`](providers/namecheap.md)|@willpower232|
-|[`NETCUP`](providers/netcup.md)|@kordianbruck|
-|[`NETLIFY`](providers/netlify.md)|@SphericalKat|
-|[`NS1`](providers/ns1.md)|@costasd|
-|[`OPENSRS`](providers/opensrs.md)|@pierre-emmanuelJ|
-|[`ORACLE`](providers/oracle.md)|@kallsyms|
-|[`OVH`](providers/ovh.md)|@masterzen|
-|[`PACKETFRAME`](providers/packetframe.md)|@hamptonmoore|
-|[`POWERDNS`](providers/powerdns.md)|@jpbede|
-|[`ROUTE53`](providers/route53.md)|@tresni|
-|[`RWTH`](providers/rwth.md)|@MisterErwin|
-|[`SOFTLAYER`](providers/softlayer.md)|@jamielennox|
-|[`TRANSIP`](providers/transip.md)|@blackshadev|
-|[`VULTR`](providers/vultr.md)|@pgaskin|
+|[`AZURE_PRIVATE_DNS`](provider/azure_private_dns.md)|@matthewmgamble|
+|[`AKAMAIEDGEDNS`](provider/akamaiedgedns.md)|@edglynes|
+|[`AXFRDDNS`](provider/axfrddns.md)|@hnrgrgr|
+|[`BUNNY_DNS`](provider/bunny_dns.md)|@ppmathis|
+|[`CLOUDFLAREAPI`](provider/cloudflareapi.md)|@tresni|
+|[`CLOUDNS`](provider/cloudns.md)|@pragmaton|
+|[`CNR`](provider/cnr.md)|@KaiSchwarz-cnic|
+|[`CSCGLOBAL`](provider/cscglobal.md)|@Air-New-Zealand|
+|[`DESEC`](provider/desec.md)|@D3luxee|
+|[`DIGITALOCEAN`](provider/digitalocean.md)|@Deraen|
+|[`DNSIMPLE`](provider/dnsimple.md)|@onlyhavecans|
+|[`DNSMADEEASY`](provider/dnsmadeeasy.md)|@vojtad|
+|[`DNSOVERHTTPS`](provider/dnsoverhttps.md)|@mikenz|
+|[`DOMAINNAMESHOP`](provider/domainnameshop.md)|@SimenBai|
+|[`EASYNAME`](provider/easyname.md)|@tresni|
+|[`EXOSCALE`](provider/exoscale.md)|@pierre-emmanuelJ|
+|[`GANDI_V5`](provider/gandi_v5.md)|@TomOnTime|
+|[`GCORE`](provider/gcore.md)|@xddxdd|
+|[`HEDNS`](provider/hedns.md)|@rblenkinsopp|
+|[`HETZNER`](provider/hetzner.md)|@das7pad|
+|[`HEXONET`](provider/hexonet.md)|@KaiSchwarz-cnic|
+|[`HOSTINGDE`](provider/hostingde.md)|@membero|
+|[`HUAWEICLOUD`](provider/huaweicloud.md)|@huihuimoe|
+|[`INTERNETBS`](provider/internetbs.md)|@pragmaton|
+|[`INWX`](provider/inwx.md)|@patschi|
+|[`LINODE`](provider/linode.md)|@koesie10|
+|[`LOOPIA`](provider/loopia.md)|@systemcrash|
+|[`LUADNS`](provider/luadns.md)|@riku22|
+|[`NAMECHEAP`](provider/namecheap.md)|@willpower232|
+|[`NETCUP`](provider/netcup.md)|@kordianbruck|
+|[`NETLIFY`](provider/netlify.md)|@SphericalKat|
+|[`NS1`](provider/ns1.md)|@costasd|
+|[`OPENSRS`](provider/opensrs.md)|@philhug|
+|[`ORACLE`](provider/oracle.md)|@kallsyms|
+|[`OVH`](provider/ovh.md)|@masterzen|
+|[`PACKETFRAME`](provider/packetframe.md)|@hamptonmoore|
+|[`POWERDNS`](provider/powerdns.md)|@jpbede|
+|[`REALTIMEREGISTER`](provider/realtimeregister.md)|@PJEilers|
+|[`ROUTE53`](provider/route53.md)|@tresni|
+|[`RWTH`](provider/rwth.md)|@MisterErwin|
+|[`SAKURACLOUD`](provider/sakuracloud.md)|@ttkzw|
+|[`SOFTLAYER`](provider/softlayer.md)|@jamielennox|
+|[`TRANSIP`](provider/transip.md)|@blackshadev|
+|[`VULTR`](provider/vultr.md)|@pgaskin|
### Requested providers
@@ -150,23 +164,22 @@ code to support this provider, we'd be glad to help in any way.
* [1984 Hosting](https://github.com/StackExchange/dnscontrol/issues/1251) (#1251)
* [Alibaba Cloud DNS](https://github.com/StackExchange/dnscontrol/issues/420)(#420)
-* [BunnyDNS](https://github.com/StackExchange/dnscontrol/issues/2265)(#2265)
* [Constellix (DNSMadeEasy)](https://github.com/StackExchange/dnscontrol/issues/842) (#842)
* [CoreDNS](https://github.com/StackExchange/dnscontrol/issues/1284) (#1284)
* [EU.ORG](https://github.com/StackExchange/dnscontrol/issues/1176) (#1176)
* [EnCirca](https://github.com/StackExchange/dnscontrol/issues/1048) (#1048)
+* [GoDaddy](https://github.com/StackExchange/dnscontrol/issues/2596) (#2596)
* [Imperva](https://github.com/StackExchange/dnscontrol/issues/1484) (#1484)
* [Infoblox DNS](https://github.com/StackExchange/dnscontrol/issues/1077) (#1077)
* [Joker.com](https://github.com/StackExchange/dnscontrol/issues/854) (#854)
* [Plesk](https://github.com/StackExchange/dnscontrol/issues/2261) (#2261)
-* [RRPPRoxy](https://github.com/StackExchange/dnscontrol/issues/1656) (#1656)
* [RcodeZero](https://github.com/StackExchange/dnscontrol/issues/884) (#884)
* [SynergyWholesale](https://github.com/StackExchange/dnscontrol/issues/1605) (#1605)
* [UltraDNS by Neustar / CSCGlobal](https://github.com/StackExchange/dnscontrol/issues/1533) (#1533)
#### Q: Why are the above GitHub issues marked "closed"?
-A: Following [the bug triage process](/developer-info/bug-triage), the request
+A: Following [the bug triage process](bug-triage.md), the request
is closed once it is added to this list. If someone chooses to implement the
provider, they re-open the issue.
@@ -179,5 +192,5 @@ DNSControl tries to make writing a provider as easy as possible. DNSControl
does most of the work for you, you only have to write code to authenticate,
download DNS records, and perform create/modify/delete operations on those
records. Please read the directions for [Writing new DNS
-providers](/developer-info/writing-providers). The DNS maintainers will gladly
+providers](writing-providers.md). The DNS maintainers will gladly
coach you through the process.
diff --git a/documentation/providers/gcore.md b/documentation/providers/gcore.md
deleted file mode 100644
index 39d505efd6..0000000000
--- a/documentation/providers/gcore.md
+++ /dev/null
@@ -1,40 +0,0 @@
-## Configuration
-
-To use this provider, add an entry to `creds.json` with `TYPE` set to `GCORE`
-along with a Gcore account API token.
-
-Example:
-
-{% code title="creds.json" %}
-```json
-{
- "gcore": {
- "TYPE": "GCORE",
- "api-key": "your-gcore-api-key"
- }
-}
-```
-{% endcode %}
-
-## Metadata
-This provider does not recognize any special metadata fields unique to Gcore.
-
-## Usage
-An example configuration:
-
-{% code title="dnsconfig.js" %}
-```javascript
-var REG_NONE = NewRegistrar("none");
-var DSP_GCORE = NewDnsProvider("gcore");
-
-D("example.com", REG_NONE, DnsProvider(DSP_GCORE),
- A("test", "1.2.3.4")
-);
-```
-{% endcode %}
-
-## Activation
-
-DNSControl depends on a Gcore account API token.
-
-You can obtain your API token on this page:
diff --git a/documentation/release-engineering.md b/documentation/release-engineering.md
index 17e21f07d6..c06bbd8dd2 100644
--- a/documentation/release-engineering.md
+++ b/documentation/release-engineering.md
@@ -7,24 +7,41 @@ GitHub Actions (GHA) will do most of the work for you. You will need to edit the
Please change the version number as appropriate. Substitute (for example)
`v4.2.0` any place you see `$VERSION` in this doc.
+## Step 0. Update dependencies
+
+```shell
+git checkout -b update_deps
+go install github.com/oligot/go-mod-upgrade@latest
+go-mod-upgrade
+go mod tidy
+git commit -a -m "CHORE: Update dependencies"
+```
+
## Step 1. Rebuild generated files
```shell
-export VERSION=v4.2.0
-git checkout master
+git checkout main
git pull
go fmt ./...
go generate ./...
go mod tidy
+git status
+```
+
+There should be no modified files. If there are, check them in then start over from the beginning:
+
+```
+git checkout -b gogenerate
git commit -a -m "Update generated files for $VERSION"
```
-## Step 2. Tag the commit in master that you want to release
+## Step 2. Tag the commit in main that you want to release
```shell
-export VERSION=v4.2.0
+export VERSION=v4.x.0
+git checkout main
git tag -m "Release $VERSION" -a $VERSION
-git push origin --tags
+git push origin HEAD --tags
```
Soon after
@@ -48,33 +65,6 @@ Release notes style guide:
See [https://github.com/StackExchange/dnscontrol/releases](https://github.com/StackExchange/dnscontrol/releases) for examples for recent release notes and copy that style.
-Template:
-
-```text
-## Changelog
-
-This release includes many new providers (FILL IN), dozens
-of bug fixes, and FILL IN.
-
-### Breaking changes:
-
-* FILL IN
-
-### Major features:
-
-* FILL IN
-
-### Provider-specific changes:
-
-* FILL IN
-
-### Other changes and improvements:
-
-* FILL IN
-
-### Deprecation warnings:
-```
-
## Step 4. Announce it via email
Email the release notes to the mailing list: (note the format of the Subject line and that the first line of the email is the URL of the release)
@@ -93,22 +83,13 @@ https://github.com/StackExchange/dnscontrol/releases/tag/v$VERSION
it. [Click here to join](https://groups.google.com/g/dnscontrol-discuss).
{% endhint %}
-## Step 5. Announce it via chat
-
-Mention on [https://gitter.im/dnscontrol/Lobby](https://gitter.im/dnscontrol/Lobby) that the new release has shipped.
-
-```text
-ANNOUNCEMENT: dnscontrol v$VERSION has been released! https://github.com/StackExchange/dnscontrol/releases/tag/v$VERSION
-```
-
-## Step 6. Get credit
+## Step 5. Get credit
Mention the fact that you did this release in your weekly accomplishments.
If you are at Stack Overflow:
* Add the release to your weekly snippets
-* Run this build: `dnscontrol_embed - Promote most recent artifact into ExternalDNS repo`
## Tip: How to bump the major version
@@ -130,7 +111,7 @@ find * -name \*.bak -delete
GHA is configured to run an integration test for any provider listed in the "provider" list. However the test is skipped if the `*_DOMAIN` variable is not set. For example, the Google Cloud provider integration test is only run if `GCLOUD_DOMAIN` is set.
* Q: Where is the list of providers to run integration tests on?
-* A: In `.github/workflows/build.yml`: (1) the "PROVIDERS" list, (2) the `integrtests-diff2` section.
+* A: In `.github/workflows/pr_test.yml`: (1) the "PROVIDERS" list, (2) the `integrtests-diff2` section.
* Q: Where are non-secret environment variables stored?
* A: GHA calls them "Variables". Update them here: https://github.com/StackExchange/dnscontrol/settings/variables/actions
@@ -140,10 +121,10 @@ GHA is configured to run an integration test for any provider listed in the "pro
### How do I add a single new integration test?
-1. Edit `.github/workflows/build.yml`
+1. Edit `.github/workflows/pr_test.yml`
2. Add the `FOO_DOMAIN` variable name of the provider to the "PROVIDERS" list.
3. Set the `FOO_DOMAIN` variables in GHA via https://github.com/StackExchange/dnscontrol/settings/variables/actions
-4. All other variables should be stored as secrets (for consistency). Add them to the `integrtests-diff2` section.
+4. All other variables should be stored as secrets (for consistency). Add them to the `integration-tests` section.
Set them in GHA via https://github.com/StackExchange/dnscontrol/settings/secrets/actions
### How do I add a "bring your own keys" integration test?
@@ -152,7 +133,7 @@ Overview: You will fork the repo and add any secrets to your fork. For security
1. [Fork StackExchange/dnscontrol](https://github.com/StackExchange/dnscontrol/fork) in GitHub.
- If you already have a fork, be sure to use the "sync fork" button on the main page to sync with master.
+ If you already have a fork, be sure to use the "sync fork" button on the main page to sync with the upstream.
2. In your fork, set the `${DOMAIN}_DOMAIN` variable in GHA via Settings :: Secrets and variables :: Actions :: Variables.
diff --git a/documentation/styleguide-doc.md b/documentation/styleguide-doc.md
index cf0c551d4d..dbbff9ead1 100644
--- a/documentation/styleguide-doc.md
+++ b/documentation/styleguide-doc.md
@@ -2,18 +2,18 @@
## Where are the docs?
-TL;DR version: [`docs`](https://github.com/StackExchange/dnscontrol/tree/master/docs) is the [marketing website](https://dnscontrol.org). [`documentation`](https://github.com/StackExchange/dnscontrol/tree/master/documentation) is the [docs.dnscontrol.org](https://docs.dnscontrol.org/) website. (Yes, the names are backwards!)
+TL;DR version: [`docs`](https://github.com/StackExchange/dnscontrol/tree/main/docs) is the [marketing website](https://dnscontrol.org). [`documentation`](https://github.com/StackExchange/dnscontrol/tree/main/documentation) is the [docs.dnscontrol.org](https://docs.dnscontrol.org/) website. (Yes, the names are backwards!)
**The two websites**
1.
* The main website
- * Source code: [`docs`](https://github.com/StackExchange/dnscontrol/tree/master/docs)
+ * Source code: [`docs`](https://github.com/StackExchange/dnscontrol/tree/main/docs)
* Mostly "marketing" for the project.
* Rarely changes. Updated via GitHub "pages" feature.
2.
* Project documentation
- * Source code: [`documentation`](https://github.com/StackExchange/dnscontrol/tree/master/documentation)
+ * Source code: [`documentation`](https://github.com/StackExchange/dnscontrol/tree/main/documentation)
* Users and developer documentation
* Changes frequently. Updated via [GitBook](https://www.gitbook.com/)
@@ -21,10 +21,10 @@ TL;DR version: [`docs`](https://github.com/StackExchange/dnscontrol/tree/master/
Within the git repo, docs are grouped:
-* [`documentation/`](https://github.com/StackExchange/dnscontrol/tree/master/documentation): general docs
-* [`documentation/providers/`](https://github.com/StackExchange/dnscontrol/tree/master/documentation/providers/): One file per provider
-* [`documentation/functions/`](https://github.com/StackExchange/dnscontrol/tree/master/documentation/functions/): One file per `dnsconfig.js` language feature
-* [`documentation/assets/FOO/`](https://github.com/StackExchange/dnscontrol/tree/master/documentation/assets/): Images for page FOO(PNGs only, please!)
+* [`documentation/`](https://github.com/StackExchange/dnscontrol/tree/main/documentation): general docs
+* [`documentation/provider/`](https://github.com/StackExchange/dnscontrol/tree/main/documentation/provider/): One file per provider
+* [`documentation/language-reference/`](https://github.com/StackExchange/dnscontrol/tree/main/documentation/language-reference/): One file per `dnsconfig.js` language feature
+* [`documentation/assets/FOO/`](https://github.com/StackExchange/dnscontrol/tree/main/documentation/assets/): Images for page FOO(PNGs only, please!)
## How to add a new page?
@@ -33,10 +33,10 @@ Within the git repo, docs are grouped:
## Top-of-Document parameters
-Files in the `documentation/functions/{record,domain,global}` subdirectories
+Files in the `documentation/language-reference/{record,domain,global}` subdirectories
have a header at the top that is used to populate other systems.
-Here's an example from [`A`](functions/domain/A.md)
+Here's an example from [`A`](language-reference/domain-modifiers/A.md)
```
---
@@ -58,9 +58,19 @@ parameter_types:
* `parameter_types`: The typescript type for each parameter. This is used when generating `types-dnscontrol.d.ts`
* `provider`: If a feature is only available for one provider
-## Documentation previews
+## GitHub pull request preview
-> "Preview links are only accessible by GitBook users. We're working on a feature that will allow preview links to be viewable by anyone who accesses the PR." â _[GitBook](https://docs.gitbook.com/product-tour/git-sync/github-pull-request-preview#how-to-access-preview-links)_
+When you submit a GitHub pull request, you can view the result in advance. This allows you to check the impact of changes.
+
+
+
+### How to access preview links
+
+For every pull request, youâll see a status added to the GitHub pull request with a unique preview URL. Clicking the **Details** link on the status will take you to the preview URL for your content. You can then make sure the content is as expected.
+
+{% hint style="info" %}
+**NOTE**: A new preview URL is created for every git update. Please check the GitHub status for the most up to date URL.
+{% endhint %}
## Formatting tips
@@ -70,7 +80,7 @@ Break lines every 80 chars.
Include a blank line between paragraphs.
-Leave one blank line before and after a heading.
+Leave exactly one blank line before and after a heading.
JavaScript code should use double quotes (`"`) for strings, not single quotes
(`'`). They are equivalent but consistency is good.
@@ -111,7 +121,7 @@ var REG_NONE = NewRegistrar("none");
var DNS_BIND = NewDnsProvider("bind");
D("example.com", REG_NONE, DnsProvider(DNS_BIND),
- A("@", "1.2.3.4")
+ A("@", "1.2.3.4"),
);
```
{% endcode %}
@@ -183,7 +193,7 @@ However, the first mention on a page should always
be a link. Others are at the authors digression.
```markdown
-The [`PTR`](functions/domain/PTR.md) feature is helpful in LANs.
+The [`PTR`](language-reference/domain-modifiers/PTR.md) feature is helpful in LANs.
```
#### Mentioning functions from the Source code
@@ -197,7 +207,7 @@ The function `GetRegistrarCorrections()` returns...
#### Internal links
```markdown
-Blah blah blah [M365_BUILDER](functions/record/M365_BUILDER.md)
+Blah blah blah [M365_BUILDER](language-reference/domain-modifiers/M365_BUILDER.md)
```
{% hint style="info" %}
@@ -218,6 +228,13 @@ Blah blah blah blah blah.
Blah blah blah [a search engine](https://www.google.com) blah blah.
```
+## Capitalization matters
+
+Please capitalize these terms as you see them here:
+
+ * DNSControl
+ * GitHub
+
## Proofreading
Please spellcheck documents before submitting a PR.
diff --git a/documentation/test-a-branch.md b/documentation/test-a-branch.md
new file mode 100644
index 0000000000..cd3f342d7e
--- /dev/null
+++ b/documentation/test-a-branch.md
@@ -0,0 +1,62 @@
+### Test A Branch
+
+Instructions for testing DNSControl at a particular PR or branch.
+
+Assumptions:
+* `/THE/PATH` -- Change this to the full path to where your dnsconfig.js and other files are located.
+* `INSERT_BRANCH_HERE` -- The branch you want to test. The branch associated with a PR is listed on [https://github.com/StackExchange/dnscontrol/branches](https://github.com/StackExchange/dnscontrol/branches).
+
+## Using Docker
+
+Using Docker assures you're using the latest version of Go and doesn't require you to install anything on your machine, other than Docker!
+
+```shell
+docker run -it -v /THE/PATH:/dns golang
+git clone -b INSERT_BRANCH_HERE --single-branch https://github.com/StackExchange/dnscontrol.git
+cd dnscontrol
+go install
+```
+
+```shell
+cd /dns
+dnscontrol preview
+```
+
+If you want to run the integration tests, follow the
+[Integration Tests](integration-tests.md) document
+as usual. The directory to be in is `/go/dnscontrol/integrationTest`.
+
+```shell
+cd /go/dnscontrol/integrationTest
+go test -v -verbose -provider INSERT_PROVIDER_NAME -start 1 -end 3
+```
+
+Change `INSERT_PROVIDER_NAME` to the name of your provider (`BIND`, `ROUTE53`, `GCLOUD`, etc.)
+
+## Not using Docker
+
+Step 1: Install Go
+
+[https://go.dev/dl/](https://go.dev/dl/)
+
+Step 2: Check out the software
+
+```shell
+git clone -b INSERT_BRANCH_HERE --single-branch https://github.com/StackExchange/dnscontrol.git
+cd dnscontrol
+go install
+```
+
+```shell
+cd /THE/PATH
+dnscontrol preview
+```
+
+Step 3: Clean up
+
+`go install` put the `dnscontrol` program in your `$HOME/bin` directory. You probably want to remove it.
+
+```shell
+rm -i $HOME/bin/dnscontrol
+```
+
diff --git a/documentation/why-the-dot.md b/documentation/why-the-dot.md
index 8354592154..11edef984f 100644
--- a/documentation/why-the-dot.md
+++ b/documentation/why-the-dot.md
@@ -46,7 +46,7 @@ How are they ambiguous?
* Should $DOMAIN be added to "bar.com"? Well, obviously not, because it already ends with ".com" and we all know that "bar.com.bar.com" is probably not what they want. No, it isn't that obvious! Why? (see the next bullet point)
* Should $DOMAIN be added to "meta.xyz"? Everyone knows that ".xyz" isn't a TLD. Obviously, yes, $DOMAIN should be appended. However, wait... ".xyz" became a TLD in June 2014. We don't want to be surprised by changes like that. Also, users should not be required to memorize all the TLDs. (In the old days it was reasonable to expect people to memorize the 7 TLDS (gov/edu/com/mil/org/net) but since 2000 that's all changed. By the way, we forgot to include "int" in the original and you didn't notice.)
- * What is the CNAME target is "www.bar.com" and the domain is "bar.com". Then It is reasonable to infer the user's intent, right? "www.bar.com.bar.com." would be silly, right? Maybe. What if we are copying 100 lines of dnsconfig.js from one `D()` to another. Buried in the middle is this one CNAME that means something entirely different when in a new $DOMAIN. That would be bad. We've seen this in production and want to prevent this kind of error.
+ * What if the CNAME target is "www.bar.com" and the domain is "bar.com"? Then It is reasonable to infer the user's intent, right? `www.bar.com.bar.com.` would be silly, right? Maybe. What if we are copying 100 lines of `dnsconfig.js` from one `D()` to another. Buried in the middle is this one CNAME that means something entirely different when in a new $DOMAIN. That would be bad. We've seen this in production and want to prevent this kind of error.
Yes, we could layer rule upon rule upon rule. Eventually we'd get
all the rules right. However, now a user would have to know all the
diff --git a/documentation/writing-providers.md b/documentation/writing-providers.md
index b83e83f962..129aa48537 100644
--- a/documentation/writing-providers.md
+++ b/documentation/writing-providers.md
@@ -10,6 +10,8 @@ assigned bugs related to the provider in the future (unless
you designate someone else as the maintainer). More details
[here](providers.md).
+Please follow the [DNSControl Code Style Guide](styleguide-code.md) and the [DNSControl Documentation Style Guide](styleguide-doc.md).
+
## Overview
I'll ignore all the small stuff and get to the point.
@@ -62,67 +64,48 @@ you write the DnsProvider first, release it, and then write the
Registrar if needed.
If you have any questions, please discuss them in the GitHub issue
-related to the request for this provider. Please let us know what
-was confusing so we can update this document with advice for future
-authors (or even better, update [this document](https://github.com/StackExchange/dnscontrol/blob/master/documentation/writing-providers.md)
-yourself.)
+related to the request for this provider.
-## NOTE: diff2
-
-We are in the process of changing how providers work. Sadly this document
-hasn't been fully updated yet.
+This document is constantly being updated. Please let us know what
+was confusing so we can update this document with advice for future
+authors (or even better send a PR!).
-We are in the process of changing all providers from using `pkg/diff` to
-`pkg/diff2`. diff2 is much easier to use as it does all the hard work for you.
-Providers are easier to write, there's less code for you to write, and fewer
-chances to make mistakes.
+## Step 2: Pick a base provider
-New providers only need to implement diff2. Older providers are implemented
-both ways, with a flag (`--diff2`) enabling the newer code. Soon the new code
-will become the default, then the old code will be removed.
+It's a good idea to start by copying a similar provider.
-The file `pkg/diff2/diff2.go` has instructions about how to use the new diff2 system.
-You can also do `grep diff2.By providers/*/*.go` to find providers that use
-the new system.
+How can you tell a provider is similar?
-Each DNS provider's API is different. Some update one DNS record at a time.
+Each DNS provider's API falls into one of 4 category. Some update one DNS record at a time.
Others, the only change they permit is to upload the entire zone even if only one record changed!
Others are somewhere in between: all records at a label must be updated at once, or all records
-in a RecordSet (the label + rType). diff2 provides functions for all of these situations:
-
-diff2.ByRecord() -- Updates are done one DNS record at a time. New records are added. Changes and deletes refer to an ID assigned to the record by the provider.
-diff2.ByLabel() -- Updates are done for an entire label. Adds and changes are done by sending one or more records that will appear at that label (i.e. www.example.com). Deletes delete all records at that label.
-diff2.ByRecordSet() -- Similar to ByLabel() but updates are done on the label+type level. If www.example.com has 2 A records and 2 MX records,
-
+in a RecordSet (the label + rType).
+In summary, provider APIs basically fall into four general categories:
+* Updates are done one record at a time (Record)
+* Updates are done one label at a time (Label)
+* Updates are done one label+type at a time (RecordSet)
+* Updates require the entire zone to be uploaded (Zone).
-## Step 2: Pick a base provider
+To determine your provider's category, review your API documentation.
-Pick a similar provider as your base. Providers basically fall
-into three general categories:
+To determine an existing provider's category, see which `diff2.By*()` function is used.
-NOTE: diff2 changes this. For now, you can simply run `grep diff2.By providers/*/*.go` to see which
-providers use ByZone, ByLabel, ByRecord, ByRecordSet and pick a similar provider to copy from.
+DNSControl provides 4 helper functions that do all the hard work for
+you. As input, they take the existing zone (what was downloaded via
+the API) and the desired zone (what is in `dnsconfig.js`). They
+return a list of instructions. Implement handlers for the instructions
+and DNSControl is able to perform `dnscontrol push`.
-* **zone:** The API requires you to upload the entire zone every time. (BIND, NAMECHEAP).
-* **incremental-record:** The API lets you add/change/delete individual DNS records. (CLOUDFLARE, DNSIMPLE, NAMEDOTCOM, GCLOUD, HEXONET)
-* **incremental-label:** Like incremental-record, but if there are
- multiple records on a label (for example, example www.example.com
-has A and MX records), you have to replace all the records at that
-label. (GANDI_V5)
-* **incremental-label-type:** Like incremental-record, but updates to any records at a label have to be done by type. For example, if a label (www.example.com) has many A and MX records, even the smallest change to one of the A records requires replacing all the A records. Any changes to the MX records requires replacing all the MX records. If an A record is converted to a CNAME, one must remove all the A records in one call, and add the CNAME record with another call. This is deceptively difficult to get right; if you have the choice between incremental-label-type and incremental-label, pick incremental-label. (DESEC, ROUTE53)
-* **registrar only:** These providers are registrars but do not provide DNS service. (EASYNAME, INTERNETBS, OPENSRS)
+The functions are:
-All DNS providers use the "diff" module to detect differences. It takes
-two zones and returns records that are unchanged, created, deleted,
-and modified.
-The zone providers use the
-information to print a human-readable list of what is being changed,
-but upload the entire new zone.
-The incremental providers use the differences to
-update individual records or recordsets.
+* diff2.ByRecord() -- Updates are done one DNS record at a time. New records are added. Changes and deletes refer to an ID assigned to the record by the provider.
+* diff2.ByLabel() -- Updates are done for an entire label. Adds and changes are done by sending one or more records that will appear at that label (i.e. www.example.com). Deletes delete all records at that label.
+* diff2.ByRecordSet() -- Similar to ByLabel() but updates are done on the label+type level. If www.example.com has 2 A records and 2 MX records, updates must replace all the A records, or all the MX records, or add records of a different type.
+* diff2.ByZone() -- Updates are done by uploading the entire zone every time.
+The file `pkg/diff2/diff2.go` has instructions about how to use the diff2 system.
## Step 3: Create the driver skeleton
@@ -134,11 +117,12 @@ The main driver should be called `providers/name/nameProvider.go`.
The API abstraction is usually in a separate file (often called
`api.go`).
+Directory names should be consitent. It should be all lowercase and match the ALLCAPS provider name. Avoid `_`s.
## Step 4: Activate the driver
Edit
-[providers/\_all/all.go](https://github.com/StackExchange/dnscontrol/blob/master/providers/_all/all.go).
+[providers/\_all/all.go](https://github.com/StackExchange/dnscontrol/blob/main/providers/_all/all.go).
Add the provider list so DNSControl knows it exists.
## Step 5: Implement
@@ -170,38 +154,42 @@ Run the unit tests with this command:
go test ./...
-
## Step 7: Integration Test
This is the most important kind of testing when adding a new provider.
-Integration tests use a test account and a real domain.
+Integration tests use a test account and a test domain.
+
+{% hint style="danger" %}
+All records will be deleted from the test domain! Use a OTE domain or a real domain that isn't otherwise in use and can be destroyed.
+{% endhint %}
+
+* Edit [integrationTest/providers.json](https://github.com/StackExchange/dnscontrol/blob/main/integrationTest/providers.json):
+ * Add the `creds.json` info required for this provider in the form of environment variables.
-* Edit [integrationTest/providers.json](https://github.com/StackExchange/dnscontrol/blob/master/integrationTest/providers.json): Add the `creds.json` info required for this provider.
+Now you can run the integration tests.
-For example, this will run the tests using BIND:
+For example, test BIND:
```shell
-cd integrationTest/
+cd integrationTest # NOTE: Not needed if already there
+export BIND_DOMAIN='example.com'
go test -v -verbose -provider BIND
```
-(BIND is a good place to start since it doesn't require any API keys.)
+(BIND is a good place to start since it doesn't require API keys.)
This will run the tests on Amazon AWS Route53:
```shell
-export R53_DOMAIN=dnscontroltest-r53.com # Use a test domain.
+export R53_DOMAIN='dnscontroltest-r53.com' # Use a test domain.
export R53_KEY_ID='CHANGE_TO_THE_ID'
export R53_KEY='CHANGE_TO_THE_KEY'
+cd integrationTest # NOTE: Not needed if already there
go test -v -verbose -provider ROUTE53
```
Some useful `go test` flags:
-* Slow tests? Add `-timeout n` to increase the timeout for tests
- * `go test` kills the tests after 10 minutes by default. Some providers need more time.
- * This flag must be *before* the `-verbose` flag. Usually it is the first flag after `go test`.
- * Example: `go test -timeout 20m -v -verbose -provider CLOUDFLAREAPI`
* Run only certain tests using the `-start` and `-end` flags.
* Rather than running all the tests, run just the tests you want.
* These flags must be *after* the `-provider FOO` flag.
@@ -209,34 +197,40 @@ Some useful `go test` flags:
* Example: `go test -v -verbose -provider ROUTE53 -start 5 -end 5` runs only test 5.
* Example: `go test -v -verbose -provider ROUTE53 -start 20` skip the first 19 tests.
* Example: `go test -v -verbose -provider ROUTE53 -end 20` only run the first 20 tests.
+* Slow tests? Add `-timeout n` to increase the timeout for tests
+ * `go test` kills the tests after 10 minutes by default. Some providers need more time.
+ * This flag must be *before* the `-verbose` flag. Usually it is the first flag after `go test`.
+ * Example: `go test -timeout 20m -v -verbose -provider CLOUDFLAREAPI`
* If a test will always fail because the provider doesn't support the feature, you can opt out of the test. Look at `func makeTests()` in [integrationTest/integration_test.go](https://github.com/StackExchange/dnscontrol/blob/2f65533e1b92c2967229a92a304fff7c14f7f4b6/integrationTest/integration_test.go#L675) for more details.
-
## Step 8: Manual tests
+This is optional.
+
There is a potential bug in how TXT records are handled. Sadly we haven't found
an automated way to test for this bug. The manual steps are here in
[documentation/testing-txt-records.md](testing-txt-records.md)
+## Step 9: Update docs, CICD and other files
-## Step 9: Update docs
+* Edit `README.md`:
+ * Add the provider to the bullet list.
+* Edit `.github/workflows/pr_test.yml`
+ * Add the name of the provider to the PROVIDERS list.
+* Edit `documentation/providers.md`:
+ * Remove the provider from the `Requested providers` list (near the end of the doc) (if needed).
+ * Add the new provider to the [Providers with "contributor support"](providers.md#providers-with-contributor-support) section.
+* Edit `documentation/SUMMARY.md`:
+ * Add the provider to the "Providers" list.
+* Create `documentation/provider/PROVIDERNAME.md`:
+ * Use one of the other files in that directory as a base.
-* Edit [README.md](https://github.com/StackExchange/dnscontrol): Add the provider to the bullet list.
-* Edit [documentation/providers.md](https://github.com/StackExchange/dnscontrol/blob/master/documentation/providers.md): Add the provider to the provider list.
-* Edit [documentation/SUMMARY.md](https://github.com/StackExchange/dnscontrol/blob/master/documentation/SUMMARY.md): Add the provider to the provider list.
-* Create `documentation/providers/PROVIDERNAME.md`: Use one of the other files in that directory as a base.
-* Edit [OWNERS](https://github.com/StackExchange/dnscontrol/blob/master/OWNERS): Add the directory name and your GitHub username.
+{% hint style="success" %}
+**Need feedback?** Submit a draft PR! It's a great way to get early feedback, ask about fixing
+a particular integration test, or request feedback.
+{% endhint %}
-## Step 10: Submit a PR
-
-At this point you can submit a PR.
-
-Actually you can submit the PR even earlier if you just want feedback,
-input, or have questions. This is just a good stopping place to
-submit a PR if you haven't already.
-
-
-## Step 11: Capabilities
+## Step 10: Capabilities
Some DNS providers have features that others do not. For example some
support the SRV record. A provider announces what it can do using
@@ -269,10 +263,9 @@ you want to implement.
FYI: If a provider's capabilities changes, run `go generate` to update
the documentation.
+## Step 11: Automated code tests
-## Step 12: Clean up
-
-Run "go vet" and ["staticcheck"](https://staticcheck.io/) and clean up any errors found.
+Run `go vet` and [`staticcheck`](https://staticcheck.io/) and clean up any errors found.
```shell
go vet ./...
@@ -294,30 +287,99 @@ go install golang.org/x/lint/golint
golint ./...
```
-
-## Step 13: Dependencies
+## Step 12: Dependencies
See [documentation/release-engineering.md](release-engineering.md)
for tips about managing modules and checking for outdated
dependencies.
-
-## Step 14: Modify the release regexp
+## Step 13: Modify the release regexp
In the repo root, open `.goreleaser.yml` and add the provider to `Provider-specific changes` regexp.
+## Step 14: Update `pr_test.yml`
+
+This assures that in the future it will be easy to test this provider using GitHub Actions.
+
+Edit `.github/workflows/pr_test.yml`
+
+* Add the name of the provider to the PROVIDERS list.
+* Please keep this list sorted alphabetically.
+
+The entry looks something like:
+
+{% code title=".github/workflows/pr_test.yml" %}
+```yaml
+ env:
+ PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','HUAWEICLOUD','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']"
+ ENV_CONTEXT: ${{ toJson(env) }}
+```
+{% endcode %}
+
+2. Add your providers `_DOMAIN` env variable:
+
+* Add it to the `env` section of `integration-tests`.
+* Please keep this list sorted alphabetically.
+
+To find this section, search for `PROVIDER SECRET LIST`.
+
+For example, the entry for BIND looks like:
+
+{% code title=".github/workflows/pr_test.yml" %}
+```
+ BIND_DOMAIN: ${{ vars.BIND_DOMAIN }}
+```
+{% endcode %}
+
+3. Add your providers other ENV variables:
+
+Every provider requires different variables set to perform the integration tests. The list of such variables is in `integrationTest/providers.json`.
+
+You've already added `*_DOMAIN` to `pr_test.yml`. Now we're going to add the remaining ones.
+
+To find this section, search for `PROVIDER SECRET LIST`.
+
+For example, the entry for CLOUDFLAREAPI looks like this:
+
+{% code title=".github/workflows/pr_test.yml" %}
+```
+ CLOUDFLAREAPI_ACCOUNTID: ${{ secrets.CLOUDFLAREAPI_ACCOUNTID }}
+ CLOUDFLAREAPI_TOKEN: ${{ secrets.CLOUDFLAREAPI_TOKEN }}
+```
+{% endcode %}
## Step 15: Check your work
-Here are some last-minute things to check before you submit your PR.
+These are the things we'll be checking when you submit the PR. Please try to complete all or as many of these as possible.
+
+1. Run `go generate ./...` to make sure all generated files are fresh.
+2. Make sure the following files were created and/or updated:
+ * `OWNERS`
+ * `README.md`
+ * `.github/workflows/pr_test.yml` (The PROVIDERS list)
+ * `.goreleaser.yml` (Search for `Provider-specific changes`)
+ * `documentation/SUMMARY.md`
+ * `documentation/providers.md` (the autogenerated table + the second one; make sure it is removed from the `requested` list)
+ * `documentation/provider/`PROVIDERNAME`.md`
+ * `integrationTest/providers.json`
+ * `providers/_all/all.go`
+3. Review the code for style issues, remove debug statements, make sure all exported functions have a comment, and generally tighten up the code.
+4. Verify you're using the most recent version of anything you import. (See [Step 12](#step-12-dependencies))
+5. Re-run the [integration test](#step-7-integration-test) one last time.
+ * Post the results as a comment to your PR.
+6. Re-read the [maintainer's responsibilities](providers.md#providers-with-contributor-support) bullet list. By submitting a provider you agree to maintain it, respond to bugs, periodically re-run the integration test to verify nothing has broken, and if we don't hear from you for 2 months we may disable the provider.
+
+## Step 15: Submit a PR
+
+At this point you can submit a PR.
+
+The PR should include the sentence: "Please create the GitHub label 'provider-PROVIDERNAME'" (change `PROVIDERNAME` to the name of your provider.) This is
+
+Actually you can submit the PR earlier if you just want feedback,
+or have questions. However if you haven't submitted a PR by now, this is the time to do it.
-1. Run `go generate` to make sure all generated files are fresh.
-2. Make sure all appropriate documentation is current. (See [Step 8](#step-8-manual-tests))
-3. Check that dependencies are current (See [Step 13](#step-13-dependencies))
-4. Re-run the integration test one last time (See [Step 7](#step-7-integration-test))
-5. Re-read the [maintainer's responsibilities](providers.md) bullet list. By submitting a provider you agree to maintain it, respond to bugs, perioidically re-run the integration test to verify nothing has broken, and if we don't hear from you for 2 months we may disable the provider.
## Step 16: After the PR is merged
-1. Remove the "provider-request" label from the PR.
-2. Verify that [documentation/providers.md](providers.md) no longer shows the provider as "requested"
+1. Close any related GitHub issues.
+3. Would you like your provider to be tested automatically as part of every PR? Sure you would! Follow the instructions in [Bring-Your-Own-Secrets for automated testing](byo-secrets.md)
diff --git a/go.mod b/go.mod
index dcc4ced70e..48e97bd3de 100644
--- a/go.mod
+++ b/go.mod
@@ -1,152 +1,167 @@
module github.com/StackExchange/dnscontrol/v4
-go 1.21
+go 1.23.0
-toolchain go1.21.1
+retract v4.8.0
+
+require google.golang.org/protobuf v1.35.2 // indirect
+
+require golang.org/x/net v0.33.0
require (
- github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
- github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7
- github.com/PuerkitoBio/goquery v1.8.1
+ github.com/PuerkitoBio/goquery v1.10.0
github.com/TomOnTime/utfutil v0.0.0-20230223141146-125e65197b36
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
- github.com/aws/aws-sdk-go-v2 v1.21.2
- github.com/aws/aws-sdk-go-v2/config v1.19.0
- github.com/aws/aws-sdk-go-v2/credentials v1.13.43
- github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2
- github.com/aws/aws-sdk-go-v2/service/route53domains v1.17.5
+ github.com/aws/aws-sdk-go-v2 v1.32.6
+ github.com/aws/aws-sdk-go-v2/config v1.28.6
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.47
+ github.com/aws/aws-sdk-go-v2/service/route53 v1.46.3
+ github.com/aws/aws-sdk-go-v2/service/route53domains v1.28.0
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
- github.com/bhendo/go-powershell v0.0.0-20190719160123-219e7fb4e41e
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9
- github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v3 v3.5.5
- github.com/cloudflare/cloudflare-go v0.79.0
- github.com/digitalocean/godo v1.105.0
+ github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7
+ github.com/cloudflare/cloudflare-go v0.111.0
+ github.com/digitalocean/godo v1.131.1
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
- github.com/dnsimple/dnsimple-go v1.2.0
- github.com/exoscale/egoscale v0.90.2
- github.com/go-acme/lego v2.7.2+incompatible
- github.com/go-gandi/go-gandi v0.6.0
+ github.com/dnsimple/dnsimple-go v1.7.0
+ github.com/exoscale/egoscale v0.102.4
+ github.com/go-gandi/go-gandi v0.7.0
github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe
- github.com/google/go-github/v35 v35.3.0
github.com/gopherjs/jquery v0.0.0-20191017083323-73f4c7416038
- github.com/hashicorp/vault/api v1.10.0
- github.com/jarcoal/httpmock v1.0.8 // indirect
+ github.com/hashicorp/vault/api v1.15.0
github.com/jinzhu/copier v0.4.0
- github.com/miekg/dns v1.1.56
- github.com/mittwald/go-powerdns v0.6.2
+ github.com/miekg/dns v1.1.62
+ github.com/mittwald/go-powerdns v0.6.6
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04
- github.com/nrdcg/goinwx v0.9.0
- github.com/oracle/oci-go-sdk/v32 v32.0.0
- github.com/ovh/go-ovh v1.1.0
+ github.com/nrdcg/goinwx v0.10.0
+ github.com/ovh/go-ovh v1.6.0
github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494
- github.com/robertkrimen/otto v0.2.1
- github.com/softlayer/softlayer-go v1.1.2
- github.com/stretchr/testify v1.8.4
- github.com/transip/gotransip/v6 v6.22.0
- github.com/urfave/cli/v2 v2.25.7
+ github.com/robertkrimen/otto v0.5.1
+ github.com/softlayer/softlayer-go v1.1.7
+ github.com/stretchr/testify v1.10.0
+ github.com/transip/gotransip/v6 v6.26.0
+ github.com/urfave/cli/v2 v2.27.5
github.com/xddxdd/ottoext v0.0.0-20221109171055-210517fa4419
- golang.org/x/net v0.17.0
- golang.org/x/oauth2 v0.13.0
- google.golang.org/api v0.148.0
- gopkg.in/ns1/ns1-go.v2 v2.7.13
+ golang.org/x/crypto v0.31.0 // indirect
+ golang.org/x/oauth2 v0.24.0
+ google.golang.org/api v0.212.0
+ gopkg.in/ns1/ns1-go.v2 v2.13.0
)
require (
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0
- github.com/G-Core/gcore-dns-sdk-go v0.2.6
- github.com/fatih/color v1.15.0
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
+ github.com/G-Core/gcore-dns-sdk-go v0.2.9
+ github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.3
+ github.com/containrrr/shoutrrr v0.8.0
+ github.com/fatih/color v1.18.0
github.com/fbiville/markdown-table-formatter v0.3.0
+ github.com/go-acme/lego/v4 v4.20.4
+ github.com/google/go-cmp v0.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
+ github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.127
+ github.com/juju/errors v1.0.0
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-isatty v0.0.20
+ github.com/oracle/oci-go-sdk/v65 v65.80.0
github.com/vultr/govultr/v2 v2.17.2
- golang.org/x/exp v0.0.0-20231006140011-7918f672742d
- golang.org/x/text v0.13.0
+ golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e
+ golang.org/x/text v0.21.0
+ golang.org/x/time v0.8.0
gopkg.in/yaml.v3 v3.0.1
)
require (
- cloud.google.com/go/compute v1.23.0 // indirect
- cloud.google.com/go/compute/metadata v0.2.3 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
+ cloud.google.com/go/auth v0.13.0 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
+ cloud.google.com/go/compute/metadata v0.6.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
- github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
- github.com/andybalholm/cascadia v1.3.1 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect
- github.com/aws/smithy-go v1.15.0 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
+ github.com/andybalholm/cascadia v1.3.2 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect
+ github.com/aws/smithy-go v1.22.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
- github.com/cenkalti/backoff v2.2.1+incompatible // indirect
- github.com/cenkalti/backoff/v3 v3.0.0 // indirect
- github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
- github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
- github.com/go-jose/go-jose/v3 v3.0.0 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.4 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-test/deep v1.0.3 // indirect
- github.com/goccy/go-json v0.10.2 // indirect
- github.com/gofrs/uuid v4.0.0+incompatible // indirect
- github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
- github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/protobuf v1.5.3 // indirect
+ github.com/goccy/go-json v0.10.3 // indirect
+ github.com/gofrs/flock v0.12.1 // indirect
+ github.com/gofrs/uuid v4.4.0+incompatible // indirect
+ github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
- github.com/google/s2a-go v0.1.7 // indirect
- github.com/google/uuid v1.3.1 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
- github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+ github.com/google/s2a-go v0.1.8 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
+ github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
- github.com/hashicorp/go-retryablehttp v0.7.4 // indirect
+ github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
- github.com/juju/errors v0.0.0-20200330140219-3fe23663418f // indirect
- github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/peterhellberg/link v1.1.0 // indirect
- github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
- github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/shopspring/decimal v1.3.1 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/smartystreets/assertions v1.2.0 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
- github.com/stretchr/objx v0.5.0 // indirect
- github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
- go.opencensus.io v0.24.0 // indirect
- golang.org/x/crypto v0.14.0 // indirect
- golang.org/x/mod v0.13.0 // indirect
- golang.org/x/sync v0.4.0 // indirect
- golang.org/x/sys v0.13.0 // indirect
- golang.org/x/time v0.3.0 // indirect
- golang.org/x/tools v0.14.0 // indirect
- google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect
- google.golang.org/grpc v1.58.3 // indirect
- google.golang.org/protobuf v1.31.0 // indirect
- gopkg.in/ini.v1 v1.66.6 // indirect
+ github.com/sony/gobreaker v0.5.0 // indirect
+ github.com/stretchr/objx v0.5.2 // indirect
+ github.com/tjfoc/gmsm v1.4.1 // indirect
+ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
+ go.mongodb.org/mongo-driver v1.12.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
+ go.opentelemetry.io/otel v1.29.0 // indirect
+ go.opentelemetry.io/otel/metric v1.29.0 // indirect
+ go.opentelemetry.io/otel/trace v1.29.0 // indirect
+ golang.org/x/mod v0.22.0 // indirect
+ golang.org/x/sync v0.10.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/tools v0.28.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect
+ google.golang.org/grpc v1.67.1 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
- gopkg.in/square/go-jose.v2 v2.5.1 // indirect
moul.io/http2curl v1.0.0 // indirect
)
diff --git a/go.sum b/go.sum
index a90b47734f..baeed00588 100644
--- a/go.sum
+++ b/go.sum
@@ -1,132 +1,156 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
-cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 h1:9kDVnTz3vbfweTqAUmk/a/pH5pWFCHtvRpHYC0G/dcA=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
-github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 h1:8iR6OLffWWorFdzL2JFCab5xpD8VKEE2DUBBl+HNTDY=
-github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0/go.mod h1:copqlcjMWc/wgQ1N2fzsJFQxDdqKGg1EQt8T5wJMOGE=
+cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
+cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
+cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
+cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7 h1:AJKJCKcb/psppPl/9CUiQQnTG+Bce0/cIweD5w5Q7aQ=
github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI=
-github.com/G-Core/gcore-dns-sdk-go v0.2.6 h1:R82ANd7BnhIe2mV/12Ebdx9QYVl2++E4kfDIu97JEOk=
-github.com/G-Core/gcore-dns-sdk-go v0.2.6/go.mod h1:KliUjfPonDvXyAGNiuO+MYVG/7lmWHZ+4Hi0sPxgOjg=
-github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
-github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
+github.com/G-Core/gcore-dns-sdk-go v0.2.9 h1:LMMZIRX8y3aJJuAviNSpFmLbovZUw+6Om+8VElp1F90=
+github.com/G-Core/gcore-dns-sdk-go v0.2.9/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
+github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
+github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/TomOnTime/utfutil v0.0.0-20230223141146-125e65197b36 h1:vfVc5pSCq58ljNpXXwUcLnHATYi/x+YUdqFc9uBhLbM=
github.com/TomOnTime/utfutil v0.0.0-20230223141146-125e65197b36/go.mod h1:MwE/QxFCN65F0hKGWFHUh2U2o1p2tMPNR1zHkX6vEh8=
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY=
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
-github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
-github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
+github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
+github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA=
-github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM=
-github.com/aws/aws-sdk-go-v2/config v1.19.0 h1:AdzDvwH6dWuVARCl3RTLGRc4Ogy+N7yLFxVxXe1ClQ0=
-github.com/aws/aws-sdk-go-v2/config v1.19.0/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2 h1:/RPQNjh1sDIezpXaFIkZb7MlXnSyAqjVdAwcJuGYTqg=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2/go.mod h1:TQZBt/WaQy+zTHoW++rnl8JBrmZ0VO6EUbVua1+foCA=
-github.com/aws/aws-sdk-go-v2/service/route53domains v1.17.5 h1:18p6+HD6xj5oOtDNi1XvW0PkZw/xticjOvSbIWFyw6I=
-github.com/aws/aws-sdk-go-v2/service/route53domains v1.17.5/go.mod h1:BhMj1pZPuQfzuS96s4ScniYr9qhPwDMA19N4eWPM1Lg=
-github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k=
-github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg=
-github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU=
-github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ=
-github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8=
-github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
+github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4=
+github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
+github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo=
+github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.46.3 h1:pDBrvz7CMK381q5U+nPqtSQZZid5z1XH8lsI6kHNcSY=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.46.3/go.mod h1:rDMeB13C/RS0/zw68RQD4LLiWChf5tZBKjEQmjtHa/c=
+github.com/aws/aws-sdk-go-v2/service/route53domains v1.28.0 h1:D7xRgsQnKiC/BQOVfG9DrsdRo2PIPwCwgHb6EpKLjFQ=
+github.com/aws/aws-sdk-go-v2/service/route53domains v1.28.0/go.mod h1:c4Zcr9bz35UJ07RC3cR/zBdpFn7ZjZqy/ow+oN0+NEo=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8=
+github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
+github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bhendo/go-powershell v0.0.0-20190719160123-219e7fb4e41e h1:KCjb01YiNoRaJ5c+SbnPLWjVzU9vqRYHg3e5JcN50nM=
-github.com/bhendo/go-powershell v0.0.0-20190719160123-219e7fb4e41e/go.mod h1:f7vw6ObmmNcyFQLhZX9eUGBJGpnwTJFDvVjqZxIxHWY=
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9 h1:2vQTbEJvFsyd1VefzZ34GUkUD6TkJleYYJh9/25WBE4=
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9/go.mod h1:bqqNsI2akL+lLWyApkYY0cxquWPKwEBU0Wd3chi3TEg=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
-github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
-github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
-github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
-github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v3 v3.5.5 h1:awE2kiwdJa409MC5i3OH9fJHCr2yE75yHuWWoA5zx8Q=
-github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v3 v3.5.5/go.mod h1:1usm1EQvugrIio3ODIAMrDG9NzA86AHIqhZCxJgNdxY=
+github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7 h1:Jk7uhY5q11fE5PlEupX2Lo12w82UhGC6bE1CI5jwFbc=
+github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7/go.mod h1:FnQtD0+Q/1NZxi0eEWN+3ZRyMsE9vzSB3YjyunkbKD0=
+github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.3 h1:SNG47Lpt7bVp6p479hU4OK/3yiwuOTmBOQ0reC/VR+Q=
+github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.3/go.mod h1:OpHRysfX3AwVJdry9iiFq+zCp25cLl9juonwxssfn6U=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cloudflare/cloudflare-go v0.79.0 h1:ErwCYDjFCYppDJlDJ/5WhsSmzegAUe2+K9qgFyQDg3M=
-github.com/cloudflare/cloudflare-go v0.79.0/go.mod h1:gkHQf9xEubaQPEuerBuoinR9P8bf8a05Lq0X6WKy1Oc=
+github.com/cloudflare/cloudflare-go v0.111.0 h1:bFgl5OyR7iaV9DkTaoI2jU8X4rXDzEaFDaPfMTp+Ewo=
+github.com/cloudflare/cloudflare-go v0.111.0/go.mod h1:w5c4Vm00JjZM+W0mPi6QOC+eWLncGQPURtgDck3z5xU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
+github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
+github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
+github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/deepmap/oapi-codegen v1.9.1 h1:yHmEnA7jSTUMQgV+uN02WpZtwHnz2CBW3mZRIxr1vtI=
github.com/deepmap/oapi-codegen v1.9.1/go.mod h1:PLqNAhdedP8ttRpBBkzLKU3bp+Fpy+tTgeAMlztR2cw=
-github.com/digitalocean/godo v1.105.0 h1:bUfWVsyQCYZ7OQLK+p2EBFYWD5BoOgpyq/PMSQHEeMg=
-github.com/digitalocean/godo v1.105.0/go.mod h1:R6EmmWI8CT1+fCtjWY9UCB+L5uufuZH13wk3YhxycCs=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/digitalocean/godo v1.131.1 h1:2QsRwjNukKgOQbflMxOsTDoC05o5UKBpqQMFKXegYKE=
+github.com/digitalocean/godo v1.131.1/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9GH0RoeVZQKzFJcTLoAixx5s5Gq3pTIS+n354=
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE=
-github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
-github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
-github.com/dnsimple/dnsimple-go v1.2.0 h1:ddTGyLVKly5HKb5L65AkLqFqwZlWo3WnR0BlFZlIddM=
-github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U=
+github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
+github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/exoscale/egoscale v0.90.2 h1:oGSJy5Dxbcn5m5F0/DcnU4WXJg+2j3g+UgEu4yyKG9M=
-github.com/exoscale/egoscale v0.90.2/go.mod h1:NDhQbdGNKwnLVC2YGTB6ds9WIPw+V5ckvEEV8ho7pFE=
+github.com/exoscale/egoscale v0.102.4 h1:GBKsZMIOzwBfSu+4ZmWka3Ejf2JLiaBDHp4CQUgvp2E=
+github.com/exoscale/egoscale v0.102.4/go.mod h1:ROSmPtle0wvf91iLZb09++N/9BH2Jo9XxIpAEumvocA=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
-github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNYvwXQMMMIKNZop2SSho=
github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
-github.com/go-acme/lego v2.7.2+incompatible h1:ThhpPBgf6oa9X/vRd0kEmWOsX7+vmYdckmGZSb+FEp0=
-github.com/go-acme/lego v2.7.2+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
+github.com/go-acme/lego/v4 v4.20.4 h1:yCQGBX9jOfMbriEQUocdYm7EBapdTp8nLXYG8k6SqSU=
+github.com/go-acme/lego/v4 v4.20.4/go.mod h1:foauPlhnhoq8WUphaWx5U04uDc+JGhk4ZZtPz/Vqsjg=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
-github.com/go-gandi/go-gandi v0.6.0 h1:RgFoevggRRp7hF9XsOmWmtwbUg2axhe2ygEdd6Mtstc=
-github.com/go-gandi/go-gandi v0.6.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw=
-github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
-github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-gandi/go-gandi v0.7.0 h1:gsP33dUspsN1M+ZW9HEgHchK9HiaSkYnltO73RHhSZA=
+github.com/go-gandi/go-gandi v0.7.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw=
+github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
+github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -136,25 +160,26 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe h1:zn8tqiUbec4wR94o7Qj3LZCAT6uGobhEgnDRg6isG5U=
github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
-github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
+github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
+github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
-github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-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=
@@ -162,43 +187,38 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/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/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
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.3/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.8/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/v35 v35.3.0 h1:fU+WBzuukn0VssbayTT+Zo3/ESKX9JYWjbZTLOTEyho=
-github.com/google/go-github/v35 v35.3.0/go.mod h1:yWB7uCcVWaUbUP74Aq3whuMySRMatyRmq5U9FTNlbio=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
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/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
-github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
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.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
-github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ=
-github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
-github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
-github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
+github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
@@ -212,14 +232,13 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
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.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
-github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
-github.com/hashicorp/go-hclog v1.2.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-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
-github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
+github.com/hashicorp/go-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.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
@@ -231,10 +250,12 @@ github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0S
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
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/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ=
-github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8=
-github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
-github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
+github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
+github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
+github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.127 h1:TOGDOGmY7YOzTSkFDIx0nxEF7fxpqiFNYvSxuSPGaC4=
+github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.127/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
+github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
+github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -242,44 +263,23 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/juju/ansiterm v0.0.0-20160907234532-b99631de12cf/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
-github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
-github.com/juju/cmd v0.0.0-20171107070456-e74f39857ca0/go.mod h1:yWJQHl73rdSX4DHVKGqkAip+huBslxRwS8m9CrOLq18=
-github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY=
-github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
-github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM=
-github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
-github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
-github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767/go.mod h1:+MaLYz4PumRkkyHYeXJ2G5g5cIW0sli2bOfpmbaMV/g=
-github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
-github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e h1:FdDd7bdI6cjq5vaoYlK1mfQYfF9sF2VZw8VEZMsl5t8=
-github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
-github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208 h1:/WiCm+Vpj87e4QWuWwPD/bNE9kDrWCLvPBHOQNcG2+A=
-github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg=
-github.com/juju/mutex v0.0.0-20171110020013-1fe2a4bf0a3a/go.mod h1:Y3oOzHH8CQ0Ppt0oCKJ2JFO81/EsWenH5AEqigLH+yY=
-github.com/juju/retry v0.0.0-20151029024821-62c620325291/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
-github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
-github.com/juju/testing v0.0.0-20180402130637-44801989f0f7/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
-github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
-github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098 h1:yrhek184cGp0IRyHg0uV1khLaorNg6GtDLkry4oNNjE=
-github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM=
-github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
-github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
-github.com/juju/utils/v2 v2.0.0-20200923005554-4646bfea2ef1/go.mod h1:fdlDtQlzundleLLz/ggoYinEt/LmnrpNKcNTABQATNI=
-github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
-github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
-github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
-github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
+github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
+github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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=
@@ -297,28 +297,23 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
-github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/masterzen/azure-sdk-for-go v3.2.0-beta.0.20161014135628-ee4f0065d00c+incompatible/go.mod h1:mf8fjOu33zCqxUjuiU3I8S1lJMyEAlH+0F2+M5xl3hE=
-github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
-github.com/masterzen/winrm v0.0.0-20161014151040-7a535cd943fc/go.mod h1:CfZSN7zwz5gJiFhZJz49Uzk7mEBHIceWmbFmYx7Hf7E=
-github.com/masterzen/xmlpath v0.0.0-20140218185901-13f4951698ad/go.mod h1:A0zPC53iKKKcXYxr4ROjpQRQ5FgJXtelNdSmHHuq/tY=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
-github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.11/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.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
-github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
+github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
+github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
+github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
+github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -326,49 +321,57 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb
github.com/mitchellh/mapstructure v1.4.1/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/mittwald/go-powerdns v0.6.2 h1:jNZoW5vPzsQvUQ3BzXEWteHPWoJ1GwcHyF4mSh4YZ7Y=
-github.com/mittwald/go-powerdns v0.6.2/go.mod h1:adWJ860laOgm14afg+7V0nCa5NQT37oEYe2HRhoS/CA=
+github.com/mittwald/go-powerdns v0.6.6 h1:yQcuszhl98+jJgELjD5ecfxCQWoshhnArexpwrwQxLY=
+github.com/mittwald/go-powerdns v0.6.6/go.mod h1:adWJ860laOgm14afg+7V0nCa5NQT37oEYe2HRhoS/CA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/nrdcg/goinwx v0.9.0 h1:oh+yPdRDwc1IWsAU2nfsNorI/fkR+Gxm4O7yS+0NnjM=
-github.com/nrdcg/goinwx v0.9.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4=
-github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
-github.com/oracle/oci-go-sdk/v32 v32.0.0 h1:SSbzrQO3WRcPJEZ8+b3SFPYsPtkFM96clqrp03lrwbU=
-github.com/oracle/oci-go-sdk/v32 v32.0.0/go.mod h1:aZc4jC59IuNP3cr5y1nj555QvwojMX2nMJaBiozuuEs=
-github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
-github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
+github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM=
+github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4=
+github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
+github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
+github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
+github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
+github.com/oracle/oci-go-sdk/v65 v65.80.0 h1:Rr7QLMozd2DfDBKo6AB3DzLYQxAwuOG118+K5AAD5E8=
+github.com/oracle/oci-go-sdk/v65 v65.80.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
+github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
+github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/peterhellberg/link v1.1.0 h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu5kc=
github.com/peterhellberg/link v1.1.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8=
github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d h1:nf4+lHs8TQeIGFYZMcNg4iQOnZndLfYxnQaKEdqHVA4=
github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d/go.mod h1:VDIGNBy0tHXMDAu4gxHFQJDq3NuwqUxw2Kok7wi+6ck=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
+github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
+github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
-github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
-github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
+github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
+github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
-github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
+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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -376,23 +379,29 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+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.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
+github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
-github.com/softlayer/softlayer-go v1.1.2 h1:rUSSGCyaxymvTOsaFjwr+cGxA8muw3xg2LSrIMNcN/c=
-github.com/softlayer/softlayer-go v1.1.2/go.mod h1:hvAbzGH4LRXA6yXY8BNx99yoqZ7urfDdtl9mvBf0G+g=
+github.com/softlayer/softlayer-go v1.1.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE=
+github.com/softlayer/softlayer-go v1.1.7/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw=
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
+github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
+github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
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/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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=
@@ -401,85 +410,102 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/transip/gotransip/v6 v6.22.0 h1:22nRVa0wQwCakRB4zlCVBgexEACBnvLjYuvDRm31q3w=
-github.com/transip/gotransip/v6 v6.22.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
+github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
+github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=
+github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
-github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
-github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
+github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
github.com/xddxdd/ottoext v0.0.0-20221109171055-210517fa4419 h1:PT5KYEimicg1GRkBtBxCLcHWvMcBRGljOLwG/y4+T5c=
github.com/xddxdd/ottoext v0.0.0-20221109171055-210517fa4419/go.mod h1:BxZUa1xZ189Ww28wRT0LjHcmHgQmPh27hqfHIwET0ok=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
-github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
-github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
-go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
+go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
+go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
+go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
+go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4=
+golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/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.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
-golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
-golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
-golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -492,7 +518,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210616045830-e2b7044e8c71/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-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -501,14 +526,23 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
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.1.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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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=
@@ -516,12 +550,15 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
+golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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=
@@ -532,77 +569,58 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
-golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
+golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
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/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs=
-google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU=
+google.golang.org/api v0.212.0 h1:BcRj3MJfHF3FYD29rk7u9kuu1SyfGqfHcA0hSwKqkHg=
+google.golang.org/api v0.212.0/go.mod h1:gICpLlpp12/E8mycRMzgy3SQ9cFh2XnVJ6vJi/kQbvI=
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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0=
-google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk=
-google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
-google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a h1:a2MQQVoTo96JC9PMGtGBymLp7+/RzpFc2yX/9WfFg1c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0=
+google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
+google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
+google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
-google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+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 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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
+google.golang.org/protobuf v1.35.2/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-20160105164936-4f90aeace3a2/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/errgo.v1 v1.0.0-20161222125816-442357a80af5/go.mod h1:u0ALmqvLRxLI95fkdCEWrE6mhWYZW1aMOJHp5YXLHTg=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
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/httprequest.v1 v1.1.1/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
-gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
-gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
-gopkg.in/ns1/ns1-go.v2 v2.7.13 h1:r07CLALg18f/L1KIK1ZJdbirBV349UtYT1rDWGjnaTk=
-gopkg.in/ns1/ns1-go.v2 v2.7.13/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ns1/ns1-go.v2 v2.13.0 h1:I5NNqI9Bi1SGK92TVkOvLTwux5LNrix/99H2datVh48=
+gopkg.in/ns1/ns1-go.v2 v2.13.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375/go.mod h1:lNEQeAhU009zbRxng+XOj5ITVgY24WcbNnQopyfKoYQ=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
-gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
-gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
-gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.2/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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -615,7 +633,5 @@ 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=
-launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=
-launchpad.net/xmlpath v0.0.0-20130614043138-000000000004/go.mod h1:vqyExLOM3qBx7mvYRkoxjSCF945s0mbe7YynlKYXtsA=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go
index 046aa6926d..4b7039c999 100644
--- a/integrationTest/integration_test.go
+++ b/integrationTest/integration_test.go
@@ -5,7 +5,6 @@ import (
"flag"
"fmt"
"os"
- "strconv"
"strings"
"testing"
"time"
@@ -17,6 +16,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/providers"
_ "github.com/StackExchange/dnscontrol/v4/providers/_all"
"github.com/StackExchange/dnscontrol/v4/providers/cloudflare"
+ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
"github.com/miekg/dns/dnsutil"
)
@@ -26,6 +26,7 @@ var endIdx = flag.Int("end", -1, "Test index to stop after")
var verbose = flag.Bool("verbose", false, "Print corrections as you run them")
var printElapsed = flag.Bool("elapsed", false, "Print elapsed time for each testgroup")
var enableCFWorkers = flag.Bool("cfworkers", true, "Set false to disable CF worker tests")
+var enableCFRedirectMode = flag.String("cfredirect", "", "cloudflare pagerule tests: default=page_rules, c=convert old to enw, n=new-style, o=none")
func init() {
testing.Init()
@@ -45,48 +46,48 @@ func CfCProxyFull() *TestCase { return tc("cproxyf", cfProxyCNAME("cproxy", "exa
// ---
-func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[int]bool, map[string]string) {
+func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[string]string) {
if *providerToRun == "" {
t.Log("No provider specified with -provider")
- return nil, "", nil, nil
+ return nil, "", nil
}
jsons, err := credsfile.LoadProviderConfigs("providers.json")
if err != nil {
t.Fatalf("Error loading provider configs: %s", err)
}
- fails := map[int]bool{}
for name, cfg := range jsons {
if *providerToRun != name {
continue
}
var metadata json.RawMessage
- // CLOUDFLAREAPI tests related to CF_REDIRECT/CF_TEMP_REDIRECT
+ // CLOUDFLAREAPI tests related to CLOUDFLAREAPI_SINGLE_REDIRECT/CF_REDIRECT/CF_TEMP_REDIRECT
// requires metadata to enable this feature.
// In hindsight, I have no idea why this metadata flag is required to
// use this feature. Maybe because we didn't have the capabilities
// feature at the time?
if name == "CLOUDFLAREAPI" {
+ items := []string{}
if *enableCFWorkers {
- metadata = []byte(`{ "manage_redirects": true, "manage_workers": true }`)
- } else {
- metadata = []byte(`{ "manage_redirects": true }`)
+ items = append(items, `"manage_workers": true`)
}
+ switch *enableCFRedirectMode {
+ case "":
+ items = append(items, `"manage_redirects": true`)
+ case "c":
+ items = append(items, `"manage_redirects": true`)
+ items = append(items, `"manage_single_redirects": true`)
+ case "n":
+ items = append(items, `"manage_single_redirects": true`)
+ case "o":
+ }
+ metadata = []byte(`{ ` + strings.Join(items, `, `) + ` }`)
}
provider, err := providers.CreateDNSProvider(name, cfg, metadata)
if err != nil {
t.Fatal(err)
}
- if f := cfg["knownFailures"]; f != "" {
- for _, s := range strings.Split(f, ",") {
- i, err := strconv.Atoi(s)
- if err != nil {
- t.Fatal(err)
- }
- fails[i] = true
- }
- }
if name == "CLOUDFLAREAPI" && *enableCFWorkers {
// Cloudflare only. Will do nothing if provider != *cloudflareProvider.
@@ -95,15 +96,15 @@ func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[int]bo
}
}
- return provider, cfg["domain"], fails, cfg
+ return provider, cfg["domain"], cfg
}
t.Fatalf("Provider %s not found", *providerToRun)
- return nil, "", nil, nil
+ return nil, "", nil
}
func TestDNSProviders(t *testing.T) {
- provider, domain, fails, cfg := getProvider(t)
+ provider, domain, cfg := getProvider(t)
if provider == nil {
return
}
@@ -112,7 +113,7 @@ func TestDNSProviders(t *testing.T) {
}
t.Run(domain, func(t *testing.T) {
- runTests(t, provider, domain, fails, cfg)
+ runTests(t, provider, domain, cfg)
})
}
@@ -135,7 +136,7 @@ func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvid
// testPermitted returns nil if the test is permitted, otherwise an
// error explaining why it is not.
-func testPermitted(t *testing.T, p string, f TestGroup) error {
+func testPermitted(p string, f TestGroup) error {
// not() and only() can't be mixed.
if len(f.only) != 0 && len(f.not) != 0 {
@@ -227,13 +228,13 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
}
// get and run corrections for first time
- _, corrections, err := zonerecs.CorrectZoneRecords(prv, dom)
+ _, corrections, actualChangeCount, err := zonerecs.CorrectZoneRecords(prv, dom)
if err != nil {
t.Fatal(fmt.Errorf("runTests: %w", err))
}
if tst.Changeless {
- if count := len(corrections); count != 0 {
- t.Logf("Expected 0 corrections on FIRST run, but found %d.", count)
+ if actualChangeCount != 0 {
+ t.Logf("Expected 0 corrections on FIRST run, but found %d.", actualChangeCount)
for i, c := range corrections {
t.Logf("UNEXPECTED #%d: %s", i, c.Msg)
}
@@ -260,12 +261,12 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
}
// run a second time and expect zero corrections
- _, corrections, err = zonerecs.CorrectZoneRecords(prv, dom2)
+ _, corrections, actualChangeCount, err = zonerecs.CorrectZoneRecords(prv, dom2)
if err != nil {
t.Fatal(err)
}
- if count := len(corrections); count != 0 {
- t.Logf("Expected 0 corrections on second run, but found %d.", count)
+ if actualChangeCount != 0 {
+ t.Logf("Expected 0 corrections on second run, but found %d.", actualChangeCount)
for i, c := range corrections {
t.Logf("UNEXPECTED #%d: %s", i, c.Msg)
}
@@ -275,9 +276,9 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
})
}
-func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, knownFailures map[int]bool, origConfig map[string]string) {
+func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, origConfig map[string]string) {
dc := getDomainConfigWithNameservers(t, prv, domainName)
- testGroups := makeTests(t)
+ testGroups := makeTests()
firstGroup := *startIdx
if firstGroup == -1 {
@@ -288,12 +289,8 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string,
lastGroup = len(testGroups)
}
- // Start the zone with a clean slate.
- makeChanges(t, prv, dc, tc("Empty"), "Clean Slate", false, nil)
-
curGroup := -1
for gIdx, group := range testGroups {
- start := time.Now()
// Abide by -start -end flags
curGroup++
@@ -302,13 +299,17 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string,
}
// Abide by filter
- if err := testPermitted(t, *providerToRun, *group); err != nil {
+ if err := testPermitted(*providerToRun, *group); err != nil {
//t.Logf("%s: ***SKIPPED(%v)***", group.Desc, err)
makeChanges(t, prv, dc, tc("Empty"), fmt.Sprintf("%02d:%s ***SKIPPED(%v)***", gIdx, group.Desc, err), false, origConfig)
continue
}
+ // Start the testgroup with a clean slate.
+ makeChanges(t, prv, dc, tc("Empty"), "Clean Slate", false, nil)
+
// Run the tests.
+ start := time.Now()
for _, tst := range group.tests {
@@ -326,9 +327,6 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string,
}
- // Remove all records so next group starts with a clean slate.
- makeChanges(t, prv, dc, tc("Empty"), "Post cleanup", true, nil)
-
elapsed := time.Since(start)
if *printElapsed {
fmt.Printf("ELAPSED %02d %7.2f %q\n", gIdx, elapsed.Seconds(), group.Desc)
@@ -339,7 +337,7 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string,
}
func TestDualProviders(t *testing.T) {
- p, domain, _, _ := getProvider(t)
+ p, domain, _ := getProvider(t)
if p == nil {
return
}
@@ -355,7 +353,7 @@ func TestDualProviders(t *testing.T) {
run := func() {
dom, _ := dc.Copy()
- rs, cs, err := zonerecs.CorrectZoneRecords(p, dom)
+ rs, cs, _, err := zonerecs.CorrectZoneRecords(p, dom)
if err != nil {
t.Fatal(err)
}
@@ -376,16 +374,16 @@ func TestDualProviders(t *testing.T) {
nslist, _ := models.ToNameservers([]string{"ns1.example.com", "ns2.example.com"})
dc.Nameservers = append(dc.Nameservers, nslist...)
nameservers.AddNSRecords(dc)
- t.Log("Adding nameservers from another provider")
+ t.Log("Adding test nameservers")
run()
// run again to make sure no corrections
t.Log("Running again to ensure stability")
- rs, cs, err := zonerecs.CorrectZoneRecords(p, dc)
+ rs, cs, actualChangeCount, err := zonerecs.CorrectZoneRecords(p, dc)
if err != nil {
t.Fatal(err)
}
- if count := len(cs); count != 0 {
- t.Logf("Expect no corrections on second run, but found %d.", count)
+ if actualChangeCount != 0 {
+ t.Logf("Expect no corrections on second run, but found %d.", actualChangeCount)
for i, c := range rs {
t.Logf("INFO#%d:\n%s", i+1, c.Msg)
}
@@ -394,6 +392,20 @@ func TestDualProviders(t *testing.T) {
}
t.FailNow()
}
+
+ t.Log("Removing test nameservers")
+ dc.Records = []*models.RecordConfig{}
+ n := 0
+ for _, ns := range dc.Nameservers {
+ if ns.Name == "ns1.example.com" || ns.Name == "ns2.example.com" {
+ continue
+ }
+ dc.Nameservers[n] = ns
+ n++
+ }
+ dc.Nameservers = dc.Nameservers[:n]
+ nameservers.AddNSRecords(dc)
+ run()
}
func TestNameserverDots(t *testing.T) {
@@ -403,7 +415,7 @@ func TestNameserverDots(t *testing.T) {
// or vise-versa.
// Setup:
- p, domain, _, _ := getProvider(t)
+ p, domain, _ := getProvider(t)
if p == nil {
return
}
@@ -460,10 +472,19 @@ func SetLabel(r *models.RecordConfig, label, domain string) {
r.NameFQDN = dnsutil.AddOrigin(label, "**current-domain**")
}
+func withMeta(record *models.RecordConfig, metadata map[string]string) *models.RecordConfig {
+ record.Metadata = metadata
+ return record
+}
+
func a(name, target string) *models.RecordConfig {
return makeRec(name, target, "A")
}
+func aaaa(name, target string) *models.RecordConfig {
+ return makeRec(name, target, "AAAA")
+}
+
func alias(name, target string) *models.RecordConfig {
return makeRec(name, target, "ALIAS")
}
@@ -496,6 +517,19 @@ func cfProxyCNAME(name, target, status string) *models.RecordConfig {
return r
}
+func cfSingleRedirectEnabled() bool {
+ return ((*enableCFRedirectMode) != "")
+}
+
+func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig {
+ r := makeRec("@", name, cfsingleredirect.SINGLEREDIRECT)
+ err := cfsingleredirect.FromRaw(r, []any{name, code, when, then})
+ if err != nil {
+ panic("Should not happen... cfSingleRedirect")
+ }
+ return r
+}
+
func cfWorkerRoute(pattern, target string) *models.RecordConfig {
t := fmt.Sprintf("%s,%s", pattern, target)
r := makeRec("@", t, "CF_WORKER_ROUTE")
@@ -522,18 +556,35 @@ func dhcid(name, target string) *models.RecordConfig {
return makeRec(name, target, "DHCID")
}
+func dname(name, target string) *models.RecordConfig {
+ return makeRec(name, target, "DNAME")
+}
+
func ds(name string, keyTag uint16, algorithm, digestType uint8, digest string) *models.RecordConfig {
r := makeRec(name, "", "DS")
r.SetTargetDS(keyTag, algorithm, digestType, digest)
return r
}
+func dnskey(name string, flags uint16, protocol, algorithm uint8, publicKey string) *models.RecordConfig {
+ r := makeRec(name, "", "DNSKEY")
+ r.SetTargetDNSKEY(flags, protocol, algorithm, publicKey)
+ return r
+}
+
+func https(name string, priority uint16, target string, params string) *models.RecordConfig {
+ r := makeRec(name, target, "HTTPS")
+ r.SvcPriority = priority
+ r.SvcParams = params
+ return r
+}
+
func ignoreName(labelSpec string) *models.RecordConfig {
return ignore(labelSpec, "*", "*")
}
func ignoreTarget(targetSpec string, typeSpec string) *models.RecordConfig {
- return ignore("*", "*", targetSpec)
+ return ignore("*", typeSpec, targetSpec)
}
func ignore(labelSpec string, typeSpec string, targetSpec string) *models.RecordConfig {
@@ -549,7 +600,7 @@ func ignore(labelSpec string, typeSpec string, targetSpec string) *models.Record
}
func loc(name string, d1 uint8, m1 uint8, s1 float32, ns string,
- d2 uint8, m2 uint8, s2 float32, ew string, al int32, sz float32, hp float32, vp float32) *models.RecordConfig {
+ d2 uint8, m2 uint8, s2 float32, ew string, al float32, sz float32, hp float32, vp float32) *models.RecordConfig {
r := makeRec(name, "", "LOC")
r.SetLOCParams(d1, m1, s1, ns, d2, m2, s2, ew, al, sz, hp, vp)
return r
@@ -593,10 +644,11 @@ func ptr(name, target string) *models.RecordConfig {
return makeRec(name, target, "PTR")
}
-func r53alias(name, aliasType, target string) *models.RecordConfig {
+func r53alias(name, aliasType, target, evalTargetHealth string) *models.RecordConfig {
r := makeRec(name, target, "R53_ALIAS")
r.R53Alias = map[string]string{
- "type": aliasType,
+ "type": aliasType,
+ "evaluate_target_health": evalTargetHealth,
}
return r
}
@@ -619,6 +671,13 @@ func sshfp(name string, algorithm uint8, fingerprint uint8, target string) *mode
return r
}
+func svcb(name string, priority uint16, target string, params string) *models.RecordConfig {
+ r := makeRec(name, target, "SVCB")
+ r.SvcPriority = priority
+ r.SvcParams = params
+ return r
+}
+
func ovhdkim(name, target string) *models.RecordConfig {
return makeOvhNativeRecord(name, target, "DKIM")
}
@@ -635,7 +694,6 @@ func makeOvhNativeRecord(name, target, rType string) *models.RecordConfig {
r := makeRec(name, "", "TXT")
r.Metadata = make(map[string]string)
r.Metadata["create_ovh_native_record"] = rType
- r.TxtStrings = []string{target}
r.SetTarget(target)
return r
}
@@ -681,6 +739,9 @@ func tc(desc string, recs ...*models.RecordConfig) *TestCase {
var records []*models.RecordConfig
var unmanagedItems []*models.UnmanagedConfig
for _, r := range recs {
+ if r == nil {
+ continue
+ }
switch r.Type {
case "IGNORE":
unmanagedItems = append(unmanagedItems, &models.UnmanagedConfig{
@@ -718,11 +779,16 @@ func tlsa(name string, usage, selector, matchingtype uint8, target string) *mode
return r
}
-func ns1Urlfwd(name, target string) *models.RecordConfig {
- return makeRec(name, target, "NS1_URLFWD")
+func porkbunUrlfwd(name, target, t, includePath, wildcard string) *models.RecordConfig {
+ r := makeRec(name, target, "PORKBUN_URLFWD")
+ r.Metadata = make(map[string]string)
+ r.Metadata["type"] = t
+ r.Metadata["includePath"] = includePath
+ r.Metadata["wildcard"] = wildcard
+ return r
}
-func clear(items ...interface{}) *TestCase {
+func clear() *TestCase {
return tc("Empty")
}
@@ -760,7 +826,7 @@ func alltrue(f ...bool) alltrueFilter {
//
-func makeTests(t *testing.T) []*TestGroup {
+func makeTests() []*TestGroup {
sha256hash := strings.Repeat("0123456789abcdef", 4)
sha512hash := strings.Repeat("0123456789abcdef", 8)
@@ -781,7 +847,7 @@ func makeTests(t *testing.T) []*TestGroup {
// Only run this test if all these bool flags are true:
// alltrue(*enableCFWorkers, *anotherFlag, myBoolValue)
// NOTE: You can't mix not() and only()
- // reset(not("ROUTE53"), only("GCLOUD")), // ERROR!
+ // not("ROUTE53"), only("GCLOUD"), // ERROR!
// NOTE: All requires()/not()/only() must appear before any tc().
// tc()
@@ -796,11 +862,9 @@ func makeTests(t *testing.T) []*TestGroup {
// whether or not a certain kind of record can be created and
// deleted.
- // clear() is the same as tc("Empty"). It removes all records. You
- // can use this to verify a provider can delete all the records in
- // the last tc(), or to provide a clean slate for the next tc().
- // Each testgroup() begins and ends with clear(), so you don't have
- // to list the clear() yourself.
+ // clear() is the same as tc("Empty"). It removes all records.
+ // Each testgroup() begins with clear() automagically. You do not
+ // have to include the clear() in each testgroup().
tests := []*TestGroup{
@@ -873,7 +937,10 @@ func makeTests(t *testing.T) []*TestGroup {
// Narrative: That wasn't as hard as expected, eh? Let's test the
// other basic record types like AAAA, CNAME, MX and TXT.
- // AAAA: TODO(tlim) Add AAAA test.
+ testgroup("AAAA",
+ tc("Create AAAA", aaaa("testaaaa", "2607:f8b0:4006:820::2006")),
+ tc("Change AAAA target", aaaa("testaaaa", "2607:f8b0:4006:820::2013")),
+ ),
// CNAME
@@ -882,6 +949,13 @@ func makeTests(t *testing.T) []*TestGroup {
tc("Change CNAME target", cname("testcname", "www.yahoo.com.")),
),
+ testgroup("CNAME-short",
+ tc("Create a CNAME",
+ a("foo", "1.2.3.4"),
+ cname("testcname", "foo"),
+ ),
+ ),
+
// MX
// Narrative: MX is the first record we're going to test with
@@ -1008,6 +1082,25 @@ func makeTests(t *testing.T) []*TestGroup {
tc("Change back to CNAME", cname("foo", "google2.com.")),
),
+ testgroup("HTTPS",
+ requires(providers.CanUseHTTPS),
+ tc("Create a HTTPS record", https("@", 1, "test.com.", "port=80")),
+ tc("Change HTTPS priority", https("@", 2, "test.com.", "port=80")),
+ tc("Change HTTPS target", https("@", 2, ".", "port=80")),
+ tc("Change HTTPS params", https("@", 2, ".", "port=99")),
+ tc("Change HTTPS params-empty", https("@", 2, ".", "")),
+ tc("Change HTTPS all", https("@", 3, "example.com.", "port=100")),
+ ),
+
+ testgroup("SVCB",
+ requires(providers.CanUseSVCB),
+ tc("Create a SVCB record", svcb("@", 1, "test.com.", "port=80")),
+ tc("Change SVCB priority", svcb("@", 2, "test.com.", "port=80")),
+ tc("Change SVCB target", svcb("@", 2, ".", "port=80")),
+ tc("Change SVCB params", svcb("@", 2, ".", "port=99")),
+ tc("Change SVCB params-empty", svcb("@", 2, ".", "")),
+ tc("Change SVCB all", svcb("@", 3, "example.com.", "port=100")),
+ ),
//// Test edge cases from various types.
// Narrative: Every DNS record type has some weird edge-case that
@@ -1027,12 +1120,63 @@ func makeTests(t *testing.T) []*TestGroup {
// that.
testgroup("CNAME",
- tc("Record pointing to @", cname("foo", "**current-domain**")),
+ tc("Record pointing to @",
+ cname("foo", "**current-domain**"),
+ a("@", "1.2.3.4"),
+ ),
),
- testgroup("MX",
- tc("Record pointing to @", mx("foo", 8, "**current-domain**")),
- tc("Null MX", mx("@", 0, ".")), // RFC 7505
+ testgroup("ApexMX",
+ tc("Record pointing to @",
+ mx("foo", 8, "**current-domain**"),
+ a("@", "1.2.3.4"),
+ ),
+ ),
+
+ // RFC 7505 NullMX
+ testgroup("NullMX",
+ not(
+ "TRANSIP", // TRANSIP is slow and doesn't support NullMX. Skip to save time.
+ ),
+ tc("create", // Install a Null MX.
+ a("nmx", "1.2.3.3"), // Install this so it is ready for the next tc()
+ a("www", "1.2.3.9"), // Install this so it is ready for the next tc()
+ mx("nmx", 0, "."),
+ ),
+ tc("unnull", // Change to regular MX.
+ a("nmx", "1.2.3.3"),
+ a("www", "1.2.3.9"),
+ mx("nmx", 3, "nmx.**current-domain**"),
+ mx("nmx", 9, "www.**current-domain**"),
+ ),
+ tc("renull", // Change back to Null MX.
+ a("nmx", "1.2.3.3"),
+ a("www", "1.2.3.9"),
+ mx("nmx", 0, "."),
+ ),
+ ),
+
+ // RFC 7505 NullMX at Apex
+ testgroup("NullMXApex",
+ not(
+ "TRANSIP", // TRANSIP is slow and doesn't support NullMX. Skip to save time.
+ ),
+ tc("create", // Install a Null MX.
+ a("@", "1.2.3.2"), // Install this so it is ready for the next tc()
+ a("www", "1.2.3.8"), // Install this so it is ready for the next tc()
+ mx("@", 0, "."),
+ ),
+ tc("unnull", // Change to regular MX.
+ a("@", "1.2.3.2"),
+ a("www", "1.2.3.8"),
+ mx("@", 2, "**current-domain**"),
+ mx("@", 8, "www.**current-domain**"),
+ ),
+ tc("renull", // Change back to Null MX.
+ a("@", "1.2.3.2"),
+ a("www", "1.2.3.8"),
+ mx("@", 0, "."),
+ ),
),
testgroup("NS",
@@ -1071,39 +1215,64 @@ func makeTests(t *testing.T) []*TestGroup {
// Do not use only()/not()/requires() in this section.
// If your provider needs to skip one of these tests, update
// "provider/*/recordaudit.AuditRecords()" to reject that kind
- // of record. When the provider fixes the bug or changes behavior,
- // update the AuditRecords().
-
- //clear(),
- //tc("a 255-byte TXT", txt("foo255", strings.Repeat("C", 255))),
- //clear(),
- //tc("a 256-byte TXT", txt("foo256", strings.Repeat("D", 256))),
- //clear(),
- //tc("a 512-byte TXT", txt("foo512", strings.Repeat("C", 512))),
- //clear(),
- //tc("a 513-byte TXT", txt("foo513", strings.Repeat("D", 513))),
+ // of record.
+
+ // Some of these test cases are commented out because they test
+ // something that isn't widely used or supported. For example
+ // many APIs don't support a backslack (`\`) in a TXT record;
+ // luckily we've never seen a need for that "in the wild". If
+ // you want to future-proof your provider, temporarily remove
+ // the comments and get those tests working, or reject it using
+ // auditrecords.go.
+
+ // ProTip: Unsure how a provider's API escapes something? Try
+ // adding the TXT record via the Web UI and watch how the string
+ // is escaped when you download the records.
+
+ // Nobody needs this and many APIs don't allow it.
+ tc("a 0-byte TXT", txt("foo0", "")),
+
+ // Test edge cases around 255, 255*2, 255*3:
+ tc("a 254-byte TXT", txt("foo254", strings.Repeat("A", 254))), // 255-1
+ tc("a 255-byte TXT", txt("foo255", strings.Repeat("B", 255))), // 255
+ tc("a 256-byte TXT", txt("foo256", strings.Repeat("C", 256))), // 255+1
+ tc("a 509-byte TXT", txt("foo509", strings.Repeat("D", 509))), // 255*2-1
+ tc("a 510-byte TXT", txt("foo510", strings.Repeat("E", 510))), // 255*2
+ tc("a 511-byte TXT", txt("foo511", strings.Repeat("F", 511))), // 255*2+1
+ tc("a 764-byte TXT", txt("foo764", strings.Repeat("G", 764))), // 255*3-1
+ tc("a 765-byte TXT", txt("foo765", strings.Repeat("H", 765))), // 255*3
+ tc("a 766-byte TXT", txt("foo766", strings.Repeat("J", 766))), // 255*3+1
//clear(),
tc("TXT with 1 single-quote", txt("foosq", "quo'te")),
- //clear(),
tc("TXT with 1 backtick", txt("foobt", "blah`blah")),
- //clear(),
- tc("TXT with 1 double-quotes", txt("foodq", `quo"te`)),
- //clear(),
- tc("TXT with 2 double-quotes", txt("foodqs", `q"uo"te`)),
- //clear(),
+ tc("TXT with 1 dq-1interior", txt("foodq", `in"side`)),
+ tc("TXT with 2 dq-2interior", txt("foodqs", `in"ter"ior`)),
+ tc("TXT with 1 dq-left", txt("foodqs", `"left`)),
+ tc("TXT with 1 dq-right", txt("foodqs", `right"`)),
- tc("a TXT with interior ws", txt("foosp", "with spaces")),
- //clear(),
- tc("TXT with ws at end", txt("foows1", "with space at end ")),
- //clear(),
+ // Semicolons don't need special treatment.
+ // https://serverfault.com/questions/743789
+ tc("TXT with semicolon", txt("foosc1", `semi;colon`)),
+ tc("TXT with semicolon ws", txt("foosc2", `wssemi ; colon`)),
- //tc("Create a TXT/SPF", txt("foo", "v=spf1 ip4:99.99.99.99 -all")),
- // This was added because Vultr syntax-checks TXT records with SPF contents.
- //clear(),
+ tc("TXT interior ws", txt("foosp", "with spaces")),
+ //tc("TXT leading ws", txt("foowsb", " leadingspace")),
+ tc("TXT trailing ws", txt("foows1", "trailingws ")),
+
+ // Vultr syntax-checks TXT records with SPF contents.
+ tc("Create a TXT/SPF", txt("foo", "v=spf1 ip4:99.99.99.99 -all")),
+
+ // Nobody needs this and many APIs don't allow it.
+ //tc("Create TXT with frequently difficult characters", txt("fooex", `!^.*$@#%^&()([][{}{<>1000 corrections. See https://github.com/StackExchange/dnscontrol/issues/1440
//"CSCGLOBAL", // Doesn't page. Works fine. Due to the slow API we skip.
+ //"DESEC", // Skip due to daily update limits.
//"GANDI_V5", // Their API is so damn slow. We'll add it back as needed.
//"HEDNS", // No paging done. No need to test.
//"MSDNS", // No paging done. No need to test.
@@ -1277,6 +1451,23 @@ func makeTests(t *testing.T) []*TestGroup {
tc("Update 1200 records", manyA("rec%04d", "1.2.3.5", 1200)...),
),
+ // Test the boundaries of Google' batch system.
+ // 1200 is used because it is larger than batchMax.
+ // https://github.com/StackExchange/dnscontrol/pull/2762#issuecomment-1877825559
+ testgroup("batchRecordswithOthers",
+ only(
+ "GCLOUD",
+ ),
+ tc("1200 records",
+ manyA("rec%04d", "1.2.3.4", 1200)...),
+ tc("Update 1200 records and Create others", append(
+ manyA("arec%04d", "1.2.3.4", 1200),
+ manyA("rec%04d", "1.2.3.5", 1200)...)...),
+ tc("Update 1200 records and Create and Delete others", append(
+ manyA("rec%04d", "1.2.3.4", 1200),
+ manyA("zrec%04d", "1.2.3.4", 1200)...)...),
+ ),
+
//// CanUse* types:
// Narrative: Many DNS record types are optional. If the provider
@@ -1302,16 +1493,16 @@ func makeTests(t *testing.T) []*TestGroup {
testgroup("LOC",
requires(providers.CanUseLOC),
//42 21 54 N 71 06 18 W -24m 30m
- tc("Single LOC record", loc("@", 42, 21, 54, "N", 71, 6, 18, "W", -24, 30, 0, 0)),
+ tc("Single LOC record", loc("@", 42, 21, 54, "N", 71, 6, 18, "W", -24.05, 30, 0, 0)),
//42 21 54 N 71 06 18 W -24m 30m
- tc("Update single LOC record", loc("@", 42, 21, 54, "N", 71, 6, 18, "W", -24, 30, 10, 0)),
+ tc("Update single LOC record", loc("@", 42, 21, 54, "N", 71, 6, 18, "W", -24.06, 30, 10, 0)),
tc("Multiple LOC records-create a-d modify apex", //create a-d, modify @
//42 21 54 N 71 06 18 W -24m 30m
loc("@", 42, 21, 54, "N", 71, 6, 18, "W", -24, 30, 0, 0),
//42 21 43.952 N 71 5 6.344 W -24m 1m 200m
- loc("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24, 1, 200, 10),
+ loc("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24.33, 1, 200, 10),
//52 14 05 N 00 08 50 E 10m
- loc("b", 52, 14, 5, "N", 0, 8, 50, "E", 10, 0, 0, 0),
+ loc("b", 52, 14, 5, "N", 0, 8, 50, "E", 10.22, 0, 0, 0),
//32 7 19 S 116 2 25 E 10m
loc("c", 32, 7, 19, "S", 116, 2, 25, "E", 10, 0, 0, 0),
//42 21 28.764 N 71 00 51.617 W -44m 2000m
@@ -1494,10 +1685,25 @@ func makeTests(t *testing.T) []*TestGroup {
// ns("another-child", "ns101.cloudns.net."),
//),
),
- testgroup("DHCPID",
+ testgroup("DHCID",
requires(providers.CanUseDHCID),
- tc("Create DHCPID record", dhcid("test", "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=")),
- tc("Modify DHCPID record", dhcid("test", "Test/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=")),
+ tc("Create DHCID record", dhcid("test", "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=")),
+ tc("Modify DHCID record", dhcid("test", "AAAAAAAAuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=")),
+ ),
+
+ testgroup("DNAME",
+ requires(providers.CanUseDNAME),
+ tc("Create DNAME record", dname("test", "example.com.")),
+ tc("Modify DNAME record", dname("test", "example.net.")),
+ tc("Create DNAME record in non-FQDN", dname("a", "b")),
+ ),
+
+ testgroup("DNSKEY",
+ requires(providers.CanUseDNSKEY),
+ tc("Create DNSKEY record", dnskey("test", 257, 3, 13, "fRnjbeUVyKvz1bDx2lPmu3KY1k64T358t8kP6Hjveos=")),
+ tc("Modify DNSKEY record 1", dnskey("test", 256, 3, 13, "fRnjbeUVyKvz1bDx2lPmu3KY1k64T358t8kP6Hjveos=")),
+ tc("Modify DNSKEY record 2", dnskey("test", 256, 3, 13, "whjtMiJP9C86l0oTJUxemuYtQ0RIZePWt6QETC2kkKM=")),
+ tc("Modify DNSKEY record 3", dnskey("test", 256, 3, 15, "whjtMiJP9C86l0oTJUxemuYtQ0RIZePWt6QETC2kkKM=")),
),
//// Vendor-specific record types
@@ -1507,10 +1713,23 @@ func makeTests(t *testing.T) []*TestGroup {
// them here. If you are writing a new provider, I have some good
// news: These don't apply to you!
- testgroup("ALIAS",
+ testgroup("ALIAS on apex",
requires(providers.CanUseAlias),
tc("ALIAS at root", alias("@", "foo.com.")),
tc("change it", alias("@", "foo2.com.")),
+ ),
+
+ testgroup("ALIAS to nonfqdn",
+ requires(providers.CanUseAlias),
+ tc("ALIAS at root",
+ a("foo", "1.2.3.4"),
+ alias("@", "foo"),
+ ),
+ ),
+
+ testgroup("ALIAS on subdomain",
+ requires(providers.CanUseAlias),
+ not("TRANSIP"), // TransIP does support ALIAS records, but only for apex records (@)
tc("ALIAS at subdomain", alias("test", "foo.com.")),
tc("change it", alias("test", "foo2.com.")),
),
@@ -1574,12 +1793,12 @@ func makeTests(t *testing.T) []*TestGroup {
tc("ALIAS to A record in same zone",
a("kyle", "1.2.3.4"),
a("cartman", "2.3.4.5"),
- r53alias("kenny", "A", "kyle.**current-domain**"),
+ r53alias("kenny", "A", "kyle.**current-domain**", "false"),
),
tc("modify an r53 alias",
a("kyle", "1.2.3.4"),
a("cartman", "2.3.4.5"),
- r53alias("kenny", "A", "cartman.**current-domain**"),
+ r53alias("kenny", "A", "cartman.**current-domain**", "false"),
),
),
@@ -1592,12 +1811,12 @@ func makeTests(t *testing.T) []*TestGroup {
tc("add an alias to 18",
cname("dev-system18", "ec2-54-91-33-155.compute-1.amazonaws.com."),
cname("dev-system19", "ec2-54-91-99-999.compute-1.amazonaws.com."),
- r53alias("dev-system", "CNAME", "dev-system18.**current-domain**"),
+ r53alias("dev-system", "CNAME", "dev-system18.**current-domain**", "false"),
),
tc("modify alias to 19",
cname("dev-system18", "ec2-54-91-33-155.compute-1.amazonaws.com."),
cname("dev-system19", "ec2-54-91-99-999.compute-1.amazonaws.com."),
- r53alias("dev-system", "CNAME", "dev-system19.**current-domain**"),
+ r53alias("dev-system", "CNAME", "dev-system19.**current-domain**", "false"),
),
tc("remove alias",
cname("dev-system18", "ec2-54-91-33-155.compute-1.amazonaws.com."),
@@ -1606,17 +1825,17 @@ func makeTests(t *testing.T) []*TestGroup {
tc("add an alias back",
cname("dev-system18", "ec2-54-91-33-155.compute-1.amazonaws.com."),
cname("dev-system19", "ec2-54-91-99-999.compute-1.amazonaws.com."),
- r53alias("dev-system", "CNAME", "dev-system19.**current-domain**"),
+ r53alias("dev-system", "CNAME", "dev-system19.**current-domain**", "false"),
),
tc("remove cnames",
- r53alias("dev-system", "CNAME", "dev-system19.**current-domain**"),
+ r53alias("dev-system", "CNAME", "dev-system19.**current-domain**", "false"),
),
),
testgroup("R53_ALIAS_CNAME",
requires(providers.CanUseRoute53Alias),
tc("create alias+cname in one step",
- r53alias("dev-system", "CNAME", "dev-system18.**current-domain**"),
+ r53alias("dev-system", "CNAME", "dev-system18.**current-domain**", "false"),
cname("dev-system18", "ec2-54-91-33-155.compute-1.amazonaws.com."),
),
),
@@ -1627,7 +1846,7 @@ func makeTests(t *testing.T) []*TestGroup {
// See https://github.com/StackExchange/dnscontrol/issues/2107
requires(providers.CanUseRoute53Alias),
tc("loop should fail",
- r53alias("test-islandora", "CNAME", "test-islandora.**current-domain**"),
+ r53alias("test-islandora", "CNAME", "test-islandora.**current-domain**", "false"),
),
),
@@ -1635,7 +1854,7 @@ func makeTests(t *testing.T) []*TestGroup {
testgroup("R53_alias pre-existing",
requires(providers.CanUseRoute53Alias),
tc("Create some records",
- r53alias("dev-system", "CNAME", "dev-system18.**current-domain**"),
+ r53alias("dev-system", "CNAME", "dev-system18.**current-domain**", "false"),
cname("dev-system18", "ec2-54-91-33-155.compute-1.amazonaws.com."),
),
tc("Add a new record - ignoring foo",
@@ -1644,8 +1863,28 @@ func makeTests(t *testing.T) []*TestGroup {
),
),
+ testgroup("R53_alias evaluate_target_health",
+ requires(providers.CanUseRoute53Alias),
+ tc("Create alias and cname",
+ r53alias("test-record", "CNAME", "test-record-1.**current-domain**", "false"),
+ cname("test-record-1", "ec2-54-91-33-155.compute-1.amazonaws.com."),
+ ),
+ tc("modify evaluate target health",
+ r53alias("test-record", "CNAME", "test-record-1.**current-domain**", "true"),
+ cname("test-record-1", "ec2-54-91-33-155.compute-1.amazonaws.com."),
+ ),
+ ),
+
// CLOUDFLAREAPI features
+ // CLOUDFLAREAPI: Redirects:
+
+ // go test -v -verbose -provider CLOUDFLAREAPI // PAGE_RULEs
+ // go test -v -verbose -provider CLOUDFLAREAPI -cfredirect=c // Convert: Convert page rules to Single Redirect
+ // go test -v -verbose -provider CLOUDFLAREAPI -cfredirect=n // New: Convert old to new Single Redirect
+ // ProTip: Add this to just run this test:
+ // -start 59 -end 60
+
testgroup("CF_REDIRECT",
only("CLOUDFLAREAPI"),
tc("redir", cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1")),
@@ -1654,32 +1893,32 @@ func makeTests(t *testing.T) []*TestGroup {
// Removed these for speed. They tested if order matters,
// which it doesn't seem to. Re-add if needed.
- //clear(),
- //tc("multipleA",
- // cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
- // cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
- //),
- //clear(),
- //tc("multipleB",
- // cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
- // cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
- //),
- //tc("change1",
- // cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
- // cfRedir("cnn.**current-domain-no-trailing**/*", "https://change.cnn.com/$1"),
- //),
- //tc("change1",
- // cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
- // cfRedir("cablenews.**current-domain-no-trailing**/*", "https://change.cnn.com/$1"),
- //),
+ clear(),
+ tc("multipleA",
+ cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
+ cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
+ ),
+ clear(),
+ tc("multipleB",
+ cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
+ cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
+ ),
+ tc("change1",
+ cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
+ cfRedir("cnn.**current-domain-no-trailing**/*", "https://change.cnn.com/$1"),
+ ),
+ tc("change1",
+ cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
+ cfRedir("cablenews.**current-domain-no-trailing**/*", "https://change.cnn.com/$1"),
+ ),
- // TODO(tlim): Fix this test case. It is currently failing.
- //clear(),
- //tc("multiple3",
- // cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
- // cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
- // cfRedir("nytimes.**current-domain-no-trailing**/*", "https://www.nytimes.com/$1"),
- //),
+ // NB(tlim): This test case used to fail but mysteriously started working.
+ clear(),
+ tc("multiple3",
+ cfRedir("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
+ cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
+ cfRedir("nytimes.**current-domain-no-trailing**/*", "https://www.nytimes.com/$1"),
+ ),
// Repeat the above tests using CF_TEMP_REDIR instead
clear(),
@@ -1704,14 +1943,34 @@ func makeTests(t *testing.T) []*TestGroup {
cfRedirTemp("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
cfRedirTemp("cablenews.**current-domain-no-trailing**/*", "https://change.cnn.com/$1"),
),
- // TODO(tlim): Fix this test case:
- //tc("tempmultiple3",
- // cfRedirTemp("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
- // cfRedirTemp("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
- // cfRedirTemp("nytimes.**current-domain-no-trailing**/*", "https://www.nytimes.com/$1"),
- //),
+ // NB(tlim): This test case used to fail but mysteriously started working.
+ tc("tempmultiple3",
+ cfRedirTemp("msnbc.**current-domain-no-trailing**/*", "https://msnbc.cnn.com/$1"),
+ cfRedirTemp("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1"),
+ cfRedirTemp("nytimes.**current-domain-no-trailing**/*", "https://www.nytimes.com/$1"),
+ ),
+ ),
+
+ testgroup("CF_REDIRECT_CONVERT",
+ only("CLOUDFLAREAPI"),
+ alltrue(cfSingleRedirectEnabled()),
+ tc("start301", cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1")),
+ tc("convert302", cfRedirTemp("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1")),
+ tc("convert301", cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1")),
),
+ testgroup("CLOUDFLAREAPI_SINGLE_REDIRECT",
+ only("CLOUDFLAREAPI"),
+ alltrue(cfSingleRedirectEnabled()),
+ tc("start301", cfSingleRedirect(`name1`, `301`, `http.host eq "cnn.slackoverflow.com"`, `concat("https://www.cnn.com", http.request.uri.path)`)),
+ tc("changecode", cfSingleRedirect(`name1`, `302`, `http.host eq "cnn.slackoverflow.com"`, `concat("https://www.cnn.com", http.request.uri.path)`)),
+ tc("changewhen", cfSingleRedirect(`name1`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.cnn.com", http.request.uri.path)`)),
+ tc("changethen", cfSingleRedirect(`name1`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.msnbc.com", http.request.uri.path)`)),
+ tc("changename", cfSingleRedirect(`name1bis`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.msnbc.com", http.request.uri.path)`)),
+ ),
+
+ // CLOUDFLAREAPI: PROXY
+
testgroup("CF_PROXY A create",
only("CLOUDFLAREAPI"),
CfProxyOff(), clear(),
@@ -1812,14 +2071,6 @@ func makeTests(t *testing.T) []*TestGroup {
),
),
- // NS1 features
-
- testgroup("NS1_URLFWD tests",
- only("NS1"),
- tc("Add a urlfwd", ns1Urlfwd("urlfwd1", "/ http://example.com 302 2 0")),
- tc("Update a urlfwd", ns1Urlfwd("urlfwd1", "/ http://example.org 301 2 0")),
- ),
-
//// IGNORE* features
// Narrative: You're basically done now. These remaining tests
@@ -1830,159 +2081,488 @@ func makeTests(t *testing.T) []*TestGroup {
testgroup("IGNORE main",
tc("Create some records",
- txt("foo", "simple"),
a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
),
- tc("ignore label=foo",
+
+ tc("ignore label",
+ // NB(tlim): This ignores 1 record of a recordSet. This should
+ // fail for diff2.ByRecordSet() providers if diff2 is not
+ // implemented correctly.
+ //a("foo", "1.2.3.4"),
+ //a("foo", "2.3.4.5"),
+ //txt("foo", "simple"),
a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
ignore("foo", "", ""),
).ExpectNoChanges(),
- tc("ignore type=txt",
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("ignore label,type",
+ //a("foo", "1.2.3.4"),
+ //a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("foo", "A", ""),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("ignore label,type,target",
+ //a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("foo", "A", "1.2.3.4"),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("ignore type",
+ //a("foo", "1.2.3.4"),
+ //a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ //a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("", "A", ""),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("ignore type,target",
+ a("foo", "1.2.3.4"),
+ //a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("", "A", "2.3.4.5"),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
a("bar", "5.5.5.5"),
- ignore("", "TXT", ""),
+ cname("mail", "ghs.googlehosted.com."),
).ExpectNoChanges(),
- tc("ignore target=1.2.3.4",
+
+ tc("ignore target",
+ a("foo", "1.2.3.4"),
+ //a("foo", "2.3.4.5"),
txt("foo", "simple"),
a("bar", "5.5.5.5"),
- ignore("", "", "1.2.3.4"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("", "", "2.3.4.5"),
).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ // Many types:
tc("ignore manytypes",
+ //a("foo", "1.2.3.4"),
+ //a("foo", "2.3.4.5"),
+ //txt("foo", "simple"),
+ //a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
ignore("", "A,TXT", ""),
).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ // Target with wildcard:
+ tc("ignore label,type,target=*",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ //cname("mail", "ghs.googlehosted.com."),
+ ignore("", "CNAME", "*.googlehosted.com."),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.2.3.4"),
+ a("foo", "2.3.4.5"),
+ txt("foo", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
),
+ // Same as "main" but with an apex ("@") record.
testgroup("IGNORE apex",
tc("Create some records",
- txt("@", "simple"),
a("@", "1.2.3.4"),
- ).UnsafeIgnore(),
- tc("ignore label=apex",
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ),
+
+ tc("apex label",
+ // NB(tlim): This ignores 1 record of a recordSet. This should
+ // fail for diff2.ByRecordSet() providers if diff2 is not
+ // implemented correctly.
+ //a("@", "1.2.3.4"),
+ //a("@", "2.3.4.5"),
+ //txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
ignore("@", "", ""),
+ //ignore("", "NS", ""),
+ // NB(tlim): .UnsafeIgnore is needed because the NS records
+ // that providers injects into zones are treated like input
+ // from dnsconfig.js.
).ExpectNoChanges().UnsafeIgnore(),
- tc("ignore type=txt",
+ tc("VERIFY PREVIOUS",
a("@", "1.2.3.4"),
- ignore("", "TXT", ""),
- ).ExpectNoChanges().UnsafeIgnore(),
- tc("ignore target=1.2.3.4",
+ a("@", "2.3.4.5"),
txt("@", "simple"),
- ignore("", "", "1.2.3.4"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("apex label,type",
+ //a("@", "1.2.3.4"),
+ //a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("@", "A", ""),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("@", "1.2.3.4"),
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("apex label,type,target",
+ //a("@", "1.2.3.4"),
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("@", "A", "1.2.3.4"),
+ // NB(tlim): .UnsafeIgnore is needed because the NS records
+ // that providers injects into zones are treated like input
+ // from dnsconfig.js.
).ExpectNoChanges().UnsafeIgnore(),
- tc("ignore manytypes",
+ tc("VERIFY PREVIOUS",
+ a("@", "1.2.3.4"),
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("apex type",
+ //a("@", "1.2.3.4"),
+ //a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ //a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("", "A", ""),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("@", "1.2.3.4"),
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("apex type,target",
+ a("@", "1.2.3.4"),
+ //a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("", "A", "2.3.4.5"),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("@", "1.2.3.4"),
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("apex target",
+ a("@", "1.2.3.4"),
+ //a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ignore("", "", "2.3.4.5"),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("@", "1.2.3.4"),
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ // Many types:
+ tc("apex manytypes",
+ //a("@", "1.2.3.4"),
+ //a("@", "2.3.4.5"),
+ //txt("@", "simple"),
+ //a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
ignore("", "A,TXT", ""),
- ).ExpectNoChanges().UnsafeIgnore(),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("@", "1.2.3.4"),
+ a("@", "2.3.4.5"),
+ txt("@", "simple"),
+ a("bar", "5.5.5.5"),
+ cname("mail", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
),
- // Legacy IGNORE_NAME and IGNORE_TARGET tests.
+ // IGNORE with unsafe notation
- testgroup("IGNORE_NAME function",
+ testgroup("IGNORE unsafe",
tc("Create some records",
txt("foo", "simple"),
a("foo", "1.2.3.4"),
- a("bar", "1.2.3.4"),
+ txt("@", "asimple"),
+ a("@", "2.2.2.2"),
),
- tc("ignore foo",
- ignoreName("foo"),
- a("bar", "1.2.3.4"),
+
+ tc("ignore unsafe apex",
+ txt("foo", "simple"),
+ a("foo", "1.2.3.4"),
+ txt("@", "asimple"),
+ a("@", "2.2.2.2"),
+ ignore("@", "", ""),
+ ).ExpectNoChanges().UnsafeIgnore(),
+ tc("VERIFY PREVIOUS",
+ txt("foo", "simple"),
+ a("foo", "1.2.3.4"),
+ txt("@", "asimple"),
+ a("@", "2.2.2.2"),
).ExpectNoChanges(),
- clear(),
- tc("Create some records",
- txt("bar.foo", "simple"),
- a("bar.foo", "1.2.3.4"),
- a("bar", "1.2.3.4"),
- ),
- tc("ignore *.foo",
- ignoreName("*.foo"),
- a("bar", "1.2.3.4"),
+
+ tc("ignore unsafe label",
+ txt("foo", "simple"),
+ a("foo", "1.2.3.4"),
+ txt("@", "asimple"),
+ a("@", "2.2.2.2"),
+ ignore("foo", "", ""),
+ ).ExpectNoChanges().UnsafeIgnore(),
+ tc("VERIFY PREVIOUS",
+ txt("foo", "simple"),
+ a("foo", "1.2.3.4"),
+ txt("@", "asimple"),
+ a("@", "2.2.2.2"),
).ExpectNoChanges(),
- clear(),
- tc("Create some records",
- txt("bar.foo", "simple"),
- a("bar.foo", "1.2.3.4"),
- ),
- tc("ignore *.foo while we add 1",
- ignoreName("*.foo"),
- a("bar", "1.2.3.4"),
- ),
),
- testgroup("IGNORE_NAME apex",
+ // IGNORE with wildcards
+
+ testgroup("IGNORE wilds",
tc("Create some records",
- txt("@", "simple"),
- a("@", "1.2.3.4"),
- txt("bar", "stringbar"),
- a("bar", "2.4.6.8"),
- ).UnsafeIgnore(),
- tc("ignore apex",
- ignoreName("@"),
- txt("bar", "stringbar"),
- a("bar", "2.4.6.8"),
- ).ExpectNoChanges().UnsafeIgnore(),
- clear(),
- tc("Add a new record - ignoring apex",
- ignoreName("@"),
- txt("bar", "stringbar"),
- a("bar", "2.4.6.8"),
- a("added", "4.6.8.9"),
- ).UnsafeIgnore(),
+ a("foo.bat", "1.2.3.4"),
+ a("foo.bat", "2.3.4.5"),
+ txt("foo.bat", "simple"),
+ a("bar.bat", "5.5.5.5"),
+ cname("mail.bat", "ghs.googlehosted.com."),
+ ),
+
+ tc("ignore label=foo.*",
+ //a("foo.bat", "1.2.3.4"),
+ //a("foo.bat", "2.3.4.5"),
+ //txt("foo.bat", "simple"),
+ a("bar.bat", "5.5.5.5"),
+ cname("mail.bat", "ghs.googlehosted.com."),
+ ignore("foo.*", "", ""),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo.bat", "1.2.3.4"),
+ a("foo.bat", "2.3.4.5"),
+ txt("foo.bat", "simple"),
+ a("bar.bat", "5.5.5.5"),
+ cname("mail.bat", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("ignore label=foo.bat,type",
+ //a("foo.bat", "1.2.3.4"),
+ //a("foo.bat", "2.3.4.5"),
+ txt("foo.bat", "simple"),
+ //a("bar.bat", "5.5.5.5"),
+ cname("mail.bat", "ghs.googlehosted.com."),
+ ignore("*.bat", "A", ""),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo.bat", "1.2.3.4"),
+ a("foo.bat", "2.3.4.5"),
+ txt("foo.bat", "simple"),
+ a("bar.bat", "5.5.5.5"),
+ cname("mail.bat", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
+
+ tc("ignore target=*.domain",
+ a("foo.bat", "1.2.3.4"),
+ a("foo.bat", "2.3.4.5"),
+ txt("foo.bat", "simple"),
+ a("bar.bat", "5.5.5.5"),
+ //cname("mail.bat", "ghs.googlehosted.com."),
+ ignore("", "", "*.googlehosted.com."),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("foo.bat", "1.2.3.4"),
+ a("foo.bat", "2.3.4.5"),
+ txt("foo.bat", "simple"),
+ a("bar.bat", "5.5.5.5"),
+ cname("mail.bat", "ghs.googlehosted.com."),
+ ).ExpectNoChanges(),
),
- testgroup("IGNORE_TARGET function CNAME",
+ // IGNORE with changes
+ testgroup("IGNORE with modify",
tc("Create some records",
- cname("foo", "test.foo.com."),
- cname("keep", "keeper.example.com."),
+ a("foo", "1.1.1.1"),
+ a("foo", "10.10.10.10"),
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ mx("foo", 10, "aspmx.l.google.com."),
+ mx("foo", 20, "alt1.aspmx.l.google.com."),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
+ ),
+
+ // ByZone: Change (anywhere)
+ tc("IGNORE change ByZone",
+ ignore("zzz", "A", ""),
+ a("foo", "1.1.1.1"),
+ a("foo", "11.11.11.11"), // CHANGE
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ mx("foo", 10, "aspmx.l.google.com."),
+ mx("foo", 20, "alt1.aspmx.l.google.com."),
+ //a("zzz", "3.3.3.3"),
+ //a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
),
- tc("ignoring CNAME=test.foo.com.",
- ignoreTarget("test.foo.com.", "CNAME"),
- cname("keep", "keeper.example.com."),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.1.1.1"),
+ a("foo", "11.11.11.11"),
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ mx("foo", 10, "aspmx.l.google.com."),
+ mx("foo", 20, "alt1.aspmx.l.google.com."),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
).ExpectNoChanges(),
- tc("ignoring CNAME=test.foo.com. and add",
- ignoreTarget("test.foo.com.", "CNAME"),
- cname("keep", "keeper.example.com."),
- a("adding", "1.2.3.4"),
- cname("another", "www.example.com."),
+
+ // ByLabel: Change within a (name) while we ignore the rest
+ tc("IGNORE change ByLabel",
+ ignore("foo", "MX", ""),
+ a("foo", "1.1.1.1"),
+ a("foo", "12.12.12.12"), // CHANGE
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ //mx("foo", 10, "aspmx.l.google.com."),
+ //mx("foo", 20, "alt1.aspmx.l.google.com"),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
),
- ),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.1.1.1"),
+ a("foo", "12.12.12.12"),
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ mx("foo", 10, "aspmx.l.google.com."),
+ mx("foo", 20, "alt1.aspmx.l.google.com."),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
+ ).ExpectNoChanges(),
- testgroup("IGNORE_TARGET function CNAME*",
- tc("Create some records",
- cname("foo1", "test.foo.com."),
- cname("foo2", "my.test.foo.com."),
- cname("bar", "test.example.com."),
- ).UnsafeIgnore(),
- tc("ignoring CNAME=test.foo.com.",
- ignoreTarget("*.foo.com.", "CNAME"),
- cname("foo2", "my.test.foo.com."),
- cname("bar", "test.example.com."),
- ).ExpectNoChanges().UnsafeIgnore(),
- tc("ignoring CNAME=test.foo.com. and add",
- ignoreTarget("*.foo.com.", "CNAME"),
- cname("foo2", "my.test.foo.com."),
- cname("bar", "test.example.com."),
- a("adding", "1.2.3.4"),
- cname("another", "www.example.com."),
- ).UnsafeIgnore(),
- ),
+ // ByRecordSet: Change within a (name+type) while we ignore the rest
+ tc("IGNORE change ByRecordSet",
+ ignore("foo", "MX,AAAA", ""),
+ a("foo", "1.1.1.1"),
+ a("foo", "13.13.13.13"), // CHANGE
+ //aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ //mx("foo", 10, "aspmx.l.google.com."),
+ //mx("foo", 20, "alt1.aspmx.l.google.com"),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
+ ),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.1.1.1"),
+ a("foo", "13.13.13.13"),
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ mx("foo", 10, "aspmx.l.google.com."),
+ mx("foo", 20, "alt1.aspmx.l.google.com."),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
+ ).ExpectNoChanges(),
- testgroup("IGNORE_TARGET function CNAME**",
- tc("Create some records",
- cname("foo1", "test.foo.com."),
- cname("foo2", "my.test.foo.com."),
- cname("bar", "test.example.com."),
- ).UnsafeIgnore(),
- tc("ignoring CNAME=test.foo.com.",
- ignoreTarget("**.foo.com.", "CNAME"),
- cname("bar", "test.example.com."),
- ).ExpectNoChanges().UnsafeIgnore(),
- tc("ignoring CNAME=test.foo.com. and add",
- ignoreTarget("**.foo.com.", "CNAME"),
- cname("bar", "test.example.com."),
- a("adding", "1.2.3.4"),
- cname("another", "www.example.com."),
- ).UnsafeIgnore(),
+ // Change within a (name+type+data) ("ByRecord")
+ tc("IGNORE change ByRecord",
+ ignore("foo", "A", "1.1.1.1"),
+ //a("foo", "1.1.1.1"),
+ a("foo", "14.14.14.14"),
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ mx("foo", 10, "aspmx.l.google.com."),
+ mx("foo", 20, "alt1.aspmx.l.google.com."),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
+ ),
+ tc("VERIFY PREVIOUS",
+ a("foo", "1.1.1.1"),
+ a("foo", "14.14.14.14"),
+ aaaa("foo", "2003:dd:d7ff::fe71:aaaa"),
+ mx("foo", 10, "aspmx.l.google.com."),
+ mx("foo", 20, "alt1.aspmx.l.google.com."),
+ a("zzz", "3.3.3.3"),
+ a("zzz", "4.4.4.4"),
+ aaaa("zzz", "2003:dd:d7ff::fe71:cccc"),
+ ).ExpectNoChanges(),
),
+ // IGNORE repro bug reports
+
// https://github.com/StackExchange/dnscontrol/issues/2285
testgroup("IGNORE_TARGET b2285",
tc("Create some records",
@@ -1992,8 +2572,78 @@ func makeTests(t *testing.T) []*TestGroup {
tc("Add a new record - ignoring test.foo.com.",
ignoreTarget("**.acm-validations.aws.", "CNAME"),
).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ cname("foo", "redact1.acm-validations.aws."),
+ cname("bar", "redact2.acm-validations.aws."),
+ ).ExpectNoChanges(),
),
+ // https://github.com/StackExchange/dnscontrol/issues/2822
+ // Don't send empty updates.
+ // A carefully constructed IGNORE() can ignore all the
+ // changes. This resulted in the deSEC provider generating an
+ // empty upsert, which the API rejected.
+ testgroup("IGNORE everything b2822",
+ tc("Create some records",
+ a("dyndns-city1", "91.42.1.1"),
+ a("dyndns-city2", "91.42.1.2"),
+ aaaa("dyndns-city1", "2003:dd:d7ff::fe71:ce77"),
+ aaaa("dyndns-city2", "2003:dd:d7ff::fe71:ce78"),
+ ),
+ tc("ignore them all",
+ a("dyndns-city1", "91.42.1.1"),
+ a("dyndns-city2", "91.42.1.2"),
+ aaaa("dyndns-city1", "2003:dd:d7ff::fe71:ce77"),
+ aaaa("dyndns-city2", "2003:dd:d7ff::fe71:ce78"),
+ ignore("dyndns-city1", "A,AAAA", ""),
+ ignore("dyndns-city2", "A,AAAA", ""),
+ ).ExpectNoChanges().UnsafeIgnore(),
+ tc("VERIFY PREVIOUS",
+ a("dyndns-city1", "91.42.1.1"),
+ a("dyndns-city2", "91.42.1.2"),
+ aaaa("dyndns-city1", "2003:dd:d7ff::fe71:ce77"),
+ aaaa("dyndns-city2", "2003:dd:d7ff::fe71:ce78"),
+ ).ExpectNoChanges(),
+ ),
+
+ // https://github.com/StackExchange/dnscontrol/issues/3227
+ testgroup("IGNORE w/change b3227",
+ tc("Create some records",
+ a("testignore", "8.8.8.8"),
+ a("testdefined", "9.9.9.9"),
+ ),
+ tc("ignore",
+ //a("testignore", "8.8.8.8"),
+ a("testdefined", "9.9.9.9"),
+ ignore("testignore", "", ""),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("testignore", "8.8.8.8"),
+ a("testdefined", "9.9.9.9"),
+ ).ExpectNoChanges(),
+
+ tc("Verify nothing changed",
+ a("testignore", "8.8.8.8"),
+ a("testdefined", "9.9.9.9"),
+ ).ExpectNoChanges(),
+ tc("VERIFY PREVIOUS",
+ a("testignore", "8.8.8.8"),
+ a("testdefined", "9.9.9.9"),
+ ).ExpectNoChanges(),
+
+ tc("ignore with change",
+ //a("testignore", "8.8.8.8"),
+ a("testdefined", "2.2.2.2"),
+ ignore("testignore", "", ""),
+ ),
+ tc("VERIFY PREVIOUS",
+ a("testignore", "8.8.8.8"),
+ a("testdefined", "2.2.2.2"),
+ ).ExpectNoChanges(),
+ ),
+
+ // OVH features
+
testgroup("structured TXT",
only("OVH"),
tc("Create TXT",
@@ -2018,6 +2668,58 @@ func makeTests(t *testing.T) []*TestGroup {
ovhdmarc("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@example.com")),
),
+ // PORKBUN features
+
+ testgroup("PORKBUN_URLFWD tests",
+ only("PORKBUN"),
+ tc("Add a urlfwd", porkbunUrlfwd("urlfwd1", "http://example.com", "", "", "")),
+ tc("Update a urlfwd", porkbunUrlfwd("urlfwd1", "http://example.org", "", "", "")),
+ tc("Update a urlfwd with metadata", porkbunUrlfwd("urlfwd1", "http://example.org", "permanent", "no", "no")),
+ ),
+
+ // GCORE features
+
+ testgroup("GCORE metadata tests",
+ only("GCORE"),
+ tc("Add record with metadata", withMeta(a("@", "1.2.3.4"), map[string]string{
+ "gcore_filters": "geodistance,false;first_n,false,2",
+ "gcore_asn": "1234,2345",
+ "gcore_continents": "as,na,an,sa,oc,eu,af",
+ "gcore_countries": "cn,us",
+ "gcore_latitude": "12.34",
+ "gcore_longitude": "67.89",
+ "gcore_notes": "test",
+ "gcore_weight": "12",
+ "gcore_ip": "1.2.3.4",
+ })),
+ tc("Update record with metadata", withMeta(a("@", "1.2.3.4"), map[string]string{
+ "gcore_filters": "healthcheck,false;geodns,false;first_n,false,3",
+ "gcore_failover_protocol": "HTTP",
+ "gcore_failover_port": "443",
+ "gcore_failover_frequency": "30",
+ "gcore_failover_timeout": "10",
+ "gcore_failover_method": "POST",
+ "gcore_failover_url": "/test",
+ "gcore_failover_tls": "false",
+ "gcore_failover_regexp": "",
+ "gcore_failover_host": "example.com",
+ "gcore_asn": "2345,3456",
+ "gcore_continents": "as,na",
+ "gcore_countries": "gb,fr",
+ "gcore_latitude": "12.89",
+ "gcore_longitude": "34.56",
+ "gcore_notes": "test2",
+ "gcore_weight": "34",
+ "gcore_ip": "4.3.2.1",
+ })),
+ tc("Delete metadata from record", a("@", "1.2.3.4")),
+ ),
+
+ // This MUST be the last test.
+ testgroup("final",
+ tc("final", txt("final", `TestDNSProviders was successful!`)),
+ ),
+
// Narrative: Congrats! You're done! If you've made it this far
// you're very close to being able to submit your PR. Here's
// some tips:
diff --git a/integrationTest/providers.json b/integrationTest/providers.json
index 019876b8d5..5b1f538706 100644
--- a/integrationTest/providers.json
+++ b/integrationTest/providers.json
@@ -38,6 +38,11 @@
"TYPE": "BIND",
"domain": "$BIND_DOMAIN"
},
+ "BUNNY_DNS": {
+ "TYPE": "BUNNY_DNS",
+ "domain": "$BUNNY_DNS_DOMAIN",
+ "api_key": "$BUNNY_DNS_API_KEY"
+ },
"CLOUDFLAREAPI": {
"TYPE": "CLOUDFLAREAPI",
"accountid": "$CLOUDFLAREAPI_ACCOUNTID",
@@ -67,6 +72,14 @@
"notification_emails": "$CSCGLOBAL_NOTIFICATION",
"user-token": "$CSCGLOBAL_USERTOKEN"
},
+ "CNR": {
+ "TYPE": "CNR",
+ "apientity": "$CNR_ENTITY",
+ "apilogin": "$CNR_UID",
+ "apipassword": "$CNR_PW",
+ "debugmode": "$CNR_DEBUGMODE",
+ "domain": "$CNR_DOMAIN"
+ },
"DESEC": {
"TYPE": "DESEC",
"auth-token": "$DESEC_TOKEN",
@@ -107,7 +120,8 @@
"GANDI_V5": {
"TYPE": "GANDI_V5",
"apikey": "$GANDI_V5_APIKEY",
- "domain": "$GANDI_V5_DOMAIN"
+ "domain": "$GANDI_V5_DOMAIN",
+ "token": "$GANDI_V5_TOKEN"
},
"GCLOUD": {
"TYPE": "GCLOUD",
@@ -149,6 +163,13 @@
"authToken": "$HOSTINGDE_AUTHTOKEN",
"domain": "$HOSTINGDE_DOMAIN"
},
+ "HUAWEICLOUD": {
+ "TYPE": "HUAWEICLOUD",
+ "domain": "$HUAWEICLOUD_DOMAIN",
+ "Region": "$HUAWEICLOUD_REGION",
+ "KeyId": "$HUAWEICLOUD_KEY_ID",
+ "SecretKey": "$HUAWEICLOUD_KEY"
+ },
"INWX": {
"TYPE": "INWX",
"domain": "$INWX_DOMAIN",
@@ -232,7 +253,8 @@
"app-key": "$OVH_APP_KEY",
"app-secret-key": "$OVH_APP_SECRET_KEY",
"consumer-key": "$OVH_CONSUMER_KEY",
- "domain": "$OVH_DOMAIN"
+ "domain": "$OVH_DOMAIN",
+ "endpoint": "$OVH_ENDPOINT"
},
"PACKETFRAME": {
"TYPE": "PACKETFRAME",
@@ -252,12 +274,25 @@
"domain": "$POWERDNS_DOMAIN",
"serverName": "$POWERDNS_SERVERNAME"
},
+ "REALTIMEREGISTER": {
+ "TYPE": "REALTIMEREGISTER",
+ "apikey": "$REALTIMEREGISTER_APIKEY",
+ "sandbox" : "$REALTIMEREGISTER_SANDBOX",
+ "domain": "$REALTIMEREGISTER_DOMAIN",
+ "premium": "$REALTIMEREGISTER_PREMIUM"
+ },
"ROUTE53": {
"KeyId": "$ROUTE53_KEY_ID",
"SecretKey": "$ROUTE53_KEY",
"TYPE": "ROUTE53",
"domain": "$ROUTE53_DOMAIN"
},
+ "SAKURACLOUD": {
+ "TYPE": "SAKURACLOUD",
+ "access_token": "$SAKURACLOUD_ACCESS_TOKEN",
+ "access_token_secret": "$SAKURACLOUD_ACCESS_TOKEN_SECRET",
+ "domain": "$SAKURACLOUD_DOMAIN"
+ },
"SOFTLAYER": {
"TYPE": "SOFTLAYER",
"api_key": "$SL_API_KEY",
diff --git a/main.go b/main.go
index c86971cb88..02ee05b8d7 100644
--- a/main.go
+++ b/main.go
@@ -2,37 +2,30 @@ package main
import (
"fmt"
- "log"
"os"
"runtime/debug"
"github.com/StackExchange/dnscontrol/v4/commands"
- "github.com/StackExchange/dnscontrol/v4/pkg/version"
_ "github.com/StackExchange/dnscontrol/v4/providers/_all"
"github.com/fatih/color"
)
-//go:generate go run build/generate/generate.go build/generate/featureMatrix.go build/generate/functionTypes.go build/generate/dtsFile.go
+//go:generate go run build/generate/generate.go build/generate/featureMatrix.go build/generate/functionTypes.go build/generate/dtsFile.go build/generate/ownersFile.go
// Version management. Goals:
// 1. Someone who just does "go get" has at least some information.
// 2. If built with build/build.go, more specific build information gets put in.
+// GoReleaser: version
var (
- SHA = ""
- Version = ""
- BuildTime = ""
+ version = "dev"
)
func main() {
- version.SHA = SHA
- version.Semver = Version
- version.BuildTime = BuildTime
if os.Getenv("CI") == "true" {
color.NoColor = false
}
- log.SetFlags(log.LstdFlags | log.Lshortfile)
if info, ok := debug.ReadBuildInfo(); !ok && info == nil {
fmt.Fprint(os.Stderr, "Warning: dnscontrol was built without Go modules. See https://docs.dnscontrol.org/getting-started/getting-started#source for more information on how to build dnscontrol correctly.\n\n")
}
- os.Exit(commands.Run("dnscontrol " + version.Banner()))
+ os.Exit(commands.Run("DNSControl version " + version))
}
diff --git a/models/dns.go b/models/dns.go
index 7b73eb895c..a2ed16f626 100644
--- a/models/dns.go
+++ b/models/dns.go
@@ -118,19 +118,3 @@ func (config *DNSConfig) DomainContainingFQDN(fqdn string) *DomainConfig {
}
return d
}
-
-//// IgnoreName describes an IGNORE_NAME rule.
-//type IgnoreName struct {
-// Pattern string `json:"pattern"` // Glob pattern.
-// Types string `json:"types"` // All caps rtype names, comma separated.
-//}
-//
-//// IgnoreTarget describes an IGNORE_TARGET rule.
-//type IgnoreTarget struct {
-// Pattern string `json:"pattern"` // Glob pattern.
-// Type string `json:"type"` // All caps rtype name.
-//}
-//
-//func (i *IgnoreTarget) String() string {
-// return i.Pattern
-//}
diff --git a/models/dnsrr.go b/models/dnsrr.go
index 1ef0f82120..7c7c4f9c5f 100644
--- a/models/dnsrr.go
+++ b/models/dnsrr.go
@@ -9,53 +9,25 @@ import (
"github.com/miekg/dns"
)
-//// Header Header returns the header of an resource record.
-//func (rc *RecordConfig) Header() *dns.RR_Header {
-// log.Fatal("Header not implemented")
-// return nil
-//}
-
// String returns the text representation of the resource record.
func (rc *RecordConfig) String() string {
return rc.GetTargetCombined()
}
-//// copy returns a copy of the RR
-//func (rc *RecordConfig) copy() dns.RR {
-// log.Fatal("Copy not implemented")
-// return dns.TypeToRR[dns.TypeA]()
-//}
-//
-//// len returns the length (in octets) of the uncompressed RR in wire format.
-//func (rc *RecordConfig) len() int {
-// log.Fatal("len not implemented")
-// return 0
-//}
-//
-//// pack packs an RR into wire format.
-//func (rc *RecordConfig) pack([]byte, int, map[string]int, bool) (int, error) {
-// log.Fatal("pack not implemented")
-// return 0, nil
-//}
-
// Conversions
-// RRstoRCs converts []dns.RR to []RecordConfigs.
-func RRstoRCs(rrs []dns.RR, origin string) (Records, error) {
- rcs := make(Records, 0, len(rrs))
- for _, r := range rrs {
- rc, err := RRtoRC(r, origin)
- if err != nil {
- return nil, err
- }
+// RRtoRC converts dns.RR to RecordConfig
+func RRtoRC(rr dns.RR, origin string) (RecordConfig, error) {
+ return helperRRtoRC(rr, origin, false)
+}
- rcs = append(rcs, &rc)
- }
- return rcs, nil
+// RRtoRCTxtBug converts dns.RR to RecordConfig. Compensates for the backslash bug in github.com/miekg/dns/issues/1384.
+func RRtoRCTxtBug(rr dns.RR, origin string) (RecordConfig, error) {
+ return helperRRtoRC(rr, origin, true)
}
-// RRtoRC converts dns.RR to RecordConfig
-func RRtoRC(rr dns.RR, origin string) (RecordConfig, error) {
+// helperRRtoRC converts dns.RR to RecordConfig. If fixBug is true, replaces `\\` to `\` in TXT records to compensate for github.com/miekg/dns/issues/1384.
+func helperRRtoRC(rr dns.RR, origin string, fixBug bool) (RecordConfig, error) {
// Convert's dns.RR into our native data type (RecordConfig).
// Records are translated directly with no changes.
header := rr.Header()
@@ -76,8 +48,14 @@ func RRtoRC(rr dns.RR, origin string) (RecordConfig, error) {
err = rc.SetTarget(v.Target)
case *dns.DHCID:
err = rc.SetTarget(v.Digest)
+ case *dns.DNAME:
+ err = rc.SetTarget(v.Target)
case *dns.DS:
err = rc.SetTargetDS(v.KeyTag, v.Algorithm, v.DigestType, v.Digest)
+ case *dns.DNSKEY:
+ err = rc.SetTargetDNSKEY(v.Flags, v.Protocol, v.Algorithm, v.PublicKey)
+ case *dns.HTTPS:
+ err = rc.SetTargetSVCB(v.Priority, v.Target, v.Value)
case *dns.LOC:
err = rc.SetTargetLOC(v.Version, v.Latitude, v.Longitude, v.Altitude, v.Size, v.HorizPre, v.VertPre)
case *dns.MX:
@@ -94,10 +72,20 @@ func RRtoRC(rr dns.RR, origin string) (RecordConfig, error) {
err = rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target)
case *dns.SSHFP:
err = rc.SetTargetSSHFP(v.Algorithm, v.Type, v.FingerPrint)
+ case *dns.SVCB:
+ err = rc.SetTargetSVCB(v.Priority, v.Target, v.Value)
case *dns.TLSA:
err = rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate)
case *dns.TXT:
- err = rc.SetTargetTXTs(v.Txt)
+ if fixBug {
+ t := strings.Join(v.Txt, "")
+ te := t
+ te = strings.ReplaceAll(te, `\\`, `\`)
+ te = strings.ReplaceAll(te, `\"`, `"`)
+ err = rc.SetTargetTXT(te)
+ } else {
+ err = rc.SetTargetTXTs(v.Txt)
+ }
default:
return *rc, fmt.Errorf("rrToRecord: Unimplemented zone record type=%s (%v)", rc.Type, rr)
}
diff --git a/models/domain.go b/models/domain.go
index c25d9f7514..3054a443d1 100644
--- a/models/domain.go
+++ b/models/domain.go
@@ -3,6 +3,7 @@ package models
import (
"fmt"
"strings"
+ "sync"
"github.com/qdm12/reprint"
"golang.org/x/net/idna"
@@ -23,9 +24,10 @@ type DomainConfig struct {
// Metadata[DomainUniqueName] // .Name + "!" + .Tag
// Metadata[DomainTag] // split horizon tag
- Metadata map[string]string `json:"meta,omitempty"`
- Records Records `json:"records"`
- Nameservers []*Nameserver `json:"nameservers,omitempty"`
+ Metadata map[string]string `json:"meta,omitempty"`
+ Records Records `json:"records"`
+ Nameservers []*Nameserver `json:"nameservers,omitempty"`
+ NameserversMutex sync.Mutex `json:"-"`
EnsureAbsent Records `json:"recordsabsent,omitempty"` // ENSURE_ABSENT
KeepUnknown bool `json:"keepunknown,omitempty"` // NO_PURGE
@@ -42,6 +44,15 @@ type DomainConfig struct {
// 2. Final driver instances are loaded after we load credentials. Any actual provider interaction requires that.
RegistrarInstance *RegistrarInstance `json:"-"`
DNSProviderInstances []*DNSProviderInstance `json:"-"`
+
+ // Raw user-input from dnsconfig.js that will be processed into RecordConfigs later:
+ RawRecords []RawRecordConfig `json:"rawrecords,omitempty"`
+
+ // Pending work to do for each provider. Provider may be a registrar or DSP.
+ pendingCorrectionsMutex sync.Mutex // Protect pendingCorrections*
+ pendingCorrections map[string]([]*Correction) // Work to be done for each provider
+ pendingCorrectionsOrder []string // Call the providers in this order
+ pendingActualChangeCount map[string](int) // Number of changes to report (cumulative)
}
// GetSplitHorizonNames returns the domain's name, uniquename, and tag.
@@ -84,17 +95,6 @@ func (dc *DomainConfig) Copy() (*DomainConfig, error) {
newDc := &DomainConfig{}
err := reprint.FromTo(dc, newDc) // Deep copy
return newDc, err
-
- // NB(tlim): The old version of this copied the structure by gob-encoding
- // and decoding it. gob doesn't like the dc.RegisterInstance or
- // dc.DNSProviderInstances fields, so we saved a temporary copy of those,
- // nil'ed out the original, did the gob copy, and then manually copied those
- // fields using the temp variables we saved. It looked like:
- //reg, dnsps := dc.RegistrarInstance, dc.DNSProviderInstances
- //dc.RegistrarInstance, dc.DNSProviderInstances = nil, nil
- // (perform the copy)
- //dc.RegistrarInstance, dc.DNSProviderInstances = reg, dnsps
- //newDc.RegistrarInstance, newDc.DNSProviderInstances = reg, dnsps
}
// Filter removes all records that don't match the filter f.
@@ -124,16 +124,16 @@ func (dc *DomainConfig) Punycode() error {
// Set the target:
switch rec.Type { // #rtype_variations
- case "ALIAS", "MX", "NS", "CNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS", "NS1_URLFWD", "AKAMAICDN", "CLOUDNS_WR":
+ case "ALIAS", "MX", "NS", "CNAME", "DNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS", "NS1_URLFWD", "AKAMAICDN", "CLOUDNS_WR", "PORKBUN_URLFWD":
// These rtypes are hostnames, therefore need to be converted (unlike, for example, an AAAA record)
t, err := idna.ToASCII(rec.GetTargetField())
if err != nil {
return err
}
rec.SetTarget(t)
- case "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
+ case "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
rec.SetTarget(rec.GetTargetField())
- case "A", "AAAA", "CAA", "DHCPID", "DS", "LOC", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS":
+ case "A", "AAAA", "CAA", "DHCID", "DNSKEY", "DS", "HTTPS", "LOC", "NAPTR", "SOA", "SSHFP", "SVCB", "TXT", "TLSA", "AZURE_ALIAS":
// Nothing to do.
default:
return fmt.Errorf("Punycode rtype %v unimplemented", rec.Type)
@@ -141,3 +141,59 @@ func (dc *DomainConfig) Punycode() error {
}
return nil
}
+
+// StoreCorrections accumulates corrections in a thread-safe way.
+func (dc *DomainConfig) StoreCorrections(providerName string, corrections []*Correction) {
+ dc.pendingCorrectionsMutex.Lock()
+ defer dc.pendingCorrectionsMutex.Unlock()
+
+ if dc.pendingCorrections == nil {
+ // First time storing anything.
+ dc.pendingCorrections = make(map[string]([]*Correction))
+ dc.pendingCorrections[providerName] = corrections
+ dc.pendingCorrectionsOrder = []string{providerName}
+ } else if c, ok := dc.pendingCorrections[providerName]; !ok {
+ // First time key used
+ dc.pendingCorrections[providerName] = corrections
+ dc.pendingCorrectionsOrder = []string{providerName}
+ } else {
+ // Add to existing.
+ dc.pendingCorrections[providerName] = append(c, corrections...)
+ dc.pendingCorrectionsOrder = append(dc.pendingCorrectionsOrder, providerName)
+ }
+}
+
+// GetCorrections returns the accumulated corrections for providerName.
+func (dc *DomainConfig) GetCorrections(providerName string) []*Correction {
+ dc.pendingCorrectionsMutex.Lock()
+ defer dc.pendingCorrectionsMutex.Unlock()
+
+ if dc.pendingCorrections == nil {
+ // First time storing anything.
+ return nil
+ }
+ if c, ok := dc.pendingCorrections[providerName]; ok {
+ return c
+ }
+ return nil
+}
+
+// IncrementChangeCount accumulates change count in a thread-safe way.
+func (dc *DomainConfig) IncrementChangeCount(providerName string, delta int) {
+ dc.pendingCorrectionsMutex.Lock()
+ defer dc.pendingCorrectionsMutex.Unlock()
+
+ if dc.pendingActualChangeCount == nil {
+ // First time storing anything.
+ dc.pendingActualChangeCount = make(map[string](int))
+ }
+ dc.pendingActualChangeCount[providerName] += delta
+}
+
+// GetChangeCount accumulates change count in a thread-safe way.
+func (dc *DomainConfig) GetChangeCount(providerName string) int {
+ dc.pendingCorrectionsMutex.Lock()
+ defer dc.pendingCorrectionsMutex.Unlock()
+
+ return dc.pendingActualChangeCount[providerName]
+}
diff --git a/models/provider.go b/models/provider.go
index 11c7453bd7..e98e83259a 100644
--- a/models/provider.go
+++ b/models/provider.go
@@ -4,7 +4,7 @@ package models
type DNSProvider interface {
GetNameservers(domain string) ([]*Nameserver, error)
GetZoneRecords(domain string, meta map[string]string) (Records, error)
- GetZoneRecordsCorrections(dc *DomainConfig, existing Records) ([]*Correction, error)
+ GetZoneRecordsCorrections(dc *DomainConfig, existing Records) ([]*Correction, int, error)
}
// Registrar is an interface for Registrar plug-ins.
diff --git a/models/quotes.go b/models/quotes.go
index 62c68d1ed9..b7736c89b5 100644
--- a/models/quotes.go
+++ b/models/quotes.go
@@ -7,8 +7,12 @@ import (
"github.com/miekg/dns"
)
-// IsQuoted returns true if the string starts and ends with a double quote.
-func IsQuoted(s string) bool {
+/*
+TODO(tlim): Move this file to pkgs/txtutil. It doesn't need to be part
+*/
+
+// isQuoted returns true if the string starts and ends with a double quote.
+func isQuoted(s string) bool {
if s == "" {
return false
}
@@ -24,7 +28,7 @@ func IsQuoted(s string) bool {
// StripQuotes returns the string with the starting and ending quotes removed.
// If it is not quoted, the original string is returned.
func StripQuotes(s string) string {
- if IsQuoted(s) {
+ if isQuoted(s) {
return s[1 : len(s)-1]
}
return s
@@ -41,7 +45,7 @@ func StripQuotes(s string) string {
// NOTE: This doesn't handle escaped quotes.
// NOTE: You probably want to use ParseQuotedFields() for RFC 1035-compliant quoting.
func ParseQuotedTxt(s string) []string {
- if !IsQuoted(s) {
+ if !isQuoted(s) {
return []string{s}
}
return strings.Split(StripQuotes(s), `" "`)
diff --git a/models/quotes_test.go b/models/quotes_test.go
index 86cc80f730..595bdb124a 100644
--- a/models/quotes_test.go
+++ b/models/quotes_test.go
@@ -1,6 +1,9 @@
package models
-import "testing"
+import (
+ "strings"
+ "testing"
+)
func TestIsQuoted(t *testing.T) {
tests := []struct {
@@ -16,7 +19,7 @@ func TestIsQuoted(t *testing.T) {
{`"aaa" "bbb"`, true},
}
for i, test := range tests {
- r := IsQuoted(test.d1)
+ r := isQuoted(test.d1)
if r != test.e1 {
t.Errorf("%v: expected (%v) got (%v)", i, test.e1, r)
}
@@ -48,6 +51,8 @@ func TestStripQuotes(t *testing.T) {
}
}
+func r(s string, c int) string { return strings.Repeat(s, c) }
+
func TestParseQuotedTxt(t *testing.T) {
tests := []struct {
d1 string
@@ -59,15 +64,35 @@ func TestParseQuotedTxt(t *testing.T) {
{`foo bar`, []string{`foo bar`}},
{`"aaa" "bbb"`, []string{`aaa`, `bbb`}},
{`"a"a" "bbb"`, []string{`a"a`, `bbb`}},
+ // Seen in live traffic:
+ {"\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\"",
+ []string{r("B", 254)}},
+ {"\"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\"",
+ []string{r("C", 255)}},
+ {"\"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\" \"D\"",
+ []string{r("D", 255), "D"}},
+ {"\"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\" \"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\"",
+ []string{r("E", 255), r("E", 255)}},
+ {"\"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\" \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\" \"F\"",
+ []string{r("F", 255), r("F", 255), "F"}},
+ {"\"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\" \"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\" \"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\"",
+ []string{r("G", 255), r("G", 255), r("G", 255)}},
+ {"\"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"H\"",
+ []string{r("H", 255), r("H", 255), r("H", 255), "H"}},
+ {"\"quo'te\"", []string{`quo'te`}},
+ {"\"blah`blah\"", []string{"blah`blah"}},
+ //{"\"quo\\\"te\"", []string{`quo"te`}},
+ //{"\"q\\\"uo\\\"te\"", []string{`q"uo"te`}},
+ //{"\"backs\\\\lash\"", []string{`back\slash`}},
}
for i, test := range tests {
ls := ParseQuotedTxt(test.d1)
if len(ls) != len(test.e2) {
- t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e2, ls)
+ t.Errorf("%v: expected LEN TxtStrings=q(%q) got q(%q)", i, test.e2, ls)
}
for i := range ls {
if ls[i] != test.e2[i] {
- t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e2, ls)
+ t.Errorf("%v: expected TxtStrings=q(%q) got q(%q)", i, test.e2, ls)
}
}
}
diff --git a/models/rawrecord.go b/models/rawrecord.go
new file mode 100644
index 0000000000..47d30ef189
--- /dev/null
+++ b/models/rawrecord.go
@@ -0,0 +1,12 @@
+package models
+
+// RawRecordConfig stores the user-input from dnsconfig.js for a DNS
+// Record. This is later processed (in Go) to become a RecordConfig.
+// NOTE: Only newer rtypes are processed this way. Eventually the
+// legacy types will be converted.
+type RawRecordConfig struct {
+ Type string `json:"type"`
+ Args []any `json:"args,omitempty"`
+ Metas []map[string]any `json:"metas,omitempty"`
+ TTL uint32 `json:"ttl,omitempty"`
+}
diff --git a/models/record.go b/models/record.go
index 894c49dace..d83a11d7d7 100644
--- a/models/record.go
+++ b/models/record.go
@@ -4,9 +4,9 @@ import (
"encoding/json"
"fmt"
"log"
- "sort"
"strings"
+ "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/jinzhu/copier"
"github.com/miekg/dns"
"github.com/miekg/dns/dnsutil"
@@ -22,6 +22,7 @@ import (
// ANAME // Technically not an official rtype yet.
// CAA
// CNAME
+// HTTPS
// LOC
// MX
// NAPTR
@@ -30,6 +31,7 @@ import (
// SOA
// SRV
// SSHFP
+// SVCB
// TLSA
// TXT
// Pseudo-Types: (alphabetical)
@@ -37,6 +39,7 @@ import (
// CF_REDIRECT
// CF_TEMP_REDIRECT
// CF_WORKER_ROUTE
+// CLOUDFLAREAPI_SINGLE_REDIRECT
// CLOUDNS_WR
// FRAME
// IMPORT_TRANSFORM
@@ -44,11 +47,14 @@ import (
// NO_PURGE
// NS1_URLFWD
// PAGE_RULE
+// PORKBUN_URLFWD
// PURGE
// URL
// URL301
// WORKER_ROUTE
//
+// NOTE: All NEW record types should be prefixed with the provider name (Correct: CLOUDFLAREAPI_SINGLE_REDIRECT. Wrong: CF_REDIRECT)
+//
// Notes about the fields:
//
// Name:
@@ -86,14 +92,14 @@ import (
type RecordConfig struct {
Type string `json:"type"` // All caps rtype name.
Name string `json:"name"` // The short name. See above.
+ NameFQDN string `json:"-"` // Must end with ".$origin". See above.
SubDomain string `json:"subdomain,omitempty"`
- NameFQDN string `json:"-"` // Must end with ".$origin". See above.
target string // If a name, must end with "."
TTL uint32 `json:"ttl,omitempty"`
Metadata map[string]string `json:"meta,omitempty"`
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
- // If you add a field to this struct, also add it to the list on MarshalJSON.
+ // If you add a field to this struct, also add it to the list in the UnmarshalJSON function.
MxPreference uint16 `json:"mxpreference,omitempty"`
SrvPriority uint16 `json:"srvpriority,omitempty"`
SrvWeight uint16 `json:"srvweight,omitempty"`
@@ -104,6 +110,10 @@ type RecordConfig struct {
DsAlgorithm uint8 `json:"dsalgorithm,omitempty"`
DsDigestType uint8 `json:"dsdigesttype,omitempty"`
DsDigest string `json:"dsdigest,omitempty"`
+ DnskeyFlags uint16 `json:"dnskeyflags,omitempty"`
+ DnskeyProtocol uint8 `json:"dnskeyprotocol,omitempty"`
+ DnskeyAlgorithm uint8 `json:"dnskeyalgorithm,omitempty"`
+ DnskeyPublicKey string `json:"dnskeypublickey,omitempty"`
LocVersion uint8 `json:"locversion,omitempty"`
LocSize uint8 `json:"locsize,omitempty"`
LocHorizPre uint8 `json:"lochorizpre,omitempty"`
@@ -124,12 +134,39 @@ type RecordConfig struct {
SoaRetry uint32 `json:"soaretry,omitempty"`
SoaExpire uint32 `json:"soaexpire,omitempty"`
SoaMinttl uint32 `json:"soaminttl,omitempty"`
+ SvcPriority uint16 `json:"svcpriority,omitempty"`
+ SvcParams string `json:"svcparams,omitempty"`
TlsaUsage uint8 `json:"tlsausage,omitempty"`
TlsaSelector uint8 `json:"tlsaselector,omitempty"`
TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"`
- TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores all the strings joined.
R53Alias map[string]string `json:"r53_alias,omitempty"`
AzureAlias map[string]string `json:"azure_alias,omitempty"`
+ UnknownTypeName string `json:"unknown_type_name,omitempty"`
+
+ // Cloudflare-specific fields:
+ // When these are used, .target is set to a human-readable version (only to be used for display purposes).
+ CloudflareRedirect *CloudflareSingleRedirectConfig `json:"cloudflareapi_redirect,omitempty"`
+}
+
+// CloudflareSingleRedirectConfig contains info about a Cloudflare Single Redirect.
+//
+// When these are used, .target is set to a human-readable version (only to be used for display purposes).
+type CloudflareSingleRedirectConfig struct {
+ //
+ Code uint16 `json:"code,omitempty"` // 301 or 302
+ // PR == PageRule
+ PRWhen string `json:"pr_when,omitempty"`
+ PRThen string `json:"pr_then,omitempty"`
+ PRPriority int `json:"pr_priority,omitempty"` // Really an identifier for the rule.
+ PRDisplay string `json:"pr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_REDIRECT/CF_TEMP_REDIRECT
+ //
+ // SR == SingleRedirect
+ SRName string `json:"sr_name,omitempty"` // How is this displayed to the user
+ SRWhen string `json:"sr_when,omitempty"`
+ SRThen string `json:"sr_then,omitempty"`
+ SRRRulesetID string `json:"sr_rulesetid,omitempty"`
+ SRRRulesetRuleID string `json:"sr_rulesetruleid,omitempty"`
+ SRDisplay string `json:"sr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_SINGLE_REDIRECT
}
// MarshalJSON marshals RecordConfig.
@@ -161,6 +198,7 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
TTL uint32 `json:"ttl,omitempty"`
Metadata map[string]string `json:"meta,omitempty"`
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
+ Args []any `json:"args,omitempty"`
MxPreference uint16 `json:"mxpreference,omitempty"`
SrvPriority uint16 `json:"srvpriority,omitempty"`
@@ -172,6 +210,10 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
DsAlgorithm uint8 `json:"dsalgorithm,omitempty"`
DsDigestType uint8 `json:"dsdigesttype,omitempty"`
DsDigest string `json:"dsdigest,omitempty"`
+ DnskeyFlags uint16 `json:"dnskeyflags,omitempty"`
+ DnskeyProtocol uint8 `json:"dnskeyprotocol,omitempty"`
+ DnskeyAlgorithm uint8 `json:"dnskeyalgorithm,omitempty"`
+ DnskeyPublicKey string `json:"dnskeypublickey,omitempty"`
LocVersion uint8 `json:"locversion,omitempty"`
LocSize uint8 `json:"locsize,omitempty"`
LocHorizPre uint8 `json:"lochorizpre,omitempty"`
@@ -192,12 +234,14 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
SoaRetry uint32 `json:"soaretry,omitempty"`
SoaExpire uint32 `json:"soaexpire,omitempty"`
SoaMinttl uint32 `json:"soaminttl,omitempty"`
+ SvcPriority uint16 `json:"svcpriority,omitempty"`
+ SvcParams string `json:"svcparams,omitempty"`
TlsaUsage uint8 `json:"tlsausage,omitempty"`
TlsaSelector uint8 `json:"tlsaselector,omitempty"`
TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"`
- TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one.
R53Alias map[string]string `json:"r53_alias,omitempty"`
AzureAlias map[string]string `json:"azure_alias,omitempty"`
+ UnknownTypeName string `json:"unknown_type_name,omitempty"`
EnsureAbsent bool `json:"ensure_absent,omitempty"` // Override NO_PURGE and delete this record
@@ -269,14 +313,6 @@ func (rc *RecordConfig) SetLabel(short, origin string) {
}
}
-// UnsafeSetLabelNull sets the label to "". Normally the FQDN is denoted by .Name being
-// "@" however this can be used to violate that assertion. It should only be used
-// on copies of a RecordConfig that is being used for non-standard things like
-// Marshalling yaml.
-func (rc *RecordConfig) UnsafeSetLabelNull() {
- rc.Name = ""
-}
-
// SetLabelFromFQDN sets the .Name/.NameFQDN fields given a FQDN and origin.
// fqdn may have a trailing "." but it is not required.
// origin may not have a trailing dot.
@@ -313,47 +349,24 @@ func (rc *RecordConfig) GetLabelFQDN() string {
return rc.NameFQDN
}
-// ToDiffable returns a string that is comparable by a differ.
-// extraMaps: a list of maps that should be included in the comparison.
-// NB(tlim): This will be deprecated when pkg/diff is replaced by pkg/diff2.
-// Use // ToComparableNoTTL() instead.
-func (rc *RecordConfig) ToDiffable(extraMaps ...map[string]string) string {
- var content string
- switch rc.Type {
- case "SOA":
- content = fmt.Sprintf("%s %v %d %d %d %d ttl=%d", rc.target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl, rc.TTL)
- // SoaSerial is not used in comparison
- default:
- content = fmt.Sprintf("%v ttl=%d", rc.GetTargetCombined(), rc.TTL)
- }
- for _, valueMap := range extraMaps {
- // sort the extra values map keys to perform a deterministic
- // comparison since Golang maps iteration order is not guaranteed
-
- // FIXME(tlim) The keys of each map is sorted per-map, not across
- // all maps. This may be intentional since we'd have no way to
- // deal with duplicates.
-
- keys := make([]string, 0)
- for k := range valueMap {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, k := range keys {
- v := valueMap[k]
- content += fmt.Sprintf(" %s=%s", k, v)
- }
- }
- return content
-}
-
// ToComparableNoTTL returns a comparison string. If you need to compare two
// RecordConfigs, you can simply compare the string returned by this function.
// The comparison includes all fields except TTL and any provider-specific
// metafields. Provider-specific metafields like CF_PROXY are not the same as
// pseudo-records like ANAME or R53_ALIAS
-// This replaces ToDiff()
func (rc *RecordConfig) ToComparableNoTTL() string {
+ switch rc.Type {
+ case "SOA":
+ return fmt.Sprintf("%s %v %d %d %d %d", rc.target, rc.SoaMbox, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl)
+ // SoaSerial is not included because it isn't used in comparisons.
+ case "TXT":
+ //fmt.Fprintf(os.Stdout, "DEBUG: ToComNoTTL raw txts=%s q=%q\n", rc.target, rc.target)
+ r := txtutil.EncodeQuoted(rc.target)
+ //fmt.Fprintf(os.Stdout, "DEBUG: ToComNoTTL cmp txts=%s q=%q\n", r, r)
+ return r
+ case "UNKNOWN":
+ return fmt.Sprintf("rtype=%s rdata=%s", rc.UnknownTypeName, rc.target)
+ }
return rc.GetTargetCombined()
}
@@ -392,13 +405,23 @@ func (rc *RecordConfig) ToRR() dns.RR {
rr.(*dns.CNAME).Target = rc.GetTargetField()
case dns.TypeDHCID:
rr.(*dns.DHCID).Digest = rc.GetTargetField()
+ case dns.TypeDNAME:
+ rr.(*dns.DNAME).Target = rc.GetTargetField()
case dns.TypeDS:
rr.(*dns.DS).Algorithm = rc.DsAlgorithm
rr.(*dns.DS).DigestType = rc.DsDigestType
rr.(*dns.DS).Digest = rc.DsDigest
rr.(*dns.DS).KeyTag = rc.DsKeyTag
+ case dns.TypeDNSKEY:
+ rr.(*dns.DNSKEY).Flags = rc.DnskeyFlags
+ rr.(*dns.DNSKEY).Protocol = rc.DnskeyProtocol
+ rr.(*dns.DNSKEY).Algorithm = rc.DnskeyAlgorithm
+ rr.(*dns.DNSKEY).PublicKey = rc.DnskeyPublicKey
+ case dns.TypeHTTPS:
+ rr.(*dns.HTTPS).Priority = rc.SvcPriority
+ rr.(*dns.HTTPS).Target = rc.GetTargetField()
+ rr.(*dns.HTTPS).Value = rc.GetSVCBValue()
case dns.TypeLOC:
- //this is for records from .js files and read from API
// fmt.Printf("ToRR long: %d, lat:%d, sz: %d, hz:%d, vt:%d\n", rc.LocLongitude, rc.LocLatitude, rc.LocSize, rc.LocHorizPre, rc.LocVertPre)
// fmt.Printf("ToRR rc: %+v\n", *rc)
rr.(*dns.LOC).Version = rc.LocVersion
@@ -431,7 +454,7 @@ func (rc *RecordConfig) ToRR() dns.RR {
rr.(*dns.SOA).Expire = rc.SoaExpire
rr.(*dns.SOA).Minttl = rc.SoaMinttl
case dns.TypeSPF:
- rr.(*dns.SPF).Txt = rc.TxtStrings
+ rr.(*dns.SPF).Txt = rc.GetTargetTXTSegmented()
case dns.TypeSRV:
rr.(*dns.SRV).Priority = rc.SrvPriority
rr.(*dns.SRV).Weight = rc.SrvWeight
@@ -441,13 +464,17 @@ func (rc *RecordConfig) ToRR() dns.RR {
rr.(*dns.SSHFP).Algorithm = rc.SshfpAlgorithm
rr.(*dns.SSHFP).Type = rc.SshfpFingerprint
rr.(*dns.SSHFP).FingerPrint = rc.GetTargetField()
+ case dns.TypeSVCB:
+ rr.(*dns.SVCB).Priority = rc.SvcPriority
+ rr.(*dns.SVCB).Target = rc.GetTargetField()
+ rr.(*dns.SVCB).Value = rc.GetSVCBValue()
case dns.TypeTLSA:
rr.(*dns.TLSA).Usage = rc.TlsaUsage
rr.(*dns.TLSA).MatchingType = rc.TlsaMatchingType
rr.(*dns.TLSA).Selector = rc.TlsaSelector
rr.(*dns.TLSA).Certificate = rc.GetTargetField()
case dns.TypeTXT:
- rr.(*dns.TXT).Txt = rc.TxtStrings
+ rr.(*dns.TXT).Txt = rc.GetTargetTXTSegmented()
default:
panic(fmt.Sprintf("ToRR: Unimplemented rtype %v", rc.Type))
// We panic so that we quickly find any switch statements
@@ -461,7 +488,8 @@ func (rc *RecordConfig) ToRR() dns.RR {
func (rc *RecordConfig) GetDependencies() []string {
switch rc.Type {
- case "NS", "SRV", "CNAME", "MX", "ALIAS", "AZURE_ALIAS", "R53_ALIAS":
+ // #rtype_variations
+ case "NS", "SRV", "CNAME", "DNAME", "MX", "ALIAS", "AZURE_ALIAS", "R53_ALIAS":
return []string{
rc.target,
}
@@ -499,6 +527,21 @@ func (rc *RecordConfig) Key() RecordKey {
return RecordKey{rc.NameFQDN, t}
}
+// GetSVCBValue returns the SVCB Key/Values as a list of Key/Values.
+func (rc *RecordConfig) GetSVCBValue() []dns.SVCBKeyValue {
+ record, err := dns.NewRR(fmt.Sprintf("%s %s %d %s %s", rc.NameFQDN, rc.Type, rc.SvcPriority, rc.target, rc.SvcParams))
+ if err != nil {
+ log.Fatalf("could not parse SVCB record: %s", err)
+ }
+ switch r := record.(type) {
+ case *dns.HTTPS:
+ return r.Value
+ case *dns.SVCB:
+ return r.Value
+ }
+ return nil
+}
+
// Records is a list of *RecordConfig.
type Records []*RecordConfig
@@ -512,16 +555,6 @@ func (recs Records) HasRecordTypeName(rtype, name string) bool {
return false
}
-// FQDNMap returns a map of all LabelFQDNs. Useful for making a
-// truthtable of labels that exist in Records.
-func (recs Records) FQDNMap() (m map[string]bool) {
- m = map[string]bool{}
- for _, rec := range recs {
- m[rec.GetLabelFQDN()] = true
- }
- return m
-}
-
// GetByType returns the records that match rtype typeName.
func (recs Records) GetByType(typeName string) Records {
results := Records{}
@@ -542,19 +575,6 @@ func (recs Records) GroupedByKey() map[RecordKey]Records {
return groups
}
-// GroupedByLabel returns a map of keys to records, and their original key order.
-func (recs Records) GroupedByLabel() ([]string, map[string]Records) {
- order := []string{}
- groups := map[string]Records{}
- for _, rec := range recs {
- if _, found := groups[rec.Name]; !found {
- order = append(order, rec.Name)
- }
- groups[rec.Name] = append(groups[rec.Name], rec)
- }
- return order, groups
-}
-
// GroupedByFQDN returns a map of keys to records, grouped by FQDN.
func (recs Records) GroupedByFQDN() ([]string, map[string]Records) {
order := []string{}
@@ -591,11 +611,11 @@ func Downcase(recs []*RecordConfig) {
r.Name = strings.ToLower(r.Name)
r.NameFQDN = strings.ToLower(r.NameFQDN)
switch r.Type { // #rtype_variations
- case "AKAMAICDN", "AAAA", "ANAME", "CNAME", "DS", "MX", "NS", "NAPTR", "PTR", "SRV", "TLSA":
+ case "AKAMAICDN", "ALIAS", "AAAA", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "MX", "NS", "NAPTR", "PTR", "SRV", "TLSA":
// Target is case insensitive. Downcase it.
r.target = strings.ToLower(r.target)
// BUGFIX(tlim): isn't ALIAS in the wrong case statement?
- case "A", "ALIAS", "CAA", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "DHCID", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TXT":
+ case "A", "CAA", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "DHCID", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TXT":
// Do nothing. (IP address or case sensitive target)
case "SOA":
if r.target != "DEFAULT_NOT_SET." {
@@ -616,10 +636,10 @@ func CanonicalizeTargets(recs []*RecordConfig, origin string) {
for _, r := range recs {
switch r.Type { // #rtype_variations
- case "AKAMAICDN", "ANAME", "CNAME", "DS", "MX", "NS", "NAPTR", "PTR", "SRV":
+ case "ALIAS", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "MX", "NS", "NAPTR", "PTR", "SRV":
// Target is a hostname that might be a shortname. Turn it into a FQDN.
r.target = dnsutil.AddOrigin(r.target, originFQDN)
- case "A", "ALIAS", "CAA", "DHCID", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TLSA", "TXT":
+ case "A", "AKAMAICDN", "CAA", "DHCID", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "HTTPS", "IMPORT_TRANSFORM", "LOC", "SSHFP", "SVCB", "TLSA", "TXT":
// Do nothing.
case "SOA":
if r.target != "DEFAULT_NOT_SET." {
diff --git a/models/record_test.go b/models/record_test.go
index f4015197ec..e654bd33ad 100644
--- a/models/record_test.go
+++ b/models/record_test.go
@@ -88,7 +88,6 @@ func TestRecordConfig_Copy(t *testing.T) {
TlsaUsage uint8
TlsaSelector uint8
TlsaMatchingType uint8
- TxtStrings []string
R53Alias map[string]string
AzureAlias map[string]string
Original interface{}
@@ -135,7 +134,6 @@ func TestRecordConfig_Copy(t *testing.T) {
TlsaUsage: 1,
TlsaSelector: 2,
TlsaMatchingType: 3,
- TxtStrings: []string{"one", "two", "three"},
R53Alias: map[string]string{"a": "eh", "b": "bee"},
AzureAlias: map[string]string{"az": "az", "ure": "your"},
//Original interface{},
@@ -174,7 +172,6 @@ func TestRecordConfig_Copy(t *testing.T) {
TlsaUsage: 1,
TlsaSelector: 2,
TlsaMatchingType: 3,
- TxtStrings: []string{"one", "two", "three"},
R53Alias: map[string]string{"a": "eh", "b": "bee"},
AzureAlias: map[string]string{"az": "az", "ure": "your"},
//Original interface{},
@@ -217,7 +214,6 @@ func TestRecordConfig_Copy(t *testing.T) {
TlsaUsage: tt.fields.TlsaUsage,
TlsaSelector: tt.fields.TlsaSelector,
TlsaMatchingType: tt.fields.TlsaMatchingType,
- TxtStrings: tt.fields.TxtStrings,
R53Alias: tt.fields.R53Alias,
AzureAlias: tt.fields.AzureAlias,
Original: tt.fields.Original,
diff --git a/models/t_dnskey.go b/models/t_dnskey.go
new file mode 100644
index 0000000000..4f2be60ff1
--- /dev/null
+++ b/models/t_dnskey.go
@@ -0,0 +1,52 @@
+package models
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+// SetTargetDNSKEY sets the DNSKEY fields.
+func (rc *RecordConfig) SetTargetDNSKEY(flags uint16, protocol, algorithm uint8, publicKey string) error {
+ rc.DnskeyFlags = flags
+ rc.DnskeyProtocol = protocol
+ rc.DnskeyAlgorithm = algorithm
+ rc.DnskeyPublicKey = publicKey
+
+ if rc.Type == "" {
+ rc.Type = "DNSKEY"
+ }
+ if rc.Type != "DNSKEY" {
+ panic("assertion failed: SetTargetDNSKEY called when .Type is not DNSKEY")
+ }
+
+ return nil
+}
+
+// SetTargetDNSKEYStrings is like SetTargetDNSKEY but accepts strings.
+func (rc *RecordConfig) SetTargetDNSKEYStrings(flags, protocol, algorithm, publicKey string) error {
+ u16flags, err := strconv.ParseUint(flags, 10, 16)
+ if err != nil {
+ return errors.Wrap(err, "DNSKEY Flags can't fit in 16 bits")
+ }
+ u8protocol, err := strconv.ParseUint(protocol, 10, 8)
+ if err != nil {
+ return errors.Wrap(err, "DNSKEY Protocol can't fit in 8 bits")
+ }
+ u8algorithm, err := strconv.ParseUint(algorithm, 10, 8)
+ if err != nil {
+ return errors.Wrap(err, "DNSKEY Algorithm can't fit in 8 bits")
+ }
+
+ return rc.SetTargetDNSKEY(uint16(u16flags), uint8(u8protocol), uint8(u8algorithm), publicKey)
+}
+
+// SetTargetDNSKEYString is like SetTargetDNSKEY but accepts one big string.
+func (rc *RecordConfig) SetTargetDNSKEYString(s string) error {
+ part := strings.Fields(s)
+ if len(part) != 4 {
+ return errors.Errorf("DNSKEY value does not contain 4 fieldnskey: (%#v)", s)
+ }
+ return rc.SetTargetDNSKEYStrings(part[0], part[1], part[2], part[3])
+}
diff --git a/models/t_loc.go b/models/t_loc.go
index f4c641f588..4a4ad639ae 100644
--- a/models/t_loc.go
+++ b/models/t_loc.go
@@ -2,7 +2,7 @@ package models
import (
"fmt"
- "strconv"
+ "math"
"strings"
"github.com/miekg/dns"
@@ -31,7 +31,7 @@ func (rc *RecordConfig) SetTargetLOC(ver uint8, lat uint32, lon uint32, alt uint
// for further processing to the LOC native 7 input binary format:
// LocVersion (0), LocLatitude, LocLongitude, LocAltitude, LocSize, LocVertPre, LocHorizPre
func (rc *RecordConfig) SetLOCParams(d1 uint8, m1 uint8, s1 float32, ns string,
- d2 uint8, m2 uint8, s2 float32, ew string, al int32, sz float32, hp float32, vp float32) error {
+ d2 uint8, m2 uint8, s2 float32, ew string, al float32, sz float32, hp float32, vp float32) error {
err := rc.calculateLOCFields(d1, m1, s1, ns, d2, m2, s2, ew, al, sz, hp, vp)
@@ -78,18 +78,17 @@ func (rc *RecordConfig) SetTargetLOCString(origin string, contents string) error
// the 12 variable inputs of integers and strings.
func (rc *RecordConfig) extractLOCFieldsFromStringInput(input string) error {
var d1, m1, d2, m2 uint8
- var al int32
+ var al float32
var s1, s2 float32
var ns, ew string
var sz, hp, vp float32
- var err error
- _, err = fmt.Sscanf(input+"~", "%d %d %f %s %d %d %f %s %dm %fm %fm %fm~",
+ _, err := fmt.Sscanf(input+"~", "%d %d %f %s %d %d %f %s %fm %fm %fm %fm~",
&d1, &m1, &s1, &ns, &d2, &m2, &s2, &ew, &al, &sz, &hp, &vp)
if err != nil {
- return fmt.Errorf("extractLOCFieldsFromStringInput: can't unpack LOC tex input data: %w", err)
+ return fmt.Errorf("extractLOCFieldsFromStringInput: can't unpack LOC text input data: %w", err)
}
- // fmt.Printf("\ngot: %d %d %g %s %d %d %g %s %dm %0.2fm %0.2fm %0.2fm \n", d1, m1, s1, ns, d2, m2, s2, ew, al, sz, hp, vp)
+ // fmt.Printf("\ngot: %d %d %g %s %d %d %g %s %0.2fm %0.2fm %0.2fm %0.2fm \n", d1, m1, s1, ns, d2, m2, s2, ew, al, sz, hp, vp)
rc.calculateLOCFields(d1, m1, s1, ns, d2, m2, s2, ew, al, sz, hp, vp)
@@ -98,7 +97,7 @@ func (rc *RecordConfig) extractLOCFieldsFromStringInput(input string) error {
// calculateLOCFields converts from 12 user inputs to the LOC 7 binary fields
func (rc *RecordConfig) calculateLOCFields(d1 uint8, m1 uint8, s1 float32, ns string,
- d2 uint8, m2 uint8, s2 float32, ew string, al int32, sz float32, hp float32, vp float32) error {
+ d2 uint8, m2 uint8, s2 float32, ew string, al float32, sz float32, hp float32, vp float32) error {
// Crazy hairy shit happens here.
// We already got the useful "string" version earlier. ¯\_(ã)_/¯ code golf...
const LOCEquator uint64 = 0x80000000 // 1 << 31 // RFC 1876, Section 2.
@@ -120,7 +119,10 @@ func (rc *RecordConfig) calculateLOCFields(d1 uint8, m1 uint8, s1 float32, ns st
rc.LocLongitude = uint32(LOCPrimeMeridian - lon)
}
// Altitude
- rc.LocAltitude = uint32(al+LOCAltitudeBase) * 100
+ altitude := (float64(al) + float64(LOCAltitudeBase)) * 100
+ clampedAltitude := math.Min(math.Max(0, altitude), float64(math.MaxUint32))
+ rc.LocAltitude = uint32(clampedAltitude)
+
var err error
// Size
rc.LocSize, err = getENotationInt(sz)
@@ -167,21 +169,40 @@ func getENotationInt(x float32) (uint8, error) {
1cm = 1e0 == 16 (1^4 + 0) or 0<<4 + 0
0cm = 0e0 == 0
*/
- // get int from cm value:
- num := strconv.Itoa(int(x * 100))
- // fmt.Printf("num: %s\n", num)
- // split string on zeroes to count zeroes:
- arr := strings.Split(num, "0")
- // fmt.Printf("arr: %s\n", arr)
- // get the leading digit:
- prefix, err := strconv.Atoi(arr[0])
- if err != nil {
- return 0, fmt.Errorf("can't unpack LOC base/mantissa: %w", err)
- }
- // fmt.Printf("prefix: %d\n", prefix)
- // fmt.Printf("lenArr-1: %d\n", len(arr)-1)
- // construct our x^e uint8
- value := uint8((prefix << 4) | (len(arr) - 1))
- // fmt.Printf("m_e: %d\n", value)
- return value, err
+ if x == 0 {
+ return 0, nil // both mantissa and exponent will be zero
+ }
+
+ // get cm value
+ num := float64(x) * 100
+
+ // Get exponent (base 10)
+ exp := int(math.Floor(math.Log10(num)))
+
+ // Normalize the mantissa
+ mantissa := num / math.Pow(10, float64(exp))
+
+ // Adjust mantissa and exponent to fit into 4-bit ranges (0-15)
+ for mantissa < 1 && exp > 0 {
+ mantissa *= 10
+ exp--
+ }
+
+ // Truncate the mantissa (integer value) and ensure it's within 4 bits
+ mantissaInt := int(math.Floor(mantissa))
+ if mantissaInt > 9 {
+ mantissaInt = 9 // Cap mantissa at 9
+ }
+
+ // Ensure exponent is within 4 bits
+ if exp < 0 {
+ exp = 0 // Cap negative exponents at 0
+ } else if exp > 9 {
+ exp = 9 // Cap exponent at 9
+ }
+
+ // Pack mantissa and exponent into a single uint8
+ packedValue := uint8((mantissaInt << 4) | (exp & 0x0F))
+
+ return packedValue, nil
}
diff --git a/models/t_parse.go b/models/t_parse.go
index 1cbfeea630..e224f8c600 100644
--- a/models/t_parse.go
+++ b/models/t_parse.go
@@ -5,6 +5,116 @@ import (
"net"
)
+// PopulateFromStringFunc populates a RecordConfig by parsing a common RFC1035-like format.
+//
+// rtype: the resource record type (rtype)
+// contents: a string that contains all parameters of the record's rdata (see below)
+// txtFn: If rtype == "TXT", this function is used to parse contents, or nil if no parsing is needed.
+//
+// The "contents" field is the format used in RFC1035 zonefiles. It is the text
+// after the rtype. For example, in the line: foo IN MX 10 mx.example.com.
+// contents stores everything after the "MX" (not including the space).
+//
+// Typical values for txtFn include:
+//
+// nil: no parsing required.
+// txtutil.ParseQuoted: Parse via Tom's interpretation of RFC1035.
+// txtutil.ParseCombined: Backwards compatible with Parse via miekg's interpretation of RFC1035.
+//
+// Many providers deliver record data in this format or something close to it.
+// This function is provided to reduce the amount of duplicate code across
+// providers. If a particular rtype is not handled as a particular provider
+// expects, simply handle it beforehand as a special case.
+//
+// Example 1: Normal use.
+//
+// rtype := FILL_IN_RTYPE
+// rc := &models.RecordConfig{Type: rtype, TTL: FILL_IN_TTL}
+// rc.SetLabelFromFQDN(FILL_IN_NAME, origin)
+// rc.Original = FILL_IN_ORIGINAL // The raw data received from provider (if needed later)
+// if err = rc.PopulateFromStringFunc(rtype, target, origin, nil); err != nil {
+// return nil, fmt.Errorf("unparsable record type=%q received from PROVDER_NAME: %w", rtype, err)
+// }
+// return rc, nil
+//
+// Example 2: Use your own MX parser.
+//
+// rtype := FILL_IN_RTYPE
+// rc := &models.RecordConfig{Type: rtype, TTL: FILL_IN_TTL}
+// rc.SetLabelFromFQDN(FILL_IN_NAME, origin)
+// rc.Original = FILL_IN_ORIGINAL // The raw data received from provider (if needed later)
+// switch rtype {
+// case "MX":
+// // MX priority in a separate field.
+// err = rc.SetTargetMX(cr.Priority, target)
+// default:
+// err = rc.PopulateFromString(rtype, target, origin)
+// }
+// if err != nil {
+// return nil, fmt.Errorf("unparsable record type=%q received from PROVDER_NAME: %w", rtype, err)
+// }
+// return rc, nil
+func (rc *RecordConfig) PopulateFromStringFunc(rtype, contents, origin string, txtFn func(s string) (string, error)) error {
+ if rc.Type != "" && rc.Type != rtype {
+ return fmt.Errorf("assertion failed: rtype already set (%s) (%s)", rtype, rc.Type)
+ }
+
+ switch rc.Type = rtype; rtype { // #rtype_variations
+ case "A":
+ ip := net.ParseIP(contents)
+ if ip == nil || ip.To4() == nil {
+ return fmt.Errorf("invalid IP in A record: %s", contents)
+ }
+ return rc.SetTargetIP(ip) // Reformat to canonical form.
+ case "AAAA":
+ ip := net.ParseIP(contents)
+ if ip == nil || ip.To16() == nil {
+ return fmt.Errorf("invalid IP in AAAA record: %s", contents)
+ }
+ return rc.SetTargetIP(ip) // Reformat to canonical form.
+ case "AKAMAICDN", "ALIAS", "ANAME", "CNAME", "NS", "PTR":
+ return rc.SetTarget(contents)
+ case "CAA":
+ return rc.SetTargetCAAString(contents)
+ case "DS":
+ return rc.SetTargetDSString(contents)
+ case "DNSKEY":
+ return rc.SetTargetDNSKEYString(contents)
+ case "DHCID":
+ return rc.SetTarget(contents)
+ case "DNAME":
+ return rc.SetTarget(contents)
+ case "LOC":
+ return rc.SetTargetLOCString(origin, contents)
+ case "MX":
+ return rc.SetTargetMXString(contents)
+ case "NAPTR":
+ return rc.SetTargetNAPTRString(contents)
+ case "SOA":
+ return rc.SetTargetSOAString(contents)
+ case "SPF", "TXT":
+ if txtFn == nil {
+ return rc.SetTargetTXT(contents)
+ }
+ t, err := txtFn(contents)
+ if err != nil {
+ return fmt.Errorf("invalid TXT record: %s", contents)
+ }
+ return rc.SetTargetTXT(t)
+ case "SRV":
+ return rc.SetTargetSRVString(contents)
+ case "SSHFP":
+ return rc.SetTargetSSHFPString(contents)
+ case "SVCB", "HTTPS":
+ return rc.SetTargetSVCBString(origin, contents)
+ case "TLSA":
+ return rc.SetTargetTLSAString(contents)
+ default:
+ //return fmt.Errorf("unknown rtype (%s) when parsing (%s) domain=(%s)", rtype, contents, origin)
+ return MakeUnknown(rc, rtype, contents, origin)
+ }
+}
+
// PopulateFromString populates a RecordConfig given a type and string. Many
// providers give all the parameters of a resource record in one big string.
// This helper function lets you not re-invent the wheel.
@@ -15,27 +125,26 @@ import (
// Recommended calling convention: Process the exceptions first, then use the
// PopulateFromString function for everything else.
//
-// rtype := FILL_IN_TYPE
-// var err error
-// rc := &models.RecordConfig{Type: rtype}
-// rc.SetLabelFromFQDN(FILL_IN_NAME, origin)
-// rc.TTL = uint32(FILL_IN_TTL)
-// rc.Original = FILL_IN_ORIGINAL // The raw data received from provider (if needed later)
-// switch rtype {
-// case "MX":
-// // MX priority in a separate field.
-// err = rc.SetTargetMX(cr.Priority, target)
-// case "TXT":
-// // TXT records are stored verbatim; no quoting/escaping to parse.
-// err = rc.SetTargetTXT(target)
-// default:
-// err = rc.PopulateFromString(rtype, target, origin)
-// }
-// if err != nil {
-// return nil, fmt.Errorf("unparsable record type=%q received from PROVDER_NAME: %w", rtype, err)
-// }
-// return rc, nil
-
+// rtype := FILL_IN_TYPE
+// var err error
+// rc := &models.RecordConfig{Type: rtype}
+// rc.SetLabelFromFQDN(FILL_IN_NAME, origin)
+// rc.TTL = uint32(FILL_IN_TTL)
+// rc.Original = FILL_IN_ORIGINAL // The raw data received from provider (if needed later)
+// switch rtype {
+// case "MX":
+// // MX priority in a separate field.
+// err = rc.SetTargetMX(cr.Priority, target)
+// case "TXT":
+// // TXT records are stored verbatim; no quoting/escaping to parse.
+// err = rc.SetTargetTXT(target)
+// default:
+// err = rc.PopulateFromString(rtype, target, origin)
+// }
+// if err != nil {
+// return nil, fmt.Errorf("unparsable record type=%q received from PROVDER_NAME: %w", rtype, err)
+// }
+// return rc, nil
func (rc *RecordConfig) PopulateFromString(rtype, contents, origin string) error {
if rc.Type != "" && rc.Type != rtype {
panic(fmt.Errorf("assertion failed: rtype already set (%s) (%s)", rtype, rc.Type))
@@ -59,8 +168,12 @@ func (rc *RecordConfig) PopulateFromString(rtype, contents, origin string) error
return rc.SetTargetCAAString(contents)
case "DS":
return rc.SetTargetDSString(contents)
+ case "DNSKEY":
+ return rc.SetTargetDNSKEYString(contents)
case "DHCID":
return rc.SetTarget(contents)
+ case "DNAME":
+ return rc.SetTarget(contents)
case "LOC":
return rc.SetTargetLOCString(origin, contents)
case "MX":
@@ -70,14 +183,13 @@ func (rc *RecordConfig) PopulateFromString(rtype, contents, origin string) error
case "SOA":
return rc.SetTargetSOAString(contents)
case "SPF", "TXT":
- // Parsing the contents may be unexpected. If your provider gives you a
- // string that needs no further parsing, special case TXT and use
- // rc.SetTargetTXT(target) like in the example above.
return rc.SetTargetTXTs(ParseQuotedTxt(contents))
case "SRV":
return rc.SetTargetSRVString(contents)
case "SSHFP":
return rc.SetTargetSSHFPString(contents)
+ case "SVCB", "HTTPS":
+ return rc.SetTargetSVCBString(origin, contents)
case "TLSA":
return rc.SetTargetTLSAString(contents)
default:
diff --git a/models/t_svcb.go b/models/t_svcb.go
new file mode 100644
index 0000000000..fd8b82cbb7
--- /dev/null
+++ b/models/t_svcb.go
@@ -0,0 +1,44 @@
+package models
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/miekg/dns"
+)
+
+// SetTargetSVCB sets the SVCB fields.
+func (rc *RecordConfig) SetTargetSVCB(priority uint16, target string, params []dns.SVCBKeyValue) error {
+ rc.SvcPriority = priority
+ rc.SetTarget(target)
+ paramsStr := []string{}
+ for _, kv := range params {
+ paramsStr = append(paramsStr, fmt.Sprintf("%s=%s", kv.Key(), kv.String()))
+ }
+ rc.SvcParams = strings.Join(paramsStr, " ")
+ if rc.Type == "" {
+ rc.Type = "SVCB"
+ }
+ if rc.Type != "SVCB" && rc.Type != "HTTPS" {
+ panic("assertion failed: SetTargetSVCB called when .Type is not SVCB or HTTPS")
+ }
+ return nil
+}
+
+// SetTargetSVCBString is like SetTargetSVCB but accepts one big string and the origin so parsing can be done using miekg/dns.
+func (rc *RecordConfig) SetTargetSVCBString(origin, contents string) error {
+ if rc.Type == "" {
+ rc.Type = "SVCB"
+ }
+ record, err := dns.NewRR(fmt.Sprintf("%s. %s %s", origin, rc.Type, contents))
+ if err != nil {
+ return fmt.Errorf("could not parse SVCB record: %s", err)
+ }
+ switch r := record.(type) {
+ case *dns.HTTPS:
+ return rc.SetTargetSVCB(r.Priority, r.Target, r.Value)
+ case *dns.SVCB:
+ return rc.SetTargetSVCB(r.Priority, r.Target, r.Value)
+ }
+ return nil
+}
diff --git a/models/t_txt.go b/models/t_txt.go
index 4a39c06c44..c8937121e7 100644
--- a/models/t_txt.go
+++ b/models/t_txt.go
@@ -1,113 +1,34 @@
package models
import (
- "fmt"
"strings"
)
/*
-Sadly many providers handle TXT records in strange and non-compliant
+Sadly many providers handle TXT records in strange and unexpeected
ways. DNSControl has to handle all of them. Over the years we've
tried many things. This explain the current state of the code.
-What are some of these variations?
+DNSControl stores the TXT record target as a single string of any length.
+Providers take care of any splitting, excaping, or quoting.
-* The RFCs say that a TXT record is a series of strings, each 255-octets
- or fewer. Yet, most provider APIs only support a single string which
- is split into 255-octetl chunks behind the scenes. Some only support
- a single string that is 255-octets or less.
+NOTE: Older versions of DNSControl stored the TXT record as
+represented by the provider, which could be a single string, a series
+of smaller strings, or a single string that is quoted/escaped. This
+created tons of edge-cases and other distractions.
-* The RFCs don't say much about the content of the strings. Some
- providers accept any octet, some only accept ASCII-printable chars,
- some get confused by TXT records that include backticks, quotes, or
- whitespace at the end of the string.
+If a provider doesn't support certain charactors in a TXT record, use
+the providers/$PROVIDER/auditrecords.go file to indicate this.
+DNSControl uses this information to warn users of unsupporrted input,
+and to skip related integration tests.
-DNSControl has tried many different ways to handle all these
-variations over the years. This is what we found works best:
+There are 2 ways to create a TXT record:
+ SetTargetTXT(): Create from a string.
+ SetTargetTXTs(): Create from an array of strings that need to be joined.
-Principle 1. Store the string as the user input it.
-
-DNSControl stores the string as the user specified in dnsconfig.js.
-The user can specify a string of any length, or many individual
-strings of any length.
-
-No matter how the user presented the data in dnsconfig.js, the data is
-stored as a list of strings (RecordConfig.TxtStrings []string). If
-they input 1 string, the list has one element. If the user input many
-individual strings, the list is copied into .TxtStrings.
-
-When we store the data in .TxtStrings there is no length checking. The data is not manipulated.
-
-Principle 2. When downloading zone records, receive the data as appropriate.
-
-When the API returns a TXT record, the provider's code must properly
-store it in the .TxtStrings field of RecordConfig.
-
-We've found most APIs return TXT strings in one of three ways:
-
- * The API returns a single string: use RecordConfig.SetTargetTXT().
- * The API returns multiple strings: use RecordConfig.SetTargetTXTs().
- * (THIS IS RARE) The API returns a single string that must be parsed
- into multiple strings: The provider is responsible for the
- parsing. However, usually the format is "quoted like in RFC 1035"
- which is vague, but we've implemented it as
- RecordConfig.SetTargetTXTfromRFC1035Quoted().
-
-If the format is something else, please write the parser as a separate
-function and write unit tests based on actual data received from the
-API.
-
-Principle 3. When sending TXT records to the API, send what the API expects.
-
-The provider's code must decide how to take the list of strings in
-.TxtStrings and present them to the API.
-
-Most providers fall into one of these categories:
-
- * If the API expects one long string, the provider code joins all
- the smaller strings and sends one big string. Use the helper
- function RecordConfig.GetTargetTXTJoined()
- * If the API expects many strings of any size, the provider code
- sends the individual strings. Those strings are accessed as
- the array RecordConfig.TxtStrings
- * (THIS IS RARE) If the API expects multiple strings to be sent as
- one long string, quoted RFC 1025-style, call
- RecordConfig.GetTargetRFC1035Quoted() and send that string.
-
-Note: If the API expects many strings, each 255-octets or smaller, the
-provider code must split the longer strings into smaller strings. The
-helper function txtutil.SplitSingleLongTxt(dc.Records) will iterate
-over all TXT records and split out any strings longer than 255 octets.
-Call this once in GetDomainCorrections(). (Yes, this violates
-Principle 1, but we decided it is best to do it once, than provide a
-getter that would re-split the strings on every call.)
-
-Principle 4. Providers can communicate back to DNSControl strings they can't handle.
-
-As mentioned before, some APIs reject TXT records for various reasons:
-Illegal chars, whitespace at the end, etc. We can't make a flag for
-every variation. Instead we call the provider's AuditRecords()
-function and it reports if there are any records that it can't
-process.
-
-We've provided many helper functions to make this easier. Look at any
-of the providers/.../auditrecord.go` files for examples.
-
-The integration tests call AuditRecords() to skip any tests that we
-know will fail. If one of the integration tests is failing, it is
-often better to update AuditRecords() than to try to figure out why,
-for example, the provider doesn't support backticks in strings. Don't
-spend a lot of effort trying to fix situations that are rare or will
-not appear in real-world situations.
-
-Companies do update their APIs occasionally. You might want to try
-eliminating the checks one at a time to see if the API has improved.
-Don't feel obligated to do this more than once a year.
-
-Conclusion:
-
-When we follow these 4 principles, and stick with the helper functions
-provided, we're able to handle all the variations.
+There are 2 ways to get the value (target) of a TXT record:
+ GetTargetTXTJoined(): Returns one big string
+ GetTargetTXTSegmented(): Returns an array 255-octet segments.
*/
@@ -119,8 +40,6 @@ func (rc *RecordConfig) HasFormatIdenticalToTXT() bool {
}
// SetTargetTXT sets the TXT fields when there is 1 string.
-// The string is stored in .Target, and split into 255-octet chunks
-// for .TxtStrings.
func (rc *RecordConfig) SetTargetTXT(s string) error {
if rc.Type == "" {
rc.Type = "TXT"
@@ -128,50 +47,47 @@ func (rc *RecordConfig) SetTargetTXT(s string) error {
panic("assertion failed: SetTargetTXT called when .Type is not TXT or compatible type")
}
- rc.TxtStrings = []string{s}
- rc.SetTarget(rc.zoneFileQuoted())
- return nil
+ return rc.SetTarget(s)
}
-// SetTargetTXTs sets the TXT fields when there are many strings.
-// The individual strings are stored in .TxtStrings, and joined to make .Target.
+// SetTargetTXTs sets the TXT fields when there are many strings. They are stored concatenated.
func (rc *RecordConfig) SetTargetTXTs(s []string) error {
- if rc.Type == "" {
- rc.Type = "TXT"
- } else if !rc.HasFormatIdenticalToTXT() {
- panic("assertion failed: SetTargetTXTs called when .Type is not TXT or compatible type")
- }
-
- rc.TxtStrings = s
- rc.SetTarget(rc.zoneFileQuoted())
- return nil
+ return rc.SetTargetTXT(strings.Join(s, ""))
}
-// GetTargetTXTJoined returns the TXT target as one string. If it was stored as multiple strings, concatenate them.
+// GetTargetTXTJoined returns the TXT target as one string.
func (rc *RecordConfig) GetTargetTXTJoined() string {
- return strings.Join(rc.TxtStrings, "")
+ return rc.target
}
-// SetTargetTXTfromRFC1035Quoted parses a series of quoted strings
-// and sets .TxtStrings based on the result.
-// Note: Most APIs do notThis is rarely used. Try using SetTargetTXT() first.
-// Ex:
-//
-// "foo" << 1 string
-// "foo bar" << 1 string
-// "foo" "bar" << 2 strings
-// foo << error. No quotes! Did you intend to use SetTargetTXT?
-func (rc *RecordConfig) SetTargetTXTfromRFC1035Quoted(s string) error {
- if s != "" && s[0] != '"' {
- // If you get this error, it is likely that you should use
- // SetTargetTXT() instead of SetTargetTXTfromRFC1035Quoted().
- return fmt.Errorf("non-quoted string used with SetTargetTXTfromRFC1035Quoted: (%s)", s)
- }
- many, err := ParseQuotedFields(s)
- if err != nil {
- return err
+// GetTargetTXTSegmented returns the TXT target as 255-octet segments, with the remainder in the last segment.
+func (rc *RecordConfig) GetTargetTXTSegmented() []string {
+ return splitChunks(rc.target, 255)
+}
+
+// GetTargetTXTSegmentCount returns the number of 255-octet segments required to store TXT target.
+func (rc *RecordConfig) GetTargetTXTSegmentCount() int {
+ total := len(rc.target)
+ segs := total / 255 // integer division, decimals are truncated
+ if (total % 255) > 0 {
+ return segs + 1
}
- return rc.SetTargetTXTs(many)
+ return segs
}
-// There is no GetTargetTXTfromRFC1025Quoted(). Use GetTargetRFC1035Quoted()
+func splitChunks(buf string, lim int) []string {
+ if len(buf) == 0 {
+ return nil
+ }
+
+ var chunk string
+ chunks := make([]string, 0, len(buf)/lim+1)
+ for len(buf) >= lim {
+ chunk, buf = buf[:lim], buf[lim:]
+ chunks = append(chunks, chunk)
+ }
+ if len(buf) > 0 {
+ chunks = append(chunks, buf[:])
+ }
+ return chunks
+}
diff --git a/models/target.go b/models/target.go
index 9a189d8ffa..62fbedc170 100644
--- a/models/target.go
+++ b/models/target.go
@@ -13,22 +13,9 @@ If an rType has more than one field, one field goes in .target and the remaining
Not the best design, but we're stuck with it until we re-do RecordConfig, possibly using generics.
*/
-// Set debugWarnTxtField to true if you want a warning when
-// GetTargetField is called on a TXT record.
-// GetTargetField works fine on TXT records for casual output but it
-// is often better to access .TxtStrings directly or call
-// GetTargetRFC1035Quoted() for nicely quoted text.
-var debugWarnTxtField = false
-
-// GetTargetField returns the target. There may be other fields (for example
-// an MX record also has a .MxPreference field.
+// GetTargetField returns the target. There may be other fields, but they are
+// not included. For example, the .MxPreference field of an MX record isn't included.
func (rc *RecordConfig) GetTargetField() string {
- if debugWarnTxtField {
- if rc.Type == "TXT" {
- fmt.Printf("DEBUG: WARNING: GetTargetField called on TXT record is frequently wrong: %q\n", rc.target)
- //debug.PrintStack()
- }
- }
return rc.target
}
@@ -40,15 +27,31 @@ func (rc *RecordConfig) GetTargetIP() net.IP {
return net.ParseIP(rc.target)
}
+// GetTargetCombinedFunc returns all the rdata fields of a RecordConfig as one
+// string. How TXT records are encoded is defined by encodeFn. If encodeFn is
+// nil the TXT data is returned unaltered.
+func (rc *RecordConfig) GetTargetCombinedFunc(encodeFn func(s string) string) string {
+ if rc.Type == "TXT" {
+ if encodeFn == nil {
+ return rc.target
+ }
+ return encodeFn(rc.target)
+ }
+ return rc.GetTargetCombined()
+}
+
// GetTargetCombined returns a string with the various fields combined.
// For example, an MX record might output `10 mx10.example.tld`.
+// WARNING: How TXT records are handled is buggy but we can't change it because
+// code depends on the bugs. Use Get GetTargetCombinedFunc() instead.
func (rc *RecordConfig) GetTargetCombined() string {
+
// Pseudo records:
if _, ok := dns.StringToType[rc.Type]; !ok {
switch rc.Type { // #rtype_variations
case "R53_ALIAS":
// Differentiate between multiple R53_ALIASs on the same label.
- return fmt.Sprintf("%s atype=%s zone_id=%s", rc.target, rc.R53Alias["type"], rc.R53Alias["zone_id"])
+ return fmt.Sprintf("%s atype=%s zone_id=%s evaluate_target_health=%s", rc.target, rc.R53Alias["type"], rc.R53Alias["zone_id"], rc.R53Alias["evaluate_target_health"])
case "AZURE_ALIAS":
// Differentiate between multiple AZURE_ALIASs on the same label.
return fmt.Sprintf("%s atype=%s", rc.target, rc.AzureAlias["type"])
@@ -58,7 +61,13 @@ func (rc *RecordConfig) GetTargetCombined() string {
}
}
- if rc.Type == "SOA" {
+ // Everything else
+ switch rc.Type {
+ case "UNKNOWN":
+ return fmt.Sprintf("rtype=%s rdata=%s", rc.UnknownTypeName, rc.target)
+ case "TXT":
+ return rc.zoneFileQuoted()
+ case "SOA":
return fmt.Sprintf("%s %v %d %d %d %d %d", rc.target, rc.SoaMbox, rc.SoaSerial, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl)
}
@@ -92,14 +101,13 @@ func (rc *RecordConfig) GetTargetRFC1035Quoted() string {
return rc.zoneFileQuoted()
}
-// GetTargetSortable returns a string that is sortable.
-func (rc *RecordConfig) GetTargetSortable() string {
- return rc.GetTargetDebug()
-}
-
// GetTargetDebug returns a string with the various fields spelled out.
func (rc *RecordConfig) GetTargetDebug() string {
- content := fmt.Sprintf("%s %s %s %d", rc.Type, rc.NameFQDN, rc.target, rc.TTL)
+ target := rc.target
+ if rc.Type == "TXT" {
+ target = fmt.Sprintf("%q", target)
+ }
+ content := fmt.Sprintf("%s %s %s %d", rc.Type, rc.NameFQDN, target, rc.TTL)
switch rc.Type { // #rtype_variations
case "A", "AAAA", "AKAMAICDN", "CNAME", "DHCID", "NS", "PTR", "TXT":
// Nothing special.
@@ -109,18 +117,23 @@ func (rc *RecordConfig) GetTargetDebug() string {
content += fmt.Sprintf(" caatag=%s caaflag=%d", rc.CaaTag, rc.CaaFlag)
case "DS":
content += fmt.Sprintf(" ds_algorithm=%d ds_keytag=%d ds_digesttype=%d ds_digest=%s", rc.DsAlgorithm, rc.DsKeyTag, rc.DsDigestType, rc.DsDigest)
+ case "DNSKEY":
+ content += fmt.Sprintf(" dnskey_flags=%d dnskey_protocol=%d dnskey_algorithm=%d dnskey_publickey=%s", rc.DnskeyFlags, rc.DnskeyProtocol, rc.DnskeyAlgorithm, rc.DnskeyPublicKey)
case "MX":
content += fmt.Sprintf(" pref=%d", rc.MxPreference)
case "NAPTR":
content += fmt.Sprintf(" naptrorder=%d naptrpreference=%d naptrflags=%s naptrservice=%s naptrregexp=%s", rc.NaptrOrder, rc.NaptrPreference, rc.NaptrFlags, rc.NaptrService, rc.NaptrRegexp)
case "R53_ALIAS":
- content += fmt.Sprintf(" type=%s zone_id=%s", rc.R53Alias["type"], rc.R53Alias["zone_id"])
+ content += fmt.Sprintf(" type=%s zone_id=%s evaluate_target_health=%s", rc.R53Alias["type"], rc.R53Alias["zone_id"], rc.R53Alias["evaluate_target_health"])
case "SOA":
content = fmt.Sprintf("%s ns=%v mbox=%v serial=%v refresh=%v retry=%v expire=%v minttl=%v", rc.Type, rc.target, rc.SoaMbox, rc.SoaSerial, rc.SoaRefresh, rc.SoaRetry, rc.SoaExpire, rc.SoaMinttl)
case "SRV":
content += fmt.Sprintf(" srvpriority=%d srvweight=%d srvport=%d", rc.SrvPriority, rc.SrvWeight, rc.SrvPort)
case "SSHFP":
content += fmt.Sprintf(" sshfpalgorithm=%d sshfpfingerprint=%d", rc.SshfpAlgorithm, rc.SshfpFingerprint)
+ case "SVCB", "HTTPS":
+ // HTTPS is only a special subform of the SVCB Record
+ content += fmt.Sprintf(" priority=%d params=%v", rc.SvcPriority, rc.SvcParams)
case "TLSA":
content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType)
default:
diff --git a/models/unknown.go b/models/unknown.go
new file mode 100644
index 0000000000..a4128d638c
--- /dev/null
+++ b/models/unknown.go
@@ -0,0 +1,10 @@
+package models
+
+// MakeUnknown turns an RecordConfig into an UNKNOWN type.
+func MakeUnknown(rc *RecordConfig, rtype string, contents string, origin string) error {
+ rc.Type = "UNKNOWN"
+ rc.UnknownTypeName = rtype
+ rc.target = contents
+
+ return nil
+}
diff --git a/models/unmanaged.go b/models/unmanaged.go
index e1f7f5d7e7..57e8fe0f7f 100644
--- a/models/unmanaged.go
+++ b/models/unmanaged.go
@@ -1,9 +1,6 @@
package models
import (
- "bytes"
- "fmt"
-
"github.com/gobwas/glob"
)
@@ -28,25 +25,26 @@ type UnmanagedConfig struct {
TargetGlob glob.Glob `json:"-"` // Compiled version
}
-// DebugUnmanagedConfig returns a string version of an []*UnmanagedConfig for debugging purposes.
-func DebugUnmanagedConfig(uc []*UnmanagedConfig) string {
- if len(uc) == 0 {
- return "UnmanagedConfig{}"
- }
-
- var buf bytes.Buffer
- b := &buf
-
- fmt.Fprint(b, "UnmanagedConfig{\n")
- for i, c := range uc {
- fmt.Fprintf(b, "%00d: (%v, %+v, %v)\n",
- i,
- c.LabelGlob,
- c.RTypeMap,
- c.TargetGlob,
- )
- }
- fmt.Fprint(b, "}")
-
- return b.String()
-}
+// Uncomment to use:
+// // DebugUnmanagedConfig returns a string version of an []*UnmanagedConfig for debugging purposes.
+// func DebugUnmanagedConfig(uc []*UnmanagedConfig) string {
+// if len(uc) == 0 {
+// return "UnmanagedConfig{}"
+// }
+
+// var buf bytes.Buffer
+// b := &buf
+
+// fmt.Fprint(b, "UnmanagedConfig{\n")
+// for i, c := range uc {
+// fmt.Fprintf(b, "%00d: (%v, %+v, %v)\n",
+// i,
+// c.LabelGlob,
+// c.RTypeMap,
+// c.TargetGlob,
+// )
+// }
+// fmt.Fprint(b, "}")
+
+// return b.String()
+// }
diff --git a/pkg/acme/acme.go b/pkg/acme/acme.go
index 2c0823e906..c2c4e6f76b 100644
--- a/pkg/acme/acme.go
+++ b/pkg/acme/acme.go
@@ -16,12 +16,12 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/nameservers"
"github.com/StackExchange/dnscontrol/v4/pkg/notifications"
"github.com/StackExchange/dnscontrol/v4/pkg/zonerecs"
- "github.com/go-acme/lego/certcrypto"
- "github.com/go-acme/lego/certificate"
- "github.com/go-acme/lego/challenge"
- "github.com/go-acme/lego/challenge/dns01"
- "github.com/go-acme/lego/lego"
- acmelog "github.com/go-acme/lego/log"
+ "github.com/go-acme/lego/v4/certcrypto"
+ "github.com/go-acme/lego/v4/certificate"
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/lego"
+ acmelog "github.com/go-acme/lego/v4/log"
)
// CertConfig describes a certificate's configuration.
@@ -141,7 +141,7 @@ func (c *certManager) IssueOrRenewCert(cfg *CertConfig, renewUnder int, verbose
} else {
log.Println("Renewing cert")
action = func() (*certificate.Resource, error) {
- return client.Certificate.Renew(*existing, true, cfg.MustStaple)
+ return client.Certificate.Renew(*existing, true, cfg.MustStaple, "")
}
}
}
@@ -276,7 +276,7 @@ func (c *certManager) getCorrections(d *models.DomainConfig) ([]*models.Correcti
if err != nil {
return nil, err
}
- reports, corrections, err := zonerecs.CorrectZoneRecords(p.Driver, dc)
+ reports, corrections, _, err := zonerecs.CorrectZoneRecords(p.Driver, dc)
if err != nil {
return nil, err
}
diff --git a/pkg/acme/checkDns.go b/pkg/acme/checkDns.go
index 410723d6ca..b853271e36 100644
--- a/pkg/acme/checkDns.go
+++ b/pkg/acme/checkDns.go
@@ -4,7 +4,7 @@ import (
"log"
"time"
- "github.com/go-acme/lego/challenge/dns01"
+ "github.com/go-acme/lego/v4/challenge/dns01"
)
func (c *certManager) preCheckDNS(domain, fqdn, value string, native dns01.PreCheckFunc) (bool, error) {
diff --git a/pkg/acme/directoryStorage.go b/pkg/acme/directoryStorage.go
index bf187d7292..ef64718202 100644
--- a/pkg/acme/directoryStorage.go
+++ b/pkg/acme/directoryStorage.go
@@ -8,7 +8,7 @@ import (
"os"
"path/filepath"
- "github.com/go-acme/lego/certificate"
+ "github.com/go-acme/lego/v4/certificate"
)
// directoryStorage implements storage in a local file directory
diff --git a/pkg/acme/registration.go b/pkg/acme/registration.go
index 017ca497f5..5e7214339a 100644
--- a/pkg/acme/registration.go
+++ b/pkg/acme/registration.go
@@ -6,9 +6,9 @@ import (
"crypto/elliptic"
"crypto/rand"
- "github.com/go-acme/lego/certcrypto"
- "github.com/go-acme/lego/lego"
- "github.com/go-acme/lego/registration"
+ "github.com/go-acme/lego/v4/certcrypto"
+ "github.com/go-acme/lego/v4/lego"
+ "github.com/go-acme/lego/v4/registration"
)
func (c *certManager) getOrCreateAccount() (*Account, error) {
@@ -28,7 +28,8 @@ func (c *certManager) getOrCreateAccount() (*Account, error) {
return account, err
}
-func (c *certManager) createAccount(email string) (*Account, error) {
+// func (c *certManager) createAccount(email string) (*Account, error) {
+func (c *certManager) createAccount(_ string) (*Account, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, err
diff --git a/pkg/acme/storage.go b/pkg/acme/storage.go
index cce68e1015..29e751bce7 100644
--- a/pkg/acme/storage.go
+++ b/pkg/acme/storage.go
@@ -1,6 +1,6 @@
package acme
-import "github.com/go-acme/lego/certificate"
+import "github.com/go-acme/lego/v4/certificate"
// Storage is an abstracrion around how certificates, keys, and account info are stored on disk or elsewhere.
type Storage interface {
diff --git a/pkg/acme/vaultStorage.go b/pkg/acme/vaultStorage.go
index 3a86c57be1..aa01b5d9b6 100644
--- a/pkg/acme/vaultStorage.go
+++ b/pkg/acme/vaultStorage.go
@@ -8,7 +8,7 @@ import (
"fmt"
"strings"
- "github.com/go-acme/lego/certificate"
+ "github.com/go-acme/lego/v4/certificate"
"github.com/hashicorp/vault/api"
)
diff --git a/pkg/credsfile/providerConfig.go b/pkg/credsfile/providerConfig.go
index 95bfe5836c..34c7510657 100644
--- a/pkg/credsfile/providerConfig.go
+++ b/pkg/credsfile/providerConfig.go
@@ -19,23 +19,6 @@ import (
"github.com/google/shlex"
)
-func quotedList(l []string) string {
- if len(l) == 0 {
- return ""
- }
- return `"` + strings.Join(l, `", "`) + `"`
-}
-
-func keysWithColons(list []string) []string {
- var r []string
- for _, k := range list {
- if strings.Contains(k, ":") {
- r = append(r, k)
- }
- }
- return r
-}
-
// LoadProviderConfigs will open or execute the specified file name, and parse its contents. It will replace environment variables it finds if any value matches $[A-Za-z_-0-9]+
func LoadProviderConfigs(fname string) (map[string]map[string]string, error) {
var results = map[string]map[string]string{}
diff --git a/pkg/credsfile/providerConfig_test.go b/pkg/credsfile/providerConfig_test.go
deleted file mode 100644
index 67af7541bb..0000000000
--- a/pkg/credsfile/providerConfig_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package credsfile
-
-import (
- "reflect"
- "testing"
-)
-
-func Test_keysWithColons(t *testing.T) {
- type args struct {
- list []string
- }
- tests := []struct {
- name string
- args args
- want []string
- }{
- {"0", args{list: []string{""}}, nil},
- {"1", args{list: []string{"none"}}, nil},
- {"2", args{list: []string{"a:b"}}, []string{"a:b"}},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := keysWithColons(tt.args.list); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("keysWithColons() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func Test_quotedList(t *testing.T) {
- type args struct {
- l []string
- }
- tests := []struct {
- name string
- args args
- want string
- }{
- {"none", args{}, ""},
- {"single", args{l: []string{"one"}}, `"one"`},
- {"two", args{l: []string{"ricky", "lucy"}}, `"ricky", "lucy"`},
- {"three", args{l: []string{"manny", "moe", "jack"}}, `"manny", "moe", "jack"`},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := quotedList(tt.args.l); got != tt.want {
- t.Errorf("quotedList() = %v, want %v", got, tt.want)
- }
- })
- }
-}
diff --git a/pkg/diff/diff.go b/pkg/diff/diff.go
index 16412ae2f7..d572afcd35 100644
--- a/pkg/diff/diff.go
+++ b/pkg/diff/diff.go
@@ -1,6 +1,8 @@
package diff
import (
+ "fmt"
+
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/fatih/color"
)
@@ -18,10 +20,10 @@ type Changeset []Correlation
// Differ is an interface for computing the difference between two zones.
type Differ interface {
// IncrementalDiff performs a diff on a record-by-record basis, and returns a sets for which records need to be created, deleted, or modified.
- IncrementalDiff(existing []*models.RecordConfig) (reportMsgs []string, create, toDelete, modify Changeset, err error)
+ IncrementalDiff(existing []*models.RecordConfig) (reportMsgs []string, create, toDelete, modify Changeset, actualChangeCount int, err error)
// ChangedGroups performs a diff more appropriate for providers with a "RecordSet" model, where all records with the same name and type are grouped.
// Individual record changes are often not useful in such scenarios. Instead we return a map of record keys to a list of change descriptions within that group.
- ChangedGroups(existing []*models.RecordConfig) (map[models.RecordKey][]string, []string, error)
+ ChangedGroups(existing []*models.RecordConfig) (map[models.RecordKey][]string, []string, int, error)
}
type differ struct {
@@ -29,43 +31,7 @@ type differ struct {
// get normalized content for record. target, ttl, mxprio, and specified metadata
func (d *differ) content(r *models.RecordConfig) string {
- return r.ToDiffable()
-}
-
-// ChangesetLess returns true if c[i] < c[j].
-func ChangesetLess(c Changeset, i, j int) bool {
- var a, b string
- // Which fields are we comparing?
- // Usually only Desired OR Existing content exists (we're either
- // adding or deleting records). In those cases, just use whichever
- // isn't nil.
- // In the case where both Desired AND Existing exist, it doesn't
- // matter which we use as long as we are consistent. I flipped a
- // coin and picked to use Desired in that case.
-
- if c[i].Desired != nil {
- a = c[i].Desired.NameFQDN
- } else {
- a = c[i].Existing.NameFQDN
- }
-
- if c[j].Desired != nil {
- b = c[j].Desired.NameFQDN
- } else {
- b = c[j].Existing.NameFQDN
- }
-
- return a < b
-
- // TODO(tlim): This won't correctly sort:
- // []string{"example.com", "foo.example.com", "bar.example.com"}
- // A simple way to do that correctly is to split on ".", reverse the
- // elements, and sort on the result.
-}
-
-// CorrectionLess returns true when comparing corrections.
-func CorrectionLess(c []*models.Correction, i, j int) bool {
- return c[i].Msg < c[j].Msg
+ return fmt.Sprintf("%s ttl=%d", r.ToComparableNoTTL(), r.TTL)
}
func (c Correlation) String() string {
diff --git a/pkg/diff/diff2compat.go b/pkg/diff/diff2compat.go
index b9f617da30..43bab69a7f 100644
--- a/pkg/diff/diff2compat.go
+++ b/pkg/diff/diff2compat.go
@@ -10,15 +10,10 @@ import (
// NewCompat is a constructor that uses the new pkg/diff2 system
// instead of pkg/diff.
//
-// It is for backwards compatibility only. New providers should use
-// pkg/diff2. Older providers should use this to reduce their
-// dependency on pkg/diff2 until they can move to the pkg/diff2/By*()
-// functions.
+// It is for backwards compatibility only. New providers should use pkg/diff2.
//
// To use this simply change New() to NewCompat(). If that doesn't
-// work please report a bug. The only exception is if you depend on
-// the extraValues feature, which will not be supported. That
-// parameter must be set to nil.
+// work please report a bug. The extraValues parameter is not supported.
func NewCompat(dc *models.DomainConfig, extraValues ...func(*models.RecordConfig) map[string]string) Differ {
if len(extraValues) != 0 {
panic("extraValues not supported")
@@ -37,10 +32,10 @@ type differCompat struct {
// IncrementalDiff usees pkg/diff2 to generate output compatible with systems
// still using NewCompat().
-func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (reportMsgs []string, toCreate, toDelete, toModify Changeset, err error) {
- instructions, err := diff2.ByRecord(existing, d.dc, nil)
+func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (reportMsgs []string, toCreate, toDelete, toModify Changeset, actualChangeCount int, err error) {
+ instructions, actualChangeCount, err := diff2.ByRecord(existing, d.dc, nil)
if err != nil {
- return nil, nil, nil, nil, err
+ return nil, nil, nil, nil, 0, err
}
for _, inst := range instructions {
@@ -66,6 +61,8 @@ func (d *differCompat) IncrementalDiff(existing []*models.RecordConfig) (reportM
return
}
+// GenerateMessageCorrections turns a list of strings into a list of corrections
+// that output those messages (and are otherwise a no-op).
func GenerateMessageCorrections(msgs []string) (corrections []*models.Correction) {
for _, msg := range msgs {
corrections = append(corrections, &models.Correction{Msg: msg})
@@ -74,11 +71,11 @@ func GenerateMessageCorrections(msgs []string) (corrections []*models.Correction
}
// ChangedGroups provides the same results as IncrementalDiff but grouped by key.
-func (d *differCompat) ChangedGroups(existing []*models.RecordConfig) (map[models.RecordKey][]string, []string, error) {
+func (d *differCompat) ChangedGroups(existing []*models.RecordConfig) (map[models.RecordKey][]string, []string, int, error) {
changedKeys := map[models.RecordKey][]string{}
- toReport, toCreate, toDelete, toModify, err := d.IncrementalDiff(existing)
+ toReport, toCreate, toDelete, toModify, actualChangeCount, err := d.IncrementalDiff(existing)
if err != nil {
- return nil, nil, err
+ return nil, nil, 0, err
}
for _, c := range toCreate {
changedKeys[c.Desired.Key()] = append(changedKeys[c.Desired.Key()], c.String())
@@ -89,5 +86,5 @@ func (d *differCompat) ChangedGroups(existing []*models.RecordConfig) (map[model
for _, m := range toModify {
changedKeys[m.Desired.Key()] = append(changedKeys[m.Desired.Key()], m.String())
}
- return changedKeys, toReport, nil
+ return changedKeys, toReport, actualChangeCount, nil
}
diff --git a/pkg/diff2/analyze.go b/pkg/diff2/analyze.go
index e0ebf81147..6201db7c43 100644
--- a/pkg/diff2/analyze.go
+++ b/pkg/diff2/analyze.go
@@ -9,8 +9,9 @@ import (
"github.com/fatih/color"
)
-func analyzeByRecordSet(cc *CompareConfig) ChangeList {
+func analyzeByRecordSet(cc *CompareConfig) (ChangeList, int) {
var instructions ChangeList
+ var actualChangeCount int
// For each label...
for _, lc := range cc.ldata {
// for each type at that label...
@@ -18,7 +19,9 @@ func analyzeByRecordSet(cc *CompareConfig) ChangeList {
// ...if there are changes generate an instruction.
ets := rt.existingTargets
dts := rt.desiredTargets
- msgs := genmsgs(ets, dts)
+ cs := diffTargets(ets, dts)
+ actualChangeCount += len(cs)
+ msgs := justMsgs(cs)
if len(msgs) == 0 { // No differences?
// The records at this rset are the same. No work to be done.
continue
@@ -35,11 +38,12 @@ func analyzeByRecordSet(cc *CompareConfig) ChangeList {
instructions = orderByDependencies(instructions)
- return instructions
+ return instructions, actualChangeCount
}
-func analyzeByLabel(cc *CompareConfig) ChangeList {
+func analyzeByLabel(cc *CompareConfig) (ChangeList, int) {
var instructions ChangeList
+ var actualChangeCount int
// Accumulate any changes and collect the info needed to generate instructions.
for _, lc := range cc.ldata {
// for each type at that label...
@@ -52,7 +56,9 @@ func analyzeByLabel(cc *CompareConfig) ChangeList {
// for each type at that label...
ets := rt.existingTargets
dts := rt.desiredTargets
- msgs := genmsgs(ets, dts)
+ cs := diffTargets(ets, dts)
+ actualChangeCount += len(cs)
+ msgs := justMsgs(cs)
k := models.RecordKey{NameFQDN: label, Type: rt.rType}
msgsByKey[k] = msgs
accMsgs = append(accMsgs, msgs...) // Accumulate the messages
@@ -79,25 +85,27 @@ func analyzeByLabel(cc *CompareConfig) ChangeList {
instructions = orderByDependencies(instructions)
- return instructions
+ return instructions, actualChangeCount
}
-func analyzeByRecord(cc *CompareConfig) ChangeList {
+func analyzeByRecord(cc *CompareConfig) (ChangeList, int) {
var instructions ChangeList
+ var actualChangeCount int
// For each label, for each type at that label, see if there are any changes.
for _, lc := range cc.ldata {
for _, rt := range lc.tdata {
ets := rt.existingTargets
dts := rt.desiredTargets
cs := diffTargets(ets, dts)
+ actualChangeCount += len(cs)
instructions = append(instructions, cs...)
}
}
instructions = orderByDependencies(instructions)
- return instructions
+ return instructions, actualChangeCount
}
// FYI: there is no analyzeByZone. diff2.ByZone() calls analyzeByRecords().
@@ -303,10 +311,6 @@ func diffTargets(existing, desired []targetConfig) ChangeList {
return instructions
}
-func genmsgs(existing, desired []targetConfig) []string {
- return justMsgs(diffTargets(existing, desired))
-}
-
func justMsgs(cl ChangeList) []string {
var msgs []string
for _, c := range cl {
@@ -314,8 +318,3 @@ func justMsgs(cl ChangeList) []string {
}
return msgs
}
-
-func justMsgString(cl ChangeList) string {
- msgs := justMsgs(cl)
- return strings.Join(msgs, "\n")
-}
diff --git a/pkg/diff2/analyze_test.go b/pkg/diff2/analyze_test.go
index 898735b3e0..a4bc83b2cd 100644
--- a/pkg/diff2/analyze_test.go
+++ b/pkg/diff2/analyze_test.go
@@ -1,6 +1,8 @@
package diff2
import (
+ "bytes"
+ "fmt"
"reflect"
"strings"
"testing"
@@ -18,6 +20,55 @@ func init() {
color.NoColor = true
}
+// Stringify the datastructures (for debugging)
+
+func (c Change) String() string {
+ var buf bytes.Buffer
+ b := &buf
+
+ fmt.Fprintf(b, "Change: verb=%v\n", c.Type)
+ fmt.Fprintf(b, " key=%v\n", c.Key)
+ if c.HintOnlyTTL {
+ fmt.Fprint(b, " Hints=OnlyTTL\n", c.Key)
+ }
+ if len(c.Old) != 0 {
+ fmt.Fprintf(b, " old=%v\n", c.Old)
+ }
+ if len(c.New) != 0 {
+ fmt.Fprintf(b, " new=%v\n", c.New)
+ }
+ fmt.Fprintf(b, " msg=%q\n", c.Msgs)
+
+ return b.String()
+}
+
+func (cl ChangeList) String() string {
+ var buf bytes.Buffer
+ b := &buf
+
+ fmt.Fprintf(b, "ChangeList: len=%d\n", len(cl))
+ for i, j := range cl {
+ fmt.Fprintf(b, "%02d: %s", i, j)
+ }
+
+ return b.String()
+}
+
+// Make sample data
+
+func makeRec(label, rtype, content string) *models.RecordConfig {
+ origin := "f.com"
+ r := models.RecordConfig{TTL: 300}
+ r.SetLabel(label, origin)
+ r.PopulateFromString(rtype, content, origin)
+ return &r
+}
+func makeRecTTL(label, rtype, content string, ttl uint32) *models.RecordConfig {
+ r := makeRec(label, rtype, content)
+ r.TTL = ttl
+ return r
+}
+
var testDataAA1234 = makeRec("laba", "A", "1.2.3.4") // [ 0]
var testDataAA5678 = makeRec("laba", "A", "5.6.7.8") //
var testDataAA1234ttl700 = makeRecTTL("laba", "A", "1.2.3.4", 700) //
@@ -52,6 +103,11 @@ var d12 = makeRec("labh", "A", "1.2.3.4") // [12']
var d13 = makeRec("labc", "CNAME", "labe") // [13']
var testDataApexMX22bbb = makeRec("", "MX", "22 bbb")
+func justMsgString(cl ChangeList) string {
+ msgs := justMsgs(cl)
+ return strings.Join(msgs, "\n")
+}
+
func compareMsgs(t *testing.T, fnname, testname, testpart string, gotcc ChangeList, wantstring string, wantstringdefault string) {
wantstring = coalesce(wantstring, wantstringdefault)
t.Helper()
@@ -71,7 +127,13 @@ func compareCL(t *testing.T, fnname, testname, testpart string, gotcl ChangeList
if d != "" {
t.Errorf("%s()/%s (wantChange%s):\n===got===\n%s\n===want===\n%s\n===diff===\n%s\n===", fnname, testname, testpart, gs, ws, d)
}
+}
+func compareACC(t *testing.T, fnname, testname, testpart string, gotacc int, wantacc int) {
+ t.Helper()
+ if gotacc != wantacc {
+ t.Errorf("%s()/%s (wantChange%s):\n===got===\n%v\n===want===\n%v\n", fnname, testname, testpart, gotacc, wantacc)
+ }
}
func Test_analyzeByRecordSet(t *testing.T) {
@@ -94,6 +156,7 @@ func Test_analyzeByRecordSet(t *testing.T) {
wantChangeRec string
wantMsgsRec string
wantChangeZone string
+ wantChangeCount int
}{
{
@@ -103,6 +166,7 @@ func Test_analyzeByRecordSet(t *testing.T) {
existing: models.Records{testDataAA1234},
desired: models.Records{testDataAA1234clone},
},
+ wantChangeCount: 0,
wantMsgs: "", // Empty
wantChangeRSet: "ChangeList: len=0",
wantChangeLabel: "ChangeList: len=0",
@@ -116,7 +180,8 @@ func Test_analyzeByRecordSet(t *testing.T) {
existing: models.Records{testDataAA1234, testDataAMX10a},
desired: models.Records{testDataAA1234clone, testDataAMX20b},
},
- wantMsgs: "Âą MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)",
+ wantChangeCount: 1,
+ wantMsgs: "Âą MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)",
wantChangeRSet: `
ChangeList: len=1
00: Change: verb=CHANGE
@@ -150,7 +215,8 @@ ChangeList: len=1
existing: models.Records{testDataAA1234, testDataApexMX1aaa},
desired: models.Records{testDataAA1234clone, testDataApexMX22bbb},
},
- wantMsgs: "Âą MODIFY f.com MX (1 aaa.f.com. ttl=300) -> (22 bbb.f.com. ttl=300)",
+ wantChangeCount: 1,
+ wantMsgs: "Âą MODIFY f.com MX (1 aaa.f.com. ttl=300) -> (22 bbb.f.com. ttl=300)",
wantChangeRSet: `
ChangeList: len=1
00: Change: verb=CHANGE
@@ -184,6 +250,7 @@ ChangeList: len=1
existing: models.Records{testDataAA1234, testDataAMX10a},
desired: models.Records{testDataAA1234clone, testDataAA12345, testDataAMX20b},
},
+ wantChangeCount: 2,
wantMsgs: `
Âą MODIFY laba.f.com MX (10 laba.f.com. ttl=300) -> (20 labb.f.com. ttl=300)
+ CREATE laba.f.com A 1.2.3.5 ttl=300
@@ -234,6 +301,7 @@ ChangeList: len=2
existing: models.Records{testDataAA1234, testDataCCa},
desired: models.Records{d13, d3},
},
+ wantChangeCount: 3,
wantMsgs: `
+ CREATE labe.f.com A 10.10.10.95 ttl=300
Âą MODIFY labc.f.com CNAME (laba.f.com. ttl=300) -> (labe.f.com. ttl=300)
@@ -296,6 +364,7 @@ ChangeList: len=3
existing: models.Records{testDataAA1234, testDataAMX10a, testDataCCa, testDataEA15, e4, e5, e6, e7, e8, e9, e10, e11},
desired: models.Records{testDataAA1234clone, testDataAA12345, testDataAMX20b, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12},
},
+ wantChangeCount: 11,
wantMsgs: `
+ CREATE labf.f.com TXT "foo" ttl=300
Âą MODIFY labg.f.com NS (labc.f.com. ttl=300) -> (labf.f.com. ttl=300)
@@ -469,21 +538,24 @@ ChangeList: len=11
// Therefore we have to run NewCompareConfig() each time.
t.Run(tt.name, func(t *testing.T) {
- cl := analyzeByRecordSet(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
+ cl, actualChangeCount := analyzeByRecordSet(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
compareMsgs(t, "analyzeByRecordSet", tt.name, "RSet", cl, tt.wantMsgsRSet, tt.wantMsgs)
compareCL(t, "analyzeByRecordSet", tt.name, "RSet", cl, tt.wantChangeRSet)
+ compareACC(t, "analyzeByRecordSet", tt.name, "ACC", actualChangeCount, tt.wantChangeCount)
})
t.Run(tt.name, func(t *testing.T) {
- cl := analyzeByLabel(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
+ cl, actualChangeCount := analyzeByLabel(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
compareMsgs(t, "analyzeByLabel", tt.name, "Label", cl, tt.wantMsgsLabel, tt.wantMsgs)
compareCL(t, "analyzeByLabel", tt.name, "Label", cl, tt.wantChangeLabel)
+ compareACC(t, "analyzeByLabel", tt.name, "ACC", actualChangeCount, tt.wantChangeCount)
})
t.Run(tt.name, func(t *testing.T) {
- cl := analyzeByRecord(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
+ cl, actualChangeCount := analyzeByRecord(NewCompareConfig(tt.args.origin, tt.args.existing, tt.args.desired, tt.args.compFn))
compareMsgs(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantMsgsRec, tt.wantMsgs)
compareCL(t, "analyzeByRecord", tt.name, "Rec", cl, tt.wantChangeRec)
+ compareACC(t, "analyzeByRecord", tt.name, "ACC", actualChangeCount, tt.wantChangeCount)
})
// NB(tlim): There is no analyzeByZone(). diff2.ByZone() uses analyzeByRecord().
diff --git a/pkg/diff2/compareconfig.go b/pkg/diff2/compareconfig.go
index 0b75d32a22..91d6c86f79 100644
--- a/pkg/diff2/compareconfig.go
+++ b/pkg/diff2/compareconfig.go
@@ -1,7 +1,6 @@
package diff2
import (
- "bytes"
"fmt"
"sort"
@@ -171,35 +170,6 @@ func (cc *CompareConfig) verifyCNAMEAssertions() {
}
-// String returns cc represented as a string. This is used for
-// debugging and unit tests, as the structure may otherwise be
-// difficult to compare.
-func (cc *CompareConfig) String() string {
- var buf bytes.Buffer
- b := &buf
-
- fmt.Fprintf(b, "ldata:\n")
- for i, ld := range cc.ldata {
- fmt.Fprintf(b, " ldata[%02d]: %s\n", i, ld.label)
- for j, t := range ld.tdata {
- fmt.Fprintf(b, " tdata[%d]: %q e(%d, %d) d(%d, %d)\n", j, t.rType,
- len(t.existingTargets),
- len(t.existingRecs),
- len(t.desiredTargets),
- len(t.desiredRecs),
- )
- }
- }
- fmt.Fprintf(b, "labelMap: len=%d %v\n", len(cc.labelMap), cc.labelMap)
- fmt.Fprintf(b, "keyMap: len=%d %v\n", len(cc.keyMap), cc.keyMap)
- fmt.Fprintf(b, "existing: %q\n", cc.existing)
- fmt.Fprintf(b, "desired: %q\n", cc.desired)
- fmt.Fprintf(b, "origin: %v\n", cc.origin)
- fmt.Fprintf(b, "compFn: %v\n", cc.compareableFunc)
-
- return b.String()
-}
-
// Generate a string that can be used to compare this record to others
// for equality.
func mkCompareBlobs(rc *models.RecordConfig, f func(*models.RecordConfig) string) (string, string) {
@@ -215,6 +185,7 @@ func mkCompareBlobs(rc *models.RecordConfig, f func(*models.RecordConfig) string
}
}
+ // We do this to save memory. This assures the first return value uses the same memory as the second.
lenWithoutTTL := len(comp)
compFull := comp + fmt.Sprintf(" ttl=%d", rc.TTL)
diff --git a/pkg/diff2/compareconfig_test.go b/pkg/diff2/compareconfig_test.go
index 51a15df154..0c221a8158 100644
--- a/pkg/diff2/compareconfig_test.go
+++ b/pkg/diff2/compareconfig_test.go
@@ -1,6 +1,8 @@
package diff2
import (
+ "bytes"
+ "fmt"
"strings"
"testing"
@@ -8,6 +10,35 @@ import (
"github.com/kylelemons/godebug/diff"
)
+// String returns cc represented as a string. This is used for
+// debugging and unit tests, as the structure may otherwise be
+// difficult to compare.
+func (cc *CompareConfig) String() string {
+ var buf bytes.Buffer
+ b := &buf
+
+ fmt.Fprintf(b, "ldata:\n")
+ for i, ld := range cc.ldata {
+ fmt.Fprintf(b, " ldata[%02d]: %s\n", i, ld.label)
+ for j, t := range ld.tdata {
+ fmt.Fprintf(b, " tdata[%d]: %q e(%d, %d) d(%d, %d)\n", j, t.rType,
+ len(t.existingTargets),
+ len(t.existingRecs),
+ len(t.desiredTargets),
+ len(t.desiredRecs),
+ )
+ }
+ }
+ fmt.Fprintf(b, "labelMap: len=%d %v\n", len(cc.labelMap), cc.labelMap)
+ fmt.Fprintf(b, "keyMap: len=%d %v\n", len(cc.keyMap), cc.keyMap)
+ fmt.Fprintf(b, "existing: %q\n", cc.existing)
+ fmt.Fprintf(b, "desired: %q\n", cc.desired)
+ fmt.Fprintf(b, "origin: %v\n", cc.origin)
+ fmt.Fprintf(b, "compFn: %v\n", cc.compareableFunc)
+
+ return b.String()
+}
+
func TestNewCompareConfig(t *testing.T) {
type args struct {
origin string
@@ -204,7 +235,7 @@ compFn:
got := strings.TrimSpace(cc.String())
tt.want = strings.TrimSpace(tt.want)
if got != tt.want {
- d := diff.Diff(got, tt.want)
+ d := diff.Diff(tt.want, got)
t.Errorf("NewCompareConfig() = \n%s\n", d)
}
})
diff --git a/pkg/diff2/diff2.go b/pkg/diff2/diff2.go
index f8e11404a9..4c94adddaa 100644
--- a/pkg/diff2/diff2.go
+++ b/pkg/diff2/diff2.go
@@ -6,7 +6,6 @@ package diff2
// against the desired records.
import (
- "bytes"
"fmt"
"strings"
@@ -53,6 +52,7 @@ type Change struct {
HintRecordSetLen1 bool
}
+// GetType returns the type of a change.
func (c Change) GetType() dnsgraph.NodeType {
if c.Type == REPORT {
return dnsgraph.Report
@@ -61,10 +61,12 @@ func (c Change) GetType() dnsgraph.NodeType {
return dnsgraph.Change
}
+// GetName returns the FQDN of the host being changed.
func (c Change) GetName() string {
return c.Key.NameFQDN
}
+// GetDependencies returns the depenencies of a change.
func (c Change) GetDependencies() []dnsgraph.Dependency {
var dependencies []dnsgraph.Dependency
@@ -149,7 +151,7 @@ func (c *Change) CreateCorrectionWithMessage(msg string, correctionFunction func
// www.example.com, A, and a list of all the desired IP addresses.
//
// Examples include: AZURE_DNS, GCORE, NS1, ROUTE53
-func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
+func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, int, error) {
return byHelper(analyzeByRecordSet, existing, dc, compFunc)
}
@@ -161,7 +163,7 @@ func ByRecordSet(existing models.Records, dc *models.DomainConfig, compFunc Comp
// to be served at a particular label, or the label itself is deleted.
//
// Examples include: GANDI_V5
-func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
+func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, int, error) {
return byHelper(analyzeByLabel, existing, dc, compFunc)
}
@@ -177,7 +179,7 @@ func ByLabel(existing models.Records, dc *models.DomainConfig, compFunc Comparab
// A delete always has exactly 1 old: .Old[0]
//
// Examples include: CLOUDFLAREAPI, HEDNS, INWX, MSDNS, OVH, PORKBUN, VULTR
-func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
+func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, int, error) {
return byHelper(analyzeByRecord, existing, dc, compFunc)
}
@@ -193,36 +195,47 @@ func ByRecord(existing models.Records, dc *models.DomainConfig, compFunc Compara
//
// Example usage:
//
-// msgs, changes, err := diff2.ByZone(foundRecords, dc, nil)
+// result, err := diff2.ByZone(foundRecords, dc, nil)
// if err != nil {
// return nil, err
// }
// if changes {
// // Generate a "correction" that uploads the entire zone.
-// // (dc.Records are the new records for the zone).
+// // (result.DesiredPlus are the new records for the zone).
// }
//
// Example providers include: BIND, AUTODNS
-func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) ([]string, bool, error) {
- // Only return the messages. The caller has the list of records needed to build the new zone.
- instructions, err := byHelper(analyzeByRecord, existing, dc, compFunc)
- changes := false
- for i := range instructions {
- //fmt.Printf("DEBUG: ByZone #%d: %v\n", i, ii)
- if instructions[i].Type != REPORT {
- changes = true
- }
- }
- return justMsgs(instructions), changes, err
+func ByZone(existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ByResults, error) {
+ // Only return the messages and a list of records needed to build the new zone.
+ result, err := byHelperStruct(analyzeByRecord, existing, dc, compFunc)
+ result.Msgs = justMsgs(result.Instructions)
+ return result, err
}
-//
+// ByResults is the results of ByZone() and perhaps someday all the By*() functions.
+// It is partially populated by // byHelperStruct() and partially by the By*()
+// functions that use it.
+type ByResults struct {
+ // Fields filled in by byHelperStruct():
+ Instructions ChangeList // Instructions to turn existing into desired.
+ ActualChangeCount int // Number of actual changes, not including REPORTs.
+ HasChanges bool // True if there are any changes.
+ DesiredPlus models.Records // Desired + foreign + ignored
+ // Fields filled in by ByZone():
+ Msgs []string // Just the messages from the instructions.
+}
-// byHelper does 90% of the work for the By*() calls.
-func byHelper(fn func(cc *CompareConfig) ChangeList, existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, error) {
+// byHelper is like byHelperStruct but has a signature that is compatible with legacy code.
+// Deprecated: Use byHelperStruct instead.
+func byHelper(fn func(cc *CompareConfig) (ChangeList, int), existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ChangeList, int, error) {
+ result, err := byHelperStruct(fn, existing, dc, compFunc)
+ return result.Instructions, result.ActualChangeCount, err
+}
+// byHelperStruct does 90% of the work for the By*() calls.
+func byHelperStruct(fn func(cc *CompareConfig) (ChangeList, int), existing models.Records, dc *models.DomainConfig, compFunc ComparableFunc) (ByResults, error) {
// Process NO_PURGE/ENSURE_ABSENT and IGNORE*().
- desired, msgs, err := handsoff(
+ desiredPlus, msgs, err := handsoff(
dc.Name,
existing, dc.Records, dc.EnsureAbsent,
dc.Unmanaged,
@@ -230,14 +243,14 @@ func byHelper(fn func(cc *CompareConfig) ChangeList, existing models.Records, dc
dc.KeepUnknown,
)
if err != nil {
- return nil, err
+ return ByResults{}, err
}
// Regroup existing/desiredd for easy comparison:
- cc := NewCompareConfig(dc.Name, existing, desired, compFunc)
+ cc := NewCompareConfig(dc.Name, existing, desiredPlus, compFunc)
// Analyze and generate the instructions:
- instructions := fn(cc)
+ instructions, actualChangeCount := fn(cc)
// If we have msgs, create a change to output them:
if len(msgs) != 0 {
@@ -250,39 +263,10 @@ func byHelper(fn func(cc *CompareConfig) ChangeList, existing models.Records, dc
instructions = append([]Change{chg}, instructions...)
}
- return instructions, nil
-}
-
-// Stringify the datastructures (for debugging)
-
-func (c Change) String() string {
- var buf bytes.Buffer
- b := &buf
-
- fmt.Fprintf(b, "Change: verb=%v\n", c.Type)
- fmt.Fprintf(b, " key=%v\n", c.Key)
- if c.HintOnlyTTL {
- fmt.Fprint(b, " Hints=OnlyTTL\n", c.Key)
- }
- if len(c.Old) != 0 {
- fmt.Fprintf(b, " old=%v\n", c.Old)
- }
- if len(c.New) != 0 {
- fmt.Fprintf(b, " new=%v\n", c.New)
- }
- fmt.Fprintf(b, " msg=%q\n", c.Msgs)
-
- return b.String()
-}
-
-func (cl ChangeList) String() string {
- var buf bytes.Buffer
- b := &buf
-
- fmt.Fprintf(b, "ChangeList: len=%d\n", len(cl))
- for i, j := range cl {
- fmt.Fprintf(b, "%02d: %s", i, j)
- }
-
- return b.String()
+ return ByResults{
+ Instructions: instructions,
+ ActualChangeCount: actualChangeCount,
+ HasChanges: actualChangeCount > 0,
+ DesiredPlus: desiredPlus,
+ }, nil
}
diff --git a/pkg/diff2/groupsort.go b/pkg/diff2/groupsort.go
index ea282ef7a8..872c3c2ffb 100644
--- a/pkg/diff2/groupsort.go
+++ b/pkg/diff2/groupsort.go
@@ -1,45 +1,40 @@
package diff2
-import (
- "github.com/StackExchange/dnscontrol/v4/models"
- "github.com/StackExchange/dnscontrol/v4/pkg/prettyzone"
-)
-
-type recset struct {
- Key models.RecordKey
- Recs []*models.RecordConfig
-}
-
-func groupbyRSet(recs models.Records, origin string) []recset {
-
- if len(recs) == 0 {
- return nil
- }
-
- // Sort the NameFQDN to a consistent order. The actual sort methodology
- // doesn't matter as long as equal values are adjacent.
- // Use the PrettySort ordering so that the records are extra pretty.
- pretty := prettyzone.PrettySort(recs, origin, 0, nil)
- recs = pretty.Records
-
- var result []recset
- var acc []*models.RecordConfig
-
- // Do the first element
- prevkey := recs[0].Key()
- acc = append(acc, recs[0])
-
- for i := 1; i < len(recs); i++ {
- curkey := recs[i].Key()
- if prevkey == curkey { // A run of equal keys.
- acc = append(acc, recs[i])
- } else { // New key. Append old data to result and start new acc.
- result = append(result, recset{Key: prevkey, Recs: acc})
- acc = []*models.RecordConfig{recs[i]}
- }
- prevkey = curkey
- }
- result = append(result, recset{Key: prevkey, Recs: acc}) // The remainder
-
- return result
-}
+// type recset struct {
+// Key models.RecordKey
+// Recs []*models.RecordConfig
+// }
+
+// func groupbyRSet(recs models.Records, origin string) []recset {
+
+// if len(recs) == 0 {
+// return nil
+// }
+
+// // Sort the NameFQDN to a consistent order. The actual sort methodology
+// // doesn't matter as long as equal values are adjacent.
+// // Use the PrettySort ordering so that the records are extra pretty.
+// pretty := prettyzone.PrettySort(recs, origin, 0, nil)
+// recs = pretty.Records
+
+// var result []recset
+// var acc []*models.RecordConfig
+
+// // Do the first element
+// prevkey := recs[0].Key()
+// acc = append(acc, recs[0])
+
+// for i := 1; i < len(recs); i++ {
+// curkey := recs[i].Key()
+// if prevkey == curkey { // A run of equal keys.
+// acc = append(acc, recs[i])
+// } else { // New key. Append old data to result and start new acc.
+// result = append(result, recset{Key: prevkey, Recs: acc})
+// acc = []*models.RecordConfig{recs[i]}
+// }
+// prevkey = curkey
+// }
+// result = append(result, recset{Key: prevkey, Recs: acc}) // The remainder
+
+// return result
+// }
diff --git a/pkg/diff2/groupsort_test.go b/pkg/diff2/groupsort_test.go
deleted file mode 100644
index cca1e416c3..0000000000
--- a/pkg/diff2/groupsort_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package diff2
-
-import (
- "reflect"
- "testing"
-
- "github.com/StackExchange/dnscontrol/v4/models"
-)
-
-func makeRec(label, rtype, content string) *models.RecordConfig {
- origin := "f.com"
- r := models.RecordConfig{TTL: 300}
- r.SetLabel(label, origin)
- r.PopulateFromString(rtype, content, origin)
- return &r
-}
-func makeRecTTL(label, rtype, content string, ttl uint32) *models.RecordConfig {
- r := makeRec(label, rtype, content)
- r.TTL = ttl
- return r
-}
-func makeRecSet(recs ...*models.RecordConfig) *recset {
- result := recset{}
- result.Key = recs[0].Key()
- result.Recs = append(result.Recs, recs...)
- return &result
-}
-
-func Test_groupbyRSet(t *testing.T) {
-
- wwwa1 := makeRec("www", "A", "1.1.1.1")
- wwwa2 := makeRec("www", "A", "2.2.2.2")
- zzza1 := makeRec("zzz", "A", "1.1.0.0")
- zzza2 := makeRec("zzz", "A", "2.2.0.0")
- wwwmx1 := makeRec("www", "MX", "1 mx1.foo.com.")
- wwwmx2 := makeRec("www", "MX", "2 mx2.foo.com.")
- zzzmx1 := makeRec("zzz", "MX", "1 mx.foo.com.")
- orig := models.Records{wwwa1, wwwa2, zzza1, zzza2, wwwmx1, wwwmx2, zzzmx1}
- wantResult := []recset{
- *makeRecSet(wwwa1, wwwa2),
- *makeRecSet(wwwmx1, wwwmx2),
- *makeRecSet(zzza1, zzza2),
- *makeRecSet(zzzmx1),
- }
-
- t.Run("afew", func(t *testing.T) {
- if gotResult := groupbyRSet(orig, "f.com"); !reflect.DeepEqual(gotResult, wantResult) {
- t.Errorf("groupbyRSet() = %v, want %v", gotResult, wantResult)
- }
- })
-}
diff --git a/pkg/diff2/handsoff.go b/pkg/diff2/handsoff.go
index 917c33af7b..7e2b4f6493 100644
--- a/pkg/diff2/handsoff.go
+++ b/pkg/diff2/handsoff.go
@@ -98,8 +98,6 @@ The actual implementation combines this all into one loop:
Append "foreign list" to "desired".
*/
-const maxReport = 5
-
// handsoff processes the IGNORE*()//NO_PURGE/ENSURE_ABSENT features.
func handsoff(
domain string,
@@ -116,14 +114,19 @@ func handsoff(
return nil, nil, err
}
+ var punct = ":"
+ if printer.MaxReport == 0 {
+ punct = "."
+ }
+
// Process IGNORE*() and NO_PURGE features:
ignorable, foreign := processIgnoreAndNoPurge(domain, existing, desired, absences, unmanagedConfigs, noPurge)
if len(foreign) != 0 {
- msgs = append(msgs, fmt.Sprintf("%d records not being deleted because of NO_PURGE:", len(foreign)))
+ msgs = append(msgs, fmt.Sprintf("%d records not being deleted because of NO_PURGE%s", len(foreign), punct))
msgs = append(msgs, reportSkips(foreign, !printer.SkinnyReport)...)
}
if len(ignorable) != 0 {
- msgs = append(msgs, fmt.Sprintf("%d records not being deleted because of IGNORE*():", len(ignorable)))
+ msgs = append(msgs, fmt.Sprintf("%d records not being deleted because of IGNORE*()%s", len(ignorable), punct))
msgs = append(msgs, reportSkips(ignorable, !printer.SkinnyReport)...)
}
@@ -146,21 +149,23 @@ func handsoff(
return desired, msgs, nil
}
-// reportSkips reports records being skipped, if !full only the first maxReport are output.
+// reportSkips reports records being skipped, if !full only the first
+// printer.MaxReport are output.
func reportSkips(recs models.Records, full bool) []string {
var msgs []string
- shorten := (!full) && (len(recs) > maxReport)
+ shorten := (!full) && (len(recs) > printer.MaxReport)
+
last := len(recs)
if shorten {
- last = maxReport
+ last = printer.MaxReport
}
for _, r := range recs[:last] {
msgs = append(msgs, fmt.Sprintf(" %s. %s %s", r.GetLabelFQDN(), r.Type, r.GetTargetCombined()))
}
- if shorten {
- msgs = append(msgs, fmt.Sprintf(" ...and %d more... (use --full to show all)", len(recs)-maxReport))
+ if shorten && printer.MaxReport != 0 {
+ msgs = append(msgs, fmt.Sprintf(" ...and %d more... (use --full to show all)", len(recs)-printer.MaxReport))
}
return msgs
diff --git a/pkg/diff2/handsoff_test.go b/pkg/diff2/handsoff_test.go
index 7f89fb82a3..4b28637397 100644
--- a/pkg/diff2/handsoff_test.go
+++ b/pkg/diff2/handsoff_test.go
@@ -18,7 +18,7 @@ func parseZoneContents(content string, zoneName string, zonefileName string) (mo
foundRecords := models.Records{}
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
- rec, err := models.RRtoRC(rr, zoneName)
+ rec, err := models.RRtoRCTxtBug(rr, zoneName)
if err != nil {
return nil, err
}
diff --git a/pkg/dnsgraph/dependencies.go b/pkg/dnsgraph/dependencies.go
index e2acfc8ea0..fb5794e7d2 100644
--- a/pkg/dnsgraph/dependencies.go
+++ b/pkg/dnsgraph/dependencies.go
@@ -1,5 +1,6 @@
package dnsgraph
+// CreateDependencies creates a dependency list from a list of FQDNs and a DepenendyType.
func CreateDependencies(dependencyFQDNs []string, dependencyType DependencyType) []Dependency {
var dependencies []Dependency
diff --git a/pkg/dnsgraph/dnsgraph.go b/pkg/dnsgraph/dnsgraph.go
index 24df8f2a8a..ef69b22f0d 100644
--- a/pkg/dnsgraph/dnsgraph.go
+++ b/pkg/dnsgraph/dnsgraph.go
@@ -5,32 +5,39 @@ import "github.com/StackExchange/dnscontrol/v4/pkg/dnstree"
type edgeDirection uint8
const (
+ // IncomingEdge is an edge inbound.
IncomingEdge edgeDirection = iota
+ // OutgoingEdge is an edge outbound.
OutgoingEdge
)
-type DNSGraphEdge[T Graphable] struct {
+// Edge an edge on the graph.
+type Edge[T Graphable] struct {
Dependency Dependency
- Node *DNSGraphNode[T]
+ Node *Node[T]
Direction edgeDirection
}
-type DNSGraphEdges[T Graphable] []DNSGraphEdge[T]
+// Edges a list of edges.
+type Edges[T Graphable] []Edge[T]
-type DNSGraphNode[T Graphable] struct {
+// Node a node in the graph.
+type Node[T Graphable] struct {
Data T
- Edges DNSGraphEdges[T]
+ Edges Edges[T]
}
-type dnsGraphNodes[T Graphable] []*DNSGraphNode[T]
+type dnsGraphNodes[T Graphable] []*Node[T]
-type DNSGraph[T Graphable] struct {
+// Graph a graph.
+type Graph[T Graphable] struct {
All dnsGraphNodes[T]
Tree *dnstree.DomainTree[dnsGraphNodes[T]]
}
-func CreateGraph[T Graphable](entries []T) *DNSGraph[T] {
- graph := &DNSGraph[T]{
+// CreateGraph returns a graph.
+func CreateGraph[T Graphable](entries []T) *Graph[T] {
+ graph := &Graph[T]{
All: dnsGraphNodes[T]{},
Tree: dnstree.Create[dnsGraphNodes[T]](),
}
@@ -48,7 +55,8 @@ func CreateGraph[T Graphable](entries []T) *DNSGraph[T] {
return graph
}
-func (graph *DNSGraph[T]) RemoveNode(toRemove *DNSGraphNode[T]) {
+// RemoveNode removes a node from a graph.
+func (graph *Graph[T]) RemoveNode(toRemove *Node[T]) {
for _, edge := range toRemove.Edges {
edge.Node.Edges = edge.Node.Edges.RemoveNode(toRemove)
}
@@ -62,11 +70,12 @@ func (graph *DNSGraph[T]) RemoveNode(toRemove *DNSGraphNode[T]) {
}
}
-func (graph *DNSGraph[T]) AddNode(data T) {
+// AddNode adds a node to a graph.
+func (graph *Graph[T]) AddNode(data T) {
nodes := graph.Tree.Get(data.GetName())
- node := &DNSGraphNode[T]{
+ node := &Node[T]{
Data: data,
- Edges: DNSGraphEdges[T]{},
+ Edges: Edges[T]{},
}
if nodes == nil {
nodes = dnsGraphNodes[T]{}
@@ -77,7 +86,8 @@ func (graph *DNSGraph[T]) AddNode(data T) {
graph.Tree.Set(data.GetName(), nodes)
}
-func (graph *DNSGraph[T]) AddEdge(sourceNode *DNSGraphNode[T], dependency Dependency) {
+// AddEdge adds an edge to a graph.
+func (graph *Graph[T]) AddEdge(sourceNode *Node[T], dependency Dependency) {
destinationNodes := graph.Tree.Get(dependency.NameFQDN)
if destinationNodes == nil {
@@ -93,13 +103,13 @@ func (graph *DNSGraph[T]) AddEdge(sourceNode *DNSGraphNode[T], dependency Depend
continue
}
- sourceNode.Edges = append(sourceNode.Edges, DNSGraphEdge[T]{
+ sourceNode.Edges = append(sourceNode.Edges, Edge[T]{
Dependency: dependency,
Node: destinationNode,
Direction: OutgoingEdge,
})
- destinationNode.Edges = append(destinationNode.Edges, DNSGraphEdge[T]{
+ destinationNode.Edges = append(destinationNode.Edges, Edge[T]{
Dependency: dependency,
Node: sourceNode,
Direction: IncomingEdge,
@@ -107,7 +117,8 @@ func (graph *DNSGraph[T]) AddEdge(sourceNode *DNSGraphNode[T], dependency Depend
}
}
-func (nodes dnsGraphNodes[T]) RemoveNode(toRemove *DNSGraphNode[T]) dnsGraphNodes[T] {
+// RemoveNode removes a node from a graph.
+func (nodes dnsGraphNodes[T]) RemoveNode(toRemove *Node[T]) dnsGraphNodes[T] {
var newNodes dnsGraphNodes[T]
for _, node := range nodes {
@@ -119,8 +130,9 @@ func (nodes dnsGraphNodes[T]) RemoveNode(toRemove *DNSGraphNode[T]) dnsGraphNode
return newNodes
}
-func (edges DNSGraphEdges[T]) RemoveNode(toRemove *DNSGraphNode[T]) DNSGraphEdges[T] {
- var newEdges DNSGraphEdges[T]
+// RemoveNode removes a node from a graph.
+func (edges Edges[T]) RemoveNode(toRemove *Node[T]) Edges[T] {
+ var newEdges Edges[T]
for _, edge := range edges {
if edge.Node != toRemove {
@@ -131,7 +143,8 @@ func (edges DNSGraphEdges[T]) RemoveNode(toRemove *DNSGraphNode[T]) DNSGraphEdge
return newEdges
}
-func (edges DNSGraphEdges[T]) Contains(toFind *DNSGraphNode[T], direction edgeDirection) bool {
+// Contains returns true if a node is in the graph AND is in that direction.
+func (edges Edges[T]) Contains(toFind *Node[T], direction edgeDirection) bool {
for _, edge := range edges {
if edge.Node == toFind && edge.Direction == direction {
diff --git a/pkg/dnsgraph/graphable.go b/pkg/dnsgraph/graphable.go
index 13dd2fb53f..280b86c0ab 100644
--- a/pkg/dnsgraph/graphable.go
+++ b/pkg/dnsgraph/graphable.go
@@ -1,30 +1,39 @@
package dnsgraph
+// NodeType enumerates the node types.
type NodeType uint8
const (
+ // Change is the type of change.
Change NodeType = iota
+ // Report is a Report.
Report
)
+// DependencyType enumerates the dependency types.
type DependencyType uint8
const (
+ // ForwardDependency is a forward dependency.
ForwardDependency DependencyType = iota
+ // BackwardDependency is a backwards dependency.
BackwardDependency
)
+// Dependency is a dependency.
type Dependency struct {
NameFQDN string
Type DependencyType
}
+// Graphable is an interface for things that can be in a graph.
type Graphable interface {
GetType() NodeType
GetName() string
GetDependencies() []Dependency
}
+// GetRecordsNamesForGraphables returns names in a graph.
func GetRecordsNamesForGraphables[T Graphable](graphables []T) []string {
var names []string
diff --git a/pkg/dnsgraph/testutils/stubrecords.go b/pkg/dnsgraph/testutils/stubrecords.go
index e4627ffeca..bf600febe0 100644
--- a/pkg/dnsgraph/testutils/stubrecords.go
+++ b/pkg/dnsgraph/testutils/stubrecords.go
@@ -2,24 +2,29 @@ package testutils
import "github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
+// StubRecord stub
type StubRecord struct {
NameFQDN string
Dependencies []dnsgraph.Dependency
Type dnsgraph.NodeType
}
+// GetType stub
func (record StubRecord) GetType() dnsgraph.NodeType {
return record.Type
}
+// GetName stub
func (record StubRecord) GetName() string {
return record.NameFQDN
}
+// GetDependencies stub
func (record StubRecord) GetDependencies() []dnsgraph.Dependency {
return record.Dependencies
}
+// StubRecordsAsGraphable stub
func StubRecordsAsGraphable(records []StubRecord) []dnsgraph.Graphable {
sortableRecords := make([]dnsgraph.Graphable, len(records))
diff --git a/pkg/dnssort/graphsort.go b/pkg/dnssort/graphsort.go
index b9476e44c9..3c8c1a37c6 100644
--- a/pkg/dnssort/graphsort.go
+++ b/pkg/dnssort/graphsort.go
@@ -49,7 +49,7 @@ func SortUsingGraph[T dnsgraph.Graphable](records []T) SortResult[T] {
}
type directedSortState[T dnsgraph.Graphable] struct {
- graph *dnsgraph.DNSGraph[T]
+ graph *dnsgraph.Graph[T]
sortedRecords []T
unresolvedRecords []T
hasResolvedLastRound bool
@@ -106,7 +106,7 @@ func (sortState *directedSortState[T]) finalize() {
}
}
-func hasUnmetDependencies[T dnsgraph.Graphable](node *dnsgraph.DNSGraphNode[T]) bool {
+func hasUnmetDependencies[T dnsgraph.Graphable](node *dnsgraph.Node[T]) bool {
for _, edge := range node.Edges {
if edge.Dependency.Type == dnsgraph.BackwardDependency && edge.Direction == dnsgraph.IncomingEdge {
return true
diff --git a/pkg/dnssort/result.go b/pkg/dnssort/result.go
index 5f90ecdb31..d75f76dd81 100644
--- a/pkg/dnssort/result.go
+++ b/pkg/dnssort/result.go
@@ -2,7 +2,10 @@ package dnssort
import "github.com/StackExchange/dnscontrol/v4/pkg/dnsgraph"
+// SortResult is the result of a sort function.
type SortResult[T dnsgraph.Graphable] struct {
- SortedRecords []T
+ // SortedRecords is the sorted records.
+ SortedRecords []T
+ // UnresolvedRecords is the records that could not be resolved.
UnresolvedRecords []T
}
diff --git a/pkg/js/hash.go b/pkg/js/hash.go
new file mode 100644
index 0000000000..1bf8ba317b
--- /dev/null
+++ b/pkg/js/hash.go
@@ -0,0 +1,42 @@
+package js
+
+import (
+ "crypto/sha1"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/hex"
+ "fmt"
+
+ "github.com/robertkrimen/otto"
+)
+
+// Exposes sha1, sha256, and sha512 hashing functions to Javascript
+func hashFunc(call otto.FunctionCall) otto.Value {
+ if len(call.ArgumentList) != 2 {
+ throw(call.Otto, "require takes exactly two arguments")
+ }
+ algorithm := call.Argument(0).String() // The algorithm to use for hashing
+ value := call.Argument(1).String() // The value to hash
+ //lint:ignore SA4006 work around bug in staticcheck. This value is needed if the switch statement follows the default path.
+ result := otto.Value{}
+ fmt.Printf("%s\n", value)
+
+ switch algorithm {
+ case "SHA1", "sha1":
+ tmp := sha1.New()
+ tmp.Write([]byte(value))
+ fmt.Printf("%s\n", hex.EncodeToString(tmp.Sum(nil)))
+ result, _ = otto.ToValue(hex.EncodeToString(tmp.Sum(nil)))
+ case "SHA256", "sha256":
+ tmp := sha256.New()
+ tmp.Write([]byte(value))
+ result, _ = otto.ToValue(hex.EncodeToString(tmp.Sum(nil)))
+ case "SHA512", "sha512":
+ tmp := sha512.New()
+ tmp.Write([]byte(value))
+ result, _ = otto.ToValue(hex.EncodeToString(tmp.Sum(nil)))
+ default:
+ throw(call.Otto, fmt.Sprintf("invalid algorithm %s given", algorithm))
+ }
+ return result
+}
diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js
index df15af2ca2..88f52c0fe5 100644
--- a/pkg/js/helpers.js
+++ b/pkg/js/helpers.js
@@ -104,6 +104,7 @@ function newDomain(name, registrar) {
registrar: registrar,
meta: {},
records: [],
+ rawrecords: [],
recordsabsent: [],
dnsProviders: {},
defaultTTL: 0,
@@ -328,8 +329,14 @@ var R53_ALIAS = recordBuilder('R53_ALIAS', {
record.target = args.target;
if (_.isObject(record.r53_alias)) {
record.r53_alias['type'] = args.type;
+ if (!_.isString(record.r53_alias['evaluate_target_health'])) {
+ record.r53_alias['evaluate_target_health'] = 'false';
+ }
} else {
- record.r53_alias = { type: args.type };
+ record.r53_alias = {
+ type: args.type,
+ evaluate_target_health: 'false',
+ };
}
},
});
@@ -347,6 +354,17 @@ function R53_ZONE(zone_id) {
};
}
+// R53_EVALUATE_TARGET_HEALTH(enabled)
+function R53_EVALUATE_TARGET_HEALTH(enabled) {
+ return function (r) {
+ if (_.isObject(r.r53_alias)) {
+ r.r53_alias['evaluate_target_health'] = enabled.toString();
+ } else {
+ r.r53_alias = { evaluate_target_health: enabled.toString() };
+ }
+ };
+}
+
function validateR53AliasType(value) {
if (!_.isString(value)) {
return false;
@@ -410,6 +428,44 @@ var DS = recordBuilder('DS', {
// DHCID(name,target, recordModifiers...)
var DHCID = recordBuilder('DHCID');
+// DNAME(name,target, recordModifiers...)
+var DNAME = recordBuilder('DNAME');
+
+// DNSKEY(name, flags, protocol, algorithm, publickey)
+var DNSKEY = recordBuilder('DNSKEY', {
+ args: [
+ ['name', _.isString],
+ ['flags', _.isNumber],
+ ['protocol', _.isNumber],
+ ['algorithm', _.isNumber],
+ ['publickey', _.isString],
+ ],
+ transform: function (record, args, modifiers) {
+ record.name = args.name;
+ record.dnskeyflags = args.flags;
+ record.dnskeyprotocol = args.protocol;
+ record.dnskeyalgorithm = args.algorithm;
+ record.dnskeypublickey = args.publickey;
+ record.target = args.target;
+ },
+});
+
+// name, priority, target, params
+var HTTPS = recordBuilder('HTTPS', {
+ args: [
+ ['name', _.isString],
+ ['priority', _.isNumber],
+ ['target', _.isString],
+ ['params', _.isString],
+ ],
+ transform: function (record, args, modifiers) {
+ record.name = args.name;
+ record.svcpriority = args.priority;
+ record.target = args.target;
+ record.svcparams = args.params;
+ },
+});
+
// PTR(name,target, recordModifiers...)
var PTR = recordBuilder('PTR');
@@ -491,6 +547,22 @@ var SSHFP = recordBuilder('SSHFP', {
},
});
+// name, priority, target, params
+var SVCB = recordBuilder('SVCB', {
+ args: [
+ ['name', _.isString],
+ ['priority', _.isNumber],
+ ['target', _.isString],
+ ['params', _.isString],
+ ],
+ transform: function (record, args, modifiers) {
+ record.name = args.name;
+ record.svcpriority = args.priority;
+ record.target = args.target;
+ record.svcparams = args.params;
+ },
+});
+
// name, usage, selector, matchingtype, certificate
var TLSA = recordBuilder('TLSA', {
args: [
@@ -526,11 +598,9 @@ var TXT = recordBuilder('TXT', {
record.name = args.name;
// Store the strings from the user verbatim.
if (_.isString(args.target)) {
- record.txtstrings = [args.target];
- record.target = args.target; // Overwritten by the Go code
+ record.target = args.target;
} else {
- record.txtstrings = args.target;
- record.target = args.target.join(''); // Overwritten by the Go code
+ record.target = args.target.join('');
}
},
});
@@ -613,10 +683,33 @@ function getENotationInt(x) {
0cm = 0e0 == 0
*/
size = x * 100; // get cm value
- // get the m^e version of size
- array = size.toExponential(0).split('e+').map(Number);
+
+ // Convert the number to scientific notation
+ var exp = Math.floor(Math.log10(size)); // Get the exponent (base 10)
+ var mantissa = size / Math.pow(10, exp); // Normalize mantissa
+
+ // Normalize the mantissa to fit into 4 bits (between 0 and 15)
+ while (mantissa < 1 && exp > 0) {
+ mantissa *= 10;
+ exp -= 1;
+ }
+
+ /* Four-bit values greater than 9 are undefined, as are values with a base
+ of zero and a non-zero exponent.
+ */
+
+ // Ensure mantissa and exponent are within 4-bit range (0-15) but no greater than 9
+ mantissa = Math.floor(mantissa); // We truncate decimals
+ if (mantissa > 9) {
+ mantissa = 9; // Cap mantissa at 9
+ }
+ if (exp < 0) {
+ exp = 0; // We cap negative exponents at 0
+ } else if (exp > 9) {
+ exp = 9; // Cap exponent at 9
+ }
// convert it to 4bit:4bit uint8
- m_e = (array[0] << 4) | array[1];
+ m_e = (mantissa << 4) | (exp & 0xf);
return m_e;
}
@@ -665,10 +758,30 @@ function locStringBuilder(record, args) {
}
// handle altitude, size, horizontal precision, vertical precision
- precisionbuffer = args.alt.toString() + 'm';
- precisionbuffer += ' ' + args.siz.toString() + 'm';
- precisionbuffer += ' ' + args.hp.toString() + 'm';
- precisionbuffer += ' ' + args.vp.toString() + 'm';
+ // alt -100000.00 .. 42849672.95m
+ // size, horizontal precision, vertical precision 0 .. 90000000.00m
+ precisionbuffer =
+ (args.alt < -100000
+ ? -100000
+ : args.alt > 42849672.95
+ ? 42849672.95
+ : args.alt.toString()) + 'm';
+ precisionbuffer +=
+ ' ' +
+ (args.siz > 90000000
+ ? 90000000
+ : args.siz < 0
+ ? 0
+ : args.siz.toString()) +
+ 'm';
+ precisionbuffer +=
+ ' ' +
+ (args.hp > 90000000 ? 90000000 : args.hp < 0 ? 0 : args.hp.toString()) +
+ 'm';
+ precisionbuffer +=
+ ' ' +
+ (args.vp > 90000000 ? 90000000 : args.vp < 0 ? 0 : args.vp.toString()) +
+ 'm';
record.target = nsstring + ewstring + precisionbuffer;
@@ -693,23 +806,23 @@ function locDMSBuilder(record, args) {
// W
else record.loclongitude = LOCPrimeMeridian - lon;
// Altitude
- record.localtitude = (args.alt + LOCAltitudeBase) * 100;
+ record.localtitude = parseInt((args.alt + LOCAltitudeBase) * 100);
+ // 'cast' altitude to fit 'uint32'
+ record.localtitude =
+ record.localtitude > 4294967295
+ ? 4294967295
+ : record.localtitude < 0
+ ? 0
+ : record.localtitude;
// Size
record.locsize = getENotationInt(args.siz);
// Horizontal Precision
m_e = args.hp;
record.lochorizpre = getENotationInt(args.hp);
- // if (m_e != 0) {
- // } else {
- // record.lochorizpre = 22; // 10,000m default
- // }
+
// Vertical Precision
m_e = args.vp;
record.locvertpre = getENotationInt(args.vp);
- // if (m_e != 0) {
- // } else {
- // record.lochorizpre = 19; // 10m default
- // }
}
// LOC(name,d1,m1,s1,ns,d2,m2,s2,ew,alt,siz,hp,vp, recordModifiers...)
@@ -730,11 +843,69 @@ var LOC = recordBuilder('LOC', {
['vp', _.isNumber], // vertical precision
],
transform: function (record, args, modifiers) {
+ validateIntegers(args);
+
record = locStringBuilder(record, args);
record = locDMSBuilder(record, args);
},
});
+// Post-validation function for LOC that checks if degrees and minutes are integers
+function validateIntegers(args) {
+ if (args.d1 % 1 !== 0) {
+ throw (
+ "Degrees N/S shall be an integer: record '" +
+ args.name +
+ "': *" +
+ args.d1 +
+ '*, ' +
+ args.m1 +
+ ', ' +
+ args.s1 +
+ ', ...'
+ );
+ }
+ if (args.m1 % 1 !== 0) {
+ throw (
+ "Minutes N/S shall be an integer: record '" +
+ args.name +
+ "': " +
+ args.d1 +
+ ', *' +
+ args.m1 +
+ '*, ' +
+ args.s1 +
+ ', ...'
+ );
+ }
+ if (args.d2 % 1 !== 0) {
+ throw (
+ "Degrees E/W shall be an integer: record '" +
+ args.name +
+ "': *" +
+ args.d2 +
+ '*, ' +
+ args.m2 +
+ ', ' +
+ args.s2 +
+ ', ...'
+ );
+ }
+ if (args.m2 % 1 !== 0) {
+ throw (
+ "Minutes E/W shall be an integer: record '" +
+ args.name +
+ "': " +
+ args.d2 +
+ ', *' +
+ args.m2 +
+ '*, ' +
+ args.s2 +
+ ', ...'
+ );
+ }
+}
+
function ConvertDDToDMS(D, longitude) {
//stackoverflow, baby. do not re-order the rows.
return {
@@ -853,14 +1024,14 @@ function IGNORE(labelPattern, rtypePattern, targetPattern) {
// IGNORE_NAME(name, rTypes)
function IGNORE_NAME(name, rTypes) {
- return IGNORE(name, rTypes)
+ return IGNORE(name, rTypes);
}
function IGNORE_TARGET(target, rType) {
- return IGNORE("*", rType, target)
+ return IGNORE('*', rType, target);
}
-// IMPORT_TRANSFORM(translation_table, domain)
+// IMPORT_TRANSFORM(translation_table, domain, ttl)
var IMPORT_TRANSFORM = recordBuilder('IMPORT_TRANSFORM', {
args: [['translation_table'], ['domain'], ['ttl', _.isNumber]],
transform: function (record, args, modifiers) {
@@ -871,6 +1042,23 @@ var IMPORT_TRANSFORM = recordBuilder('IMPORT_TRANSFORM', {
},
});
+// IMPORT_TRANSFORM_STRIP(translation_table, domain, ttl, suffixstrip)
+var IMPORT_TRANSFORM_STRIP = recordBuilder('IMPORT_TRANSFORM', {
+ args: [
+ ['translation_table'],
+ ['domain'],
+ ['ttl', _.isNumber],
+ ['suffixstrip'],
+ ],
+ transform: function (record, args, modifiers) {
+ record.name = '@';
+ record.target = args.domain;
+ record.meta['transform_table'] = format_tt(args.translation_table);
+ record.ttl = args.ttl;
+ record.meta['transform_suffixstrip'] = args.suffixstrip;
+ },
+});
+
// PURGE()
function PURGE(d) {
d.KeepUnknown = false;
@@ -1028,6 +1216,7 @@ function recordBuilder(type, opts) {
// Handle D_EXTEND() with subdomains.
if (
d.subdomain &&
+ record.type != 'CF_SINGLE_REDIRECT' &&
record.type != 'CF_REDIRECT' &&
record.type != 'CF_TEMP_REDIRECT' &&
record.type != 'CF_WORKER_ROUTE'
@@ -1189,6 +1378,7 @@ var URL301 = recordBuilder('URL301');
var FRAME = recordBuilder('FRAME');
var NS1_URLFWD = recordBuilder('NS1_URLFWD');
var CLOUDNS_WR = recordBuilder('CLOUDNS_WR');
+var PORKBUN_URLFWD = recordBuilder('PORKBUN_URLFWD');
// LOC_BUILDER_DD takes an object:
// label: The DNS label for the LOC record. (default: '@')
@@ -1434,6 +1624,7 @@ function SPF_BUILDER(value) {
// iodef_critical: Boolean if sending report is required/critical. If not supported, certificate should be refused. (optional)
// issue: List of CAs which are allowed to issue certificates for the domain (creates one record for each).
// issuewild: Allowed CAs which can issue wildcard certificates for this domain. (creates one record for each)
+// ttl: The time for TTL, integer or string. (default: not defined, using DefaultTTL)
function CAA_BUILDER(value) {
if (!value.label) {
@@ -1453,23 +1644,41 @@ function CAA_BUILDER(value) {
throw 'CAA_BUILDER requires at least one entry at issue or issuewild';
}
+ var CAA_TTL = function () {};
+ if (value.ttl) {
+ CAA_TTL = TTL(value.ttl);
+ }
r = []; // The list of records to return.
if (value.iodef) {
if (value.iodef_critical) {
- r.push(CAA(value.label, 'iodef', value.iodef, CAA_CRITICAL));
+ r.push(
+ CAA(value.label, 'iodef', value.iodef, CAA_CRITICAL, CAA_TTL)
+ );
} else {
- r.push(CAA(value.label, 'iodef', value.iodef));
+ r.push(CAA(value.label, 'iodef', value.iodef, CAA_TTL));
}
}
- if (value.issue)
+ if (value.issue) {
+ var flag = function () {};
+ if (value.issue_critical) {
+ flag = CAA_CRITICAL;
+ }
for (var i = 0, len = value.issue.length; i < len; i++)
- r.push(CAA(value.label, 'issue', value.issue[i]));
+ r.push(CAA(value.label, 'issue', value.issue[i], flag, CAA_TTL));
+ }
- if (value.issuewild)
+ if (value.issuewild) {
+ var flag = function () {};
+ if (value.issuewild_critical) {
+ flag = CAA_CRITICAL;
+ }
for (var i = 0, len = value.issuewild.length; i < len; i++)
- r.push(CAA(value.label, 'issuewild', value.issuewild[i]));
+ r.push(
+ CAA(value.label, 'issuewild', value.issuewild[i], flag, CAA_TTL)
+ );
+ }
return r;
}
@@ -1658,7 +1867,7 @@ function M365_BUILDER(name, value) {
);
}
- value.domainGUID = name.replace('.', '-');
+ value.domainGUID = name.replace(/\./g, '-');
}
if (value.dkim && !value.initialDomain) {
@@ -1668,7 +1877,7 @@ function M365_BUILDER(name, value) {
);
}
- r = [];
+ var r = [];
// MX (default: true)
if (value.mx) {
@@ -1827,3 +2036,70 @@ var DISABLE_REPEATED_DOMAIN_CHECK = { skip_fqdn_check: 'true' };
// D("bar.com", ...
// A("foo.bar.com", "10.1.1.1", DISABLE_REPEATED_DOMAIN_CHECK),
// )
+
+// ============================================================
+
+// RTYPES
+
+// Background:
+// Old-style commands: Commands built using recordBuild() are the original
+// style. They all validation and pre-processing here in helpers.js. This
+// seemed like a good idea at the time, but in hindsight it puts a burden on the
+// developer to know both Javascript and go.
+
+// New-style commands: Command built using rawrecordBuilder() are the new style.
+// They simply pack up the arguments listed in dnsconfig.js and store them in
+// .rawrecords. This is passed to the Go code, which is responsible for all
+// validation, pre-processing, etc. The benefit is this minimizes the need for
+// Javascript knowledge, and allows us to use the testing platform build into
+// Go.
+
+function rawrecordBuilder(type) {
+ return function () {
+ // Copy the raw args:
+ var rawArgs = [];
+ for (var i = 0; i < arguments.length; i++) {
+ rawArgs.push(arguments[i]);
+ }
+
+ return function (d) {
+ var record = {
+ type: type,
+ };
+
+ // Process the args: Functions are executed, objects are assumed to
+ // be meta and stored, strings are assumed to be args and are
+ // stored.
+ // NB(tlim): Allowing for the intermixing of args and meta seems
+ // bad. It might be better to simply preserve the first n items as
+ // args, then assume the rest are metas. That would be more similar
+ // to the old style functions. However at this time I can't think of
+ // a reason this isn't sufficient.
+ var processedArgs = [];
+ var processedMetas = [];
+ for (var i = 0; i < rawArgs.length; i++) {
+ var r = rawArgs[i];
+ if (_.isFunction(r)) {
+ r(record);
+ } else if (_.isObject(r)) {
+ processedMetas.push(r);
+ } else {
+ processedArgs.push(r);
+ }
+ }
+ // Store the processed args.
+ record.args = processedArgs;
+ record.metas = processedMetas;
+
+ // Add this raw record to the list of records.
+ d.rawrecords.push(record);
+
+ return record;
+ };
+ };
+}
+
+// PLEASE KEEP THIS LIST ALPHABETICAL!
+
+// CLOUDFLAREAPI:
+var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT');
diff --git a/pkg/js/js.go b/pkg/js/js.go
index c2e5f49ab4..06d9a7d300 100644
--- a/pkg/js/js.go
+++ b/pkg/js/js.go
@@ -11,6 +11,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
"github.com/StackExchange/dnscontrol/v4/pkg/transform"
"github.com/robertkrimen/otto" // load underscore js into vm by default
_ "github.com/robertkrimen/otto/underscore" // required by otto
@@ -70,8 +71,10 @@ func ExecuteJavascriptString(script []byte, devMode bool, variables map[string]s
vm.Set("require", require)
vm.Set("REV", reverse)
+ vm.Set("REVCOMPAT", reverseCompat)
vm.Set("glob", listFiles) // used for require_glob()
vm.Set("PANIC", jsPanic)
+ vm.Set("HASH", hashFunc)
// add cli variables to otto
for key, value := range variables {
@@ -155,7 +158,8 @@ func require(call otto.FunctionCall) otto.Value {
var value = otto.TrueValue()
// If its a json file return the json value, else default to true
- if strings.HasSuffix(filepath.Ext(relFile), "json") {
+ var ext = strings.ToLower(filepath.Ext(relFile))
+ if strings.HasSuffix(ext, "json") || strings.HasSuffix(ext, "json5") {
cmd := fmt.Sprintf(`JSON.parse(JSON.stringify(%s))`, string(data))
value, err = call.Otto.Run(cmd)
} else {
@@ -290,3 +294,16 @@ func reverse(call otto.FunctionCall) otto.Value {
v, _ := otto.ToValue(rev)
return v
}
+
+func reverseCompat(call otto.FunctionCall) otto.Value {
+ if len(call.ArgumentList) != 1 {
+ throw(call.Otto, "REVCOMPAT takes exactly one argument")
+ }
+ dom := call.Argument(0).String()
+ err := rfc4183.SetCompatibilityMode(dom)
+ if err != nil {
+ throw(call.Otto, err.Error())
+ }
+ v, _ := otto.ToValue(nil)
+ return v
+}
diff --git a/pkg/js/js_test.go b/pkg/js/js_test.go
index 9b1ef438ad..0bd35a11be 100644
--- a/pkg/js/js_test.go
+++ b/pkg/js/js_test.go
@@ -86,7 +86,7 @@ func TestParsedFiles(t *testing.T) {
as := string(actualJSON)
_, _ = es, as
// When debugging, leave behind the actual result:
- //os.WriteFile(expectedFile+".ACTUAL", []byte(as), 0644)
+ os.WriteFile(expectedFile+".ACTUAL", []byte(as), 0644) // Leave behind the actual result:
testifyrequire.JSONEqf(t, es, as, "EXPECTING %q = \n```\n%s\n```", expectedFile, as)
// For each domain, if there is a zone file, test against it:
@@ -141,6 +141,7 @@ func TestErrors(t *testing.T) {
{"Bad cidr", `D(reverse("foo.com"), "reg")`},
{"Dup domains", `D("example.org", "reg"); D("example.org", "reg")`},
{"Bad NAMESERVER", `D("example.com","reg", NAMESERVER("@","ns1.foo.com."))`},
+ {"Bad Hash function", `D(HASH("123", "abc"),"reg")`},
}
for _, tst := range tests {
t.Run(tst.desc, func(t *testing.T) {
diff --git a/pkg/js/parse_tests/007-importTransformTTL.js b/pkg/js/parse_tests/007-importTransformTTL.js
old mode 100644
new mode 100755
index 33e8bec76b..0919db1595
--- a/pkg/js/parse_tests/007-importTransformTTL.js
+++ b/pkg/js/parse_tests/007-importTransformTTL.js
@@ -1,5 +1,25 @@
-var TRANSFORM_INT = [
- {low: "0.0.0.0", high: "1.1.1.1", newBase: "2.2.2.2" }
-]
-D("foo2.com","reg");
-D("foo.com","reg",IMPORT_TRANSFORM(TRANSFORM_INT,"foo2.com",60))
+var TRANSFORM_NEWIP = [{
+ low: "0.0.0.0",
+ high: "1.1.1.1",
+ newIP: "2.2.2.2"
+}];
+var TRANSFORM_BASE = [{
+ low: "0.0.0.0",
+ high: "1.1.1.1",
+ newBase: "4.4.4.4"
+}, {
+ low: "7.7.7.7",
+ high: "8.8.8.8",
+ newBase: "9.9.9.9"
+},
+];
+
+D("foo1.com", "reg");
+
+D("foo2.com", "reg",
+ IMPORT_TRANSFORM(TRANSFORM_BASE, "foo1.com", 60)
+);
+
+D("foo3.com", "reg",
+ IMPORT_TRANSFORM_STRIP(TRANSFORM_NEWIP, "foo1.com", 99, ".com")
+);
diff --git a/pkg/js/parse_tests/007-importTransformTTL.json b/pkg/js/parse_tests/007-importTransformTTL.json
index 6ae0a67fec..cef9e0c349 100644
--- a/pkg/js/parse_tests/007-importTransformTTL.json
+++ b/pkg/js/parse_tests/007-importTransformTTL.json
@@ -1,28 +1,45 @@
{
+ "registrars": [],
"dns_providers": [],
"domains": [
{
+ "name": "foo1.com",
+ "registrar": "reg",
"dnsProviders": {},
- "name": "foo2.com",
- "records": [],
- "registrar": "reg"
+ "records": []
},
{
+ "name": "foo2.com",
+ "registrar": "reg",
"dnsProviders": {},
- "name": "foo.com",
"records": [
{
+ "type": "IMPORT_TRANSFORM",
+ "name": "@",
+ "ttl": 60,
"meta": {
- "transform_table": "0.0.0.0 ~ 1.1.1.1 ~ 2.2.2.2 ~ "
+ "transform_table": "0.0.0.0 ~ 1.1.1.1 ~ 4.4.4.4 ~ ; 7.7.7.7 ~ 8.8.8.8 ~ 9.9.9.9 ~ "
},
+ "target": "foo1.com"
+ }
+ ]
+ },
+ {
+ "name": "foo3.com",
+ "registrar": "reg",
+ "dnsProviders": {},
+ "records": [
+ {
+ "type": "IMPORT_TRANSFORM",
"name": "@",
- "target": "foo2.com",
- "ttl": 60,
- "type": "IMPORT_TRANSFORM"
+ "ttl": 99,
+ "meta": {
+ "transform_suffixstrip": ".com",
+ "transform_table": "0.0.0.0 ~ 1.1.1.1 ~ ~ 2.2.2.2"
+ },
+ "target": "foo1.com"
}
- ],
- "registrar": "reg"
+ ]
}
- ],
- "registrars": []
-}
+ ]
+}
\ No newline at end of file
diff --git a/pkg/js/parse_tests/016-backslash.js b/pkg/js/parse_tests/016-backslash.js
deleted file mode 100644
index 4e88f7167c..0000000000
--- a/pkg/js/parse_tests/016-backslash.js
+++ /dev/null
@@ -1,13 +0,0 @@
-dmarc = [
- "v=DMARC1\\;",
- 'p=reject\\;',
- 'sp=reject\\;',
- 'pct=100\\;',
- 'rua=mailto:xx...@yyyy.com\\;',
- 'ruf=mailto:xx...@yyyy.com\\;',
- 'fo=1'
- ].join(' ');
-
-D("foo.com","none",
- TXT('_dmarc', dmarc, TTL(300))
-);
diff --git a/pkg/js/parse_tests/016-backslash.json b/pkg/js/parse_tests/016-backslash.json
deleted file mode 100644
index 2b90a7ae30..0000000000
--- a/pkg/js/parse_tests/016-backslash.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "registrars": [],
- "dns_providers": [],
- "domains": [
- {
- "name": "foo.com",
- "registrar": "none",
- "dnsProviders": {},
- "records": [
- {
- "type": "TXT",
- "name": "_dmarc",
- "target": "v=DMARC1\\; p=reject\\; sp=reject\\; pct=100\\; rua=mailto:xx...@yyyy.com\\; ruf=mailto:xx...@yyyy.com\\; fo=1",
- "ttl": 300,
- "txtstrings": [
- "v=DMARC1\\; p=reject\\; sp=reject\\; pct=100\\; rua=mailto:xx...@yyyy.com\\; ruf=mailto:xx...@yyyy.com\\; fo=1"
- ]
- }
- ]
- }
- ]
-}
diff --git a/pkg/js/parse_tests/016-backslash/foo.com.zone b/pkg/js/parse_tests/016-backslash/foo.com.zone
deleted file mode 100644
index 3271869c3e..0000000000
--- a/pkg/js/parse_tests/016-backslash/foo.com.zone
+++ /dev/null
@@ -1,2 +0,0 @@
-$TTL 300
-_dmarc IN TXT "v=DMARC1; p=reject; sp=reject; pct=100; rua=mailto:xx...@yyyy.com; ruf=mailto:xx...@yyyy.com; fo=1"
diff --git a/pkg/js/parse_tests/017-txt.json b/pkg/js/parse_tests/017-txt.json
index fd509360cb..1a14e45786 100644
--- a/pkg/js/parse_tests/017-txt.json
+++ b/pkg/js/parse_tests/017-txt.json
@@ -10,45 +10,27 @@
{
"type": "TXT",
"name": "a",
- "target": "simple",
- "txtstrings": [
- "simple"
- ]
+ "target": "simple"
},
{
"type": "TXT",
"name": "b",
- "target": "ws at end ",
- "txtstrings": [
- "ws at end "
- ]
+ "target": "ws at end "
},
{
"type": "TXT",
"name": "c",
- "target": "one",
- "txtstrings": [
- "one"
- ]
+ "target": "one"
},
{
"type": "TXT",
"name": "d",
- "target": "bonieclyde",
- "txtstrings": [
- "bonie",
- "clyde"
- ]
+ "target": "bonieclyde"
},
{
"type": "TXT",
"name": "e",
- "target": "strawwoodbrick",
- "txtstrings": [
- "straw",
- "wood",
- "brick"
- ]
+ "target": "strawwoodbrick"
}
]
}
diff --git a/pkg/js/parse_tests/017-txt/foo.com.zone b/pkg/js/parse_tests/017-txt/foo.com.zone
index 9fe37d9887..a31a9e3ce4 100644
--- a/pkg/js/parse_tests/017-txt/foo.com.zone
+++ b/pkg/js/parse_tests/017-txt/foo.com.zone
@@ -2,5 +2,5 @@ $TTL 300
a IN TXT "simple"
b IN TXT "ws at end "
c IN TXT "one"
-d IN TXT "bonie" "clyde"
-e IN TXT "straw" "wood" "brick"
+d IN TXT "bonieclyde"
+e IN TXT "strawwoodbrick"
diff --git a/pkg/js/parse_tests/018-dkim.json b/pkg/js/parse_tests/018-dkim.json
index c7bd2aff82..3f7dd6a126 100644
--- a/pkg/js/parse_tests/018-dkim.json
+++ b/pkg/js/parse_tests/018-dkim.json
@@ -10,10 +10,7 @@
{
"type": "TXT",
"name": "dkimtest2",
- "target": "this string is 255 bytes long.hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKZogtjOlHoeY8iZ5o5brlPOsj/a2Q9Bopu1kHxlxrdw7tZVL9FzUMngiIYGrl8dbP7Rvk7TLMoxHxVkRZPBtIpsKIab/gOUoPLQVYbrAmzyguHYBwAApi3H/pvjUsK8+XF0dKY17AR96lokAPqvfBaUb+DSx8zNw2hrYWYVqvCtnxHUGEUhT1bTlEZBptH3jthis is the remainder. it is 156 bytes long.mOhl2JmbsFKy+RoMTwbkk0/meRvcEFWLHkr4MSgbnie6OpQvM4Y51+kO6DUVr3rwjrdVO9wpFt+n/hdQ92TNif17RMJtE5AGaQ6BN3yJQIDAQAB;",
- "txtstrings": [
- "this string is 255 bytes long.hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKZogtjOlHoeY8iZ5o5brlPOsj/a2Q9Bopu1kHxlxrdw7tZVL9FzUMngiIYGrl8dbP7Rvk7TLMoxHxVkRZPBtIpsKIab/gOUoPLQVYbrAmzyguHYBwAApi3H/pvjUsK8+XF0dKY17AR96lokAPqvfBaUb+DSx8zNw2hrYWYVqvCtnxHUGEUhT1bTlEZBptH3jthis is the remainder. it is 156 bytes long.mOhl2JmbsFKy+RoMTwbkk0/meRvcEFWLHkr4MSgbnie6OpQvM4Y51+kO6DUVr3rwjrdVO9wpFt+n/hdQ92TNif17RMJtE5AGaQ6BN3yJQIDAQAB;"
- ]
+ "target": "this string is 255 bytes long.hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKZogtjOlHoeY8iZ5o5brlPOsj/a2Q9Bopu1kHxlxrdw7tZVL9FzUMngiIYGrl8dbP7Rvk7TLMoxHxVkRZPBtIpsKIab/gOUoPLQVYbrAmzyguHYBwAApi3H/pvjUsK8+XF0dKY17AR96lokAPqvfBaUb+DSx8zNw2hrYWYVqvCtnxHUGEUhT1bTlEZBptH3jthis is the remainder. it is 156 bytes long.mOhl2JmbsFKy+RoMTwbkk0/meRvcEFWLHkr4MSgbnie6OpQvM4Y51+kO6DUVr3rwjrdVO9wpFt+n/hdQ92TNif17RMJtE5AGaQ6BN3yJQIDAQAB;"
}
]
}
diff --git a/pkg/js/parse_tests/018-dkim/foo.com.zone b/pkg/js/parse_tests/018-dkim/foo.com.zone
index fb1f901012..f8df5c84f5 100644
--- a/pkg/js/parse_tests/018-dkim/foo.com.zone
+++ b/pkg/js/parse_tests/018-dkim/foo.com.zone
@@ -1,2 +1,2 @@
$TTL 300
-dkimtest2 IN TXT "this string is 255 bytes long.hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKZogtjOlHoeY8iZ5o5brlPOsj/a2Q9Bopu1kHxlxrdw7tZVL9FzUMngiIYGrl8dbP7Rvk7TLMoxHxVkRZPBtIpsKIab/gOUoPLQVYbrAmzyguHYBwAApi3H/pvjUsK8+XF0dKY17AR96lokAPqvfBaUb+DSx8zNw2hrYWYVqvCtnxHUGEUhT1bTlEZBptH3jthis is the remainder. it is 156 bytes long.mOhl2JmbsFKy+RoMTwbkk0/meRvcEFWLHkr4MSgbnie6OpQvM4Y51+kO6DUVr3rwjrdVO9wpFt+n/hdQ92TNif17RMJtE5AGaQ6BN3yJQIDAQAB;"
+dkimtest2 IN TXT "this string is 255 bytes long.hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKZogtjOlHoeY8iZ5o5brlPOsj/a2Q9Bopu1kHxlxrdw7tZVL9FzUMngiIYGrl8dbP7Rvk7TLMoxHxVkRZPBtIpsKIab/gOUoPLQVYbrAmzyguHYBwAApi3H/pvjUsK8+XF0dKY17AR96lokAPqvfBaUb+DSx8zNw2hrYWYVqvCtnxHUGEUhT1bTlEZBptH3j" "this is the remainder. it is 156 bytes long.mOhl2JmbsFKy+RoMTwbkk0/meRvcEFWLHkr4MSgbnie6OpQvM4Y51+kO6DUVr3rwjrdVO9wpFt+n/hdQ92TNif17RMJtE5AGaQ6BN3yJQIDAQAB;"
diff --git a/pkg/js/parse_tests/019-r53-alias.js b/pkg/js/parse_tests/019-r53-alias.js
index 9fae124639..6c3f55a5e1 100644
--- a/pkg/js/parse_tests/019-r53-alias.js
+++ b/pkg/js/parse_tests/019-r53-alias.js
@@ -2,6 +2,7 @@ D("foo.com", "none",
R53_ALIAS("mxtest", "MX", "foo.com."),
R53_ALIAS("atest", "A", "foo.com."),
R53_ALIAS("atest", "A", "foo.com.", R53_ZONE("Z2FTEDLFRTF")),
+ R53_ALIAS("aevaltargethealthtest", "A", "foo.com.", R53_EVALUATE_TARGET_HEALTH(true)),
R53_ALIAS("aaaatest", "AAAA", "foo.com."),
R53_ALIAS("aaaatest", "AAAA", "foo.com.", R53_ZONE("ERERTFGFGF")),
R53_ALIAS("cnametest", "CNAME", "foo.com."),
diff --git a/pkg/js/parse_tests/019-r53-alias.json b/pkg/js/parse_tests/019-r53-alias.json
index 8e91cec69b..40136a072c 100644
--- a/pkg/js/parse_tests/019-r53-alias.json
+++ b/pkg/js/parse_tests/019-r53-alias.json
@@ -12,7 +12,8 @@
"name": "mxtest",
"target": "foo.com.",
"r53_alias": {
- "type": "MX"
+ "type": "MX",
+ "evaluate_target_health": "false"
}
},
{
@@ -20,7 +21,8 @@
"name": "atest",
"target": "foo.com.",
"r53_alias": {
- "type": "A"
+ "type": "A",
+ "evaluate_target_health": "false"
}
},
{
@@ -29,7 +31,17 @@
"target": "foo.com.",
"r53_alias": {
"type": "A",
- "zone_id": "Z2FTEDLFRTF"
+ "zone_id": "Z2FTEDLFRTF",
+ "evaluate_target_health": "false"
+ }
+ },
+ {
+ "type": "R53_ALIAS",
+ "name": "aevaltargethealthtest",
+ "target": "foo.com.",
+ "r53_alias": {
+ "type": "A",
+ "evaluate_target_health": "true"
}
},
{
@@ -37,7 +49,8 @@
"name": "aaaatest",
"target": "foo.com.",
"r53_alias": {
- "type": "AAAA"
+ "type": "AAAA",
+ "evaluate_target_health": "false"
}
},
{
@@ -46,7 +59,8 @@
"target": "foo.com.",
"r53_alias": {
"type": "AAAA",
- "zone_id": "ERERTFGFGF"
+ "zone_id": "ERERTFGFGF",
+ "evaluate_target_health": "false"
}
},
{
@@ -54,7 +68,8 @@
"name": "cnametest",
"target": "foo.com.",
"r53_alias": {
- "type": "CNAME"
+ "type": "CNAME",
+ "evaluate_target_health": "false"
}
},
{
@@ -62,7 +77,8 @@
"name": "ptrtest",
"target": "foo.com.",
"r53_alias": {
- "type": "PTR"
+ "type": "PTR",
+ "evaluate_target_health": "false"
}
},
{
@@ -70,7 +86,8 @@
"name": "txttest",
"target": "foo.com.",
"r53_alias": {
- "type": "TXT"
+ "type": "TXT",
+ "evaluate_target_health": "false"
}
},
{
@@ -78,7 +95,8 @@
"name": "srvtest",
"target": "foo.com.",
"r53_alias": {
- "type": "SRV"
+ "type": "SRV",
+ "evaluate_target_health": "false"
}
},
{
@@ -86,7 +104,8 @@
"name": "spftest",
"target": "foo.com.",
"r53_alias": {
- "type": "SPF"
+ "type": "SPF",
+ "evaluate_target_health": "false"
}
},
{
@@ -94,7 +113,8 @@
"name": "caatest",
"target": "foo.com.",
"r53_alias": {
- "type": "CAA"
+ "type": "CAA",
+ "evaluate_target_health": "false"
}
},
{
@@ -102,7 +122,8 @@
"name": "naptrtest",
"target": "foo.com.",
"r53_alias": {
- "type": "NAPTR"
+ "type": "NAPTR",
+ "evaluate_target_health": "false"
}
}
]
diff --git a/pkg/js/parse_tests/030-dextenddoc.js b/pkg/js/parse_tests/030-dextenddoc.js
index 64e4ca06f1..56f43e4cd6 100644
--- a/pkg/js/parse_tests/030-dextenddoc.js
+++ b/pkg/js/parse_tests/030-dextenddoc.js
@@ -1,7 +1,7 @@
var REG = NewRegistrar("Third-Party", "NONE");
var DNS = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
-// The example from docs/functions/global/D_EXTEND.md
+// The example from documentation/language-reference/top-level-functions/D_EXTEND.md
D("domain.tld", REG, DnsProvider(DNS),
A("@", "127.0.0.1"), // domain.tld
diff --git a/pkg/js/parse_tests/040-r53-zone.json b/pkg/js/parse_tests/040-r53-zone.json
index 08ab64f213..96f2fae7c1 100644
--- a/pkg/js/parse_tests/040-r53-zone.json
+++ b/pkg/js/parse_tests/040-r53-zone.json
@@ -21,7 +21,8 @@
"name": "atest",
"r53_alias": {
"type": "A",
- "zone_id": "Z2FTEDLFRTZ"
+ "zone_id": "Z2FTEDLFRTZ",
+ "evaluate_target_health": "false"
},
"target": "foo.com."
}
diff --git a/pkg/js/parse_tests/045-loc.js b/pkg/js/parse_tests/045-loc.js
index 65103dce0a..579ef1416f 100644
--- a/pkg/js/parse_tests/045-loc.js
+++ b/pkg/js/parse_tests/045-loc.js
@@ -1,10 +1,23 @@
D("foo.com","none"
// LOC "subdomain", d1, m1, s1, "[NnSs]", d2, m2, s2, "[EeWw]", alt, siz, hp, vp)
, LOC("@", 42, 21, 54, "N", 71, 6, 18, "W", -24, 30, 0, 0) //42 21 54 N 71 06 18 W -24m 30m
- , LOC("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24, 1, 200, 10) //42 21 43.952 N 71 5 6.344 W -24m 1m 200m
- , LOC("b", 52, 14, 5, "N", 0, 8, 50, "E", 10, 0, 0, 0) //52 14 05 N 00 08 50 E 10m
+ , LOC("a", 42, 21, 43.952, "N", 71, 5, 6.344, "W", -24.01, 1, 200, 10) //42 21 43.952 N 71 5 6.344 W -24.01m 1m 200m
+ , LOC("b", 52, 14, 5, "N", 0, 8, 50, "E", 10.33, 0, 0, 0) //52 14 05 N 00 08 50 E 10.33m
, LOC("c", 32, 7, 19, "S",116, 2, 25, "E", 10, 0, 0, 0) //32 7 19 S 116 2 25 E 10m
, LOC("d", 42, 21, 28.764, "N", 71, 0, 51.617, "W", -44, 2000, 0, 0) //42 21 28.764 N 71 00 51.617 W -44m 2000m
+ , LOC("d-alt-highest", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 42849672.95, 2000, 0, 0) //42 21 28.764 N 71 00 51.617 W 42849672.95m 2000m
+ , LOC("d-alt-lowest", 42, 21, 28.764, "N", 71, 0, 51.617, "W", -100000.00, 2000, 0, 0) //42 21 28.764 N 71 00 51.617 W -100000.00m 2000m
+ , LOC("d-alt-toohigh", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 42849672.96, 2000, 0, 0) //42 21 28.764 N 71 00 51.617 W 42849672.95m 2000m
+ , LOC("d-alt-toolow", 42, 21, 28.764, "N", 71, 0, 51.617, "W", -100000.01, 2000, 0, 0) //42 21 28.764 N 71 00 51.617 W -100000m 2000m
+ , LOC("d-horizprecision-hi", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 1, 90000000, 0) //42 21 28.764 N 71 00 51.617 W 0m 1m 90000000m 0.00m
+ , LOC("d-horizprecision-toohi", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 1, 98765432, 0) //42 21 28.764 N 71 00 51.617 W 0m 1m 90000000m 0.00m
+ , LOC("d-horizprecision-toolow", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 1, -1, 0) //42 21 28.764 N 71 00 51.617 W 0m 1m 0m 0.00m
+ , LOC("d-size-toohi", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 98765432, 0, 0) //42 21 28.764 N 71 00 51.617 W -44m 2000m
+ , LOC("d-size-toolow", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, -1, 0, 0) //42 21 28.764 N 71 00 51.617 W -44m 0m
+ , LOC("d-size-hi", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 90000000, 0, 0) //42 21 28.764 N 71 00 51.617 W -44m 90000000m
+ , LOC("d-vertprecision-hi", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 1, 0, 90000000) //42 21 28.764 N 71 00 51.617 W 0m 1m 0.00m 90000000m
+ , LOC("d-vertprecision-toohi", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 1, 0, 98765432) //42 21 28.764 N 71 00 51.617 W 0m 1m 0.00m 90000000m
+ , LOC("d-vertprecision-toolow", 42, 21, 28.764, "N", 71, 0, 51.617, "W", 0, 1, 0, -1) //42 21 28.764 N 71 00 51.617 W 0m 1m 0.00m 0m
// via the Decimal degrees to LOC builder.
, LOC_BUILDER_DD({
diff --git a/pkg/js/parse_tests/045-loc.json b/pkg/js/parse_tests/045-loc.json
index 68db2435de..70f1519bf5 100644
--- a/pkg/js/parse_tests/045-loc.json
+++ b/pkg/js/parse_tests/045-loc.json
@@ -25,16 +25,16 @@
"locvertpre": 19,
"loclatitude": 2299987600,
"loclongitude": 1891577304,
- "localtitude": 9997600,
- "target": "42 21 43.952 N 71 5 6.344 W -24m 1m 200m 10m"
+ "localtitude": 9997599,
+ "target": "42 21 43.952 N 71 5 6.344 W -24.01m 1m 200m 10m"
},
{
"type": "LOC",
"name": "b",
"loclatitude": 2335528648,
"loclongitude": 2148013648,
- "localtitude": 10001000,
- "target": "52 14 5 N 0 8 50 E 10m 0m 0m 0m"
+ "localtitude": 10001033,
+ "target": "52 14 5 N 0 8 50 E 10.33m 0m 0m 0m"
},
{
"type": "LOC",
@@ -53,6 +53,124 @@
"localtitude": 9995600,
"target": "42 21 28.764 N 71 0 51.617 W -44m 2000m 0m 0m"
},
+ {
+ "type": "LOC",
+ "name": "d-alt-highest",
+ "locsize": 37,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 4294967295,
+ "target": "42 21 28.764 N 71 0 51.617 W 42849672.95m 2000m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-alt-lowest",
+ "locsize": 37,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "target": "42 21 28.764 N 71 0 51.617 W -100000m 2000m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-alt-toohigh",
+ "locsize": 37,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 4294967295,
+ "target": "42 21 28.764 N 71 0 51.617 W 42849672.95m 2000m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-alt-toolow",
+ "locsize": 37,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "target": "42 21 28.764 N 71 0 51.617 W -100000m 2000m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-horizprecision-hi",
+ "locsize": 18,
+ "lochorizpre": 153,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 1m 90000000m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-horizprecision-toohi",
+ "locsize": 18,
+ "lochorizpre": 153,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 1m 90000000m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-horizprecision-toolow",
+ "locsize": 18,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 1m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-size-toohi",
+ "locsize": 153,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 90000000m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-size-toolow",
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 0m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-size-hi",
+ "locsize": 153,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 90000000m 0m 0m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-vertprecision-hi",
+ "locsize": 18,
+ "locvertpre": 153,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 1m 0m 90000000m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-vertprecision-toohi",
+ "locsize": 18,
+ "locvertpre": 153,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 1m 0m 90000000m"
+ },
+ {
+ "type": "LOC",
+ "name": "d-vertprecision-toolow",
+ "locsize": 18,
+ "loclatitude": 2299972412,
+ "loclongitude": 1891832031,
+ "localtitude": 10000000,
+ "target": "42 21 28.764 N 71 0 51.617 W 0m 1m 0m 0m"
+ },
{
"type": "LOC",
"name": "big-ben",
diff --git a/pkg/js/parse_tests/045-loc/foo.com.zone b/pkg/js/parse_tests/045-loc/foo.com.zone
index 1e0c3ac97d..1000171ba4 100644
--- a/pkg/js/parse_tests/045-loc/foo.com.zone
+++ b/pkg/js/parse_tests/045-loc/foo.com.zone
@@ -1,10 +1,23 @@
$TTL 300
@ IN LOC 42 21 54.000 N 71 06 18.000 W -24m 30m 0.00m 0.00m
-a IN LOC 42 21 43.952 N 71 05 6.344 W -24m 1m 200m 10m
-b IN LOC 52 14 5.000 N 00 08 50.000 E 10m 0.00m 0.00m 0.00m
+a IN LOC 42 21 43.952 N 71 05 6.344 W -24.01m 1m 200m 10m
+b IN LOC 52 14 5.000 N 00 08 50.000 E 10.33m 0.00m 0.00m 0.00m
big-ben IN LOC 51 30 3.033 N 00 07 28.651 W 6m 0.00m 0.00m 0.00m
c IN LOC 32 07 19.000 S 116 02 25.000 E 10m 0.00m 0.00m 0.00m
d IN LOC 42 21 28.764 N 71 00 51.617 W -44m 2000m 0.00m 0.00m
+d-alt-highest IN LOC 42 21 28.764 N 71 00 51.617 W 42849672.95m 2000m 0.00m 0.00m
+d-alt-lowest IN LOC 42 21 28.764 N 71 00 51.617 W -100000m 2000m 0.00m 0.00m
+d-alt-toohigh IN LOC 42 21 28.764 N 71 00 51.617 W 42849672.95m 2000m 0.00m 0.00m
+d-alt-toolow IN LOC 42 21 28.764 N 71 00 51.617 W -100000m 2000m 0.00m 0.00m
+d-horizprecision-hi IN LOC 42 21 28.764 N 71 00 51.617 W 0m 1m 90000000m 0.00m
+d-horizprecision-toohi IN LOC 42 21 28.764 N 71 00 51.617 W 0m 1m 90000000m 0.00m
+d-horizprecision-toolow IN LOC 42 21 28.764 N 71 00 51.617 W 0m 1m 0.00m 0.00m
+d-size-hi IN LOC 42 21 28.764 N 71 00 51.617 W 0m 90000000m 0.00m 0.00m
+d-size-toohi IN LOC 42 21 28.764 N 71 00 51.617 W 0m 90000000m 0.00m 0.00m
+d-size-toolow IN LOC 42 21 28.764 N 71 00 51.617 W 0m 0.00m 0.00m 0.00m
+d-vertprecision-hi IN LOC 42 21 28.764 N 71 00 51.617 W 0m 1m 0.00m 90000000m
+d-vertprecision-toohi IN LOC 42 21 28.764 N 71 00 51.617 W 0m 1m 0.00m 90000000m
+d-vertprecision-toolow IN LOC 42 21 28.764 N 71 00 51.617 W 0m 1m 0.00m 0.00m
fraser-island IN LOC 25 14 24.000 S 153 09 0.000 E 3m 0.00m 0.00m 0.00m
guinness-brewery IN LOC 53 20 40.000 N 06 17 20.000 W 300m 0.00m 0.00m 0.00m
hawaii IN LOC 21 30 0.000 N 158 00 0.000 W 920m 0.00m 0.00m 0.00m
diff --git a/pkg/js/parse_tests/047-DNAME.js b/pkg/js/parse_tests/047-DNAME.js
new file mode 100644
index 0000000000..d7d3841908
--- /dev/null
+++ b/pkg/js/parse_tests/047-DNAME.js
@@ -0,0 +1,3 @@
+D("foo.com","none",
+ DNAME("@", "bar.com.")
+);
diff --git a/pkg/js/parse_tests/047-DNAME.json b/pkg/js/parse_tests/047-DNAME.json
new file mode 100644
index 0000000000..4e58bf294d
--- /dev/null
+++ b/pkg/js/parse_tests/047-DNAME.json
@@ -0,0 +1,18 @@
+{
+ "registrars": [],
+ "dns_providers": [],
+ "domains": [
+ {
+ "name": "foo.com",
+ "registrar": "none",
+ "dnsProviders": {},
+ "records": [
+ {
+ "type": "DNAME",
+ "name": "@",
+ "target": "bar.com."
+ }
+ ]
+ }
+ ]
+}
diff --git a/pkg/js/parse_tests/047-SVCB.js b/pkg/js/parse_tests/047-SVCB.js
new file mode 100644
index 0000000000..1a69a5de08
--- /dev/null
+++ b/pkg/js/parse_tests/047-SVCB.js
@@ -0,0 +1,4 @@
+D("foo.com","none",
+ SVCB("@", 1, ".", ""),
+ HTTPS("@", 2, ".", 'alpn="h3,h2" port=443 ipv4hint=123.123.123.123 ipv6hint=dead::beaf')
+);
diff --git a/pkg/js/parse_tests/047-SVCB.json b/pkg/js/parse_tests/047-SVCB.json
new file mode 100644
index 0000000000..a3aa5c2931
--- /dev/null
+++ b/pkg/js/parse_tests/047-SVCB.json
@@ -0,0 +1,27 @@
+{
+ "registrars": [],
+ "dns_providers": [],
+ "domains": [
+ {
+ "name": "foo.com",
+ "registrar": "none",
+ "dnsProviders": {},
+ "records": [
+ {
+ "type": "SVCB",
+ "name": "@",
+ "target": ".",
+ "svcpriority": 1
+ },
+
+ {
+ "type": "HTTPS",
+ "name": "@",
+ "svcparams": "alpn=\"h3,h2\" port=443 ipv4hint=123.123.123.123 ipv6hint=dead::beaf",
+ "svcpriority": 2,
+ "target": "."
+ }
+ ]
+ }
+ ]
+}
diff --git a/pkg/js/parse_tests/048-DNSKEY.js b/pkg/js/parse_tests/048-DNSKEY.js
new file mode 100644
index 0000000000..0f066011ae
--- /dev/null
+++ b/pkg/js/parse_tests/048-DNSKEY.js
@@ -0,0 +1,3 @@
+D("foo.com","none",
+ DNSKEY("@", 257, 3, 13, "AABBCCDD")
+);
diff --git a/pkg/js/parse_tests/048-DNSKEY.json b/pkg/js/parse_tests/048-DNSKEY.json
new file mode 100644
index 0000000000..52ebf43b9a
--- /dev/null
+++ b/pkg/js/parse_tests/048-DNSKEY.json
@@ -0,0 +1,22 @@
+{
+ "registrars": [],
+ "dns_providers": [],
+ "domains": [
+ {
+ "name": "foo.com",
+ "registrar": "none",
+ "dnsProviders": {},
+ "records": [
+ {
+ "type": "DNSKEY",
+ "name": "@",
+ "dnskeyflags": 257,
+ "dnskeyprotocol": 3,
+ "dnskeyalgorithm": 13,
+ "dnskeypublickey": "AABBCCDD",
+ "target": ""
+ }
+ ]
+ }
+ ]
+}
diff --git a/pkg/js/parse_tests/049-json5-require.js b/pkg/js/parse_tests/049-json5-require.js
new file mode 100644
index 0000000000..0c672f2ce0
--- /dev/null
+++ b/pkg/js/parse_tests/049-json5-require.js
@@ -0,0 +1,8 @@
+var domains = require('./domain-ip-map.json5')
+
+var domain = "foo.com"
+var ip = domains["foo.com"]
+
+D(domain,"none",
+ A("@",ip)
+);
diff --git a/pkg/js/parse_tests/049-json5-require.json b/pkg/js/parse_tests/049-json5-require.json
new file mode 100644
index 0000000000..9e9ea70183
--- /dev/null
+++ b/pkg/js/parse_tests/049-json5-require.json
@@ -0,0 +1,18 @@
+{
+ "registrars": [],
+ "dns_providers": [],
+ "domains": [
+ {
+ "name": "foo.com",
+ "registrar": "none",
+ "dnsProviders": {},
+ "records": [
+ {
+ "type": "A",
+ "name": "@",
+ "target": "1.1.1.1"
+ }
+ ]
+ }
+ ]
+}
diff --git a/pkg/js/parse_tests/050-cfSingleRedirect.js b/pkg/js/parse_tests/050-cfSingleRedirect.js
new file mode 100644
index 0000000000..6f221b66ac
--- /dev/null
+++ b/pkg/js/parse_tests/050-cfSingleRedirect.js
@@ -0,0 +1,8 @@
+D("foo.com","none",
+ A("name1", "1.2.3.4", { meta: "value" } ),
+ CF_SINGLE_REDIRECT("name1", 301, "when1", "then1"),
+ CF_SINGLE_REDIRECT("name2", 302, "when2", "then2"),
+ CF_SINGLE_REDIRECT("name3", "301", "when3", "then3"),
+ CF_SINGLE_REDIRECT("namettl", 302, "whenttl", "thenttl", TTL(999)),
+ CF_SINGLE_REDIRECT("namemeta", 302, "whenmeta", "thenmeta", { metastr: "stringy"}, { metanum: 22 } )
+);
diff --git a/pkg/js/parse_tests/050-cfSingleRedirect.json b/pkg/js/parse_tests/050-cfSingleRedirect.json
new file mode 100644
index 0000000000..8b4965c31f
--- /dev/null
+++ b/pkg/js/parse_tests/050-cfSingleRedirect.json
@@ -0,0 +1,77 @@
+{
+ "registrars": [],
+ "dns_providers": [],
+ "domains": [
+ {
+ "name": "foo.com",
+ "registrar": "none",
+ "dnsProviders": {},
+ "records": [
+ {
+ "type": "A",
+ "name": "name1",
+ "meta": {
+ "meta": "value"
+ },
+ "target": "1.2.3.4"
+ }
+ ],
+ "rawrecords": [
+ {
+ "type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
+ "args": [
+ "name1",
+ 301,
+ "when1",
+ "then1"
+ ]
+ },
+ {
+ "type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
+ "args": [
+ "name2",
+ 302,
+ "when2",
+ "then2"
+ ]
+ },
+ {
+ "type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
+ "args": [
+ "name3",
+ "301",
+ "when3",
+ "then3"
+ ]
+ },
+ {
+ "type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
+ "args": [
+ "namettl",
+ 302,
+ "whenttl",
+ "thenttl"
+ ],
+ "ttl": 999
+ },
+ {
+ "type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
+ "args": [
+ "namemeta",
+ 302,
+ "whenmeta",
+ "thenmeta"
+ ],
+ "metas": [
+ {
+ "metastr": "stringy"
+ },
+ {
+ "metanum": 22
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/pkg/js/parse_tests/051-HASH.js b/pkg/js/parse_tests/051-HASH.js
new file mode 100644
index 0000000000..cb75995a58
--- /dev/null
+++ b/pkg/js/parse_tests/051-HASH.js
@@ -0,0 +1 @@
+D(HASH("SHA1", "abc"), "reg")
diff --git a/pkg/js/parse_tests/051-HASH.json b/pkg/js/parse_tests/051-HASH.json
new file mode 100644
index 0000000000..5d11fa116a
--- /dev/null
+++ b/pkg/js/parse_tests/051-HASH.json
@@ -0,0 +1,12 @@
+{
+ "registrars": [],
+ "dns_providers": [],
+ "domains": [
+ {
+ "name": "a9993e364706816aba3e25717850c26c9cd0d89d",
+ "registrar": "reg",
+ "dnsProviders": {},
+ "records": []
+ }
+ ]
+}
diff --git a/pkg/js/parse_tests/domain-ip-map.json5 b/pkg/js/parse_tests/domain-ip-map.json5
new file mode 100644
index 0000000000..8eec061e70
--- /dev/null
+++ b/pkg/js/parse_tests/domain-ip-map.json5
@@ -0,0 +1,7 @@
+// This is a comment.
+{
+ "foo.com": "1.1.1.1",
+ "bar.com": "5.5.5.5",
+}
+
+// The "bar.com" line has a comma at the end.
diff --git a/pkg/nameservers/nameservers.go b/pkg/nameservers/nameservers.go
index 6df91f8e6f..56faabd72f 100644
--- a/pkg/nameservers/nameservers.go
+++ b/pkg/nameservers/nameservers.go
@@ -14,24 +14,26 @@ import (
// 1. All explicitly defined NAMESERVER records will be used.
// 2. Each DSP declares how many nameservers to use. Default is all. 0 indicates to use none.
func DetermineNameservers(dc *models.DomainConfig) ([]*models.Nameserver, error) {
- return DetermineNameserversForProviders(dc, dc.DNSProviderInstances)
+ return DetermineNameserversForProviders(dc, dc.DNSProviderInstances, false)
}
// DetermineNameserversForProviders is like DetermineNameservers, for a subset of providers.
-func DetermineNameserversForProviders(dc *models.DomainConfig, providers []*models.DNSProviderInstance) ([]*models.Nameserver, error) {
- // always take explicit
+func DetermineNameserversForProviders(dc *models.DomainConfig, providers []*models.DNSProviderInstance, silent bool) ([]*models.Nameserver, error) {
+ // start with the nameservers that have been explicitly added:
ns := dc.Nameservers
+
for _, dnsProvider := range providers {
n := dnsProvider.NumberOfNameservers
if n == 0 {
continue
}
- if !printer.SkinnyReport {
+ if !silent && !printer.SkinnyReport {
fmt.Printf("----- Getting nameservers from: %s\n", dnsProvider.Name)
}
+
nss, err := dnsProvider.Driver.GetNameservers(dc.Name)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("error while getting Nameservers for zone=%q with provider=%q: %w", dc.Name, dnsProvider.Name, err)
}
// Clean up the nameservers due to
// https://github.com/StackExchange/dnscontrol/issues/491
diff --git a/pkg/normalize/flatten.go b/pkg/normalize/flatten.go
index eef90aacb5..48d848e215 100644
--- a/pkg/normalize/flatten.go
+++ b/pkg/normalize/flatten.go
@@ -32,7 +32,7 @@ func flattenSPFs(cfg *models.DNSConfig) []error {
// flatten all spf records that have the "flatten" metadata
for _, txt := range txtRecords {
var rec *spflib.SPFRecord
- txtTarget := strings.Join(txt.TxtStrings, "")
+ txtTarget := txt.GetTargetTXTJoined()
if txt.Metadata["flatten"] != "" || txt.Metadata["split"] != "" {
if cache == nil {
cache, err = spflib.NewCache("spfcache.json")
diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go
index 243b8efc3a..1094f1127a 100644
--- a/pkg/normalize/validate.go
+++ b/pkg/normalize/validate.go
@@ -1,6 +1,7 @@
package normalize
import (
+ "errors"
"fmt"
"net"
"sort"
@@ -9,7 +10,6 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/transform"
"github.com/StackExchange/dnscontrol/v4/providers"
- "github.com/miekg/dns"
"github.com/miekg/dns/dnsutil"
)
@@ -60,7 +60,10 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
"CAA": true,
"CNAME": true,
"DHCID": true,
+ "DNAME": true,
"DS": true,
+ "DNSKEY": true,
+ "HTTPS": true,
"IMPORT_TRANSFORM": false,
"LOC": true,
"MX": true,
@@ -70,6 +73,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
"SOA": true,
"SRV": true,
"SSHFP": true,
+ "SVCB": true,
"TLSA": true,
"TXT": true,
}
@@ -103,7 +107,7 @@ func errorRepeat(label, domain string) string {
)
}
-func checkLabel(label string, rType string, target, domain string, meta map[string]string) error {
+func checkLabel(label string, rType string, domain string, meta map[string]string) error {
if label == "@" {
return nil
}
@@ -115,7 +119,7 @@ func checkLabel(label string, rType string, target, domain string, meta map[stri
}
if label == domain || strings.HasSuffix(label, "."+domain) {
if m := meta["skip_fqdn_check"]; m != "true" {
- return fmt.Errorf(errorRepeat(label, domain))
+ return errors.New(errorRepeat(label, domain))
}
}
@@ -142,24 +146,24 @@ func checkLabel(label string, rType string, target, domain string, meta map[stri
return nil
}
-func checkSoa(expire uint32, minttl uint32, refresh uint32, retry uint32, serial uint32, mbox string) error {
+func checkSoa(expire uint32, minttl uint32, refresh uint32, retry uint32, mbox string) error {
if expire <= 0 {
- return fmt.Errorf("SOA Expire must be > 0")
+ return errors.New("SOA Expire must be > 0")
}
if minttl <= 0 {
- return fmt.Errorf("SOA Minimum TTL must be > 0")
+ return errors.New("SOA Minimum TTL must be > 0")
}
if refresh <= 0 {
- return fmt.Errorf("SOA Refresh must be > 0")
+ return errors.New("SOA Refresh must be > 0")
}
if retry <= 0 {
- return fmt.Errorf("SOA Retry must be > 0")
+ return errors.New("SOA Retry must be > 0")
}
if mbox == "" {
- return fmt.Errorf("SOA MBox must be specified")
+ return errors.New("SOA MBox must be specified")
}
if strings.ContainsRune(mbox, '@') {
- return fmt.Errorf("SOA MBox must have '.' instead of '@'")
+ return errors.New("SOA MBox must have '.' instead of '@'")
}
return nil
}
@@ -187,13 +191,15 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
case "CNAME":
check(checkTarget(target))
if label == "@" {
- check(fmt.Errorf("cannot create CNAME record for bare domain"))
+ check(errors.New("cannot create CNAME record for bare domain"))
}
labelFQDN := dnsutil.AddOrigin(label, domain)
targetFQDN := dnsutil.AddOrigin(target, domain)
if labelFQDN == targetFQDN {
- check(fmt.Errorf("CNAME loop (target points at itself)"))
+ check(errors.New("CNAME loop (target points at itself)"))
}
+ case "DNAME":
+ check(checkTarget(target))
case "LOC":
case "MX":
check(checkTarget(target))
@@ -204,23 +210,23 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
case "NS":
check(checkTarget(target))
if label == "@" {
- check(fmt.Errorf("cannot create NS record for bare domain. Use NAMESERVER instead"))
+ check(errors.New("cannot create NS record for bare domain. Use NAMESERVER instead"))
}
case "NS1_URLFWD":
if len(strings.Fields(target)) != 5 {
- check(fmt.Errorf("record should follow format: \"from to redirectType pathForwardingMode queryForwarding\""))
+ check(errors.New("record should follow format: \"from to redirectType pathForwardingMode queryForwarding\""))
}
case "PTR":
check(checkTarget(target))
case "SOA":
- check(checkSoa(rec.SoaExpire, rec.SoaMinttl, rec.SoaRefresh, rec.SoaRetry, rec.SoaSerial, rec.SoaMbox))
+ check(checkSoa(rec.SoaExpire, rec.SoaMinttl, rec.SoaRefresh, rec.SoaRetry, rec.SoaMbox))
check(checkTarget(target))
if label != "@" {
- check(fmt.Errorf("SOA record is only valid for bare domain"))
+ check(errors.New("SOA record is only valid for bare domain"))
}
case "SRV":
check(checkTarget(target))
- case "CAA", "DHCID", "DS", "IMPORT_TRANSFORM", "SSHFP", "TLSA", "TXT":
+ case "CAA", "DHCID", "DNSKEY", "DS", "HTTPS", "IMPORT_TRANSFORM", "SSHFP", "SVCB", "TLSA", "TXT":
default:
if rec.Metadata["orig_custom_type"] != "" {
// it is a valid custom type. We perform no validation on target
@@ -232,17 +238,39 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
return
}
-func transformCNAME(target, oldDomain, newDomain string) string {
- // Canonicalize. If it isn't a FQDN, add the newDomain.
- result := dnsutil.AddOrigin(target, oldDomain)
- if dns.IsFqdn(result) {
- result = result[:len(result)-1]
+func transformCNAME(target, oldDomain, newDomain, suffixstrip string) string {
+ // Canonicalize the target. Add the newDomain minus the suffixstrip.
+ // foo -> foo.oldDomain.newDomain
+ // foo. -> foo.newDomain
+ nd := strings.TrimPrefix(newDomain, suffixstrip+".")
+ if strings.HasSuffix(target, ".") {
+ return target + nd + "."
}
- return dnsutil.AddOrigin(result, newDomain) + "."
+ return dnsutil.AddOrigin(target, oldDomain) + "." + nd + "."
+}
+
+func newRec(rec *models.RecordConfig, ttl uint32) *models.RecordConfig {
+ rec2, _ := rec.Copy()
+ if ttl != 0 {
+ rec2.TTL = ttl
+ }
+ return rec2
+}
+
+func transformLabel(label, suffixstrip string) (string, error) {
+ if suffixstrip == "" {
+ return label, nil
+ }
+ suffixstrip = "." + suffixstrip
+ if !strings.HasSuffix(label, suffixstrip) {
+ return "", fmt.Errorf("label %q does not end with %q", label, suffixstrip)
+ }
+ return label[:len(label)-len(suffixstrip)], nil
}
// import_transform imports the records of one zone into another, modifying records along the way.
-func importTransform(srcDomain, dstDomain *models.DomainConfig, transforms []transform.IPConversion, ttl uint32) error {
+func importTransform(srcDomain, dstDomain *models.DomainConfig,
+ transforms []transform.IPConversion, ttl uint32, suffixstrip string) error {
// Read srcDomain.Records, transform, and append to dstDomain.Records:
// 1. Skip any that aren't A or CNAMEs.
// 2. Append destDomainname to the end of the label.
@@ -250,17 +278,13 @@ func importTransform(srcDomain, dstDomain *models.DomainConfig, transforms []tra
// 4. For As, change the target as described the transforms.
for _, rec := range srcDomain.Records {
- if dstDomain.Records.HasRecordTypeName(rec.Type, rec.GetLabelFQDN()) {
+ // If this record is marked to be skipped, skip it.
+ if rec.Metadata["import_transform_skip"] != "" {
continue
}
- newRec := func() *models.RecordConfig {
- rec2, _ := rec.Copy()
- newlabel := rec2.GetLabelFQDN()
- rec2.SetLabel(newlabel, dstDomain.Name)
- if ttl != 0 {
- rec2.TTL = ttl
- }
- return rec2
+ // If the dstDomain already has a record with this type+label, skip it.
+ if dstDomain.Records.HasRecordTypeName(rec.Type, rec.GetLabelFQDN()) {
+ continue
}
switch rec.Type {
case "A":
@@ -269,13 +293,23 @@ func importTransform(srcDomain, dstDomain *models.DomainConfig, transforms []tra
return fmt.Errorf("import_transform: TransformIP(%v, %v) returned err=%s", rec.GetTargetField(), transforms, err)
}
for _, tr := range trs {
- r := newRec()
+ r := newRec(rec, ttl)
+ l, err := transformLabel(r.GetLabelFQDN(), suffixstrip)
+ if err != nil {
+ return err
+ }
+ r.SetLabel(l, dstDomain.Name)
r.SetTarget(tr.String())
dstDomain.Records = append(dstDomain.Records, r)
}
case "CNAME":
- r := newRec()
- r.SetTarget(transformCNAME(r.GetTargetField(), srcDomain.Name, dstDomain.Name))
+ r := newRec(rec, ttl)
+ l, err := transformLabel(r.GetLabelFQDN(), suffixstrip)
+ if err != nil {
+ return err
+ }
+ r.SetLabel(l, dstDomain.Name)
+ r.SetTarget(transformCNAME(r.GetTargetField(), srcDomain.Name, dstDomain.Name, suffixstrip))
dstDomain.Records = append(dstDomain.Records, r)
default:
// Anything else is ignored.
@@ -321,10 +355,10 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
// be performed.
continue
}
- // If NO_PURGE is in use, make sure this *isn't* a provider that *doesn't* support NO_PURGE.
- if domain.KeepUnknown && providers.ProviderHasCapability(pType, providers.CantUseNOPURGE) {
- errs = append(errs, fmt.Errorf("%s uses NO_PURGE which is not supported by %s(%s)", domain.Name, provider.Name, pType))
- }
+ // // If NO_PURGE is in use, make sure this *isn't* a provider that *doesn't* support NO_PURGE.
+ // if domain.KeepUnknown && providers.ProviderHasCapability(pType, providers.CantUseNOPURGE) {
+ // errs = append(errs, fmt.Errorf("%s uses NO_PURGE which is not supported by %s(%s)", domain.Name, provider.Name, pType))
+ // }
}
// Normalize Nameservers.
@@ -373,7 +407,7 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
if err := validateRecordTypes(rec, domain.Name, pTypes); err != nil {
errs = append(errs, err)
}
- if err := checkLabel(rec.GetLabel(), rec.Type, rec.GetTargetField(), domain.Name, rec.Metadata); err != nil {
+ if err := checkLabel(rec.GetLabel(), rec.Type, domain.Name, rec.Metadata); err != nil {
errs = append(errs, err)
}
@@ -382,7 +416,7 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
}
// Canonicalize Targets.
- if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NS" || rec.Type == "SRV" {
+ if rec.Type == "ALIAS" || rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NS" || rec.Type == "SRV" {
// #rtype_variations
// These record types have a target that is a hostname.
// We normalize them to a FQDN so there is less variation to handle. If a
@@ -424,7 +458,7 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
rec.SetLabel(rec.GetLabel(), domain.Name)
if _, ok := rec.Metadata["ignore_name_disable_safety_check"]; ok {
- errs = append(errs, fmt.Errorf("IGNORE_NAME_DISABLE_SAFETY_CHECK no longer supported. Please use DISABLE_IGNORE_SAFETY_CHECK for the entire domain"))
+ errs = append(errs, errors.New("IGNORE_NAME_DISABLE_SAFETY_CHECK no longer supported. Please use DISABLE_IGNORE_SAFETY_CHECK for the entire domain"))
}
}
@@ -439,6 +473,7 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
for _, domain := range config.Domains {
for _, rec := range domain.Records {
if rec.Type == "IMPORT_TRANSFORM" {
+ suffixstrip := rec.Metadata["transform_suffixstrip"]
table, err := transform.DecodeTransformTable(rec.Metadata["transform_table"])
if err != nil {
errs = append(errs, err)
@@ -449,7 +484,7 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
err = fmt.Errorf("IMPORT_TRANSFORM mentions non-existant domain %q", rec.GetTargetField())
errs = append(errs, err)
}
- err = importTransform(c, domain, table, rec.TTL)
+ err = importTransform(c, domain, table, rec.TTL, suffixstrip)
if err != nil {
errs = append(errs, err)
}
@@ -569,7 +604,7 @@ func checkCNAMEs(dc *models.DomainConfig) (errs []error) {
func checkDuplicates(records []*models.RecordConfig) (errs []error) {
seen := map[string]*models.RecordConfig{}
for _, r := range records {
- diffable := fmt.Sprintf("%s %s %s", r.GetLabelFQDN(), r.Type, r.ToDiffable())
+ diffable := fmt.Sprintf("%s %s %s", r.GetLabelFQDN(), r.Type, r.ToComparableNoTTL())
if seen[diffable] != nil {
errs = append(errs, fmt.Errorf("exact duplicate record found: %s", diffable))
}
@@ -679,6 +714,9 @@ var providerCapabilityChecks = []pairTypeCapability{
capabilityCheck("AZURE_ALIAS", providers.CanUseAzureAlias),
capabilityCheck("CAA", providers.CanUseCAA),
capabilityCheck("DHCID", providers.CanUseDHCID),
+ capabilityCheck("DNAME", providers.CanUseDNAME),
+ capabilityCheck("DNSKEY", providers.CanUseDNSKEY),
+ capabilityCheck("HTTPS", providers.CanUseHTTPS),
capabilityCheck("LOC", providers.CanUseLOC),
capabilityCheck("NAPTR", providers.CanUseNAPTR),
capabilityCheck("PTR", providers.CanUsePTR),
@@ -686,6 +724,7 @@ var providerCapabilityChecks = []pairTypeCapability{
capabilityCheck("SOA", providers.CanUseSOA),
capabilityCheck("SRV", providers.CanUseSRV),
capabilityCheck("SSHFP", providers.CanUseSSHFP),
+ capabilityCheck("SVCB", providers.CanUseSVCB),
capabilityCheck("TLSA", providers.CanUseTLSA),
// DS needs special record-level checks
diff --git a/pkg/normalize/validate_test.go b/pkg/normalize/validate_test.go
index 0bd8420140..68076822a7 100644
--- a/pkg/normalize/validate_test.go
+++ b/pkg/normalize/validate_test.go
@@ -41,35 +41,34 @@ func TestCheckSoa(t *testing.T) {
minttl uint32
refresh uint32
retry uint32
- serial uint32
mbox string
}{
// Expire
- {false, 123, 123, 123, 123, 123, "foo.bar.com."},
- {true, 0, 123, 123, 123, 123, "foo.bar.com."},
+ {false, 123, 123, 123, 123, "foo.bar.com."},
+ {true, 0, 123, 123, 123, "foo.bar.com."},
// MinTTL
- {false, 123, 123, 123, 123, 123, "foo.bar.com."},
- {true, 123, 0, 123, 123, 123, "foo.bar.com."},
+ {false, 123, 123, 123, 123, "foo.bar.com."},
+ {true, 123, 0, 123, 123, "foo.bar.com."},
// Refresh
- {false, 123, 123, 123, 123, 123, "foo.bar.com."},
- {true, 123, 123, 0, 123, 123, "foo.bar.com."},
+ {false, 123, 123, 123, 123, "foo.bar.com."},
+ {true, 123, 123, 0, 123, "foo.bar.com."},
// Retry
- {false, 123, 123, 123, 123, 123, "foo.bar.com."},
- {true, 123, 123, 123, 0, 123, "foo.bar.com."},
+ {false, 123, 123, 123, 123, "foo.bar.com."},
+ {true, 123, 123, 123, 0, "foo.bar.com."},
// Serial
- {false, 123, 123, 123, 123, 123, "foo.bar.com."},
- {false, 123, 123, 123, 123, 0, "foo.bar.com."},
+ {false, 123, 123, 123, 123, "foo.bar.com."},
+ {false, 123, 123, 123, 123, "foo.bar.com."},
// MBox
- {true, 123, 123, 123, 123, 123, ""},
- {true, 123, 123, 123, 123, 123, "foo@bar.com."},
- {false, 123, 123, 123, 123, 123, "foo.bar.com."},
+ {true, 123, 123, 123, 123, ""},
+ {true, 123, 123, 123, 123, "foo@bar.com."},
+ {false, 123, 123, 123, 123, "foo.bar.com."},
}
for _, test := range tests {
- experiment := fmt.Sprintf("%d %d %d %d %d %s", test.expire, test.minttl, test.refresh,
- test.retry, test.serial, test.mbox)
+ experiment := fmt.Sprintf("%d %d %d %d %s", test.expire, test.minttl, test.refresh,
+ test.retry, test.mbox)
t.Run(experiment, func(t *testing.T) {
- err := checkSoa(test.expire, test.minttl, test.refresh, test.retry, test.serial, test.mbox)
+ err := checkSoa(test.expire, test.minttl, test.refresh, test.retry, test.mbox)
checkError(t, err, test.isError, experiment)
})
}
@@ -102,7 +101,7 @@ func TestCheckLabel(t *testing.T) {
if test.hasSkipMeta {
meta["skip_fqdn_check"] = "true"
}
- err := checkLabel(test.label, test.rType, test.target, "foo.tld", meta)
+ err := checkLabel(test.label, test.rType, "foo.tld", meta)
if err != nil && !test.isError {
t.Errorf("%02d: Expected no error but got %s", i, err)
}
@@ -175,13 +174,59 @@ func Test_transform_cname(t *testing.T) {
}
for _, test := range tests {
- actual := transformCNAME(test.experiment, "old.com", "new.com")
+ actual := transformCNAME(test.experiment, "old.com", "new.com", "")
if test.expected != actual {
t.Errorf("%v: expected (%v) got (%v)\n", test.experiment, test.expected, actual)
}
}
}
+func Test_transform_cname_strip(t *testing.T) {
+ var tests = []struct {
+ p []string
+ expected string
+ }{
+ {[]string{"ai.meta.stackexchange.com.", "stackexchange.com", "com.internal", "com"},
+ "ai.meta.stackexchange.com.internal."},
+ {[]string{"askubuntu.com.", "askubuntu.com", "com.internal", "com"},
+ "askubuntu.com.internal."},
+ {[]string{"blogoverflow.com.", "stackoverflow.com", "com.internal", "com"},
+ "blogoverflow.com.internal."},
+ {[]string{"careers.stackoverflow.com.", "stackoverflow.com", "com.internal", "com"},
+ "careers.stackoverflow.com.internal."},
+ {[]string{"chat.stackexchange.com.", "askubuntu.com", "com.internal", "com"},
+ "chat.stackexchange.com.internal."},
+ {[]string{"chat.stackexchange.com.", "stackoverflow.com", "com.internal", "com"},
+ "chat.stackexchange.com.internal."},
+ {[]string{"chat.stackexchange.com.", "superuser.com", "com.internal", "com"},
+ "chat.stackexchange.com.internal."},
+ {[]string{"sstatic.net.", "sstatic.net", "net.internal", "net"},
+ "sstatic.net.internal."},
+ {[]string{"stackapps.com.", "stackapps.com", "com.internal", "com"},
+ "stackapps.com.internal."},
+ {[]string{"stackexchange.com.", "stackexchange.com", "com.internal", "com"},
+ "stackexchange.com.internal."},
+ {[]string{"stackoverflow.com.", "stackoverflow.com", "com.internal", "com"},
+ "stackoverflow.com.internal."},
+ {[]string{"superuser.com.", "superuser.com", "com.internal", "com"},
+ "superuser.com.internal."},
+ {[]string{"teststackoverflow.com.", "teststackoverflow.com", "com.internal", "com"},
+ "teststackoverflow.com.internal."},
+ {[]string{"webapps.stackexchange.com.", "stackexchange.com", "com.internal", "com"},
+ "webapps.stackexchange.com.internal."},
+ //
+ {[]string{"sstatic.net.", "sstatic.net", "com.internal", "com"},
+ "sstatic.net.internal."},
+ }
+
+ for _, test := range tests {
+ actual := transformCNAME(test.p[0], test.p[1], test.p[2], test.p[3])
+ if test.expected != actual {
+ t.Errorf("%v: expected (%v) got (%v)\n", test.p, test.expected, actual)
+ }
+ }
+}
+
func TestNSAtRoot(t *testing.T) {
// do not allow ns records for @
rec := &models.RecordConfig{Type: "NS"}
@@ -314,17 +359,15 @@ func TestCheckDuplicates(t *testing.T) {
// The only difference is the rType:
makeRC("aaa", "example.com", "uniquestring.com.", models.RecordConfig{Type: "NS"}),
makeRC("aaa", "example.com", "uniquestring.com.", models.RecordConfig{Type: "PTR"}),
- // The only difference is the TTL.
- makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 111}),
- makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 222}),
// Three records each with a different target.
makeRC("@", "example.com", "ns1.foo.com.", models.RecordConfig{Type: "NS"}),
makeRC("@", "example.com", "ns2.foo.com.", models.RecordConfig{Type: "NS"}),
makeRC("@", "example.com", "ns3.foo.com.", models.RecordConfig{Type: "NS"}),
+ // NOTE: The comparison ignores ttl. Therefore we don't test that.
}
errs := checkDuplicates(records)
if len(errs) != 0 {
- t.Errorf("Expect duplicate NOT found but found %q", errs)
+ t.Errorf("Expected duplicate NOT found but found %q", errs)
}
}
diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go
new file mode 100644
index 0000000000..a8c362bbe9
--- /dev/null
+++ b/pkg/notifications/shoutrrr.go
@@ -0,0 +1,32 @@
+package notifications
+
+import (
+ "fmt"
+
+ "github.com/containrrr/shoutrrr"
+)
+
+func init() {
+ initers = append(initers, func(cfg map[string]string) Notifier {
+ if url, ok := cfg["shoutrrr_url"]; ok {
+ return shoutrrrNotifier(url)
+ }
+ return nil
+ })
+}
+
+type shoutrrrNotifier string
+
+func (b shoutrrrNotifier) Notify(domain, provider, msg string, err error, preview bool) {
+ var payload string
+ if preview {
+ payload = fmt.Sprintf("DNSControl preview: %s[%s]:\n%s", domain, provider, msg)
+ } else if err != nil {
+ payload = fmt.Sprintf("DNSControl ERROR running correction on %s[%s]:\n%s\nError: %s", domain, provider, msg, err)
+ } else {
+ payload = fmt.Sprintf("DNSControl successfully ran correction for %s[%s]:\n%s", domain, provider, msg)
+ }
+ shoutrrr.Send(string(b), payload)
+}
+
+func (b shoutrrrNotifier) Done() {}
diff --git a/pkg/powershell/LICENSE b/pkg/powershell/LICENSE
new file mode 100644
index 0000000000..0e06ff19df
--- /dev/null
+++ b/pkg/powershell/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2017, Gorillalabs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+http://www.opensource.org/licenses/MIT
diff --git a/pkg/powershell/README.md b/pkg/powershell/README.md
new file mode 100644
index 0000000000..10c3723fbc
--- /dev/null
+++ b/pkg/powershell/README.md
@@ -0,0 +1,110 @@
+# go-powershell
+
+This package is inspired by [jPowerShell](https://github.com/profesorfalken/jPowerShell)
+and allows one to run and remote-control a PowerShell session. Use this if you
+don't have a static script that you want to execute, bur rather run dynamic
+commands.
+
+## Installation
+
+ go get github.com/bhendo/go-powershell
+
+## Usage
+
+To start a PowerShell shell, you need a backend. Backends take care of starting
+and controlling the actual powershell.exe process. In most cases, you will want
+to use the Local backend, which just uses ``os/exec`` to start the process.
+
+```go
+package main
+
+import (
+ "fmt"
+
+ ps "github.com/bhendo/go-powershell"
+ "github.com/bhendo/go-powershell/backend"
+)
+
+func main() {
+ // choose a backend
+ back := &backend.Local{}
+
+ // start a local powershell process
+ shell, err := ps.New(back)
+ if err != nil {
+ panic(err)
+ }
+ defer shell.Exit()
+
+ // ... and interact with it
+ stdout, stderr, err := shell.Execute("Get-WmiObject -Class Win32_Processor")
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(stdout)
+}
+```
+
+## Remote Sessions
+
+You can use an existing PS shell to use PSSession cmdlets to connect to remote
+computers. Instead of manually handling that, you can use the Session middleware,
+which takes care of authentication. Note that you can still use the "raw" shell
+to execute commands on the computer where the powershell host process is running.
+
+```go
+package main
+
+import (
+ "fmt"
+
+ ps "github.com/bhendo/go-powershell"
+ "github.com/bhendo/go-powershell/backend"
+ "github.com/bhendo/go-powershell/middleware"
+)
+
+func main() {
+ // choose a backend
+ back := &backend.Local{}
+
+ // start a local powershell process
+ shell, err := ps.New(back)
+ if err != nil {
+ panic(err)
+ }
+
+ // prepare remote session configuration
+ config := middleware.NewSessionConfig()
+ config.ComputerName = "remote-pc-1"
+
+ // create a new shell by wrapping the existing one in the session middleware
+ session, err := middleware.NewSession(shell, config)
+ if err != nil {
+ panic(err)
+ }
+ defer session.Exit() // will also close the underlying ps shell!
+
+ // everything run via the session is run on the remote machine
+ stdout, stderr, err = session.Execute("Get-WmiObject -Class Win32_Processor")
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(stdout)
+}
+```
+
+Note that a single shell instance is not safe for concurrent use, as are remote
+sessions. You can have as many remote sessions using the same shell as you like,
+but you must execute commands serially. If you need concurrency, you can just
+spawn multiple PowerShell processes (i.e. call ``.New()`` multiple times).
+
+Also, note that all commands that you execute are wrapped in special echo
+statements to delimit the stdout/stderr streams. After ``.Execute()``ing a command,
+you can therefore not access ``$LastExitCode`` anymore and expect meaningful
+results.
+
+## License
+
+MIT, see LICENSE file.
diff --git a/pkg/powershell/backend/local.go b/pkg/powershell/backend/local.go
new file mode 100644
index 0000000000..429b1fe245
--- /dev/null
+++ b/pkg/powershell/backend/local.go
@@ -0,0 +1,38 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package backend
+
+import (
+ "io"
+ "os/exec"
+
+ "github.com/juju/errors"
+)
+
+type Local struct{}
+
+func (b *Local) StartProcess(cmd string, args ...string) (Waiter, io.Writer, io.Reader, io.Reader, error) {
+ command := exec.Command(cmd, args...)
+
+ stdin, err := command.StdinPipe()
+ if err != nil {
+ return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the PowerShell's stdin stream")
+ }
+
+ stdout, err := command.StdoutPipe()
+ if err != nil {
+ return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the PowerShell's stdout stream")
+ }
+
+ stderr, err := command.StderrPipe()
+ if err != nil {
+ return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the PowerShell's stderr stream")
+ }
+
+ err = command.Start()
+ if err != nil {
+ return nil, nil, nil, nil, errors.Annotate(err, "Could not spawn PowerShell process")
+ }
+
+ return command, stdin, stdout, stderr, nil
+}
diff --git a/pkg/powershell/backend/ssh.go b/pkg/powershell/backend/ssh.go
new file mode 100644
index 0000000000..bdaa6fdd7e
--- /dev/null
+++ b/pkg/powershell/backend/ssh.go
@@ -0,0 +1,64 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package backend
+
+import (
+ "io"
+)
+
+// sshSession exists so we don't create a hard dependency on crypto/ssh.
+type sshSession interface {
+ Waiter
+
+ StdinPipe() (io.WriteCloser, error)
+ StdoutPipe() (io.Reader, error)
+ StderrPipe() (io.Reader, error)
+ Start(string) error
+}
+
+type SSH struct {
+ Session sshSession
+}
+
+// func (b *SSH) StartProcess(cmd string, args ...string) (Waiter, io.Writer, io.Reader, io.Reader, error) {
+// stdin, err := b.Session.StdinPipe()
+// if err != nil {
+// return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the SSH session's stdin stream")
+// }
+
+// stdout, err := b.Session.StdoutPipe()
+// if err != nil {
+// return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the SSH session's stdout stream")
+// }
+
+// stderr, err := b.Session.StderrPipe()
+// if err != nil {
+// return nil, nil, nil, nil, errors.Annotate(err, "Could not get hold of the SSH session's stderr stream")
+// }
+
+// err = b.Session.Start(b.createCmd(cmd, args))
+// if err != nil {
+// return nil, nil, nil, nil, errors.Annotate(err, "Could not spawn process via SSH")
+// }
+
+// return b.Session, stdin, stdout, stderr, nil
+// }
+
+// func (b *SSH) createCmd(cmd string, args []string) string {
+// parts := []string{cmd}
+// simple := regexp.MustCompile(`^[a-z0-9_/.~+-]+$`)
+
+// for _, arg := range args {
+// if !simple.MatchString(arg) {
+// arg = b.quote(arg)
+// }
+
+// parts = append(parts, arg)
+// }
+
+// return strings.Join(parts, " ")
+// }
+
+// func (b *SSH) quote(s string) string {
+// return fmt.Sprintf(`"%s"`, s)
+// }
diff --git a/pkg/powershell/backend/types.go b/pkg/powershell/backend/types.go
new file mode 100644
index 0000000000..505471c128
--- /dev/null
+++ b/pkg/powershell/backend/types.go
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package backend
+
+import "io"
+
+type Waiter interface {
+ Wait() error
+}
+
+type Starter interface {
+ StartProcess(cmd string, args ...string) (Waiter, io.Writer, io.Reader, io.Reader, error)
+}
diff --git a/pkg/powershell/middleware/session.go b/pkg/powershell/middleware/session.go
new file mode 100644
index 0000000000..d96a2e0cb6
--- /dev/null
+++ b/pkg/powershell/middleware/session.go
@@ -0,0 +1,47 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package middleware
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/pkg/powershell/utils"
+ "github.com/juju/errors"
+)
+
+type session struct {
+ upstream Middleware
+ name string
+}
+
+func NewSession(upstream Middleware, config *SessionConfig) (Middleware, error) {
+ asserted, ok := config.Credential.(credential)
+ if ok {
+ credentialParamValue, err := asserted.prepare(upstream)
+ if err != nil {
+ return nil, errors.Annotate(err, "Could not setup credentials")
+ }
+
+ config.Credential = credentialParamValue
+ }
+
+ name := "goSess" + utils.CreateRandomString(8)
+ args := strings.Join(config.ToArgs(), " ")
+
+ _, _, err := upstream.Execute(fmt.Sprintf("$%s = New-PSSession %s", name, args))
+ if err != nil {
+ return nil, errors.Annotate(err, "Could not create new PSSession")
+ }
+
+ return &session{upstream, name}, nil
+}
+
+func (s *session) Execute(cmd string) (string, string, error) {
+ return s.upstream.Execute(fmt.Sprintf("Invoke-Command -Session $%s -Script {%s}", s.name, cmd))
+}
+
+func (s *session) Exit() {
+ s.upstream.Execute(fmt.Sprintf("Disconnect-PSSession -Session $%s", s.name))
+ s.upstream.Exit()
+}
diff --git a/pkg/powershell/middleware/session_config.go b/pkg/powershell/middleware/session_config.go
new file mode 100644
index 0000000000..f9f47421b0
--- /dev/null
+++ b/pkg/powershell/middleware/session_config.go
@@ -0,0 +1,95 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package middleware
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/StackExchange/dnscontrol/v4/pkg/powershell/utils"
+ "github.com/juju/errors"
+)
+
+const (
+ HTTPPort = 5985
+ HTTPSPort = 5986
+)
+
+type SessionConfig struct {
+ ComputerName string
+ AllowRedirection bool
+ Authentication string
+ CertificateThumbprint string
+ Credential interface{}
+ Port int
+ UseSSL bool
+}
+
+func NewSessionConfig() *SessionConfig {
+ return &SessionConfig{}
+}
+
+func (c *SessionConfig) ToArgs() []string {
+ args := make([]string, 0)
+
+ if c.ComputerName != "" {
+ args = append(args, "-ComputerName")
+ args = append(args, utils.QuoteArg(c.ComputerName))
+ }
+
+ if c.AllowRedirection {
+ args = append(args, "-AllowRedirection")
+ }
+
+ if c.Authentication != "" {
+ args = append(args, "-Authentication")
+ args = append(args, utils.QuoteArg(c.Authentication))
+ }
+
+ if c.CertificateThumbprint != "" {
+ args = append(args, "-CertificateThumbprint")
+ args = append(args, utils.QuoteArg(c.CertificateThumbprint))
+ }
+
+ if c.Port > 0 {
+ args = append(args, "-Port")
+ args = append(args, strconv.Itoa(c.Port))
+ }
+
+ if asserted, ok := c.Credential.(string); ok {
+ args = append(args, "-Credential")
+ args = append(args, asserted) // do not quote, as it contains a variable name when using password auth
+ }
+
+ if c.UseSSL {
+ args = append(args, "-UseSSL")
+ }
+
+ return args
+}
+
+type credential interface {
+ prepare(Middleware) (interface{}, error)
+}
+
+type UserPasswordCredential struct {
+ Username string
+ Password string
+}
+
+func (c *UserPasswordCredential) prepare(s Middleware) (interface{}, error) {
+ name := "goCred" + utils.CreateRandomString(8)
+ pwname := "goPass" + utils.CreateRandomString(8)
+
+ _, _, err := s.Execute(fmt.Sprintf("$%s = ConvertTo-SecureString -String %s -AsPlainText -Force", pwname, utils.QuoteArg(c.Password)))
+ if err != nil {
+ return nil, errors.Annotate(err, "Could not convert password to secure string")
+ }
+
+ _, _, err = s.Execute(fmt.Sprintf("$%s = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList %s, $%s", name, utils.QuoteArg(c.Username), pwname))
+ if err != nil {
+ return nil, errors.Annotate(err, "Could not create PSCredential object")
+ }
+
+ return fmt.Sprintf("$%s", name), nil
+}
diff --git a/pkg/powershell/middleware/types.go b/pkg/powershell/middleware/types.go
new file mode 100644
index 0000000000..ce35ffa867
--- /dev/null
+++ b/pkg/powershell/middleware/types.go
@@ -0,0 +1,8 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package middleware
+
+type Middleware interface {
+ Execute(cmd string) (string, string, error)
+ Exit()
+}
diff --git a/pkg/powershell/middleware/utf8.go b/pkg/powershell/middleware/utf8.go
new file mode 100644
index 0000000000..292e76d4ff
--- /dev/null
+++ b/pkg/powershell/middleware/utf8.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package middleware
+
+// utf8 implements a primitive middleware that encodes all outputs
+// as base64 to prevent encoding issues between remote PowerShell
+// shells and the receiver. Just setting $OutputEncoding does not
+// work reliably enough, sadly.
+// type utf8 struct {
+// upstream Middleware
+// wrapper string
+// }
+
+// func NewUTF8(upstream Middleware) (Middleware, error) {
+// wrapper := "goUTF8" + utils.CreateRandomString(8)
+
+// _, _, err := upstream.Execute(fmt.Sprintf(`function %s { process { if ($_) { [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_)) } else { '' } } }`, wrapper))
+
+// return &utf8{upstream, wrapper}, err
+// }
+
+// func (u *utf8) Execute(cmd string) (string, string, error) {
+// // Out-String to concat all lines into a single line,
+// // Write-Host to prevent line breaks at the "window width"
+// cmd = fmt.Sprintf(`%s | Out-String | %s | Write-Host`, cmd, u.wrapper)
+
+// stdout, stderr, err := u.upstream.Execute(cmd)
+// if err != nil {
+// return stdout, stderr, err
+// }
+
+// decoded, err := base64.StdEncoding.DecodeString(stdout)
+// if err != nil {
+// return stdout, stderr, err
+// }
+
+// return string(decoded), stderr, nil
+// }
+
+// func (u *utf8) Exit() {
+// u.upstream.Exit()
+// }
diff --git a/pkg/powershell/shell.go b/pkg/powershell/shell.go
new file mode 100644
index 0000000000..2215f596c1
--- /dev/null
+++ b/pkg/powershell/shell.go
@@ -0,0 +1,120 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package powershell
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+
+ "github.com/StackExchange/dnscontrol/v4/pkg/powershell/backend"
+ "github.com/StackExchange/dnscontrol/v4/pkg/powershell/utils"
+ "github.com/juju/errors"
+)
+
+const newline = "\r\n"
+
+type Shell interface {
+ Execute(cmd string) (string, string, error)
+ Exit()
+}
+
+type shell struct {
+ handle backend.Waiter
+ stdin io.Writer
+ stdout io.Reader
+ stderr io.Reader
+}
+
+func New(backend backend.Starter) (Shell, error) {
+ handle, stdin, stdout, stderr, err := backend.StartProcess("powershell.exe", "-NoExit", "-Command", "-")
+ if err != nil {
+ return nil, err
+ }
+
+ return &shell{handle, stdin, stdout, stderr}, nil
+}
+
+func (s *shell) Execute(cmd string) (string, string, error) {
+ if s.handle == nil {
+ return "", "", errors.Annotate(errors.New(cmd), "Cannot execute commands on closed shells.")
+ }
+
+ outBoundary := createBoundary()
+ errBoundary := createBoundary()
+
+ // wrap the command in special markers so we know when to stop reading from the pipes
+ full := fmt.Sprintf("%s; echo '%s'; [Console]::Error.WriteLine('%s')%s", cmd, outBoundary, errBoundary, newline)
+
+ _, err := s.stdin.Write([]byte(full))
+ if err != nil {
+ return "", "", errors.Annotate(errors.Annotate(err, cmd), "Could not send PowerShell command")
+ }
+
+ // read stdout and stderr
+ sout := ""
+ serr := ""
+
+ waiter := &sync.WaitGroup{}
+ waiter.Add(2)
+
+ go streamReader(s.stdout, outBoundary, &sout, waiter)
+ go streamReader(s.stderr, errBoundary, &serr, waiter)
+
+ waiter.Wait()
+
+ if len(serr) > 0 {
+ return sout, serr, errors.Annotate(errors.New(cmd), serr)
+ }
+
+ return sout, serr, nil
+}
+
+func (s *shell) Exit() {
+ s.stdin.Write([]byte("exit" + newline))
+
+ // if it's possible to close stdin, do so (some backends, like the local one,
+ // do support it)
+ closer, ok := s.stdin.(io.Closer)
+ if ok {
+ closer.Close()
+ }
+
+ s.handle.Wait()
+
+ s.handle = nil
+ s.stdin = nil
+ s.stdout = nil
+ s.stderr = nil
+}
+
+func streamReader(stream io.Reader, boundary string, buffer *string, signal *sync.WaitGroup) error {
+ // read all output until we have found our boundary token
+ output := ""
+ bufsize := 64
+ marker := boundary + newline
+
+ for {
+ buf := make([]byte, bufsize)
+ read, err := stream.Read(buf)
+ if err != nil {
+ return err
+ }
+
+ output = output + string(buf[:read])
+
+ if strings.HasSuffix(output, marker) {
+ break
+ }
+ }
+
+ *buffer = strings.TrimSuffix(output, marker)
+ signal.Done()
+
+ return nil
+}
+
+func createBoundary() string {
+ return "$gorilla" + utils.CreateRandomString(12) + "$"
+}
diff --git a/pkg/powershell/utils/quote.go b/pkg/powershell/utils/quote.go
new file mode 100644
index 0000000000..84dd1d38c1
--- /dev/null
+++ b/pkg/powershell/utils/quote.go
@@ -0,0 +1,9 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package utils
+
+import "strings"
+
+func QuoteArg(s string) string {
+ return "'" + strings.Replace(s, "'", "\"", -1) + "'"
+}
diff --git a/pkg/powershell/utils/quote_test.go b/pkg/powershell/utils/quote_test.go
new file mode 100644
index 0000000000..f1c91fba2c
--- /dev/null
+++ b/pkg/powershell/utils/quote_test.go
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package utils
+
+import "testing"
+
+func TestQuotingArguments(t *testing.T) {
+ testcases := [][]string{
+ {"", "''"},
+ {"test", "'test'"},
+ {"two words", "'two words'"},
+ {"quo\"ted", "'quo\"ted'"},
+ {"quo'ted", "'quo\"ted'"},
+ {"quo\\'ted", "'quo\\\"ted'"},
+ {"quo\"t'ed", "'quo\"t\"ed'"},
+ {"es\\caped", "'es\\caped'"},
+ {"es`caped", "'es`caped'"},
+ {"es\\`caped", "'es\\`caped'"},
+ }
+
+ for i, testcase := range testcases {
+ quoted := QuoteArg(testcase[0])
+
+ if quoted != testcase[1] {
+ t.Errorf("test %02d failed: input '%s', expected %s, actual %s", i+1, testcase[0], testcase[1], quoted)
+ }
+ }
+}
diff --git a/pkg/powershell/utils/rand.go b/pkg/powershell/utils/rand.go
new file mode 100644
index 0000000000..245243e5a3
--- /dev/null
+++ b/pkg/powershell/utils/rand.go
@@ -0,0 +1,20 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package utils
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+)
+
+func CreateRandomString(bytes int) string {
+ c := bytes
+ b := make([]byte, c)
+
+ _, err := rand.Read(b)
+ if err != nil {
+ panic(err)
+ }
+
+ return hex.EncodeToString(b)
+}
diff --git a/pkg/powershell/utils/rand_test.go b/pkg/powershell/utils/rand_test.go
new file mode 100644
index 0000000000..8000a9b84a
--- /dev/null
+++ b/pkg/powershell/utils/rand_test.go
@@ -0,0 +1,16 @@
+// Copyright (c) 2017 Gorillalabs. All rights reserved.
+
+package utils
+
+import "testing"
+
+func TestRandomStrings(t *testing.T) {
+ r1 := CreateRandomString(8)
+ r2 := CreateRandomString(8)
+
+ if r1 == r2 {
+ t.Error("Failed to create random strings: The two generated strings are identical.")
+ } else if len(r1) != 16 {
+ t.Errorf("Expected the random string to contain 16 characters, but got %d.", len(r1))
+ }
+}
diff --git a/pkg/prettyzone/prettyzone.go b/pkg/prettyzone/prettyzone.go
index ae8a485b4c..cb91babf20 100644
--- a/pkg/prettyzone/prettyzone.go
+++ b/pkg/prettyzone/prettyzone.go
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/miekg/dns"
)
@@ -43,16 +44,6 @@ func MostCommonTTL(records models.Records) uint32 {
return mk
}
-// WriteZoneFileRR is a helper for when you have []dns.RR instead of models.Records
-func WriteZoneFileRR(w io.Writer, records []dns.RR, origin string) error {
- rcs, err := models.RRstoRCs(records, origin)
- if err != nil {
- return err
- }
-
- return WriteZoneFileRC(w, rcs, origin, 0, nil)
-}
-
// WriteZoneFileRC writes a beautifully formatted zone file.
func WriteZoneFileRC(w io.Writer, records models.Records, origin string, defaultTTL uint32, comments []string) error {
// This function prioritizes beauty over output size.
@@ -136,9 +127,12 @@ func (z *ZoneGenData) generateZoneFileHelper(w io.Writer) error {
// type
typeStr := rr.Type
+ if rr.Type == "UNKNOWN" {
+ typeStr = rr.UnknownTypeName
+ }
// the remaining line
- target := rr.GetTargetCombined()
+ target := rr.GetTargetCombinedFunc(txtutil.EncodeQuoted)
// comment
comment := ""
diff --git a/pkg/prettyzone/prettyzone_test.go b/pkg/prettyzone/prettyzone_test.go
index 88f93454dd..4c98cacf8c 100644
--- a/pkg/prettyzone/prettyzone_test.go
+++ b/pkg/prettyzone/prettyzone_test.go
@@ -3,8 +3,10 @@ package prettyzone
import (
"bytes"
"fmt"
+ "io"
"log"
"math/rand"
+ "strings"
"testing"
"github.com/StackExchange/dnscontrol/v4/models"
@@ -28,7 +30,7 @@ func parseAndRegen(t *testing.T, buf *bytes.Buffer, expected string) {
// Generate it back:
buf2 := &bytes.Buffer{}
- WriteZoneFileRR(buf2, parsed, "bosun.org")
+ writeZoneFileRR(buf2, parsed, "bosun.org")
// Compare:
if buf2.String() != expected {
@@ -36,6 +38,29 @@ func parseAndRegen(t *testing.T, buf *bytes.Buffer, expected string) {
}
}
+// rrstoRCs converts []dns.RR to []RecordConfigs.
+func rrstoRCs(rrs []dns.RR, origin string) (models.Records, error) {
+ rcs := make(models.Records, 0, len(rrs))
+ for _, r := range rrs {
+ rc, err := models.RRtoRC(r, origin)
+ if err != nil {
+ return nil, err
+ }
+
+ rcs = append(rcs, &rc)
+ }
+ return rcs, nil
+}
+
+// writeZoneFileRR is a helper for when you have []dns.RR instead of models.Records
+func writeZoneFileRR(w io.Writer, records []dns.RR, origin string) error {
+ rcs, err := rrstoRCs(records, origin)
+ if err != nil {
+ return err
+ }
+
+ return WriteZoneFileRC(w, rcs, origin, 0, nil)
+}
func TestMostCommonTtl(t *testing.T) {
var records []dns.RR
var g, e uint32
@@ -48,7 +73,7 @@ func TestMostCommonTtl(t *testing.T) {
// All records are TTL=100
records = nil
records, e = append(records, r1, r1, r1), 100
- x, err := models.RRstoRCs(records, "bosun.org")
+ x, err := rrstoRCs(records, "bosun.org")
if err != nil {
panic(err)
}
@@ -60,7 +85,7 @@ func TestMostCommonTtl(t *testing.T) {
// Mixture of TTLs with an obvious winner.
records = nil
records, e = append(records, r1, r2, r2), 200
- rcs, err := models.RRstoRCs(records, "bosun.org")
+ rcs, err := rrstoRCs(records, "bosun.org")
if err != nil {
panic(err)
}
@@ -72,7 +97,7 @@ func TestMostCommonTtl(t *testing.T) {
// 3-way tie. Largest TTL should be used.
records = nil
records, e = append(records, r1, r2, r3), 300
- rcs, err = models.RRstoRCs(records, "bosun.org")
+ rcs, err = rrstoRCs(records, "bosun.org")
if err != nil {
panic(err)
}
@@ -84,7 +109,7 @@ func TestMostCommonTtl(t *testing.T) {
// NS records are ignored.
records = nil
records, e = append(records, r1, r4, r5), 100
- rcs, err = models.RRstoRCs(records, "bosun.org")
+ rcs, err = rrstoRCs(records, "bosun.org")
if err != nil {
panic(err)
}
@@ -102,7 +127,7 @@ func TestWriteZoneFileSimple(t *testing.T) {
r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
+ writeZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
expected := `$TTL 300
@ IN A 192.30.252.153
IN A 192.30.252.154
@@ -123,7 +148,7 @@ func TestWriteZoneFileSimpleTtl(t *testing.T) {
r3, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.155")
r4, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org")
+ writeZoneFileRR(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org")
expected := `$TTL 100
@ IN A 192.30.252.153
IN A 192.30.252.154
@@ -153,7 +178,7 @@ func TestWriteZoneFileMx(t *testing.T) {
r8, _ := dns.NewRR("ccc.bosun.org. IN MX 40 aaa.example.com.")
r9, _ := dns.NewRR("ccc.bosun.org. IN MX 1 ttt.example.com.")
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org")
+ writeZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org")
if buf.String() != testdataZFMX {
t.Log(buf.String())
t.Log(testdataZFMX)
@@ -182,7 +207,7 @@ func TestWriteZoneFileSrv(t *testing.T) {
r4, _ := dns.NewRR(`bosun.org. 300 IN SRV 20 10 5050 foo.com.`)
r5, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org")
+ writeZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org")
if buf.String() != testdataZFSRV {
t.Log(buf.String())
t.Log(testdataZFSRV)
@@ -205,7 +230,7 @@ func TestWriteZoneFilePtr(t *testing.T) {
r2, _ := dns.NewRR(`bosun.org. 300 IN PTR barney.bosun.org.`)
r3, _ := dns.NewRR(`bosun.org. 300 IN PTR alex.bosun.org.`)
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
+ writeZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
if buf.String() != testdataZFPTR {
t.Log(buf.String())
t.Log(testdataZFPTR)
@@ -229,7 +254,7 @@ func TestWriteZoneFileCaa(t *testing.T) {
r5, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.net"`)
r6, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "mailto:example.com"`)
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org")
+ writeZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org")
if buf.String() != testdataZFCAA {
t.Log(buf.String())
t.Log(testdataZFCAA)
@@ -247,6 +272,46 @@ var testdataZFCAA = `$TTL 300
IN CAA 0 issuewild ";"
`
+// r is shorthand for strings.Repeat()
+func r(s string, c int) string { return strings.Repeat(s, c) }
+
+func TestWriteZoneFileTxt(t *testing.T) {
+ // Do round-trip tests on various length TXT records.
+ t10 := `t10 IN TXT "ten4567890"`
+ t254 := `t254 IN TXT "` + r("a", 254) + `"`
+ t255 := `t255 IN TXT "` + r("b", 255) + `"`
+ t256 := `t256 IN TXT "` + r("c", 255) + `" "` + r("D", 1) + `"`
+ t509 := `t509 IN TXT "` + r("e", 255) + `" "` + r("F", 254) + `"`
+ t510 := `t510 IN TXT "` + r("g", 255) + `" "` + r("H", 255) + `"`
+ t511 := `t511 IN TXT "` + r("i", 255) + `" "` + r("J", 255) + `" "` + r("k", 1) + `"`
+ t512 := `t511 IN TXT "` + r("L", 255) + `" "` + r("M", 255) + `" "` + r("n", 2) + `"`
+ t513 := `t511 IN TXT "` + r("o", 255) + `" "` + r("P", 255) + `" "` + r("q", 3) + `"`
+ for i, d := range []string{t10, t254, t255, t256, t509, t510, t511, t512, t513} {
+ // Make the rr:
+ rr, err := dns.NewRR(d)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Make the expected zonefile:
+ ez := "$TTL 3600\n" + d + "\n"
+
+ // Generate the zonefile:
+ buf := &bytes.Buffer{}
+ writeZoneFileRR(buf, []dns.RR{rr}, "bosun.org")
+ gz := buf.String()
+ if gz != ez {
+ t.Log("got: " + gz)
+ t.Log("wnt: " + ez)
+ t.Fatalf("Zone file %d does not match.", i)
+ }
+
+ // Reverse the process. Turn the zonefile into a list of records
+ parseAndRegen(t, buf, ez)
+ }
+
+}
+
// Test 1 of each record type
func mustNewRR(s string) dns.RR {
@@ -273,8 +338,13 @@ func TestWriteZoneFileEach(t *testing.T) {
d = append(d, mustNewRR(`sub.bosun.org. 300 IN NS bosun.org.`)) // Must be a label with no other records.
d = append(d, mustNewRR(`x.bosun.org. 300 IN CNAME bosun.org.`)) // Must be a label with no other records.
d = append(d, mustNewRR(`bosun.org. 300 IN DHCID AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=`))
+ d = append(d, mustNewRR(`dname.bosun.org. 300 IN DNAME example.com.`))
+ d = append(d, mustNewRR(`dnssec.bosun.org. 300 IN DS 31334 13 2 94cc505ebc36b1f4e051268b820efb230f1572d445e833bb5bf7380d6c2cbc0a`))
+ d = append(d, mustNewRR(`dnssec.bosun.org. 300 IN DNSKEY 257 3 13 rNR701yiOPHfqDP53GnsHZdlsRqI7O1ksk60rnFILZVk7Z4eTBd1U49oSkTNVNox9tb7N15N2hboXoMEyFFzcw==`))
+ d = append(d, mustNewRR(`bosun.org. 300 IN HTTPS 1 . alpn="h3,h2"`))
+ d = append(d, mustNewRR(`bosun.org. 300 IN SVCB 1 . alpn="h3,h2"`))
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, d, "bosun.org")
+ writeZoneFileRR(buf, d, "bosun.org")
if buf.String() != testdataZFEach {
t.Log(buf.String())
t.Log(testdataZFEach)
@@ -291,8 +361,13 @@ var testdataZFEach = `$TTL 300
IN TXT "my text"
IN CAA 0 issue "letsencrypt.org"
IN DHCID AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=
+ IN HTTPS 1 . alpn="h3,h2"
+ IN SVCB 1 . alpn="h3,h2"
4.5 IN PTR y.bosun.org.
_443._tcp IN TLSA 3 1 1 abcdef0
+dname IN DNAME example.com.
+dnssec IN DNSKEY 257 3 13 rNR701yiOPHfqDP53GnsHZdlsRqI7O1ksk60rnFILZVk7Z4eTBd1U49oSkTNVNox9tb7N15N2hboXoMEyFFzcw==
+ IN DS 31334 13 2 94CC505EBC36B1F4E051268B820EFB230F1572D445E833BB5BF7380D6C2CBC0A
sub IN NS bosun.org.
x IN CNAME bosun.org.
`
@@ -306,7 +381,7 @@ func TestWriteZoneFileSynth(t *testing.T) {
rsynz := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
rsynz.SetLabel("zalias", "bosun.org")
- recs, err := models.RRstoRCs([]dns.RR{r1, r2, r3}, "bosun.org")
+ recs, err := rrstoRCs([]dns.RR{r1, r2, r3}, "bosun.org")
if err != nil {
panic(err)
}
@@ -323,10 +398,10 @@ func TestWriteZoneFileSynth(t *testing.T) {
; c4
@ IN A 192.30.252.153
IN A 192.30.252.154
-;myalias IN R53_ALIAS atype= zone_id=
-;myalias IN R53_ALIAS atype= zone_id=
+;myalias IN R53_ALIAS atype= zone_id= evaluate_target_health=
+;myalias IN R53_ALIAS atype= zone_id= evaluate_target_health=
www IN CNAME bosun.org.
-;zalias IN R53_ALIAS atype= zone_id=
+;zalias IN R53_ALIAS atype= zone_id= evaluate_target_health=
`
if buf.String() != expected {
t.Log(buf.String())
@@ -363,7 +438,7 @@ func TestWriteZoneFileOrder(t *testing.T) {
}
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, records, "stackoverflow.com")
+ writeZoneFileRR(buf, records, "stackoverflow.com")
// Compare
if buf.String() != testdataOrder {
t.Log("Found:")
@@ -383,7 +458,7 @@ func TestWriteZoneFileOrder(t *testing.T) {
}
// Generate
buf := &bytes.Buffer{}
- WriteZoneFileRR(buf, records, "stackoverflow.com")
+ writeZoneFileRR(buf, records, "stackoverflow.com")
// Compare
if buf.String() != testdataOrder {
t.Log(buf.String())
diff --git a/pkg/prettyzone/sorting.go b/pkg/prettyzone/sorting.go
index c5c373cb85..6ae20f0711 100644
--- a/pkg/prettyzone/sorting.go
+++ b/pkg/prettyzone/sorting.go
@@ -80,6 +80,12 @@ func (z *ZoneGenData) Less(i, j int) bool {
if pa != pb {
return pa < pb
}
+ case "SVCB", "HTTPS":
+ // sort by priority. If they are equal, sort by record.
+ if a.SvcPriority == b.SvcPriority {
+ return a.GetTargetField() < b.GetTargetField()
+ }
+ return a.SvcPriority < b.SvcPriority
case "PTR":
//ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
pa, pb := a.GetTargetField(), b.GetTargetField()
@@ -99,6 +105,20 @@ func (z *ZoneGenData) Less(i, j int) bool {
// flag set goes before ones without flag set
return fa > fb
}
+ case "DS":
+ pa, pb := a.DsKeyTag, b.DsKeyTag
+ if pa != pb {
+ return pa < pb
+ }
+ case "DNSKEY":
+ pa, pb := a.DnskeyFlags, b.DnskeyFlags
+ if pa != pb {
+ return pa < pb
+ }
+ fa, fb := a.DnskeyProtocol, b.DnskeyProtocol
+ if fa != fb {
+ return fa < fb
+ }
default:
// pass through. String comparison is sufficient.
}
diff --git a/pkg/printer/printer.go b/pkg/printer/printer.go
index f23fd91005..0686a5ea0b 100644
--- a/pkg/printer/printer.go
+++ b/pkg/printer/printer.go
@@ -16,6 +16,7 @@ type CLI interface {
StartDomain(domain string)
StartDNSProvider(name string, skip bool)
EndProvider(name string, numCorrections int, err error)
+ EndProvider2(name string, numCorrections int)
StartRegistrar(name string, skip bool)
PrintCorrection(n int, c *models.Correction)
@@ -31,6 +32,7 @@ type Printer interface {
Println(lines ...string)
Warnf(fmt string, args ...interface{})
Errorf(fmt string, args ...interface{})
+ PrintfIf(print bool, fmt string, args ...interface{})
}
// Debugf is called to print/format debug information.
@@ -54,8 +56,13 @@ func Warnf(fmt string, args ...interface{}) {
}
// Errorf is called to print/format an error.
-func Errorf(fmt string, args ...interface{}) {
- DefaultPrinter.Errorf(fmt, args...)
+// func Errorf(fmt string, args ...interface{}) {
+// DefaultPrinter.Errorf(fmt, args...)
+// }
+
+// PrintfIf is called to optionally print something.
+func PrintfIf(print bool, fmt string, args ...interface{}) {
+ DefaultPrinter.PrintfIf(print, fmt, args...)
}
var (
@@ -72,6 +79,9 @@ var (
// variable name is easy to grep for when we make the conversion.
var SkinnyReport = true
+// MaxReport represents how many records to show if SkinnyReport == true
+var MaxReport = 5
+
// ConsolePrinter is a handle for the console printer.
type ConsolePrinter struct {
Reader *bufio.Reader
@@ -97,7 +107,7 @@ func (c ConsolePrinter) PrintReport(i int, correction *models.Correction) {
// PromptToRun prompts the user to see if they want to execute a correction.
func (c ConsolePrinter) PromptToRun() bool {
- fmt.Fprint(c.Writer, "Run? (Y/n): ")
+ fmt.Fprint(c.Writer, "Run? (y/N): ")
txt, err := c.Reader.ReadString('\n')
run := true
if err != nil {
@@ -126,7 +136,7 @@ func (c ConsolePrinter) EndCorrection(err error) {
func (c ConsolePrinter) StartDNSProvider(provider string, skip bool) {
lbl := ""
if skip {
- lbl = " (skipping)\n"
+ lbl = " (skipping)"
}
if !SkinnyReport {
fmt.Fprintf(c.Writer, "----- DNS Provider: %s...%s\n", provider, lbl)
@@ -137,7 +147,7 @@ func (c ConsolePrinter) StartDNSProvider(provider string, skip bool) {
func (c ConsolePrinter) StartRegistrar(provider string, skip bool) {
lbl := ""
if skip {
- lbl = " (skipping)\n"
+ lbl = " (skipping)"
}
if !SkinnyReport {
fmt.Fprintf(c.Writer, "----- Registrar: %s...%s\n", provider, lbl)
@@ -161,6 +171,18 @@ func (c ConsolePrinter) EndProvider(name string, numCorrections int, err error)
}
}
+// EndProvider2 is called at the end of each provider.
+func (c ConsolePrinter) EndProvider2(name string, numCorrections int) {
+ plural := "s"
+ if numCorrections == 1 {
+ plural = ""
+ }
+ if (SkinnyReport) && (numCorrections == 0) {
+ return
+ }
+ fmt.Fprintf(c.Writer, "%d correction%s (%s)\n", numCorrections, plural, name)
+}
+
// Debugf is called to print/format debug information.
func (c ConsolePrinter) Debugf(format string, args ...interface{}) {
if c.Verbose {
@@ -187,3 +209,10 @@ func (c ConsolePrinter) Warnf(format string, args ...interface{}) {
func (c ConsolePrinter) Errorf(format string, args ...interface{}) {
fmt.Fprintf(c.Writer, "ERROR: "+format, args...)
}
+
+// PrintfIf is called to optionally print/format a message.
+func (c ConsolePrinter) PrintfIf(print bool, format string, args ...interface{}) {
+ if print {
+ fmt.Fprintf(c.Writer, format, args...)
+ }
+}
diff --git a/pkg/rejectif/caa.go b/pkg/rejectif/caa.go
index c538e48bc1..f11d6a2715 100644
--- a/pkg/rejectif/caa.go
+++ b/pkg/rejectif/caa.go
@@ -27,10 +27,10 @@ func CaaTargetContainsWhitespace(rc *models.RecordConfig) error {
return nil
}
-// CaaTargetHasSemicolon identifies CAA records that contain semicolons.
-func CaaTargetHasSemicolon(rc *models.RecordConfig) error {
- if strings.Contains(rc.GetTargetField(), ";") {
- return fmt.Errorf("caa target contains semicolon")
- }
- return nil
-}
+// // CaaTargetHasSemicolon identifies CAA records that contain semicolons.
+// func CaaTargetHasSemicolon(rc *models.RecordConfig) error {
+// if strings.Contains(rc.GetTargetField(), ";") {
+// return fmt.Errorf("caa target contains semicolon")
+// }
+// return nil
+// }
diff --git a/pkg/rejectif/txt.go b/pkg/rejectif/txt.go
index 8a0883b8ce..0df3be1962 100644
--- a/pkg/rejectif/txt.go
+++ b/pkg/rejectif/txt.go
@@ -9,96 +9,88 @@ import (
// Keep these in alphabetical order.
-// TxtHasBackticks audits TXT records for strings that contain backticks.
-func TxtHasBackticks(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if strings.Contains(txt, "`") {
- return fmt.Errorf("txtstring contains backtick")
- }
+// TxtHasBackslash audits TXT records for strings that contains one or more backslashes.
+func TxtHasBackslash(rc *models.RecordConfig) error {
+ if strings.Contains(rc.GetTargetTXTJoined(), `\`) {
+ return fmt.Errorf("txtstring contains backslashes")
}
return nil
}
-// TxtHasSingleQuotes audits TXT records for strings that contain single-quotes.
-func TxtHasSingleQuotes(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if strings.Contains(txt, "'") {
- return fmt.Errorf("txtstring contains single-quotes")
- }
+// TxtHasBackticks audits TXT records for strings that contain backticks.
+func TxtHasBackticks(rc *models.RecordConfig) error {
+ if strings.Contains(rc.GetTargetTXTJoined(), "`") {
+ return fmt.Errorf("txtstring contains backtick")
}
return nil
}
// TxtHasDoubleQuotes audits TXT records for strings that contain doublequotes.
func TxtHasDoubleQuotes(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if strings.Contains(txt, `"`) {
- return fmt.Errorf("txtstring contains doublequotes")
- }
+ if strings.Contains(rc.GetTargetTXTJoined(), `"`) {
+ return fmt.Errorf("txtstring contains doublequotes")
}
return nil
}
-// TxtIsExactlyLen255 audits TXT records for strings exactly 255 octets long.
-// This is rare; you probably want to use TxtNoStringsLen256orLonger() instead.
-func TxtIsExactlyLen255(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if len(txt) == 255 {
- return fmt.Errorf("txtstring length is 255")
- }
+// TxtHasSemicolon audits TXT records for strings that contain backticks.
+func TxtHasSemicolon(rc *models.RecordConfig) error {
+ if strings.Contains(rc.GetTargetTXTJoined(), ";") {
+ return fmt.Errorf("txtstring contains semicolon")
}
return nil
}
-// TxtHasSegmentLen256orLonger audits TXT records for strings that are >255 octets.
-func TxtHasSegmentLen256orLonger(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if len(txt) > 255 {
- return fmt.Errorf("%q txtstring length > 255", rc.GetLabel())
- }
+// TxtHasSingleQuotes audits TXT records for strings that contain single-quotes.
+func TxtHasSingleQuotes(rc *models.RecordConfig) error {
+ if strings.Contains(rc.GetTargetTXTJoined(), "'") {
+ return fmt.Errorf("txtstring contains single-quotes")
}
return nil
}
-// TxtHasMultipleSegments audits TXT records for multiple strings
-func TxtHasMultipleSegments(rc *models.RecordConfig) error {
- if len(rc.TxtStrings) > 1 {
- return fmt.Errorf("multiple strings in one txt")
+// TxtHasTrailingSpace audits TXT records for strings that end with space.
+func TxtHasTrailingSpace(rc *models.RecordConfig) error {
+ txt := rc.GetTargetTXTJoined()
+ if txt != "" && txt[ultimate(txt)] == ' ' {
+ return fmt.Errorf("txtstring ends with space")
}
return nil
}
-// TxtHasTrailingSpace audits TXT records for strings that end with space.
-func TxtHasTrailingSpace(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if txt != "" && txt[ultimate(txt)] == ' ' {
- return fmt.Errorf("txtstring ends with space")
- }
+// TxtHasUnpairedDoubleQuotes audits TXT records for strings that contain unpaired doublequotes.
+func TxtHasUnpairedDoubleQuotes(rc *models.RecordConfig) error {
+ if strings.Count(rc.GetTargetTXTJoined(), `"`)%2 == 1 {
+ return fmt.Errorf("txtstring contains unpaired doublequotes")
}
return nil
}
// TxtIsEmpty audits TXT records for empty strings.
func TxtIsEmpty(rc *models.RecordConfig) error {
- // There must be strings.
- if len(rc.TxtStrings) == 0 {
- return fmt.Errorf("txt with no strings")
- }
- // Each string must be non-empty.
- for _, txt := range rc.TxtStrings {
- if len(txt) == 0 {
- return fmt.Errorf("txtstring is empty")
- }
+ if len(rc.GetTargetTXTJoined()) == 0 {
+ return fmt.Errorf("txtstring is empty")
}
return nil
}
-// TxtHasUnpairedDoubleQuotes audits TXT records for strings that contain unpaired doublequotes.
-func TxtHasUnpairedDoubleQuotes(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if strings.Count(txt, `"`)%2 == 1 {
- return fmt.Errorf("txtstring contains unpaired doublequotes")
+// TxtLongerThan returns a function that audits TXT records for length
+// greater than maxLength.
+func TxtLongerThan(maxLength int) func(rc *models.RecordConfig) error {
+ return func(rc *models.RecordConfig) error {
+ m := maxLength
+ if len(rc.GetTargetTXTJoined()) > m {
+ return fmt.Errorf("TXT records longer than %d octets (chars)", m)
}
+ return nil
+ }
+}
+
+// TxtStartsOrEndsWithSpaces audits TXT records that starts or ends with spaces
+func TxtStartsOrEndsWithSpaces(rc *models.RecordConfig) error {
+ txt := rc.GetTargetTXTJoined()
+ if len(txt) > 0 && (txt[0] == ' ' || txt[len(txt)-1] == ' ') {
+ return fmt.Errorf("txtstring starts or ends with spaces")
}
return nil
}
diff --git a/pkg/rfc4183/ipv6.go b/pkg/rfc4183/ipv6.go
new file mode 100644
index 0000000000..d6595413e7
--- /dev/null
+++ b/pkg/rfc4183/ipv6.go
@@ -0,0 +1,26 @@
+package rfc4183
+
+import (
+ "fmt"
+)
+
+// reverseIPv6 returns the ipv6.arpa string suitable for reverse DNS lookups.
+func reverseIPv6(ip []byte, maskbits int) (arpa string, err error) {
+ // Must be IPv6
+ if len(ip) != 16 {
+ return "", fmt.Errorf("not IPv6")
+ }
+
+ buf := []byte("x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.ip6.arpa")
+ // Poke hex digits into the template
+ pos := 128/4*2 - 2 // Position of the last "x"
+ for _, v := range ip {
+ buf[pos] = hexDigit[v>>4]
+ buf[pos-2] = hexDigit[v&0xF]
+ pos = pos - 4
+ }
+ // Return only the parts without x's
+ return string(buf[(128-maskbits)/4*2:]), nil
+}
+
+const hexDigit = "0123456789abcdef"
diff --git a/pkg/rfc4183/mode.go b/pkg/rfc4183/mode.go
new file mode 100644
index 0000000000..587171c8ca
--- /dev/null
+++ b/pkg/rfc4183/mode.go
@@ -0,0 +1,52 @@
+package rfc4183
+
+import (
+ "fmt"
+ "strings"
+)
+
+var newmode bool
+var modeset bool
+
+// SetCompatibilityMode sets REV() compatibility mode.
+func SetCompatibilityMode(m string) error {
+ if modeset {
+ return fmt.Errorf("ERROR: REVCOMPAT() already set")
+ }
+ modeset = true
+
+ switch strings.ToLower(m) {
+ case "rfc2317", "2317", "2", "old":
+ newmode = false
+ case "rfc4183", "4183", "4":
+ newmode = true
+ default:
+ return fmt.Errorf("invalid value %q, must be rfc2317 or rfc4182", m)
+ }
+ return nil
+}
+
+// IsRFC4183Mode returns true if REV() is in RFC4183 mode.
+func IsRFC4183Mode() bool {
+ return newmode
+}
+
+var warningNeeded bool = false
+
+// NeedsWarning sets that a future warning regarding RFC2317
+// compatibility is needed.
+func NeedsWarning() {
+ warningNeeded = true
+}
+
+// PrintWarning prints a warning if a warning related to RFC2317 is needed.
+func PrintWarning() {
+ if modeset {
+ // No warnings if REVCOMPAT() was used.
+ return
+ }
+ if !warningNeeded {
+ return
+ }
+ fmt.Printf("WARNING: REV() breaking change coming in v5.0. See https://docs.dnscontrol.org/functions/REVCOMPAT\n")
+}
diff --git a/pkg/rfc4183/reverse.go b/pkg/rfc4183/reverse.go
new file mode 100644
index 0000000000..25b2c9587d
--- /dev/null
+++ b/pkg/rfc4183/reverse.go
@@ -0,0 +1,79 @@
+package rfc4183
+
+import (
+ "fmt"
+ "net/netip"
+ "strings"
+)
+
+// ReverseDomainName implements RFC4183 for turning a CIDR block into
+// a in-addr name. IP addresses are assumed to be /32 or /128 CIDR blocks.
+// CIDR host bits are changed to 0s.
+func ReverseDomainName(cidr string) (string, error) {
+
+ // Mask missing? Add it.
+ if !strings.Contains(cidr, "/") {
+ a, err := netip.ParseAddr(cidr)
+ if err != nil {
+ return "", fmt.Errorf("not an IP address: %w", err)
+ }
+ if a.Is4() {
+ cidr = cidr + "/32"
+ } else {
+ cidr = cidr + "/128"
+ }
+ }
+
+ // Parse the CIDR.
+ p, err := netip.ParsePrefix(cidr)
+ if err != nil {
+ return "", fmt.Errorf("not a CIDR block: %w", err)
+ }
+
+ // RFC4183 4.1 step 4: The notion of fewer than 8 mask bits is not reasonable.
+ if p.Bits() < 8 {
+ return "", fmt.Errorf("mask fewer than 8 bits is unreasonable: %s", cidr)
+ }
+
+ // Handle IPv6 separately:
+ if p.Addr().Is6() {
+ return reverseIPv6(p.Addr().AsSlice(), p.Bits())
+ }
+
+ // Zero out any host bits.
+ p = p.Masked()
+
+ // IPv4: Implement the RFC4183 process:
+
+ // 4.p Step 1
+ b := p.Addr().AsSlice()
+ x, y, z, w := b[0], b[1], b[2], b[3]
+ m := p.Bits()
+
+ if m == 8 {
+ return fmt.Sprintf("%d.in-addr.arpa", x), nil
+ }
+ if m == 16 {
+ return fmt.Sprintf("%d.%d.in-addr.arpa", y, x), nil
+ }
+ if m == 24 {
+ return fmt.Sprintf("%d.%d.%d.in-addr.arpa", z, y, x), nil
+ }
+ if m == 32 {
+ return fmt.Sprintf("%d.%d.%d.%d.in-addr.arpa", w, z, y, x), nil
+ }
+
+ // 4.1 Step 2
+ n := w // I don't understand why the RFC changes variable names at this point, but it does.
+ if m >= 24 && m <= 32 {
+ return fmt.Sprintf("%d-%d.%d.%d.%d.in-addr.arpa", n, m, z, y, x), nil
+ }
+ if m >= 16 && m < 24 {
+ return fmt.Sprintf("%d-%d.%d.%d.in-addr.arpa", z, m, y, x), nil
+ }
+ if m >= 8 && m < 16 {
+ return fmt.Sprintf("%d-%d.%d.in-addr.arpa", y, m, x), nil
+ }
+ return "", fmt.Errorf("fewer than 8 mask bits is not reasonable: %v", cidr)
+
+}
diff --git a/pkg/rfc4183/reverse_test.go b/pkg/rfc4183/reverse_test.go
new file mode 100644
index 0000000000..4c53bced32
--- /dev/null
+++ b/pkg/rfc4183/reverse_test.go
@@ -0,0 +1,125 @@
+package rfc4183
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestReverse(t *testing.T) {
+ var tests = []struct {
+ in string
+ out string
+ }{
+ // IPv4 "Classless in-addr.arpa delegation" RFC4183.
+ // Examples in the RFC:
+ {"10.100.2.0/26", "0-26.2.100.10.in-addr.arpa"},
+ {"10.192.0.0/13", "192-13.10.in-addr.arpa"},
+ {"10.20.128.0/23", "128-23.20.10.in-addr.arpa"},
+ {"10.20.129.0/23", "128-23.20.10.in-addr.arpa"}, // Not in the RFC but should be!
+
+ // IPv6
+ {"2001::/16", "1.0.0.2.ip6.arpa"},
+ {"2001:0db8:0123:4567:89ab:cdef:1234:5670/64", "7.6.5.4.3.2.1.0.8.b.d.0.1.0.0.2.ip6.arpa"},
+ {"2001:0db8:0123:4567:89ab:cdef:1234:5670/68", "8.7.6.5.4.3.2.1.0.8.b.d.0.1.0.0.2.ip6.arpa"},
+ {"2001:0db8:0123:4567:89ab:cdef:1234:5670/124", "7.6.5.4.3.2.1.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1.0.8.b.d.0.1.0.0.2.ip6.arpa"},
+ {"2001:0db8:0123:4567:89ab:cdef:1234:5678/128", "8.7.6.5.4.3.2.1.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1.0.8.b.d.0.1.0.0.2.ip6.arpa"},
+
+ // 8-bit boundaries
+ {"174.0.0.0/8", "174.in-addr.arpa"},
+ {"174.136.43.0/8", "174.in-addr.arpa"},
+ {"174.136.0.44/8", "174.in-addr.arpa"},
+ {"174.136.45.45/8", "174.in-addr.arpa"},
+ {"174.136.0.0/16", "136.174.in-addr.arpa"},
+ {"174.136.43.0/16", "136.174.in-addr.arpa"},
+ {"174.136.44.255/16", "136.174.in-addr.arpa"},
+ {"174.136.107.0/24", "107.136.174.in-addr.arpa"},
+ {"174.136.107.1/24", "107.136.174.in-addr.arpa"},
+ {"174.136.107.14/32", "14.107.136.174.in-addr.arpa"},
+
+ // /25 (all cases)
+ {"174.1.0.0/25", "0-25.0.1.174.in-addr.arpa"},
+ {"174.1.0.128/25", "128-25.0.1.174.in-addr.arpa"},
+ {"174.1.0.129/25", "128-25.0.1.174.in-addr.arpa"}, // host bits
+ // /26 (all cases)
+ {"174.1.0.0/26", "0-26.0.1.174.in-addr.arpa"},
+ {"174.1.0.0/26", "0-26.0.1.174.in-addr.arpa"},
+ {"174.1.0.64/26", "64-26.0.1.174.in-addr.arpa"},
+ {"174.1.0.128/26", "128-26.0.1.174.in-addr.arpa"},
+ {"174.1.0.192/26", "192-26.0.1.174.in-addr.arpa"},
+ {"174.1.0.194/26", "192-26.0.1.174.in-addr.arpa"}, // host bits
+ // /27 (all cases)
+ {"174.1.0.0/27", "0-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.32/27", "32-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.64/27", "64-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.96/27", "96-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.128/27", "128-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.160/27", "160-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.192/27", "192-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.224/27", "224-27.0.1.174.in-addr.arpa"},
+ {"174.1.0.225/27", "224-27.0.1.174.in-addr.arpa"}, // host bits
+ // /28 (first 2, last 2)
+ {"174.1.0.0/28", "0-28.0.1.174.in-addr.arpa"},
+ {"174.1.0.16/28", "16-28.0.1.174.in-addr.arpa"},
+ {"174.1.0.224/28", "224-28.0.1.174.in-addr.arpa"},
+ {"174.1.0.240/28", "240-28.0.1.174.in-addr.arpa"},
+ {"174.1.0.241/28", "240-28.0.1.174.in-addr.arpa"}, // host bits
+ // /29 (first 2 cases)
+ {"174.1.0.0/29", "0-29.0.1.174.in-addr.arpa"},
+ {"174.1.0.8/29", "8-29.0.1.174.in-addr.arpa"},
+ {"174.1.0.9/29", "8-29.0.1.174.in-addr.arpa"}, // host bits
+ // /30 (first 2 cases)
+ {"174.1.0.0/30", "0-30.0.1.174.in-addr.arpa"},
+ {"174.1.0.4/30", "4-30.0.1.174.in-addr.arpa"},
+ {"174.1.0.5/30", "4-30.0.1.174.in-addr.arpa"}, // host bits
+ // /31 (first 2 cases)
+ {"174.1.0.0/31", "0-31.0.1.174.in-addr.arpa"},
+ {"174.1.0.2/31", "2-31.0.1.174.in-addr.arpa"},
+ {"174.1.0.3/31", "2-31.0.1.174.in-addr.arpa"}, // host bits
+
+ // Other tests:
+ {"10.100.2.255/23", "2-23.100.10.in-addr.arpa"},
+ {"10.100.2.255/22", "0-22.100.10.in-addr.arpa"},
+ {"10.100.2.255/21", "0-21.100.10.in-addr.arpa"},
+ {"10.100.2.255/20", "0-20.100.10.in-addr.arpa"},
+ {"10.100.2.255/19", "0-19.100.10.in-addr.arpa"},
+ {"10.100.2.255/18", "0-18.100.10.in-addr.arpa"},
+ {"10.100.2.255/17", "0-17.100.10.in-addr.arpa"},
+ //
+ {"10.100.2.255/15", "100-15.10.in-addr.arpa"},
+ {"10.100.2.255/14", "100-14.10.in-addr.arpa"},
+ {"10.100.2.255/13", "96-13.10.in-addr.arpa"},
+ {"10.100.2.255/12", "96-12.10.in-addr.arpa"},
+ {"10.100.2.255/11", "96-11.10.in-addr.arpa"},
+ {"10.100.2.255/10", "64-10.10.in-addr.arpa"},
+ {"10.100.2.255/9", "0-9.10.in-addr.arpa"},
+ }
+ for i, tst := range tests {
+ t.Run(fmt.Sprintf("%d--%s", i, tst.in), func(t *testing.T) {
+ d, err := ReverseDomainName(tst.in)
+ if err != nil {
+ t.Errorf("Should not have errored: %v", err)
+ } else if d != tst.out {
+ t.Errorf("Expected '%s' but got '%s'", tst.out, d)
+ }
+ })
+ }
+}
+
+func TestReverseErrors(t *testing.T) {
+ var tests = []struct {
+ in string
+ }{
+ {"0.0.0.0/0"},
+ {"2001::/0"},
+ {"4.5/16"},
+ {"foo.com"},
+ }
+ for i, tst := range tests {
+ t.Run(fmt.Sprintf("%d--%s", i, tst.in), func(t *testing.T) {
+ d, err := ReverseDomainName(tst.in)
+ if err == nil {
+ t.Errorf("Should have errored, but didn't. Got %s", d)
+ }
+ })
+ }
+}
diff --git a/pkg/rtypecontrol/pave.go b/pkg/rtypecontrol/pave.go
new file mode 100644
index 0000000000..871eaab654
--- /dev/null
+++ b/pkg/rtypecontrol/pave.go
@@ -0,0 +1,50 @@
+package rtypecontrol
+
+import (
+ "fmt"
+ "strconv"
+)
+
+// PaveArgs converts each arg to its desired type, or returns an error if conversion fails or if the number of arguments is wrong.
+// argTypes is a string where each rune specifies the desired type of the arg in the same position:
+// 'i': uinet16 (will convert strings, truncate floats, etc)
+// 's': Valid only if string.
+func PaveArgs(args []any, argTypes string) error {
+
+ if len(args) != len(argTypes) {
+ return fmt.Errorf("wrong number of arguments. Expected %v, got %v", len(argTypes), len(args))
+ }
+
+ for i, at := range argTypes {
+ arg := args[i]
+ switch at {
+
+ case 'i': // uint16
+ if s, ok := arg.(string); ok { // Is this a string-encoded int?
+ ni, err := strconv.Atoi(s)
+ if err != nil {
+ return fmt.Errorf("value %q is not a number (uint16 wanted)", arg)
+ }
+ args[i] = uint16(ni)
+ } else if _, ok := arg.(float64); ok {
+ args[i] = uint16(arg.(float64))
+ } else if _, ok := arg.(uint16); ok {
+ args[i] = arg.(uint16)
+ } else if _, ok := arg.(int); ok {
+ args[i] = uint16(arg.(int))
+ } else {
+ return fmt.Errorf("value %q is type %T, expected uint16", arg, arg)
+ }
+
+ case 's':
+ if _, ok := arg.(string); ok {
+ args[i] = arg.(string)
+ } else {
+ args[i] = fmt.Sprintf("%v", arg)
+ }
+
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/rtypecontrol/pave_test.go b/pkg/rtypecontrol/pave_test.go
new file mode 100644
index 0000000000..bee5f80b84
--- /dev/null
+++ b/pkg/rtypecontrol/pave_test.go
@@ -0,0 +1,63 @@
+package rtypecontrol
+
+import "testing"
+
+func TestPaveArgs(t *testing.T) {
+ tests := []struct {
+ name string
+ dataArgs []any
+ dataRule string
+ wantErr bool
+ }{
+ {
+ name: "string",
+ dataArgs: []any{"one"},
+ dataRule: "s",
+ wantErr: false,
+ },
+ {
+ name: "int to string",
+ dataArgs: []any{100},
+ dataRule: "s",
+ wantErr: false,
+ },
+
+ {
+ name: "uint16",
+ dataArgs: []any{uint16(1)},
+ dataRule: "i",
+ wantErr: false,
+ },
+ {
+ name: "float to uint16",
+ dataArgs: []any{float64(2)},
+ dataRule: "i",
+ wantErr: false,
+ },
+ {
+ name: "int uint16",
+ dataArgs: []any{int(3)},
+ dataRule: "i",
+ wantErr: false,
+ },
+ {
+ name: "string to uint16",
+ dataArgs: []any{"111"},
+ dataRule: "i",
+ wantErr: false,
+ },
+ {
+ name: "txt to uint16",
+ dataArgs: []any{"one"},
+ dataRule: "i",
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := PaveArgs(tt.dataArgs, tt.dataRule); (err != nil) != tt.wantErr {
+ t.Errorf("PaveArgs() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/pkg/rtypecontrol/rtypecontrol.go b/pkg/rtypecontrol/rtypecontrol.go
new file mode 100644
index 0000000000..f40fdf48bf
--- /dev/null
+++ b/pkg/rtypecontrol/rtypecontrol.go
@@ -0,0 +1,21 @@
+package rtypecontrol
+
+import "github.com/StackExchange/dnscontrol/v4/providers"
+
+var validTypes = map[string]struct{}{}
+
+func Register(t string) {
+ // Does this already exist?
+ if _, ok := validTypes[t]; ok {
+ panic("rtype %q already registered. Can't register it a second time!")
+ }
+
+ validTypes[t] = struct{}{}
+
+ providers.RegisterCustomRecordType(t, "", "")
+}
+
+func IsValid(t string) bool {
+ _, ok := validTypes[t]
+ return ok
+}
diff --git a/pkg/rtypes/postprocess.go b/pkg/rtypes/postprocess.go
new file mode 100644
index 0000000000..91a14ddad7
--- /dev/null
+++ b/pkg/rtypes/postprocess.go
@@ -0,0 +1,60 @@
+package rtypes
+
+import (
+ "fmt"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
+)
+
+func PostProcess(domains []*models.DomainConfig) error {
+
+ var err error
+
+ for _, dc := range domains {
+
+ for _, rawRec := range dc.RawRecords {
+ rec := &models.RecordConfig{
+ Type: rawRec.Type,
+ TTL: rawRec.TTL,
+ Name: rawRec.Args[0].(string),
+ Metadata: map[string]string{},
+ }
+
+ // Copy the metadata (convert everything to string)
+ for _, m := range rawRec.Metas {
+ for mk, mv := range m {
+ if v, ok := mv.(string); ok {
+ rec.Metadata[mk] = v // Already a string. No new malloc.
+ } else {
+ rec.Metadata[mk] = fmt.Sprintf("%v", mv)
+ }
+ }
+ }
+
+ // Call the proper initialize function.
+ // TODO(tlim): Good candiate for an interface or a lookup table.
+ switch rawRec.Type {
+
+ case "CLOUDFLAREAPI_SINGLE_REDIRECT":
+ err = cfsingleredirect.FromRaw(rec, rawRec.Args)
+ rec.SetLabel("@", dc.Name)
+
+ default:
+ err = fmt.Errorf("unknown rawrec type=%q", rawRec.Type)
+ }
+ if err != nil {
+ return fmt.Errorf("%s (%q, %q) record error: %w", rawRec.Type, rec.Name, dc.Name, err)
+ }
+
+ // Free memeory:
+ clear(rawRec.Args)
+ rawRec.Args = nil
+
+ dc.Records = append(dc.Records, rec)
+ }
+ dc.RawRecords = nil
+ }
+
+ return nil
+}
diff --git a/pkg/spflib/parse.go b/pkg/spflib/parse.go
index 3dbbf75ab9..b48f9d6b31 100644
--- a/pkg/spflib/parse.go
+++ b/pkg/spflib/parse.go
@@ -1,9 +1,7 @@
package spflib
import (
- "bytes"
"fmt"
- "io"
"strings"
)
@@ -12,20 +10,6 @@ type SPFRecord struct {
Parts []*SPFPart
}
-// Lookups returns the number of DNS lookups required by s.
-func (s *SPFRecord) Lookups() int {
- count := 0
- for _, p := range s.Parts {
- if p.IsLookup {
- count++
- }
- if p.IncludeRecord != nil {
- count += p.IncludeRecord.Lookups()
- }
- }
- return count
-}
-
// SPFPart stores a part of an SPF record, with attributes.
type SPFPart struct {
Text string
@@ -101,29 +85,3 @@ func Parse(text string, dnsres Resolver) (*SPFRecord, error) {
}
return rec, nil
}
-
-func dump(rec *SPFRecord, indent string, w io.Writer) {
-
- fmt.Fprintf(w, "%sTotal Lookups: %d\n", indent, rec.Lookups())
- fmt.Fprint(w, indent+"v=spf1")
- for _, p := range rec.Parts {
- fmt.Fprint(w, " "+p.Text)
- }
- fmt.Fprintln(w)
- indent += "\t"
- for _, p := range rec.Parts {
- if p.IsLookup {
- fmt.Fprintln(w, indent+p.Text)
- }
- if p.IncludeRecord != nil {
- dump(p.IncludeRecord, indent+"\t", w)
- }
- }
-}
-
-// Print prints an SPFRecord.
-func (s *SPFRecord) Print() string {
- w := &bytes.Buffer{}
- dump(s, "", w)
- return w.String()
-}
diff --git a/pkg/spflib/parse_test.go b/pkg/spflib/parse_test.go
index 8faedb5409..098e4f7794 100644
--- a/pkg/spflib/parse_test.go
+++ b/pkg/spflib/parse_test.go
@@ -1,10 +1,53 @@
package spflib
import (
+ "bytes"
+ "fmt"
+ "io"
"strings"
"testing"
)
+func dump(rec *SPFRecord, indent string, w io.Writer) {
+
+ fmt.Fprintf(w, "%sTotal Lookups: %d\n", indent, rec.Lookups())
+ fmt.Fprint(w, indent+"v=spf1")
+ for _, p := range rec.Parts {
+ fmt.Fprint(w, " "+p.Text)
+ }
+ fmt.Fprintln(w)
+ indent += "\t"
+ for _, p := range rec.Parts {
+ if p.IsLookup {
+ fmt.Fprintln(w, indent+p.Text)
+ }
+ if p.IncludeRecord != nil {
+ dump(p.IncludeRecord, indent+"\t", w)
+ }
+ }
+}
+
+// Lookups returns the number of DNS lookups required by s.
+func (s *SPFRecord) Lookups() int {
+ count := 0
+ for _, p := range s.Parts {
+ if p.IsLookup {
+ count++
+ }
+ if p.IncludeRecord != nil {
+ count += p.IncludeRecord.Lookups()
+ }
+ }
+ return count
+}
+
+// Print prints an SPFRecord.
+func (s *SPFRecord) Print() string {
+ w := &bytes.Buffer{}
+ dump(s, "", w)
+ return w.String()
+}
+
func TestParse(t *testing.T) {
dnsres, err := NewCache("testdata-dns1.json")
if err != nil {
diff --git a/pkg/transform/arpa.go b/pkg/transform/arpa.go
index 12a8370d72..704222bbe4 100644
--- a/pkg/transform/arpa.go
+++ b/pkg/transform/arpa.go
@@ -2,115 +2,60 @@ package transform
import (
"fmt"
- "net"
+ "net/netip"
"strings"
+
+ "github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
)
// ReverseDomainName turns a CIDR block into a reversed (in-addr) name.
+// For cases not covered by RFC2317, implement RFC4183
+// The host bits must all be zeros.
func ReverseDomainName(cidr string) (string, error) {
- // If it is an IP address, add the /32 or /128
- ip := net.ParseIP(cidr)
- if ip != nil {
- if ip.To4() != nil {
+ if rfc4183.IsRFC4183Mode() {
+ return rfc4183.ReverseDomainName(cidr)
+ }
+
+ // Mask missing? Add it.
+ if !strings.Contains(cidr, "/") {
+ a, err := netip.ParseAddr(cidr)
+ if err != nil {
+ return "", fmt.Errorf("not an IP address: %w", err)
+ }
+ if a.Is4() {
cidr = cidr + "/32"
} else {
cidr = cidr + "/128"
}
}
- a, c, err := net.ParseCIDR(cidr)
- if err != nil {
- return "", err
- }
- base, err := reverseaddr(a.String())
+ // Parse the CIDR.
+ p, err := netip.ParsePrefix(cidr)
if err != nil {
- return "", err
- }
- base = strings.TrimRight(base, ".")
- if !a.Equal(c.IP) {
- return "", fmt.Errorf("CIDR %v has 1 bits beyond the mask", cidr)
- }
-
- bits, total := c.Mask.Size()
- var toTrim int
- if bits == 0 {
- return "", fmt.Errorf("cannot use /0 in reverse CIDR")
+ return "", fmt.Errorf("not a CIDR block: %w", err)
}
+ bits := p.Bits()
- // Handle IPv4 "Classless in-addr.arpa delegation" RFC2317:
- if total == 32 && bits >= 25 && bits < 32 {
- // first address / netmask . Class-b-arpa.
- fparts := strings.Split(c.IP.String(), ".")
- first := fparts[3]
- bparts := strings.SplitN(base, ".", 2)
- return fmt.Sprintf("%s/%d.%s", first, bits, bparts[1]), nil
+ if p.Masked() != p {
+ return "", fmt.Errorf("CIDR %v has 1 bits beyond the mask", cidr)
}
- // Handle IPv4 Class-full and IPv6:
- if total == 32 {
- if bits%8 != 0 {
- return "", fmt.Errorf("IPv4 mask must be multiple of 8 bits")
- }
- toTrim = (total - bits) / 8
- } else if total == 128 {
- if bits%4 != 0 {
- return "", fmt.Errorf("IPv6 mask must be multiple of 4 bits")
- }
- toTrim = (total - bits) / 4
- } else {
- return "", fmt.Errorf("invalid address (not IPv4 or IPv6): %v", cidr)
+ // Cases where RFC4183 is the same as RFC2317:
+ // IPV6, /0 - /24, /32
+ if strings.Contains(cidr, ":") || bits <= 24 || bits == 32 {
+ // There is no p.Is6() so we test for ":" as a workaround.
+ return rfc4183.ReverseDomainName(cidr)
}
- parts := strings.SplitN(base, ".", toTrim+1)
- return parts[len(parts)-1], nil
-}
-
-// copied from go source.
-// https://github.com/golang/go/blob/bfc164c64d33edfaf774b5c29b9bf5648a6447fb/src/net/dnsclient.go#L15
+ // Record that the change to --revmode default will affect this configuration
+ rfc4183.NeedsWarning()
-// reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP
-// address addr suitable for rDNS (PTR) record lookup or an error if it fails
-// to parse the IP address.
-func reverseaddr(addr string) (arpa string, err error) {
- ip := net.ParseIP(addr)
- if ip == nil {
- return "", &net.DNSError{Err: "unrecognized address", Name: addr}
- }
- if ip.To4() != nil {
- return uitoa(uint(ip[15])) + "." + uitoa(uint(ip[14])) + "." + uitoa(uint(ip[13])) + "." + uitoa(uint(ip[12])) + ".in-addr.arpa.", nil
- }
- // Must be IPv6
- buf := make([]byte, 0, len(ip)*4+len("ip6.arpa."))
- // Add it, in reverse, to the buffer
- for i := len(ip) - 1; i >= 0; i-- {
- v := ip[i]
- buf = append(buf, hexDigit[v&0xF])
- buf = append(buf, '.')
- buf = append(buf, hexDigit[v>>4])
- buf = append(buf, '.')
- }
- // Append "ip6.arpa." and return (buf already has the final .)
- buf = append(buf, "ip6.arpa."...)
- return string(buf), nil
-}
+ // Handle IPv4 "Classless in-addr.arpa delegation" RFC2317:
+ // if bits >= 25 && bits < 32 {
+ // first address / netmask . Class-b-arpa.
-// Convert unsigned integer to decimal string.
-func uitoa(val uint) string {
- if val == 0 { // avoid string allocation
- return "0"
- }
- var buf [20]byte // big enough for 64bit value base 10
- i := len(buf) - 1
- for val >= 10 {
- q := val / 10
- buf[i] = byte('0' + val - q*10)
- i--
- val = q
- }
- // val < 10
- buf[i] = byte('0' + val)
- return string(buf[i:])
+ ip := p.Addr().AsSlice()
+ return fmt.Sprintf("%d/%d.%d.%d.%d.in-addr.arpa",
+ ip[3], bits, ip[2], ip[1], ip[0]), nil
}
-
-const hexDigit = "0123456789abcdef"
diff --git a/pkg/transform/arpa_test.go b/pkg/transform/arpa_test.go
index 67646e0376..a1486a9369 100644
--- a/pkg/transform/arpa_test.go
+++ b/pkg/transform/arpa_test.go
@@ -73,6 +73,10 @@ func TestReverse(t *testing.T) {
{"174.1.0.0/31", false, "0/31.0.1.174.in-addr.arpa"},
{"174.1.0.2/31", false, "2/31.0.1.174.in-addr.arpa"},
+ // Use RFC4183 for cases not covered by RFC2317:
+ {"10.20.128.0/23", false, "128-23.20.10.in-addr.arpa"},
+ {"10.192.0.0/13", false, "192-13.10.in-addr.arpa"},
+
// Error Cases:
{"0.0.0.0/0", true, ""},
{"2001::/0", true, ""},
diff --git a/pkg/txtutil/state_string.go b/pkg/txtutil/state_string.go
new file mode 100644
index 0000000000..13d1b27c18
--- /dev/null
+++ b/pkg/txtutil/state_string.go
@@ -0,0 +1,27 @@
+// Code generated by "stringer -type=State"; DO NOT EDIT.
+
+package txtutil
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[StateStart-0]
+ _ = x[StateUnquoted-1]
+ _ = x[StateQuoted-2]
+ _ = x[StateBackslash-3]
+ _ = x[StateWantSpace-4]
+}
+
+const _State_name = "StateStartStateUnquotedStateQuotedStateBackslashStateWantSpace"
+
+var _State_index = [...]uint8{0, 10, 23, 34, 48, 62}
+
+func (i State) String() string {
+ if i < 0 || i >= State(len(_State_index)-1) {
+ return "State(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _State_name[_State_index[i]:_State_index[i+1]]
+}
diff --git a/pkg/txtutil/txtcode.go b/pkg/txtutil/txtcode.go
new file mode 100644
index 0000000000..b125cac4fa
--- /dev/null
+++ b/pkg/txtutil/txtcode.go
@@ -0,0 +1,165 @@
+//go:generate stringer -type=State
+
+package txtutil
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+// ParseQuoted parses a string of RFC1035-style quoted items. The resulting
+// items are then joined into one string. This is useful for parsing TXT
+// records.
+// Examples:
+// `foo` => foo
+// `"foo"` => foo
+// `"f\"oo"` => f"oo
+// `"f\\oo"` => f\oo
+// `"foo" "bar"` => foobar
+// `"foo" bar` => foobar
+func ParseQuoted(s string) (string, error) {
+ return txtDecode(s)
+}
+
+// EncodeQuoted encodes a string into a series of quoted 255-octet chunks. That
+// is, when decoded each chunk would be 255-octets with the remainder in the
+// last chunk.
+//
+// The output looks like:
+//
+// `""` empty
+// `"255\"octets"` quotes are escaped
+// `"255\\octets"` backslashes are escaped
+// `"255octets" "255octets" "remainder"` long strings are chunked
+func EncodeQuoted(t string) string {
+ return txtEncode(ToChunks(t))
+}
+
+// State denotes the parser state.
+type State int
+
+const (
+ // StateStart indicates parser is looking for a non-space
+ StateStart State = iota
+
+ // StateUnquoted indicates parser is in a run of unquoted text
+ StateUnquoted
+
+ // StateQuoted indicates parser is in quoted text
+ StateQuoted
+
+ // StateBackslash indicates the last char was backlash in a quoted string
+ StateBackslash
+
+ // StateWantSpace indicates parser expects a space (the previous token was a closing quote)
+ StateWantSpace
+)
+
+func isRemaining(s string, i, r int) bool {
+ return (len(s) - 1 - i) > r
+}
+
+// txtDecode decodes TXT strings quoted/escaped as Tom interprets RFC10225.
+func txtDecode(s string) (string, error) {
+ // Parse according to RFC1035 zonefile specifications.
+ // "foo" -> one string: `foo``
+ // "foo" "bar" -> two strings: `foo` and `bar`
+ // quotes and backslashes are escaped using \
+
+ /*
+
+ BNF:
+ txttarget := `""`` | item | item ` ` item*
+ item := quoteditem | unquoteditem
+ quoteditem := quote innertxt quote
+ quote := `"`
+ innertxt := (escaped | printable )*
+ escaped := `\\` | `\"`
+ printable := (printable ASCII chars)
+ unquoteditem := (printable ASCII chars but not `"` nor ' ')
+
+ */
+
+ //printer.Printf("DEBUG: txtDecode txt inboundv=%v\n", s)
+
+ b := &bytes.Buffer{}
+ state := StateStart
+ for i, c := range s {
+
+ //printer.Printf("DEBUG: state=%v rune=%v\n", state, string(c))
+
+ switch state {
+
+ case StateStart:
+ if c == ' ' {
+ // skip whitespace
+ } else if c == '"' {
+ state = StateQuoted
+ } else {
+ state = StateUnquoted
+ b.WriteRune(c)
+ }
+
+ case StateUnquoted:
+
+ if c == ' ' {
+ state = StateStart
+ } else {
+ b.WriteRune(c)
+ }
+
+ case StateQuoted:
+
+ if c == '\\' {
+ if isRemaining(s, i, 1) {
+ state = StateBackslash
+ } else {
+ return "", fmt.Errorf("txtDecode quoted string ends with backslash q(%q)", s)
+ }
+ } else if c == '"' {
+ state = StateWantSpace
+ } else {
+ b.WriteRune(c)
+ }
+
+ case StateBackslash:
+ b.WriteRune(c)
+ state = StateQuoted
+
+ case StateWantSpace:
+ if c == ' ' {
+ state = StateStart
+ } else {
+ return "", fmt.Errorf("txtDecode expected whitespace after close quote q(%q)", s)
+ }
+
+ }
+ }
+
+ r := b.String()
+ //printer.Printf("DEBUG: txtDecode txt decodedv=%v\n", r)
+ return r, nil
+}
+
+// txtEncode encodes TXT strings in RFC1035 format as interpreted by Tom.
+func txtEncode(ts []string) string {
+ //printer.Printf("DEBUG: txtEncode txt outboundv=%v\n", ts)
+ if (len(ts) == 0) || (strings.Join(ts, "") == "") {
+ return `""`
+ }
+
+ var r []string
+
+ for i := range ts {
+ tx := ts[i]
+ tx = strings.ReplaceAll(tx, `\`, `\\`)
+ tx = strings.ReplaceAll(tx, `"`, `\"`)
+ tx = `"` + tx + `"`
+ r = append(r, tx)
+ }
+ t := strings.Join(r, ` `)
+
+ //printer.Printf("DEBUG: txtEncode txt encodedv=%v\n", t)
+ return t
+}
diff --git a/pkg/txtutil/txtcode_test.go b/pkg/txtutil/txtcode_test.go
new file mode 100644
index 0000000000..d8375ba40d
--- /dev/null
+++ b/pkg/txtutil/txtcode_test.go
@@ -0,0 +1,97 @@
+package txtutil
+
+import (
+ "strings"
+ "testing"
+)
+
+func r(s string, c int) string { return strings.Repeat(s, c) }
+
+func TestTxtDecode(t *testing.T) {
+ tests := []struct {
+ data string
+ expected []string
+ }{
+ {``, []string{``}},
+ {`""`, []string{``}},
+ {`foo`, []string{`foo`}},
+ {`"foo"`, []string{`foo`}},
+ {`"foo bar"`, []string{`foo bar`}},
+ {`foo bar`, []string{`foo`, `bar`}},
+ {`"aaa" "bbb"`, []string{`aaa`, `bbb`}},
+ {`"a\"a" "bbb"`, []string{`a"a`, `bbb`}},
+ // Seen in live traffic:
+ {"\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\"",
+ []string{r("B", 254)}},
+ {"\"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\"",
+ []string{r("C", 255)}},
+ {"\"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\" \"D\"",
+ []string{r("D", 255), "D"}},
+ {"\"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\" \"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\"",
+ []string{r("E", 255), r("E", 255)}},
+ {"\"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\" \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\" \"F\"",
+ []string{r("F", 255), r("F", 255), "F"}},
+ {"\"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\" \"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\" \"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\"",
+ []string{r("G", 255), r("G", 255), r("G", 255)}},
+ {"\"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"H\"",
+ []string{r("H", 255), r("H", 255), r("H", 255), "H"}},
+ {"\"quo'te\"", []string{`quo'te`}},
+ {"\"blah`blah\"", []string{"blah`blah"}},
+ {"\"quo\\\"te\"", []string{`quo"te`}},
+ {"\"q\\\"uo\\\"te\"", []string{`q"uo"te`}},
+ /// Backslashes are meaningless in unquoted strings. Unquoted strings run until they hit a space.
+ {`1backs\lash`, []string{`1backs\lash`}},
+ {`2backs\\lash`, []string{`2backs\\lash`}},
+ {`3backs\\\lash`, []string{`3backs\\\lash`}},
+ {`4backs\\\\lash`, []string{`4backs\\\\lash`}},
+ /// Inside quotes, a backlash means take the next byte literally.
+ {`"q1backs\lash"`, []string{`q1backslash`}},
+ {`"q2backs\\lash"`, []string{`q2backs\lash`}},
+ {`"q3backs\\\lash"`, []string{`q3backs\lash`}},
+ {`"q4backs\\\\lash"`, []string{`q4backs\\lash`}},
+ // HETZNER includes a space after the last quote. Make sure we handle that.
+ {`"one" "more" `, []string{`one`, `more`}},
+ }
+ for i, test := range tests {
+ got, err := txtDecode(test.data)
+ if err != nil {
+ t.Error(err)
+ }
+
+ want := strings.Join(test.expected, "")
+ if got != want {
+ t.Errorf("%v: expected TxtStrings=(%q) got (%q)", i, want, got)
+ }
+ }
+}
+
+func TestTxtEncode(t *testing.T) {
+ tests := []struct {
+ data []string
+ expected string
+ }{
+ {[]string{}, `""`},
+ {[]string{``}, `""`},
+ {[]string{`foo`}, `"foo"`},
+ {[]string{`aaa`, `bbb`}, `"aaa" "bbb"`},
+ {[]string{`ccc`, `ddd`, `eee`}, `"ccc" "ddd" "eee"`},
+ {[]string{`a"a`, `bbb`}, `"a\"a" "bbb"`},
+ {[]string{`quo'te`}, "\"quo'te\""},
+ {[]string{"blah`blah"}, "\"blah`blah\""},
+ {[]string{`quo"te`}, "\"quo\\\"te\""},
+ {[]string{`quo"te`}, `"quo\"te"`},
+ {[]string{`q"uo"te`}, "\"q\\\"uo\\\"te\""},
+ {[]string{`1backs\lash`}, `"1backs\\lash"`},
+ {[]string{`2backs\\lash`}, `"2backs\\\\lash"`},
+ {[]string{`3backs\\\lash`}, `"3backs\\\\\\lash"`},
+ {[]string{strings.Repeat("M", 26), `quo"te`}, `"MMMMMMMMMMMMMMMMMMMMMMMMMM" "quo\"te"`},
+ }
+ for i, test := range tests {
+ got := txtEncode(test.data)
+
+ want := test.expected
+ if got != want {
+ t.Errorf("%v: expected TxtStrings=v(%v) got (%v)", i, want, got)
+ }
+ }
+}
diff --git a/pkg/txtutil/txtcombined.go b/pkg/txtutil/txtcombined.go
new file mode 100644
index 0000000000..8fa415a169
--- /dev/null
+++ b/pkg/txtutil/txtcombined.go
@@ -0,0 +1,53 @@
+//go:generate stringer -type=State
+
+package txtutil
+
+// func ParseCombined(s string) (string, error) {
+// return txtDecodeCombined(s)
+// }
+
+// // // txtDecode decodes TXT strings received from ROUTE53 and GCLOUD.
+// func txtDecodeCombined(s string) (string, error) {
+
+// // The dns package doesn't expose the quote parser. Therefore we create a TXT record and extract the strings.
+// rr, err := dns.NewRR("example.com. IN TXT " + s)
+// if err != nil {
+// return "", fmt.Errorf("could not parse %q TXT: %w", s, err)
+// }
+
+// return strings.Join(rr.(*dns.TXT).Txt, ""), nil
+// }
+
+// func EncodeCombined(t string) string {
+// return txtEncodeCombined(ToChunks(t))
+// }
+
+// // txtEncode encodes TXT strings as the old GetTargetCombined() function did.
+// func txtEncodeCombined(ts []string) string {
+// //printer.Printf("DEBUG: route53 txt outboundv=%v\n", ts)
+
+// // Don't call this on fake types.
+// rdtype := dns.StringToType["TXT"]
+
+// // Magically create an RR of the correct type.
+// rr := dns.TypeToRR[rdtype]()
+
+// // Fill in the header.
+// rr.Header().Name = "example.com."
+// rr.Header().Rrtype = rdtype
+// rr.Header().Class = dns.ClassINET
+// rr.Header().Ttl = 300
+
+// // Fill in the TXT data.
+// rr.(*dns.TXT).Txt = ts
+
+// // Generate the quoted string:
+// header := rr.Header().String()
+// full := rr.String()
+// if !strings.HasPrefix(full, header) {
+// panic("assertion failed. dns.Hdr.String() behavior has changed in an incompatible way")
+// }
+
+// //printer.Printf("DEBUG: route53 txt encodedv=%v\n", t)
+// return full[len(header):]
+// }
diff --git a/pkg/txtutil/txtutil.go b/pkg/txtutil/txtutil.go
index 5aa6b1d4ba..a6caf5e05e 100644
--- a/pkg/txtutil/txtutil.go
+++ b/pkg/txtutil/txtutil.go
@@ -1,20 +1,8 @@
package txtutil
-import "github.com/StackExchange/dnscontrol/v4/models"
-
-// SplitSingleLongTxt finds TXT records with a single long string and splits it
-// into 255-octet chunks. This is used by providers that, when a user specifies
-// one long TXT string, split it into smaller strings behind the scenes.
-// This should be called from GetZoneRecordsCorrections().
-func SplitSingleLongTxt(records []*models.RecordConfig) {
- for _, rc := range records {
- if rc.HasFormatIdenticalToTXT() {
- s := rc.TxtStrings[0]
- if len(rc.TxtStrings) == 1 && len(s) > 255 {
- rc.SetTargetTXTs(splitChunks(s, 255))
- }
- }
- }
+// ToChunks returns the string as chunks of 255-octet strings (the last string being the remainder).
+func ToChunks(s string) []string {
+ return splitChunks(s, 255)
}
func splitChunks(buf string, lim int) []string {
diff --git a/pkg/version/version.go b/pkg/version/version.go
deleted file mode 100644
index 2bba701a01..0000000000
--- a/pkg/version/version.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package version
-
-import (
- "fmt"
- "runtime/debug"
- "strconv"
- "time"
-)
-
-// NOTE: main() updates these.
-var (
- BuildTime = ""
- SHA = ""
- Semver = ""
-)
-
-var versionCache string
-
-// Banner returns the version banner.
-func Banner() string {
- if versionCache != "" {
- return versionCache
- }
-
- var version string
- if SHA != "" {
- version = fmt.Sprintf("%s (%s)", Semver, SHA)
- } else {
- version = fmt.Sprintf("%s-dev", Semver) // no SHA. '0.x.y-dev' indicates it is run from source without build script.
- }
- if info, ok := debug.ReadBuildInfo(); !ok && info == nil {
- version += " (non-modules)"
- }
- if BuildTime != "" {
- i, err := strconv.ParseInt(BuildTime, 10, 64)
- if err == nil {
- tm := time.Unix(i, 0)
- version += fmt.Sprintf(" built %s", tm.Format(time.RFC822))
- }
- }
-
- versionCache = version
- return version
-}
diff --git a/pkg/zonerecs/zonerecords.go b/pkg/zonerecs/zonerecords.go
index be8c882ae7..b43f06b8a5 100644
--- a/pkg/zonerecs/zonerecords.go
+++ b/pkg/zonerecs/zonerecords.go
@@ -7,11 +7,11 @@ import (
// CorrectZoneRecords calls both GetZoneRecords, does any
// post-processing, and then calls GetZoneRecordsCorrections. The
// name sucks because all the good names were taken.
-func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]*models.Correction, []*models.Correction, error) {
+func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]*models.Correction, []*models.Correction, int, error) {
existingRecords, err := driver.GetZoneRecords(dc.Name, dc.Metadata)
if err != nil {
- return nil, nil, err
+ return nil, nil, 0, err
}
// downcase
@@ -26,7 +26,7 @@ func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]*
// dc.Records.
dc, err = dc.Copy()
if err != nil {
- return nil, nil, err
+ return nil, nil, 0, err
}
// punycode
@@ -34,9 +34,9 @@ func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]*
// FIXME(tlim) It is a waste to PunyCode every iteration.
// This should be moved to where the JavaScript is processed.
- everything, err := driver.GetZoneRecordsCorrections(dc, existingRecords)
+ everything, actualChangeCount, err := driver.GetZoneRecordsCorrections(dc, existingRecords)
reports, corrections := splitReportsAndCorrections(everything)
- return reports, corrections, err
+ return reports, corrections, actualChangeCount, err
}
func splitReportsAndCorrections(everything []*models.Correction) (reports, corrections []*models.Correction) {
diff --git a/providers/_all/all.go b/providers/_all/all.go
index 9f6ab843b6..617697fa18 100644
--- a/providers/_all/all.go
+++ b/providers/_all/all.go
@@ -7,7 +7,10 @@ import (
_ "github.com/StackExchange/dnscontrol/v4/providers/autodns"
_ "github.com/StackExchange/dnscontrol/v4/providers/axfrddns"
_ "github.com/StackExchange/dnscontrol/v4/providers/azuredns"
+ _ "github.com/StackExchange/dnscontrol/v4/providers/azureprivatedns"
_ "github.com/StackExchange/dnscontrol/v4/providers/bind"
+ _ "github.com/StackExchange/dnscontrol/v4/providers/bunnydns"
+ _ "github.com/StackExchange/dnscontrol/v4/providers/cnr"
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare"
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudns"
_ "github.com/StackExchange/dnscontrol/v4/providers/cscglobal"
@@ -17,6 +20,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v4/providers/dnsmadeeasy"
_ "github.com/StackExchange/dnscontrol/v4/providers/doh"
_ "github.com/StackExchange/dnscontrol/v4/providers/domainnameshop"
+ _ "github.com/StackExchange/dnscontrol/v4/providers/dynadot"
_ "github.com/StackExchange/dnscontrol/v4/providers/easyname"
_ "github.com/StackExchange/dnscontrol/v4/providers/exoscale"
_ "github.com/StackExchange/dnscontrol/v4/providers/gandiv5"
@@ -26,6 +30,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v4/providers/hetzner"
_ "github.com/StackExchange/dnscontrol/v4/providers/hexonet"
_ "github.com/StackExchange/dnscontrol/v4/providers/hostingde"
+ _ "github.com/StackExchange/dnscontrol/v4/providers/huaweicloud"
_ "github.com/StackExchange/dnscontrol/v4/providers/internetbs"
_ "github.com/StackExchange/dnscontrol/v4/providers/inwx"
_ "github.com/StackExchange/dnscontrol/v4/providers/linode"
@@ -44,8 +49,10 @@ import (
_ "github.com/StackExchange/dnscontrol/v4/providers/packetframe"
_ "github.com/StackExchange/dnscontrol/v4/providers/porkbun"
_ "github.com/StackExchange/dnscontrol/v4/providers/powerdns"
+ _ "github.com/StackExchange/dnscontrol/v4/providers/realtimeregister"
_ "github.com/StackExchange/dnscontrol/v4/providers/route53"
_ "github.com/StackExchange/dnscontrol/v4/providers/rwth"
+ _ "github.com/StackExchange/dnscontrol/v4/providers/sakuracloud"
_ "github.com/StackExchange/dnscontrol/v4/providers/softlayer"
_ "github.com/StackExchange/dnscontrol/v4/providers/transip"
_ "github.com/StackExchange/dnscontrol/v4/providers/vultr"
diff --git a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
index a42fe4b523..64c5a673a4 100644
--- a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
+++ b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
@@ -17,15 +17,15 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
)
var features = providers.DocumentationNotes{
// The default for unlisted capabilities is 'Cannot'.
- // See providers/capabilities.go for the entire list of capabilties.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAKAMAICDN: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
@@ -38,7 +38,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(),
- providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
@@ -50,12 +49,15 @@ type edgeDNSProvider struct {
}
func init() {
+ const providerName = "AKAMAIEDGEDNS"
+ const providerMaintainer = "@edglynes"
fns := providers.DspFuncs{
Initializer: newEdgeDNSDSP,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("AKAMAIEDGEDNS", fns, features)
- providers.RegisterCustomRecordType("AKAMAICDN", "AKAMAIEDGEDNS", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterCustomRecordType("AKAMAICDN", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// DnsServiceProvider
@@ -105,12 +107,10 @@ func (a *edgeDNSProvider) EnsureZoneExists(domain string) error {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (a *edgeDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(existingRecords)
-
- keysToUpdate, toReport, err := diff.NewCompat(dc).ChangedGroups(existingRecords)
+func (a *edgeDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
+ keysToUpdate, toReport, actualChangeCount, err := diff.NewCompat(dc).ChangedGroups(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -180,7 +180,7 @@ func (a *edgeDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
// AutoDnsSec correction
existingAutoDNSSecEnabled, err := isAutoDNSSecEnabled(dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
desiredAutoDNSSecEnabled := dc.AutoDNSSEC == "on"
@@ -205,7 +205,7 @@ func (a *edgeDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
printer.Debugf("autoDNSSecEnable: Disable AutoDnsSec for zone %s\n", dc.Name)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// GetNameservers returns the nameservers for a domain.
diff --git a/providers/akamaiedgedns/akamaiEdgeDnsService.go b/providers/akamaiedgedns/akamaiEdgeDnsService.go
index b62c898acf..e47b1c20fe 100644
--- a/providers/akamaiedgedns/akamaiEdgeDnsService.go
+++ b/providers/akamaiedgedns/akamaiEdgeDnsService.go
@@ -186,7 +186,7 @@ func getAuthorities(contractID string) ([]string, error) {
}
// rcToRs converts DNSControl RecordConfig records to an AkamaiEdgeDNS recordset.
-func rcToRs(records []*models.RecordConfig, zonename string) (*dnsv2.RecordBody, error) {
+func rcToRs(records []*models.RecordConfig) (*dnsv2.RecordBody, error) {
if len(records) == 0 {
return nil, fmt.Errorf("no records to replace")
}
@@ -206,7 +206,7 @@ func rcToRs(records []*models.RecordConfig, zonename string) (*dnsv2.RecordBody,
// createRecordset creates a new AkamaiEdgeDNS recordset in the zone.
func createRecordset(records []*models.RecordConfig, zonename string) error {
- akaRecord, err := rcToRs(records, zonename)
+ akaRecord, err := rcToRs(records)
if err != nil {
return err
}
@@ -220,7 +220,7 @@ func createRecordset(records []*models.RecordConfig, zonename string) error {
// replaceRecordset replaces an existing AkamaiEdgeDNS recordset in the zone.
func replaceRecordset(records []*models.RecordConfig, zonename string) error {
- akaRecord, err := rcToRs(records, zonename)
+ akaRecord, err := rcToRs(records)
if err != nil {
return err
}
@@ -234,7 +234,7 @@ func replaceRecordset(records []*models.RecordConfig, zonename string) error {
// deleteRecordset deletes an existing AkamaiEdgeDNS recordset in the zone.
func deleteRecordset(records []*models.RecordConfig, zonename string) error {
- akaRecord, err := rcToRs(records, zonename)
+ akaRecord, err := rcToRs(records)
if err != nil {
return err
}
diff --git a/providers/akamaiedgedns/auditrecords.go b/providers/akamaiedgedns/auditrecords.go
index 255dafd559..a866503f06 100644
--- a/providers/akamaiedgedns/auditrecords.go
+++ b/providers/akamaiedgedns/auditrecords.go
@@ -1,10 +1,17 @@
package akamaiedgedns
-import "github.com/StackExchange/dnscontrol/v4/models"
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
// AuditRecords returns a list of errors corresponding to the records
// that aren't supported by this provider. If all records are
// supported, an empty list is returned.
func AuditRecords(records []*models.RecordConfig) []error {
- return nil
+ a := rejectif.Auditor{}
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-12-12
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2023-12-12
+
+ return a.Audit(records)
}
diff --git a/providers/autodns/autoDnsProvider.go b/providers/autodns/autoDnsProvider.go
index 6da132afe9..87795e0915 100644
--- a/providers/autodns/autoDnsProvider.go
+++ b/providers/autodns/autoDnsProvider.go
@@ -2,6 +2,7 @@ package autodns
import (
"encoding/json"
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -11,16 +12,18 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
- providers.CanUsePTR: providers.Cannot(),
+ providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
@@ -35,11 +38,14 @@ type autoDNSProvider struct {
}
func init() {
+ const providerName = "AUTODNS"
+ const providerMaintainer = "@arnoschoon"
fns := providers.DspFuncs{
Initializer: New,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("AUTODNS", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// New creates a new API handle.
@@ -66,22 +72,23 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *autoDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (api *autoDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
domain := dc.Name
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
var corrections []*models.Correction
- msgs, changed, err := diff2.ByZone(existingRecords, dc, nil)
+ result, err := diff2.ByZone(existingRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
+ msgs, changed, actualChangeCount := result.Msgs, result.HasChanges, result.ActualChangeCount
+
if changed {
msgs = append(msgs, "Zone update for "+domain)
msg := strings.Join(msgs, "\n")
- nameServers, zoneTTL, resourceRecords := recordsToNative(dc.Records)
+ nameServers, zoneTTL, resourceRecords := recordsToNative(result.DesiredPlus)
corrections = append(corrections,
&models.Correction{
@@ -94,7 +101,7 @@ func (api *autoDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, e
err := api.updateZone(domain, resourceRecords, nameServers, zoneTTL)
if err != nil {
- return fmt.Errorf(err.Error())
+ return errors.New(err.Error())
}
return nil
@@ -103,7 +110,7 @@ func (api *autoDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, e
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func recordsToNative(recs models.Records) ([]*models.Nameserver, uint32, []*ResourceRecord) {
diff --git a/providers/axfrddns/axfrddnsProvider.go b/providers/axfrddns/axfrddnsProvider.go
index b7a1f17cef..942b4355fc 100644
--- a/providers/axfrddns/axfrddnsProvider.go
+++ b/providers/axfrddns/axfrddnsProvider.go
@@ -25,7 +25,6 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/fatih/color"
"github.com/miekg/dns"
@@ -38,17 +37,21 @@ const (
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can("Just warn when DNSSEC is requested but no RRSIG is found in the AXFR or warn when DNSSEC is not requested but RRSIG are found in the AXFR."),
providers.CanGetZones: providers.Cannot(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
- providers.CanUseLOC: providers.Unimplemented(),
+ providers.CanUseDHCID: providers.Can(),
+ providers.CanUseHTTPS: providers.Can(),
+ providers.CanUseLOC: providers.Can(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.CanUseTLSA: providers.Can(),
- providers.CanUseDHCID: providers.Can(),
- providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(),
providers.DocDualHost: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
@@ -59,6 +62,7 @@ type axfrddnsProvider struct {
rand *rand.Rand
master string
updateMode string
+ transferServer string
transferMode string
nameservers []*models.Nameserver
transferKey *Key
@@ -126,6 +130,14 @@ func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (provi
} else {
return nil, fmt.Errorf("nameservers list is empty: creds.json needs a default `nameservers` or an explicit `master`")
}
+ if config["transfer-server"] != "" {
+ api.transferServer = config["transfer-server"]
+ if !strings.Contains(api.transferServer, ":") {
+ api.transferServer = api.transferServer + ":53"
+ }
+ } else {
+ api.transferServer = api.master
+ }
api.updateKey, err = readKey(config["update-key"], "update-key")
if err != nil {
return nil, err
@@ -146,6 +158,7 @@ func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (provi
"nameservers",
"update-key",
"transfer-key",
+ "transfer-server",
"update-mode",
"transfer-mode",
"domain",
@@ -159,11 +172,14 @@ func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (provi
}
func init() {
+ const providerName = "AXFRDDNS"
+ const providerMaintainer = "@hnrgrgr"
fns := providers.DspFuncs{
Initializer: initAxfrDdns,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("AXFRDDNS", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// Param is used to decode extra parameters sent to provider.
@@ -203,7 +219,8 @@ func readKey(raw string, kind string) (*Key, error) {
if err != nil {
return nil, fmt.Errorf("cannot decode Base64 secret (%s) in AXFRDDNS.TSIG", kind)
}
- return &Key{algo: algo, id: arr[1] + ".", secret: arr[2]}, nil
+ id := dns.CanonicalName(arr[1])
+ return &Key{algo: algo, id: id, secret: arr[2]}, nil
}
// GetNameservers returns the nameservers for a domain.
@@ -215,9 +232,9 @@ func (c *axfrddnsProvider) getAxfrConnection() (*dns.Transfer, error) {
var con net.Conn = nil
var err error = nil
if c.transferMode == "tcp-tls" {
- con, err = tls.Dial("tcp", c.master, &tls.Config{})
+ con, err = tls.Dial("tcp", c.transferServer, &tls.Config{})
} else {
- con, err = net.Dial("tcp", c.master)
+ con, err = net.Dial("tcp", c.transferServer)
}
if err != nil {
return nil, err
@@ -248,7 +265,7 @@ func (c *axfrddnsProvider) FetchZoneRecords(domain string) ([]dns.RR, error) {
}
}
- envelope, err := transfer.In(request, c.master)
+ envelope, err := transfer.In(request, c.transferServer)
if err != nil {
return nil, err
}
@@ -327,8 +344,8 @@ func (c *axfrddnsProvider) GetZoneRecords(domain string, meta map[string]string)
last := foundRecords[len(foundRecords)-1]
if last.Type == "TXT" &&
last.Name == dnssecDummyLabel &&
- len(last.TxtStrings) == 1 &&
- last.TxtStrings[0] == dnssecDummyTxt {
+ last.GetTargetTXTSegmentCount() == 1 &&
+ last.GetTargetTXTSegmented()[0] == dnssecDummyTxt {
c.hasDnssecRecords = true
foundRecords = foundRecords[0:(len(foundRecords) - 1)]
}
@@ -409,9 +426,7 @@ func hasNSDeletion(changes diff2.ChangeList) bool {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *axfrddnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(foundRecords) // Autosplit long TXT records
-
+func (c *axfrddnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, int, error) {
// Ignoring the SOA, others providers don't manage it either.
if len(foundRecords) >= 1 && foundRecords[0].Type == "SOA" {
foundRecords = foundRecords[1:]
@@ -451,19 +466,19 @@ func (c *axfrddnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, fo
dummyNs1, err := dns.NewRR(dc.Name + ". IN NS 255.255.255.255")
if err != nil {
- return nil, err
+ return nil, 0, err
}
dummyNs2, err := dns.NewRR(dc.Name + ". IN NS 255.255.255.255")
if err != nil {
- return nil, err
+ return nil, 0, err
}
- changes, err := diff2.ByRecord(foundRecords, dc, nil)
+ changes, actualChangeCount, err := diff2.ByRecord(foundRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
if changes == nil {
- return nil, nil
+ return nil, 0, nil
}
// A DNS server should silently ignore a DDNS update that removes
@@ -532,5 +547,5 @@ func (c *axfrddnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, fo
if len(reports) > 0 {
returnValue = append(returnValue, c.BuildCorrection(dc, reports, nil))
}
- return returnValue, nil
+ return returnValue, actualChangeCount, nil
}
diff --git a/providers/azuredns/auditrecords.go b/providers/azuredns/auditrecords.go
index 13d73026d4..6c46e4357b 100644
--- a/providers/azuredns/auditrecords.go
+++ b/providers/azuredns/auditrecords.go
@@ -13,5 +13,7 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("MX", rejectif.MxNull) // Last verified 2020-12-28
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-11-11
+
return a.Audit(records)
}
diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go
index cb2230132b..856077ba7b 100644
--- a/providers/azuredns/azureDnsProvider.go
+++ b/providers/azuredns/azureDnsProvider.go
@@ -14,7 +14,6 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
)
@@ -24,15 +23,13 @@ type azurednsProvider struct {
zones map[string]*adns.Zone
resourceGroup *string
subscriptionID *string
- rawRecords map[string][]*adns.RecordSet
- zoneName map[string]string
}
func newAzureDNSDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
return newAzureDNS(conf, metadata)
}
-func newAzureDNS(m map[string]string, metadata json.RawMessage) (*azurednsProvider, error) {
+func newAzureDNS(m map[string]string, _ json.RawMessage) (*azurednsProvider, error) {
subID, rg := m["SubscriptionID"], m["ResourceGroup"]
clientID, clientSecret, tenantID := m["ClientID"], m["ClientSecret"], m["TenantID"]
credential, authErr := aauth.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
@@ -53,8 +50,6 @@ func newAzureDNS(m map[string]string, metadata json.RawMessage) (*azurednsProvid
recordsClient: recordsClient,
resourceGroup: to.StringPtr(rg),
subscriptionID: to.StringPtr(subID),
- rawRecords: map[string][]*adns.RecordSet{},
- zoneName: map[string]string{},
}
err := api.getZones()
if err != nil {
@@ -64,7 +59,10 @@ func newAzureDNS(m map[string]string, metadata json.RawMessage) (*azurednsProvid
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Cannot("Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead."),
providers.CanUseAzureAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
@@ -80,12 +78,15 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "AZURE_DNS"
+ const providerMaintainer = "@vatsalyagoel"
fns := providers.DspFuncs{
Initializer: newAzureDNSDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("AZURE_DNS", fns, features)
- providers.RegisterCustomRecordType("AZURE_ALIAS", "AZURE_DNS", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterCustomRecordType("AZURE_ALIAS", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func (a *azurednsProvider) getExistingZones() ([]*adns.Zone, error) {
@@ -185,23 +186,18 @@ func (a *azurednsProvider) getExistingRecords(domain string) (models.Records, []
existingRecords = append(existingRecords, nativeToRecords(set, zoneName)...)
}
- a.rawRecords[domain] = rawRecords
- a.zoneName[domain] = zoneName
-
return existingRecords, rawRecords, zoneName, nil
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(existingRecords) // Autosplit long TXT records
-
+func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
// Azure is a "ByRecordSet" API.
- changes, err := diff2.ByRecordSet(existingRecords, dc, nil)
+ changes, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, change := range changes {
@@ -226,7 +222,7 @@ func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
corrections = append(corrections, &models.Correction{
Msg: msgs,
F: func() error {
- return a.recordDelete(dcn, chaKey, change.Old)
+ return a.recordDelete(dcn, chaKey)
},
})
default:
@@ -234,7 +230,7 @@ func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
}
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (a *azurednsProvider) recordCreate(zoneName string, reckey models.RecordKey, recs models.Records) error {
@@ -274,7 +270,7 @@ retry:
return err
}
-func (a *azurednsProvider) recordDelete(zoneName string, reckey models.RecordKey, recs models.Records) error {
+func (a *azurednsProvider) recordDelete(zoneName string, reckey models.RecordKey) error {
shortName := strings.TrimSuffix(reckey.NameFQDN, "."+zoneName)
if shortName == zoneName {
@@ -525,9 +521,9 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor
recordSet.Properties.TxtRecords = []*adns.TxtRecord{}
}
// Empty TXT record needs to have no value set in it's properties
- if !(len(rec.TxtStrings) == 1 && rec.TxtStrings[0] == "") {
+ if !(rec.GetTargetTXTSegmentCount() == 1 && rec.GetTargetTXTSegmented()[0] == "") {
var txts []*string
- for _, txt := range rec.TxtStrings {
+ for _, txt := range rec.GetTargetTXTSegmented() {
txts = append(txts, to.StringPtr(txt))
}
recordSet.Properties.TxtRecords = append(recordSet.Properties.TxtRecords, &adns.TxtRecord{Value: txts})
diff --git a/providers/azureprivatedns/auditrecords.go b/providers/azureprivatedns/auditrecords.go
new file mode 100644
index 0000000000..cea8335b2a
--- /dev/null
+++ b/providers/azureprivatedns/auditrecords.go
@@ -0,0 +1,17 @@
+package azureprivatedns
+
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
+
+// AuditRecords returns a list of errors corresponding to the records
+// that aren't supported by this provider. If all records are
+// supported, an empty list is returned.
+func AuditRecords(records []*models.RecordConfig) []error {
+ a := rejectif.Auditor{}
+
+ a.Add("MX", rejectif.MxNull) // Last verified 2020-12-28
+
+ return a.Audit(records)
+}
diff --git a/providers/azureprivatedns/azurePrivateDnsProvider.go b/providers/azureprivatedns/azurePrivateDnsProvider.go
new file mode 100644
index 0000000000..2e2b05fe63
--- /dev/null
+++ b/providers/azureprivatedns/azurePrivateDnsProvider.go
@@ -0,0 +1,537 @@
+package azureprivatedns
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ aauth "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+ adns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns"
+ "github.com/Azure/go-autorest/autorest/to"
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/StackExchange/dnscontrol/v4/providers"
+)
+
+type azurednsProvider struct {
+ zonesClient *adns.PrivateZonesClient
+ recordsClient *adns.RecordSetsClient
+ zones map[string]*adns.PrivateZone
+ resourceGroup *string
+ subscriptionID *string
+ rawRecords map[string][]*adns.RecordSet
+ zoneName map[string]string
+}
+
+func newAzureDNSDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
+ return newAzureDNS(conf, metadata)
+}
+
+func newAzureDNS(m map[string]string, _ json.RawMessage) (*azurednsProvider, error) {
+ subID, rg := m["SubscriptionID"], m["ResourceGroup"]
+ clientID, clientSecret, tenantID := m["ClientID"], m["ClientSecret"], m["TenantID"]
+ credential, authErr := aauth.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
+ if authErr != nil {
+ return nil, authErr
+ }
+ zonesClient, zoneErr := adns.NewPrivateZonesClient(subID, credential, nil)
+ if zoneErr != nil {
+ return nil, zoneErr
+ }
+ recordsClient, recordErr := adns.NewRecordSetsClient(subID, credential, nil)
+ if recordErr != nil {
+ return nil, recordErr
+ }
+
+ api := &azurednsProvider{
+ zonesClient: zonesClient,
+ recordsClient: recordsClient,
+ resourceGroup: to.StringPtr(rg),
+ subscriptionID: to.StringPtr(subID),
+ rawRecords: map[string][]*adns.RecordSet{},
+ zoneName: map[string]string{},
+ }
+ err := api.getZones()
+ if err != nil {
+ return nil, err
+ }
+ return api, nil
+}
+
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
+ providers.CanUseAlias: providers.Cannot("Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead."),
+ providers.CanUseAzureAlias: providers.Can(),
+ providers.CanUseCAA: providers.Cannot("Azure Private DNS does not support CAA records"),
+ providers.CanUseLOC: providers.Cannot(),
+ providers.CanUseNAPTR: providers.Cannot(),
+ providers.CanUsePTR: providers.Can(),
+ providers.CanUseSRV: providers.Can(),
+ providers.CanUseSSHFP: providers.Cannot(),
+ providers.CanUseTLSA: providers.Cannot(),
+ providers.DocCreateDomains: providers.Can(),
+ providers.DocDualHost: providers.Can("Azure does not permit modifying the existing NS records, only adding/removing additional records."),
+ providers.DocOfficiallySupported: providers.Can(),
+}
+
+func init() {
+ const providerName = "AZURE_PRIVATE_DNS"
+ const providerMaintainer = "@matthewmgamble"
+ fns := providers.DspFuncs{
+ Initializer: newAzureDNSDsp,
+ RecordAuditor: AuditRecords,
+ }
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterCustomRecordType("AZURE_ALIAS", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+}
+
+func (a *azurednsProvider) getExistingZones() ([]*adns.PrivateZone, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
+ defer cancel()
+ zonesPager := a.zonesClient.NewListByResourceGroupPager(*a.resourceGroup, nil)
+ var zones []*adns.PrivateZone
+ for zonesPager.More() {
+ nextResult, zonesErr := zonesPager.NextPage(ctx)
+ if zonesErr != nil {
+ return nil, zonesErr
+ }
+ zones = append(zones, nextResult.Value...)
+ }
+ return zones, nil
+}
+
+func (a *azurednsProvider) getZones() error {
+ a.zones = make(map[string]*adns.PrivateZone)
+
+ zones, err := a.getExistingZones()
+ if err != nil {
+ return err
+ }
+
+ for _, z := range zones {
+ zone := z
+ domain := strings.TrimSuffix(*z.Name, ".")
+ a.zones[domain] = zone
+ }
+
+ return nil
+}
+
+type errNoExist struct {
+ domain string
+}
+
+func (e errNoExist) Error() string {
+ return fmt.Sprintf("Private Domain %s not found in you Azure account", e.domain)
+}
+
+func (a *azurednsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ // Azure Private DNS does not have the concept of "Name Servers" since these are local, private views of zones unique to the Azure environment
+ var nss []string
+ return models.ToNameserversStripTD(nss)
+}
+
+func (a *azurednsProvider) ListZones() ([]string, error) {
+ zonesResult, err := a.getExistingZones()
+ if err != nil {
+ return nil, err
+ }
+ var zones []string
+
+ for _, z := range zonesResult {
+ domain := strings.TrimSuffix(*z.Name, ".")
+ zones = append(zones, domain)
+ }
+
+ return zones, nil
+}
+
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (a *azurednsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
+ existingRecords, _, _, err := a.getExistingRecords(domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return existingRecords, nil
+}
+
+func (a *azurednsProvider) getExistingRecords(domain string) (models.Records, []*adns.RecordSet, string, error) {
+ zone, ok := a.zones[domain]
+ if !ok {
+ return nil, nil, "", errNoExist{domain}
+ }
+ zoneName := *zone.Name
+ rawRecords, err := a.fetchRecordSets(zoneName)
+ if err != nil {
+ return nil, nil, "", err
+ }
+
+ var existingRecords models.Records
+ for _, set := range rawRecords {
+ existingRecords = append(existingRecords, nativeToRecords(set, zoneName)...)
+ }
+
+ a.rawRecords[domain] = rawRecords
+ a.zoneName[domain] = zoneName
+
+ return existingRecords, rawRecords, zoneName, nil
+}
+
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
+ var corrections []*models.Correction
+
+ changes, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ for _, change := range changes {
+
+ // Copy all param values to local variables to avoid overwrites
+ msgs := change.MsgsJoined
+ dcn := dc.Name
+ chaKey := change.Key
+
+ switch change.Type {
+ case diff2.REPORT:
+ corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined})
+ case diff2.CHANGE, diff2.CREATE:
+ changeNew := change.New
+ corrections = append(corrections, &models.Correction{
+ Msg: msgs,
+ F: func() error {
+ return a.recordCreate(dcn, chaKey, changeNew)
+ },
+ })
+ case diff2.DELETE:
+ corrections = append(corrections, &models.Correction{
+ Msg: msgs,
+ F: func() error {
+ return a.recordDelete(dcn, chaKey)
+ },
+ })
+ default:
+ panic(fmt.Sprintf("unhandled change.Type %s", change.Type))
+ }
+ }
+
+ return corrections, actualChangeCount, nil
+}
+
+func (a *azurednsProvider) recordCreate(zoneName string, reckey models.RecordKey, recs models.Records) error {
+
+ rrset, azRecType, err := a.recordToNativeDiff2(reckey, recs)
+ if err != nil {
+ return err
+ }
+
+ var recordName string
+ var i int64
+ for _, r := range recs {
+ i = int64(r.TTL)
+ recordName = r.Name
+ }
+ rrset.Properties.TTL = &i
+
+ waitTime := 1
+retry:
+
+ ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
+ defer cancel()
+ _, err = a.recordsClient.CreateOrUpdate(ctx, *a.resourceGroup, zoneName, azRecType, recordName, *rrset, nil)
+
+ if e, ok := err.(*azcore.ResponseError); ok {
+ if e.StatusCode == 429 {
+ waitTime = waitTime * 2
+ if waitTime > 300 {
+ return err
+ }
+ printer.Printf("AZURE_PRIVATE_DNS: rate-limit paused for %v.\n", waitTime)
+ time.Sleep(time.Duration(waitTime+1) * time.Second)
+ goto retry
+ }
+ }
+
+ return err
+}
+
+func (a *azurednsProvider) recordDelete(zoneName string, reckey models.RecordKey) error {
+
+ shortName := strings.TrimSuffix(reckey.NameFQDN, "."+zoneName)
+ if shortName == zoneName {
+ shortName = "@"
+ }
+
+ azRecType, err := nativeToRecordTypeDiff(to.StringPtr(reckey.Type))
+ if err != nil {
+ return nil
+ }
+
+ waitTime := 1
+retry:
+
+ ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
+ defer cancel()
+ _, err = a.recordsClient.Delete(ctx, *a.resourceGroup, zoneName, azRecType, shortName, nil)
+
+ if e, ok := err.(*azcore.ResponseError); ok {
+ if e.StatusCode == 429 {
+ waitTime = waitTime * 2
+ if waitTime > 300 {
+ return err
+ }
+ printer.Printf("AZURE_PRIVATE_DNS: rate-limit paused for %v.\n", waitTime)
+ time.Sleep(time.Duration(waitTime+1) * time.Second)
+ goto retry
+ }
+ }
+
+ return err
+}
+
+func nativeToRecordTypeDiff(recordType *string) (adns.RecordType, error) {
+ recordTypeStripped := strings.TrimPrefix(*recordType, "Microsoft.Network/dnszones/")
+ switch recordTypeStripped {
+ case "A", "AZURE_ALIAS_A":
+ return adns.RecordTypeA, nil
+ case "AAAA", "AZURE_ALIAS_AAAA":
+ return adns.RecordTypeAAAA, nil
+ case "CAA":
+ // CAA doesn't make any senese in a private dns zone in azure
+ return adns.RecordTypeA, fmt.Errorf("nativeToRecordTypeDiff RTYPE %v UNIMPLEMENTED", *recordType)
+ case "CNAME", "AZURE_ALIAS_CNAME":
+ return adns.RecordTypeCNAME, nil
+ case "MX":
+ return adns.RecordTypeMX, nil
+ case "NS":
+ // NS record types don't make any sense in a private azure dns zone
+ return adns.RecordTypeA, fmt.Errorf("nativeToRecordTypeDiff RTYPE %v UNIMPLEMENTED", *recordType)
+ case "PTR":
+ return adns.RecordTypePTR, nil
+ case "SRV":
+ return adns.RecordTypeSRV, nil
+ case "TXT":
+ return adns.RecordTypeTXT, nil
+ case "SOA":
+ return adns.RecordTypeSOA, nil
+ default:
+ // Unimplemented type. Return adns.A as a decoy, but send an error.
+ return adns.RecordTypeA, fmt.Errorf("nativeToRecordTypeDiff RTYPE %v UNIMPLEMENTED", *recordType)
+ }
+}
+
+func nativeToRecords(set *adns.RecordSet, origin string) []*models.RecordConfig {
+ var results []*models.RecordConfig
+ switch rtype := *set.Type; rtype {
+ case "Microsoft.Network/privateDnsZones/A":
+ if set.Properties.ARecords != nil {
+ // This is an A recordset. Process all the targets there.
+ for _, rec := range set.Properties.ARecords {
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "A"
+ _ = rc.SetTarget(*rec.IPv4Address)
+ results = append(results, rc)
+ }
+ } else {
+ panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type))
+
+ }
+ case "Microsoft.Network/privateDnsZones/AAAA":
+ if set.Properties.AaaaRecords != nil {
+ // This is an AAAA recordset. Process all the targets there.
+ for _, rec := range set.Properties.AaaaRecords {
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "AAAA"
+ _ = rc.SetTarget(*rec.IPv6Address)
+ results = append(results, rc)
+ }
+ } else {
+ panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type))
+ }
+ case "Microsoft.Network/privateDnsZones/CNAME":
+ if set.Properties.CnameRecord != nil {
+ // This is a CNAME recordset. Process the targets. (there can only be one)
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "CNAME"
+ _ = rc.SetTarget(*set.Properties.CnameRecord.Cname)
+ results = append(results, rc)
+ } else {
+ panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type))
+ }
+ case "Microsoft.Network/privateDnsZones/PTR":
+ for _, rec := range set.Properties.PtrRecords {
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "PTR"
+ _ = rc.SetTarget(*rec.Ptrdname)
+ results = append(results, rc)
+ }
+ case "Microsoft.Network/privateDnsZones/TXT":
+ if len(set.Properties.TxtRecords) == 0 { // Empty String Record Parsing
+ // This is a null TXT record.
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "TXT"
+ _ = rc.SetTargetTXT("")
+ results = append(results, rc)
+ } else {
+ // This is a normal TXT record. Collect all its segments.
+ for _, rec := range set.Properties.TxtRecords {
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "TXT"
+ var txts []string
+ for _, txt := range rec.Value {
+ txts = append(txts, *txt)
+ }
+ _ = rc.SetTargetTXTs(txts)
+ results = append(results, rc)
+ }
+ }
+ case "Microsoft.Network/privateDnsZones/MX":
+ for _, rec := range set.Properties.MxRecords {
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "MX"
+ _ = rc.SetTargetMX(uint16(*rec.Preference), *rec.Exchange)
+ results = append(results, rc)
+ }
+ case "Microsoft.Network/privateDnsZones/SRV":
+ for _, rec := range set.Properties.SrvRecords {
+ rc := &models.RecordConfig{TTL: uint32(*set.Properties.TTL), Original: set}
+ rc.SetLabelFromFQDN(*set.Properties.Fqdn, origin)
+ rc.Type = "SRV"
+ _ = rc.SetTargetSRV(uint16(*rec.Priority), uint16(*rec.Weight), uint16(*rec.Port), *rec.Target)
+ results = append(results, rc)
+ }
+ case "Microsoft.Network/privateDnsZones/SOA":
+ default:
+ panic(fmt.Errorf("nativeToRecords rtype %v unimplemented", *set.Type))
+ }
+ return results
+}
+
+// NOTE recordToNativeDiff2 is really "convert []RecordConfig to rrset".
+
+func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recordConfig []*models.RecordConfig) (*adns.RecordSet, adns.RecordType, error) {
+
+ recordKeyType := recordKey.Type
+ // if recordKeyType == "AZURE_ALIAS" {
+ // fmt.Fprintf(os.Stderr, "DEBUG: XXXXXXXXXXXXXXXXXXXXXXX %v\n", recordKeyType)
+ // }
+
+ recordSet := &adns.RecordSet{Type: to.StringPtr(recordKeyType), Properties: &adns.RecordSetProperties{}}
+ for _, rec := range recordConfig {
+ switch recordKeyType {
+ case "A":
+ if recordSet.Properties.ARecords == nil {
+ recordSet.Properties.ARecords = []*adns.ARecord{}
+ }
+ recordSet.Properties.ARecords = append(recordSet.Properties.ARecords, &adns.ARecord{IPv4Address: to.StringPtr(rec.GetTargetField())})
+ case "AAAA":
+ if recordSet.Properties.AaaaRecords == nil {
+ recordSet.Properties.AaaaRecords = []*adns.AaaaRecord{}
+ }
+ recordSet.Properties.AaaaRecords = append(recordSet.Properties.AaaaRecords, &adns.AaaaRecord{IPv6Address: to.StringPtr(rec.GetTargetField())})
+ case "CNAME":
+ recordSet.Properties.CnameRecord = &adns.CnameRecord{Cname: to.StringPtr(rec.GetTargetField())}
+ case "PTR":
+ if recordSet.Properties.PtrRecords == nil {
+ recordSet.Properties.PtrRecords = []*adns.PtrRecord{}
+ }
+ recordSet.Properties.PtrRecords = append(recordSet.Properties.PtrRecords, &adns.PtrRecord{Ptrdname: to.StringPtr(rec.GetTargetField())})
+ case "TXT":
+ if recordSet.Properties.TxtRecords == nil {
+ recordSet.Properties.TxtRecords = []*adns.TxtRecord{}
+ }
+ // Empty TXT record needs to have no value set in it's properties
+ if rec.GetTargetTXTJoined() == "" {
+ var txts []*string
+ for _, txt := range rec.GetTargetTXTSegmented() {
+ txts = append(txts, to.StringPtr(txt))
+ }
+ recordSet.Properties.TxtRecords = append(recordSet.Properties.TxtRecords, &adns.TxtRecord{Value: txts})
+ }
+ case "MX":
+ if recordSet.Properties.MxRecords == nil {
+ recordSet.Properties.MxRecords = []*adns.MxRecord{}
+ }
+ recordSet.Properties.MxRecords = append(recordSet.Properties.MxRecords, &adns.MxRecord{Exchange: to.StringPtr(rec.GetTargetField()), Preference: to.Int32Ptr(int32(rec.MxPreference))})
+ case "SRV":
+ if recordSet.Properties.SrvRecords == nil {
+ recordSet.Properties.SrvRecords = []*adns.SrvRecord{}
+ }
+ recordSet.Properties.SrvRecords = append(recordSet.Properties.SrvRecords, &adns.SrvRecord{Target: to.StringPtr(rec.GetTargetField()), Port: to.Int32Ptr(int32(rec.SrvPort)), Weight: to.Int32Ptr(int32(rec.SrvWeight)), Priority: to.Int32Ptr(int32(rec.SrvPriority))})
+ /* CAA records don't work in a private zone */
+ case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME":
+ return nil, adns.RecordTypeA, fmt.Errorf("recordToNativeDiff2 RTYPE %v UNIMPLEMENTED", recordKeyType) // ands.A is a placeholder
+ default:
+ return nil, adns.RecordTypeA, fmt.Errorf("recordToNativeDiff2 RTYPE %v UNIMPLEMENTED", recordKeyType) // ands.A is a placeholder
+ }
+ }
+
+ rt, err := nativeToRecordTypeDiff(to.StringPtr(*recordSet.Type))
+ if err != nil {
+ return nil, adns.RecordTypeA, err // adns.A is a placeholder
+ }
+ return recordSet, rt, nil
+}
+
+func (a *azurednsProvider) fetchRecordSets(zoneName string) ([]*adns.RecordSet, error) {
+ if zoneName == "" {
+ return nil, nil
+ }
+ var records []*adns.RecordSet
+ ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
+ defer cancel()
+
+ recordsPager := a.recordsClient.NewListPager(*a.resourceGroup, zoneName, nil)
+
+ for recordsPager.More() {
+
+ waitTime := 1
+ retry:
+
+ nextResult, recordsErr := recordsPager.NextPage(ctx)
+
+ if recordsErr != nil {
+ err := recordsErr
+ if e, ok := err.(*azcore.ResponseError); ok {
+
+ if e.StatusCode == 429 {
+ waitTime = waitTime * 2
+ if waitTime > 300 {
+ return nil, err
+ }
+ printer.Printf("AZURE_PRIVATE_DNS: rate-limit paused for %v.\n", waitTime)
+ time.Sleep(time.Duration(waitTime+1) * time.Second)
+ goto retry
+ }
+ }
+ }
+
+ records = append(records, nextResult.Value...)
+ }
+
+ return records, nil
+}
+
+func (a *azurednsProvider) EnsureZoneExists(domain string) error {
+ if _, ok := a.zones[domain]; ok {
+ return nil
+ }
+ return nil
+}
diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go
index 91fbff0629..0c853f95bd 100644
--- a/providers/bind/bindProvider.go
+++ b/providers/bind/bindProvider.go
@@ -22,28 +22,34 @@ import (
"time"
"github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/bindserial"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/prettyzone"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns"
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can("Just writes out a comment indicating DNSSEC was requested"),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDHCID: providers.Can(),
+ providers.CanUseDNAME: providers.Can(),
providers.CanUseDS: providers.Can(),
+ providers.CanUseDNSKEY: providers.Can(),
+ providers.CanUseHTTPS: providers.Can(),
providers.CanUseLOC: providers.Can(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSOA: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.CanUseTLSA: providers.Can(),
- providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Can(),
@@ -87,11 +93,14 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
}
func init() {
+ const providerName = "BIND"
+ const providerMaintainer = "@tlimoncelli"
fns := providers.DspFuncs{
Initializer: initBind,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("BIND", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// SoaDefaults contains the parts of the default SOA settings.
@@ -112,14 +121,11 @@ func (s SoaDefaults) String() string {
// bindProvider is the provider handle for the bindProvider driver.
type bindProvider struct {
- DefaultNS []string `json:"default_ns"`
- DefaultSoa SoaDefaults `json:"default_soa"`
- nameservers []*models.Nameserver
- directory string
- filenameformat string
- zonefile string // Where the zone data is e texpected
- zoneFileFound bool // Did the zonefile exist?
- skipNextSoaIncrease bool // skip next SOA increment (for testing only)
+ DefaultNS []string `json:"default_ns"`
+ DefaultSoa SoaDefaults `json:"default_soa"`
+ nameservers []*models.Nameserver
+ directory string
+ filenameformat string
}
// GetNameservers returns the nameservers for a domain.
@@ -154,6 +160,7 @@ func (c *bindProvider) ListZones() ([]string, error) {
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
+ var zonefile string
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
printer.Printf("\nWARNING: BIND directory %q does not exist! (will create)\n", c.directory)
@@ -164,29 +171,25 @@ func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (mo
// This layering violation is needed for tests only.
// Otherwise, this is set already.
// Note: In this situation there is no "uniquename" or "tag".
- c.zonefile = filepath.Join(c.directory,
+ zonefile = filepath.Join(c.directory,
makeFileName(c.filenameformat, domain, domain, ""))
} else {
- c.zonefile = filepath.Join(c.directory,
+ zonefile = filepath.Join(c.directory,
makeFileName(c.filenameformat,
meta[models.DomainUniqueName], domain, meta[models.DomainTag]),
)
}
- content, err := os.ReadFile(c.zonefile)
+ content, err := os.ReadFile(zonefile)
if os.IsNotExist(err) {
// If the file doesn't exist, that's not an error. Just informational.
- c.zoneFileFound = false
- fmt.Fprintf(os.Stderr, "File does not yet exist: %q (will create)\n", c.zonefile)
+ fmt.Fprintf(os.Stderr, "File does not yet exist: %q (will create)\n", zonefile)
return nil, nil
}
if err != nil {
- return nil, fmt.Errorf("can't open %s: %w", c.zonefile, err)
+ return nil, fmt.Errorf("can't open %s: %w", zonefile, err)
}
- c.zoneFileFound = true
- zonefileName := c.zonefile
-
- return ParseZoneContents(string(content), domain, zonefileName)
+ return ParseZoneContents(string(content), domain, zonefile)
}
// ParseZoneContents parses a string as a BIND zone and returns the records.
@@ -195,7 +198,7 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo
foundRecords := models.Records{}
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
- rec, err := models.RRtoRC(rr, zoneName)
+ rec, err := models.RRtoRCTxtBug(rr, zoneName)
if err != nil {
return nil, err
}
@@ -208,10 +211,14 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo
return foundRecords, nil
}
+func (c *bindProvider) EnsureZoneExists(_ string) error {
+ return nil
+}
+
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records)
+func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
+ var zonefile string
changes := false
var msg string
@@ -237,17 +244,17 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
desiredSoa = dc.Records[len(dc.Records)-1]
} else {
*desiredSoa = *soaRec
- c.skipNextSoaIncrease = true
}
var msgs []string
- var err error
- msgs, changes, err = diff2.ByZone(foundRecords, dc, nil)
+ var actualChangeCount int
+ result, err := diff2.ByZone(foundRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
+ msgs, changes, actualChangeCount = result.Msgs, result.HasChanges, result.ActualChangeCount
if !changes {
- return nil, nil
+ return nil, 0, nil
}
msg = strings.Join(msgs, "\n")
@@ -263,22 +270,25 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
comments = append(comments, "Automatic DNSSEC signing requested")
}
- c.zonefile = filepath.Join(c.directory,
+ zonefile = filepath.Join(c.directory,
makeFileName(c.filenameformat,
dc.Metadata[models.DomainUniqueName], dc.Name, dc.Metadata[models.DomainTag]),
)
// We only change the serial number if there is a change.
- if !c.skipNextSoaIncrease {
- desiredSoa.SoaSerial = nextSerial
+ desiredSoa.SoaSerial = nextSerial
+
+ // If the --bindserial flag is used, force the serial to that value
+ if bindserial.ForcedValue != 0 {
+ desiredSoa.SoaSerial = uint32(bindserial.ForcedValue & 0xFFFF)
}
corrections = append(corrections,
&models.Correction{
Msg: msg,
F: func() error {
- printer.Printf("WRITING ZONEFILE: %v\n", c.zonefile)
- fname, err := preprocessFilename(c.zonefile)
+ printer.Printf("WRITING ZONEFILE: %v\n", zonefile)
+ fname, err := preprocessFilename(zonefile)
if err != nil {
return fmt.Errorf("could not create zonefile: %w", err)
}
@@ -289,7 +299,7 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
// Beware that if there are any fake types, then they will
// be commented out on write, but we don't reverse that when
// reading, so there will be a diff on every invocation.
- err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name, 0, comments)
+ err = prettyzone.WriteZoneFileRC(zf, result.DesiredPlus, dc.Name, 0, comments)
if err != nil {
return fmt.Errorf("failed WriteZoneFile: %w", err)
@@ -302,7 +312,7 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
},
})
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// preprocessFilename pre-processes a filename we're about to os.Create()
diff --git a/providers/bunnydns/api.go b/providers/bunnydns/api.go
new file mode 100644
index 0000000000..f7c2629887
--- /dev/null
+++ b/providers/bunnydns/api.go
@@ -0,0 +1,227 @@
+package bunnydns
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "golang.org/x/exp/slices"
+ "io"
+ "net/http"
+ "strconv"
+)
+
+const (
+ baseURL = "https://api.bunny.net"
+ pageSize = 100
+)
+
+type zone struct {
+ ID int64 `json:"Id"`
+ Domain string `json:"Domain"`
+ Nameserver1 string `json:"Nameserver1"`
+ Nameserver2 string `json:"Nameserver2"`
+}
+
+func (zone *zone) Nameservers() []string {
+ return []string{zone.Nameserver1, zone.Nameserver2}
+}
+
+type record struct {
+ ID int64 `json:"Id,omitempty"`
+ Type recordType `json:"Type"`
+ Name string `json:"Name"`
+ Value string `json:"Value"`
+ Disabled bool `json:"Disabled"`
+ TTL uint32 `json:"Ttl"`
+ Flags uint8 `json:"Flags"`
+ Priority uint16 `json:"Priority"`
+ Weight uint16 `json:"Weight"`
+ Port uint16 `json:"Port"`
+ Tag string `json:"Tag"`
+}
+
+type listZonesResponse struct {
+ Items []zone `json:"Items"`
+ TotalItems int32 `json:"TotalItems"`
+ HasMoreItems bool `json:"HasMoreItems"`
+}
+
+type getZoneResponse struct {
+ zone
+ Records []record `json:"Records"`
+}
+
+type queryParams map[string]string
+
+func (b *bunnydnsProvider) getImplicitRecordConfigs(zone *zone) (models.Records, error) {
+ nameservers := zone.Nameservers()
+ records := make(models.Records, 0, len(nameservers))
+
+ // NS records on the zone apex must be implicitly added, as Bunny DNS does not expose them via API
+ for _, ns := range nameservers {
+ rc := &models.RecordConfig{
+ Type: "NS",
+ Original: &record{},
+ }
+ rc.SetLabelFromFQDN(zone.Domain, zone.Domain)
+ if err := rc.SetTarget(ns + "."); err != nil {
+ return nil, err
+ }
+
+ records = append(records, rc)
+ }
+
+ return records, nil
+}
+
+func (b *bunnydnsProvider) findZoneByDomain(domain string) (*zone, error) {
+ if b.zones == nil {
+ zones, err := b.getAllZones()
+ if err != nil {
+ return nil, err
+ }
+
+ b.zones = make(map[string]*zone, len(zones))
+ for _, zone := range zones {
+ b.zones[zone.Domain] = zone
+ }
+ }
+
+ zone, ok := b.zones[domain]
+ if !ok {
+ return nil, fmt.Errorf("%q is not a zone in this BUNNY_DNS account", domain)
+ }
+
+ return zone, nil
+}
+
+func (b *bunnydnsProvider) getAllZones() ([]*zone, error) {
+ var zones []*zone
+ page := 1
+
+ for {
+ res := listZonesResponse{}
+ query := queryParams{"page": strconv.Itoa(page), "perPage": strconv.Itoa(pageSize)}
+ if err := b.request("GET", "/dnszone", query, nil, &res, nil); err != nil {
+ return nil, fmt.Errorf("could not fetch zones: %w", err)
+ }
+
+ if zones == nil {
+ zones = make([]*zone, 0, res.TotalItems)
+ }
+ for i := range res.Items {
+ zones = append(zones, &res.Items[i])
+ }
+
+ if !res.HasMoreItems {
+ break
+ }
+ page++
+ }
+
+ return zones, nil
+}
+
+func (b *bunnydnsProvider) createZone(domain string) (*zone, error) {
+ zone := &zone{}
+ body := map[string]string{"domain": domain}
+ err := b.request("POST", "/dnszone", nil, body, &zone, []int{http.StatusCreated})
+
+ if err != nil {
+ return nil, err
+ }
+
+ b.zones[domain] = zone
+ return zone, nil
+}
+
+func (b *bunnydnsProvider) getAllRecords(zoneID int64) ([]*record, error) {
+ zone := &getZoneResponse{}
+ err := b.request("GET", fmt.Sprintf("/dnszone/%d", zoneID), nil, nil, zone, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ records := make([]*record, 0, len(zone.Records))
+ for i := range zone.Records {
+ records = append(records, &zone.Records[i])
+ }
+
+ return records, nil
+}
+
+func (b *bunnydnsProvider) createRecord(zoneID int64, r *record) error {
+ url := fmt.Sprintf("/dnszone/%d/records", zoneID)
+ return b.request("PUT", url, nil, r, nil, []int{http.StatusCreated})
+}
+
+func (b *bunnydnsProvider) modifyRecord(zoneID int64, recordID int64, r *record) error {
+ url := fmt.Sprintf("/dnszone/%d/records/%d", zoneID, recordID)
+ return b.request("POST", url, nil, r, nil, []int{http.StatusNoContent})
+}
+
+func (b *bunnydnsProvider) deleteRecord(zoneID, recordID int64) error {
+ url := fmt.Sprintf("/dnszone/%d/records/%d", zoneID, recordID)
+ return b.request("DELETE", url, nil, nil, nil, []int{http.StatusNoContent})
+}
+
+func (b *bunnydnsProvider) request(method, endpoint string, query queryParams, body, target any, validStatus []int) error {
+ if validStatus == nil {
+ validStatus = []int{http.StatusOK}
+ }
+
+ var requestBody io.Reader
+ if body != nil {
+ requestBodyJSON, err := json.Marshal(body)
+ if err != nil {
+ return err
+ }
+ requestBody = bytes.NewBuffer(requestBodyJSON)
+ }
+
+ req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
+ if err != nil {
+ return err
+ }
+
+ req.Header.Add("AccessKey", b.apiKey)
+ if requestBody != nil {
+ req.Header.Add("Content-Type", "application/json")
+ }
+
+ if query != nil {
+ q := req.URL.Query()
+ for k, v := range query {
+ q.Add(k, v)
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+ cleanup := func() {
+ if err := resp.Body.Close(); err != nil {
+ printer.Printf("BUNNY_DNS: Could not close response body after API call: %q\n", err)
+ }
+ }
+
+ if !slices.Contains(validStatus, resp.StatusCode) {
+ data, _ := io.ReadAll(resp.Body)
+ printer.Println(fmt.Sprintf("BUNNY_DNS: Bad API response for %s %s: %s", method, endpoint, string(data)))
+ cleanup()
+ return fmt.Errorf("bad status code from BUNNY_DNS: %d not in %v", resp.StatusCode, validStatus)
+ }
+
+ if target == nil {
+ cleanup()
+ return nil
+ }
+
+ err = json.NewDecoder(resp.Body).Decode(target)
+ cleanup()
+ return err
+}
diff --git a/providers/bunnydns/auditrecords.go b/providers/bunnydns/auditrecords.go
new file mode 100644
index 0000000000..de810830b3
--- /dev/null
+++ b/providers/bunnydns/auditrecords.go
@@ -0,0 +1,17 @@
+package bunnydns
+
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
+
+// AuditRecords returns a list of errors corresponding to the records
+// that aren't supported by this provider. If all records are
+// supported, an empty list is returned.
+func AuditRecords(records []*models.RecordConfig) []error {
+ a := rejectif.Auditor{}
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-01-02
+ a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2024-01-02
+
+ return a.Audit(records)
+}
diff --git a/providers/bunnydns/bunnydnsProvider.go b/providers/bunnydns/bunnydnsProvider.go
new file mode 100644
index 0000000000..aa797b2886
--- /dev/null
+++ b/providers/bunnydns/bunnydnsProvider.go
@@ -0,0 +1,68 @@
+package bunnydns
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/providers"
+)
+
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanAutoDNSSEC: providers.Cannot(),
+ providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
+ providers.CanUseAlias: providers.Can("Bunny flattens CNAME records into A/AAAA records dynamically"),
+ providers.CanUseCAA: providers.Can(),
+ providers.CanUseDHCID: providers.Cannot(),
+ providers.CanUseDS: providers.Cannot(),
+ providers.CanUseDSForChildren: providers.Cannot(),
+ providers.CanUseLOC: providers.Cannot(),
+ providers.CanUseNAPTR: providers.Cannot(),
+ providers.CanUsePTR: providers.Can(),
+ providers.CanUseSOA: providers.Cannot(),
+ providers.CanUseSRV: providers.Can(),
+ providers.CanUseSSHFP: providers.Cannot(),
+ providers.CanUseTLSA: providers.Cannot(),
+ providers.DocCreateDomains: providers.Can(),
+ providers.DocDualHost: providers.Cannot(),
+ providers.DocOfficiallySupported: providers.Cannot(),
+}
+
+type bunnydnsProvider struct {
+ apiKey string
+ zones map[string]*zone
+}
+
+func init() {
+ const providerName = "BUNNY_DNS"
+ const providerMaintainer = "@ppmathis"
+ fns := providers.DspFuncs{
+ Initializer: newBunnydns,
+ RecordAuditor: AuditRecords,
+ }
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+}
+
+func newBunnydns(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
+ apiKey := settings["api_key"]
+ if apiKey == "" {
+ return nil, fmt.Errorf("missing BUNNY_DNS api_key")
+ }
+
+ return &bunnydnsProvider{
+ apiKey: apiKey,
+ }, nil
+}
+
+func (b *bunnydnsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ zone, err := b.findZoneByDomain(domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return models.ToNameservers(zone.Nameservers())
+}
diff --git a/providers/bunnydns/convert.go b/providers/bunnydns/convert.go
new file mode 100644
index 0000000000..97a40c946b
--- /dev/null
+++ b/providers/bunnydns/convert.go
@@ -0,0 +1,162 @@
+package bunnydns
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/miekg/dns/dnsutil"
+ "golang.org/x/exp/slices"
+)
+
+var fqdnTypes = []recordType{recordTypeCNAME, recordTypeMX, recordTypeNS, recordTypePTR, recordTypeSRV}
+
+func fromRecordConfig(rc *models.RecordConfig) (*record, error) {
+ r := record{
+ Type: recordTypeFromString(rc.Type),
+ Name: rc.GetLabel(),
+ Value: rc.GetTargetField(),
+ TTL: rc.TTL,
+ }
+
+ // While Bunny DNS does not use trailing dots, it still accepts and even preserves them for certain record types.
+ // To avoid confusion, any trailing dots are removed from the record value.
+ if slices.Contains(fqdnTypes, r.Type) && strings.HasSuffix(r.Value, ".") {
+ r.Value = strings.TrimSuffix(r.Value, ".")
+ }
+
+ switch r.Type {
+ case recordTypeNS:
+ if r.Name == "" {
+ r.TTL = 0
+ }
+ case recordTypeSRV:
+ r.Priority = rc.SrvPriority
+ r.Weight = rc.SrvWeight
+ r.Port = rc.SrvPort
+ case recordTypeCAA:
+ r.Flags = rc.CaaFlag
+ r.Tag = rc.CaaTag
+ case recordTypeMX:
+ r.Priority = rc.MxPreference
+ }
+
+ return &r, nil
+}
+
+func toRecordConfig(domain string, r *record) (*models.RecordConfig, error) {
+ rc := models.RecordConfig{
+ Type: recordTypeToString(r.Type),
+ TTL: r.TTL,
+ Original: r,
+ }
+ rc.SetLabel(r.Name, domain)
+
+ // Bunny DNS always operates with fully-qualified names and does not use any trailing dots.
+ // If a record already contains a trailing dot, which the provider UI also accepts, the record value is left as-is.
+ recordValue := r.Value
+ if slices.Contains(fqdnTypes, r.Type) && !strings.HasSuffix(r.Value, ".") {
+ recordValue = dnsutil.AddOrigin(r.Value+".", domain)
+ }
+
+ var err error
+ switch rc.Type {
+ case "CAA":
+ err = rc.SetTargetCAA(r.Flags, r.Tag, recordValue)
+ case "MX":
+ err = rc.SetTargetMX(r.Priority, recordValue)
+ case "SRV":
+ err = rc.SetTargetSRV(r.Priority, r.Weight, r.Port, recordValue)
+ default:
+ err = rc.PopulateFromStringFunc(rc.Type, recordValue, domain, nil)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return &rc, nil
+}
+
+type recordType int
+
+const (
+ recordTypeA recordType = 0
+ recordTypeAAAA recordType = 1
+ recordTypeCNAME recordType = 2
+ recordTypeTXT recordType = 3
+ recordTypeMX recordType = 4
+ recordTypeRedirect recordType = 5
+ recordTypeFlatten recordType = 6
+ recordTypePullZone recordType = 7
+ recordTypeSRV recordType = 8
+ recordTypeCAA recordType = 9
+ recordTypePTR recordType = 10
+ recordTypeScript recordType = 11
+ recordTypeNS recordType = 12
+)
+
+func recordTypeFromString(t string) recordType {
+ switch t {
+ case "A":
+ return recordTypeA
+ case "AAAA":
+ return recordTypeAAAA
+ case "CNAME":
+ return recordTypeCNAME
+ case "TXT":
+ return recordTypeTXT
+ case "MX":
+ return recordTypeMX
+ case "REDIRECT":
+ return recordTypeRedirect
+ case "FLATTEN":
+ return recordTypeFlatten
+ case "PULL_ZONE":
+ return recordTypePullZone
+ case "SRV":
+ return recordTypeSRV
+ case "CAA":
+ return recordTypeCAA
+ case "PTR":
+ return recordTypePTR
+ case "SCRIPT":
+ return recordTypeScript
+ case "NS":
+ return recordTypeNS
+ default:
+ panic(fmt.Errorf("BUNNY_DNS: rtype %v unimplemented", t))
+ }
+}
+
+func recordTypeToString(t recordType) string {
+ switch t {
+ case recordTypeA:
+ return "A"
+ case recordTypeAAAA:
+ return "AAAA"
+ case recordTypeCNAME:
+ return "CNAME"
+ case recordTypeTXT:
+ return "TXT"
+ case recordTypeMX:
+ return "MX"
+ case recordTypeRedirect:
+ return "REDIRECT"
+ case recordTypeFlatten:
+ return "FLATTEN"
+ case recordTypePullZone:
+ return "PULL_ZONE"
+ case recordTypeSRV:
+ return "SRV"
+ case recordTypeCAA:
+ return "CAA"
+ case recordTypePTR:
+ return "PTR"
+ case recordTypeScript:
+ return "SCRIPT"
+ case recordTypeNS:
+ return "NS"
+ default:
+ panic(fmt.Errorf("BUNNY_DNS: native rtype %v unimplemented", t))
+ }
+}
diff --git a/providers/bunnydns/listzones.go b/providers/bunnydns/listzones.go
new file mode 100644
index 0000000000..f73fe74c68
--- /dev/null
+++ b/providers/bunnydns/listzones.go
@@ -0,0 +1,32 @@
+package bunnydns
+
+import "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+
+func (b *bunnydnsProvider) ListZones() ([]string, error) {
+ zones, err := b.getAllZones()
+ if err != nil {
+ return nil, err
+ }
+
+ zoneNames := make([]string, 0, len(zones))
+ for _, zone := range zones {
+ zoneNames = append(zoneNames, zone.Domain)
+ }
+
+ return zoneNames, nil
+}
+
+func (b *bunnydnsProvider) EnsureZoneExists(domain string) error {
+ _, err := b.findZoneByDomain(domain)
+ if err == nil {
+ return nil
+ }
+
+ zone, err := b.createZone(domain)
+ if err != nil {
+ return err
+ }
+
+ printer.Warnf("BUNNY_DNS: Added zone %s with ID %d", domain, zone.ID)
+ return nil
+}
diff --git a/providers/bunnydns/records.go b/providers/bunnydns/records.go
new file mode 100644
index 0000000000..34e247bc0b
--- /dev/null
+++ b/providers/bunnydns/records.go
@@ -0,0 +1,152 @@
+package bunnydns
+
+import (
+ "fmt"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "golang.org/x/exp/slices"
+)
+
+func (b *bunnydnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
+ zone, err := b.findZoneByDomain(domain)
+ if err != nil {
+ return nil, err
+ }
+
+ nativeRecs, err := b.getAllRecords(zone.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ implicitRecs, err := b.getImplicitRecordConfigs(zone)
+ if err != nil {
+ return nil, err
+ }
+
+ recs := make(models.Records, 0, len(nativeRecs)+len(implicitRecs))
+ recs = append(recs, implicitRecs...)
+
+ // Define a list of record types that are currently not supported by this provider.
+ unsupportedTypes := []recordType{
+ recordTypeRedirect,
+ recordTypeFlatten,
+ recordTypePullZone,
+ recordTypeScript,
+ }
+
+ // Loop through all native records and convert them to standardized RecordConfigs
+ // Unsupported record types are ignored with a warning and will remain untouched in the zone.
+ for _, nativeRec := range nativeRecs {
+ if slices.Contains(unsupportedTypes, nativeRec.Type) {
+ printer.Warnf("BUNNY_DNS: ignoring unsupported record type %s\n", recordTypeToString(nativeRec.Type))
+ continue
+ }
+
+ rc, err := toRecordConfig(zone.Domain, nativeRec)
+ if err != nil {
+ return nil, err
+ }
+ recs = append(recs, rc)
+ }
+
+ return recs, nil
+}
+
+func (b *bunnydnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
+ // Bunny DNS never returns NS records for the apex domain, so these are artificially added when retrieving records.
+ // As no TTL can be configured or retrieved for these NS records, we set it to 0 to avoid unnecessary updates.
+ for _, rc := range dc.Records {
+ if rc.Name == "@" && rc.Type == "NS" {
+ rc.TTL = 0
+ }
+
+ if rc.Type == "ALIAS" {
+ rc.Type = "CNAME"
+ }
+ }
+
+ zone, err := b.findZoneByDomain(dc.Name)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ instructions, actualChangeCount, err := diff2.ByRecord(existing, dc, nil)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ var corrections []*models.Correction
+ for _, inst := range instructions {
+ switch inst.Type {
+ case diff2.REPORT:
+ corrections = append(corrections, &models.Correction{
+ Msg: inst.MsgsJoined,
+ })
+ case diff2.CREATE:
+ corrections = append(corrections, b.mkCreateCorrection(
+ zone.ID, inst.New[0], inst.Msgs[0],
+ ))
+ case diff2.CHANGE:
+ corrections = append(corrections, b.mkChangeCorrection(
+ zone.ID, inst.Old[0], inst.New[0], inst.Msgs[0],
+ ))
+ case diff2.DELETE:
+ corrections = append(corrections, b.mkDeleteCorrection(
+ zone.ID, inst.Old[0], inst.Msgs[0],
+ ))
+ default:
+ panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
+ }
+ }
+
+ return corrections, actualChangeCount, nil
+}
+
+func (b *bunnydnsProvider) mkCreateCorrection(zoneID int64, newRec *models.RecordConfig, msg string) *models.Correction {
+ return &models.Correction{
+ Msg: msg,
+ F: func() error {
+ desired, err := fromRecordConfig(newRec)
+ if err != nil {
+ return err
+ }
+
+ return b.createRecord(zoneID, desired)
+ },
+ }
+}
+
+func (b *bunnydnsProvider) mkChangeCorrection(zoneID int64, oldRec, newRec *models.RecordConfig, msg string) *models.Correction {
+ return &models.Correction{
+ Msg: msg,
+ F: func() error {
+ existingID := oldRec.Original.(*record).ID
+ if existingID == 0 {
+ return fmt.Errorf("BUNNY_DNS: cannot change implicit records")
+ }
+
+ desired, err := fromRecordConfig(newRec)
+ if err != nil {
+ return err
+ }
+
+ return b.modifyRecord(zoneID, existingID, desired)
+ },
+ }
+}
+
+func (b *bunnydnsProvider) mkDeleteCorrection(zoneID int64, oldRec *models.RecordConfig, msg string) *models.Correction {
+ return &models.Correction{
+ Msg: msg,
+ F: func() error {
+ existingID := oldRec.Original.(*record).ID
+ if existingID == 0 {
+ return fmt.Errorf("BUNNY_DNS: cannot delete implicit records")
+ }
+
+ return b.deleteRecord(zoneID, existingID)
+ },
+ }
+}
diff --git a/providers/capabilities.go b/providers/capabilities.go
index 5af2817ca5..72f4519858 100644
--- a/providers/capabilities.go
+++ b/providers/capabilities.go
@@ -2,7 +2,9 @@
package providers
-import "log"
+import (
+ "log"
+)
// Capability is a bitmasked set of "features" that a provider supports. Only use constants from this package.
type Capability uint32
@@ -17,6 +19,12 @@ const (
// so folks can ask for that.
CanAutoDNSSEC Capability = iota
+ // CanConcur indicates the provider can be used concurrently. Can()
+ // indicates that it has been tested and shown to work concurrently.
+ // Cannot() indicates it has not been tested OR it has been shown to not
+ // work when used concurrently. The default is Cannot().
+ CanConcur
+
// CanGetZones indicates the provider supports the get-zones subcommand.
CanGetZones
@@ -35,6 +43,9 @@ const (
// CanUseDHCID indicates the provider can handle DHCID records
CanUseDHCID
+ // CanUseDNAME indicates the provider can handle DNAME records
+ CanUseDNAME
+
// CanUseDS indicates that the provider can handle DS record types. This
// implies CanUseDSForChildren without specifying the latter explicitly.
CanUseDS
@@ -43,6 +54,9 @@ const (
// only for children records, not at the root of the zone.
CanUseDSForChildren
+ // CanUseHTTPS indicates the provider can handle HTTPS records
+ CanUseHTTPS
+
// CanUseLOC indicates whether service provider handles LOC records
CanUseLOC
@@ -64,19 +78,19 @@ const (
// CanUseSSHFP indicates the provider can handle SSHFP records
CanUseSSHFP
+ // CanUseSVCB indicates the provider can handle SVCB records
+ CanUseSVCB
+
// CanUseTLSA indicates the provider can handle TLSA records
CanUseTLSA
- // CantUseNOPURGE indicates NO_PURGE is broken for this provider. To make it
- // work would require complex emulation of an incremental update mechanism,
- // so it is easier to simply mark this feature as not working for this
- // provider.
- CantUseNOPURGE
+ // CanUseDNSKEY indicates that the provider can handle DNSKEY records
+ CanUseDNSKEY
// DocCreateDomains means provider can add domains with the `dnscontrol create-domains` command
DocCreateDomains
- // DocDualHost means provider allows full management of apex NS records, so we can safely dual-host with anothe provider
+ // DocDualHost means provider allows full management of apex NS records, so we can safely dual-host with another provider
DocDualHost
// DocOfficiallySupported means it is actively used and maintained by stack exchange
diff --git a/providers/capability_string.go b/providers/capability_string.go
index 1363be8dd6..b41ba039fe 100644
--- a/providers/capability_string.go
+++ b/providers/capability_string.go
@@ -9,31 +9,35 @@ func _() {
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CanAutoDNSSEC-0]
- _ = x[CanGetZones-1]
- _ = x[CanUseAKAMAICDN-2]
- _ = x[CanUseAlias-3]
- _ = x[CanUseAzureAlias-4]
- _ = x[CanUseCAA-5]
- _ = x[CanUseDHCID-6]
- _ = x[CanUseDS-7]
- _ = x[CanUseDSForChildren-8]
- _ = x[CanUseLOC-9]
- _ = x[CanUseNAPTR-10]
- _ = x[CanUsePTR-11]
- _ = x[CanUseRoute53Alias-12]
- _ = x[CanUseSOA-13]
- _ = x[CanUseSRV-14]
- _ = x[CanUseSSHFP-15]
- _ = x[CanUseTLSA-16]
- _ = x[CantUseNOPURGE-17]
- _ = x[DocCreateDomains-18]
- _ = x[DocDualHost-19]
- _ = x[DocOfficiallySupported-20]
+ _ = x[CanConcur-1]
+ _ = x[CanGetZones-2]
+ _ = x[CanUseAKAMAICDN-3]
+ _ = x[CanUseAlias-4]
+ _ = x[CanUseAzureAlias-5]
+ _ = x[CanUseCAA-6]
+ _ = x[CanUseDHCID-7]
+ _ = x[CanUseDNAME-8]
+ _ = x[CanUseDS-9]
+ _ = x[CanUseDSForChildren-10]
+ _ = x[CanUseHTTPS-11]
+ _ = x[CanUseLOC-12]
+ _ = x[CanUseNAPTR-13]
+ _ = x[CanUsePTR-14]
+ _ = x[CanUseRoute53Alias-15]
+ _ = x[CanUseSOA-16]
+ _ = x[CanUseSRV-17]
+ _ = x[CanUseSSHFP-18]
+ _ = x[CanUseSVCB-19]
+ _ = x[CanUseTLSA-20]
+ _ = x[CanUseDNSKEY-21]
+ _ = x[DocCreateDomains-22]
+ _ = x[DocDualHost-23]
+ _ = x[DocOfficiallySupported-24]
}
-const _Capability_name = "CanAutoDNSSECCanGetZonesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDHCIDCanUseDSCanUseDSForChildrenCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseSOACanUseSRVCanUseSSHFPCanUseTLSACantUseNOPURGEDocCreateDomainsDocDualHostDocOfficiallySupported"
+const _Capability_name = "CanAutoDNSSECCanConcurCanGetZonesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDHCIDCanUseDNAMECanUseDSCanUseDSForChildrenCanUseHTTPSCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseSOACanUseSRVCanUseSSHFPCanUseSVCBCanUseTLSACanUseDNSKEYDocCreateDomainsDocDualHostDocOfficiallySupported"
-var _Capability_index = [...]uint16{0, 13, 24, 39, 50, 66, 75, 86, 94, 113, 122, 133, 142, 160, 169, 178, 189, 199, 213, 229, 240, 262}
+var _Capability_index = [...]uint16{0, 13, 22, 33, 48, 59, 75, 84, 95, 106, 114, 133, 144, 153, 164, 173, 191, 200, 209, 220, 230, 240, 252, 268, 279, 301}
func (i Capability) String() string {
if i >= Capability(len(_Capability_index)-1) {
diff --git a/providers/cloudflare/auditrecords.go b/providers/cloudflare/auditrecords.go
index 71a66a06ed..66cf53d9d0 100644
--- a/providers/cloudflare/auditrecords.go
+++ b/providers/cloudflare/auditrecords.go
@@ -11,8 +11,6 @@ import (
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
- a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2022-06-18
-
a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2022-06-18
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2022-06-18
diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go
index 432de0cf97..89b090b0cb 100644
--- a/providers/cloudflare/cloudflareProvider.go
+++ b/providers/cloudflare/cloudflareProvider.go
@@ -1,12 +1,14 @@
package cloudflare
import (
+ "context"
"encoding/json"
"fmt"
"net"
"os"
"strconv"
"strings"
+ "sync"
"golang.org/x/net/idna"
@@ -15,6 +17,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/transform"
"github.com/StackExchange/dnscontrol/v4/providers"
+ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
"github.com/cloudflare/cloudflare-go"
"github.com/fatih/color"
)
@@ -39,15 +42,21 @@ Domain level metadata available:
*/
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Can("CF automatically flattens CNAME records into A records dynamically"),
providers.CanUseCAA: providers.Can(),
+ providers.CanUseDNSKEY: providers.Cannot(),
providers.CanUseDSForChildren: providers.Can(),
+ providers.CanUseHTTPS: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"),
@@ -55,58 +64,65 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "CLOUDFLAREAPI"
+ const providerMaintainer = "@tresni"
fns := providers.DspFuncs{
Initializer: newCloudflare,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", fns, features)
- providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "")
- providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "")
- providers.RegisterCustomRecordType("CF_WORKER_ROUTE", "CLOUDFLAREAPI", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterCustomRecordType("CF_REDIRECT", providerName, "")
+ providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", providerName, "")
+ providers.RegisterCustomRecordType("CF_WORKER_ROUTE", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// cloudflareProvider is the handle for API calls.
type cloudflareProvider struct {
- domainIndex map[string]string // Call c.fetchDomainList() to populate before use.
- nameservers map[string][]string
ipConversions []transform.IPConversion
ignoredLabels []string
- manageRedirects bool
+ manageRedirects bool // Old "Page Rule"-style redirects.
manageWorkers bool
accountID string
cfClient *cloudflare.API
+ //
+ manageSingleRedirects bool // New "Single Redirects"-style redirects.
+ //
+ // Used by
+ tcLogFilename string // Transcode Log file name
+ tcLogFh *os.File // Transcode Log file handle
+ tcZone string // Transcode Current zone
+
+ sync.Mutex // Protects all access to the following fields:
+ domainIndex map[string]string // Cache of zone name to zone ID.
+ nameservers map[string][]string // Cache of zone name to list of nameservers.
}
-// TODO(dlemenkov): remove this function after deleting all commented code referecing it
-//func labelMatches(label string, matches []string) bool {
-// printer.Debugf("DEBUG: labelMatches(%#v, %#v)\n", label, matches)
-// for _, tst := range matches {
-// if label == tst {
-// return true
-// }
-// }
-// return false
-//}
-
// GetNameservers returns the nameservers for a domain.
func (c *cloudflareProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
- if c.domainIndex == nil {
- if err := c.fetchDomainList(); err != nil {
- return nil, err
- }
+
+ c.Lock()
+ defer c.Unlock()
+ if err := c.cacheDomainList(); err != nil {
+ return nil, err
}
+
ns, ok := c.nameservers[domain]
if !ok {
- return nil, fmt.Errorf("nameservers for %s not found in cloudflare account", domain)
+ return nil, fmt.Errorf("nameservers for %s not found in cloudflare cache(%q)", domain, c.accountID)
}
return models.ToNameservers(ns)
}
// ListZones returns a list of the DNS zones.
func (c *cloudflareProvider) ListZones() ([]string, error) {
- if err := c.fetchDomainList(); err != nil {
+
+ c.Lock()
+ defer c.Unlock()
+ if err := c.cacheDomainList(); err != nil {
return nil, err
}
+
zones := make([]string, 0, len(c.domainIndex))
for d := range c.domainIndex {
zones = append(zones, d)
@@ -140,18 +156,7 @@ func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]strin
}
}
- // // FIXME(tlim) Why is this needed???
- // // I don't know. Let's comment it out and see if anything breaks.
- // for i := len(records) - 1; i >= 0; i-- {
- // rec := records[i]
- // // Delete ignore labels
- // if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
- // printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
- // records = append(records[:i], records[i+1:]...)
- // }
- // }
-
- if c.manageRedirects {
+ if c.manageRedirects { // if old
prs, err := c.getPageRules(domainID, domain)
if err != nil {
return nil, err
@@ -159,6 +164,16 @@ func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]strin
records = append(records, prs...)
}
+ if c.manageSingleRedirects { // if new xor old
+ // Download the list of Single Redirects.
+ // For each one, generate a SINGLEREDIRECT record
+ prs, err := c.getSingleRedirects(domainID, domain)
+ if err != nil {
+ return nil, err
+ }
+ records = append(records, prs...)
+ }
+
if c.manageWorkers {
wrs, err := c.getWorkerRoutes(domainID, domain)
if err != nil {
@@ -174,11 +189,13 @@ func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]strin
}
func (c *cloudflareProvider) getDomainID(name string) (string, error) {
- if c.domainIndex == nil {
- if err := c.fetchDomainList(); err != nil {
- return "", err
- }
+
+ c.Lock()
+ defer c.Unlock()
+ if err := c.cacheDomainList(); err != nil {
+ return "", err
}
+
id, ok := c.domainIndex[name]
if !ok {
return "", fmt.Errorf("'%s' not a zone in cloudflare account", name)
@@ -187,40 +204,32 @@ func (c *cloudflareProvider) getDomainID(name string) (string, error) {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, int, error) {
+
+ for _, rec := range dc.Records {
+ if rec.Type == "ALIAS" {
+ rec.Type = "CNAME"
+ }
+ }
if err := c.preprocessConfig(dc); err != nil {
- return nil, err
+ return nil, 0, err
}
- // for i := len(records) - 1; i >= 0; i-- {
- // rec := records[i]
- // // Delete ignore labels
- // if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
- // printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
- // records = append(records[:i], records[i+1:]...)
- // }
- // }
checkNSModifications(dc)
domainID, err := c.getDomainID(dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, rec := range dc.Records {
- if rec.Type == "ALIAS" {
- rec.Type = "CNAME"
- }
// As per CF-API documentation proxied records are always forced to have a TTL of 1.
// When not forcing this property change here, dnscontrol tries each time to update
// the TTL of a record which simply cannot be changed anyway.
if rec.Metadata[metaProxy] != "off" {
rec.TTL = 1
}
- // if labelMatches(rec.GetLabel(), c.ignoredLabels) {
- // log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels)
- // }
}
checkNSModifications(dc)
@@ -228,9 +237,9 @@ func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
var corrections []*models.Correction
// Cloudflare is a "ByRecord" API.
- instructions, err := diff2.ByRecord(records, dc, genComparable)
+ instructions, actualChangeCount, err := diff2.ByRecord(records, dc, genComparable)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, inst := range instructions {
@@ -255,8 +264,7 @@ func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
case diff2.DELETE:
deleteRec := inst.Old[0]
deleteRecType := deleteRec.Type
- deleteRecOrig := deleteRec.Original
- corrs = c.mkDeleteCorrection(deleteRecType, deleteRecOrig, domainID, msg)
+ corrs = c.mkDeleteCorrection(deleteRecType, deleteRec, domainID, msg)
// DS records must always have a corresponding NS record.
// Therefore, we remove DS records before any NS records.
addToFront = (deleteRecType == "DS")
@@ -283,7 +291,7 @@ func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func genComparable(rec *models.RecordConfig) string {
@@ -307,13 +315,20 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom
case "PAGE_RULE":
return []*models.Correction{{
Msg: msg,
- F: func() error { return c.createPageRule(domainID, newrec.GetTargetField()) },
+ F: func() error { return c.createPageRule(domainID, *newrec.CloudflareRedirect) },
}}
case "WORKER_ROUTE":
return []*models.Correction{{
Msg: msg,
F: func() error { return c.createWorkerRoute(domainID, newrec.GetTargetField()) },
}}
+ case cfsingleredirect.SINGLEREDIRECT:
+ return []*models.Correction{{
+ Msg: msg,
+ F: func() error {
+ return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect)
+ },
+ }}
default:
return c.createRecDiff2(newrec, domainID, msg)
}
@@ -327,6 +342,8 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon
idTxt = oldrec.Original.(cloudflare.PageRule).ID
case "WORKER_ROUTE":
idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID
+ case cfsingleredirect.SINGLEREDIRECT:
+ idTxt = oldrec.CloudflareRedirect.SRRRulesetID
default:
idTxt = oldrec.Original.(cloudflare.DNSRecord).ID
}
@@ -337,7 +354,14 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon
return []*models.Correction{{
Msg: msg,
F: func() error {
- return c.updatePageRule(idTxt, domainID, newrec.GetTargetField())
+ return c.updatePageRule(idTxt, domainID, *newrec.CloudflareRedirect)
+ },
+ }}
+ case cfsingleredirect.SINGLEREDIRECT:
+ return []*models.Correction{{
+ Msg: msg,
+ F: func() error {
+ return c.updateSingleRedirect(domainID, oldrec, newrec)
},
}}
case "WORKER_ROUTE":
@@ -358,16 +382,18 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon
}
}
-func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec any, domainID string, msg string) []*models.Correction {
+func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec *models.RecordConfig, domainID string, msg string) []*models.Correction {
var idTxt string
switch recType {
case "PAGE_RULE":
- idTxt = origRec.(cloudflare.PageRule).ID
+ idTxt = origRec.Original.(cloudflare.PageRule).ID
case "WORKER_ROUTE":
- idTxt = origRec.(cloudflare.WorkerRoute).ID
+ idTxt = origRec.Original.(cloudflare.WorkerRoute).ID
+ case cfsingleredirect.SINGLEREDIRECT:
+ idTxt = origRec.Original.(cloudflare.RulesetRule).ID
default:
- idTxt = origRec.(cloudflare.DNSRecord).ID
+ idTxt = origRec.Original.(cloudflare.DNSRecord).ID
}
msg = msg + color.RedString(" id=%v", idTxt)
@@ -376,11 +402,13 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec any, dom
F: func() error {
switch recType {
case "PAGE_RULE":
- return c.deletePageRule(origRec.(cloudflare.PageRule).ID, domainID)
+ return c.deletePageRule(origRec.Original.(cloudflare.PageRule).ID, domainID)
case "WORKER_ROUTE":
- return c.deleteWorkerRoute(origRec.(cloudflare.WorkerRoute).ID, domainID)
+ return c.deleteWorkerRoute(origRec.Original.(cloudflare.WorkerRoute).ID, domainID)
+ case cfsingleredirect.SINGLEREDIRECT:
+ return c.deleteSingleRedirects(domainID, *origRec.CloudflareRedirect)
default:
- return c.deleteDNSRecord(origRec.(cloudflare.DNSRecord), domainID)
+ return c.deleteDNSRecord(origRec.Original.(cloudflare.DNSRecord), domainID)
}
},
}
@@ -397,10 +425,9 @@ func checkNSModifications(dc *models.DomainConfig) {
for _, rec := range dc.Records {
if rec.Type == "NS" && rec.GetLabelFQDN() == punyRoot {
- if !strings.HasSuffix(rec.GetTargetField(), ".ns.cloudflare.com.") {
- printer.Warnf("cloudflare does not support modifying NS records on base domain. %s will not be added.\n", rec.GetTargetField())
+ if strings.HasSuffix(rec.GetTargetField(), ".ns.cloudflare.com.") {
+ continue
}
- continue
}
newList = append(newList, rec)
}
@@ -472,7 +499,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
// A and CNAMEs: Validate. If null, set to default.
// else: Make sure it wasn't set. Set to default.
// iterate backwards so first defined page rules have highest priority
- currentPrPrio := 1
+ prPriority := 0
for i := len(dc.Records) - 1; i >= 0; i-- {
rec := dc.Records[i]
if rec.Metadata == nil {
@@ -506,27 +533,62 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
}
}
- // CF_REDIRECT record types. Encode target as $FROM,$TO,$PRIO,$CODE
+ // CF_REDIRECT record types:
if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" {
- if !c.manageRedirects {
- return fmt.Errorf("you must add 'manage_redirects: true' metadata to cloudflare provider to use CF_REDIRECT records")
+ if !c.manageRedirects && !c.manageSingleRedirects {
+ return fmt.Errorf("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_REDIRECT/CF_TEMP_REDIRECT records")
}
- parts := strings.Split(rec.GetTargetField(), ",")
- if len(parts) != 2 {
- return fmt.Errorf("invalid data specified for cloudflare redirect record")
- }
- code := 301
+ code := uint16(301)
if rec.Type == "CF_TEMP_REDIRECT" {
code = 302
}
- rec.SetTarget(fmt.Sprintf("%s,%d,%d", rec.GetTargetField(), currentPrPrio, code))
- currentPrPrio++
- rec.TTL = 1
- rec.Type = "PAGE_RULE"
- }
- // CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT
- if rec.Type == "CF_WORKER_ROUTE" {
+ part := strings.SplitN(rec.GetTargetField(), ",", 2)
+ prWhen, prThen := part[0], part[1]
+ prPriority++
+
+ // Convert this record to a PAGE_RULE.
+ cfsingleredirect.MakePageRule(rec, prPriority, code, prWhen, prThen)
+ rec.SetLabel("@", dc.Name)
+
+ if c.manageRedirects && !c.manageSingleRedirects {
+ // Old-Style only. No additional work needed.
+
+ } else if !c.manageRedirects && c.manageSingleRedirects {
+ // New-Style only. Convert PAGE_RULE to SINGLEREDIRECT.
+ cfsingleredirect.TranscodePRtoSR(rec)
+ if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil {
+ return err
+ }
+
+ } else {
+ // Both old-style and new-style enabled!
+ // Retain the PAGE_RULE and append an additional SINGLEREDIRECT.
+
+ // make a copy:
+ newRec, err := rec.Copy()
+ if err != nil {
+ return err
+ }
+ // The copy becomes the CF SingleRedirect
+ cfsingleredirect.TranscodePRtoSR(rec)
+ if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil {
+ return err
+ }
+ // Append the copy to the end of the list.
+ dc.Records = append(dc.Records, newRec)
+
+ // The original PAGE_RULE remains untouched.
+ }
+
+ } else if rec.Type == cfsingleredirect.SINGLEREDIRECT {
+ // SINGLEREDIRECT record types. Verify they are enabled.
+ if !c.manageSingleRedirects {
+ return fmt.Errorf("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_SINGLE__REDIRECT records")
+ }
+
+ } else if rec.Type == "CF_WORKER_ROUTE" {
+ // CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT
parts := strings.Split(rec.GetTargetField(), ",")
if len(parts) != 2 {
return fmt.Errorf("invalid data specified for cloudflare worker record")
@@ -561,6 +623,38 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
return nil
}
+func (c *cloudflareProvider) LogTranscode(zone string, redirect *models.CloudflareSingleRedirectConfig) error {
+ // No filename? Don't log anything.
+ filename := c.tcLogFilename
+ if filename == "" {
+ return nil
+ }
+
+ // File not opened already? Open it.
+ if c.tcLogFh == nil {
+ f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
+ if err != nil {
+ return err
+ }
+ c.tcLogFh = f
+ }
+ fh := c.tcLogFh
+
+ // Output "D(zone)" if needed.
+ var text string
+ if c.tcZone != zone {
+ text = fmt.Sprintf("D(%q, ...\n", zone)
+ }
+ c.tcZone = zone
+
+ // Generate the new command and output.
+ text = text + fmt.Sprintf(" CF_SINGLE_REDIRECT(%q,\n %03d,\n '%s',\n '%s'\n ),\n",
+ redirect.SRName, redirect.Code,
+ redirect.SRWhen, redirect.SRThen)
+ _, err := fh.WriteString(text)
+ return err
+}
+
func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
api := &cloudflareProvider{}
// check api keys from creds json file
@@ -601,14 +695,19 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
parsedMeta := &struct {
IPConversions string `json:"ip_conversions"`
IgnoredLabels []string `json:"ignored_labels"`
- ManageRedirects bool `json:"manage_redirects"`
+ ManageRedirects bool `json:"manage_redirects"` // Old-style PAGE_RULE-based redirects
ManageWorkers bool `json:"manage_workers"`
+ //
+ ManageSingleRedirects bool `json:"manage_single_redirects"` // New-style Dynamic "Single Redirects"
+ TranscodeLogFilename string `json:"transcode_log"` // Log the PAGE_RULE conversions.
}{}
err := json.Unmarshal([]byte(metadata), parsedMeta)
if err != nil {
return nil, err
}
+ api.manageSingleRedirects = parsedMeta.ManageSingleRedirects
api.manageRedirects = parsedMeta.ManageRedirects
+ api.tcLogFilename = parsedMeta.TranscodeLogFilename
api.manageWorkers = parsedMeta.ManageWorkers
// ignored_labels:
api.ignoredLabels = append(api.ignoredLabels, parsedMeta.IgnoredLabels...)
@@ -636,15 +735,17 @@ type cfRecData struct {
Weight uint16 `json:"weight"` // SRV
Port uint16 `json:"port"` // SRV
Tag string `json:"tag"` // CAA
- Flags uint8 `json:"flags"` // CAA
+ Flags uint16 `json:"flags"` // CAA/DNSKEY
Value string `json:"value"` // CAA
Usage uint8 `json:"usage"` // TLSA
Selector uint8 `json:"selector"` // TLSA
MatchingType uint8 `json:"matching_type"` // TLSA
Certificate string `json:"certificate"` // TLSA
- Algorithm uint8 `json:"algorithm"` // SSHFP/DS
+ Algorithm uint8 `json:"algorithm"` // SSHFP/DNSKEY/DS
HashType uint8 `json:"type"` // SSHFP
Fingerprint string `json:"fingerprint"` // SSHFP
+ Protocol uint8 `json:"protocol"` // DNSKEY
+ PublicKey string `json:"public_key"` // DNSKEY
KeyTag uint16 `json:"key_tag"` // DS
DigestType uint8 `json:"digest_type"` // DS
Digest string `json:"digest"` // DS
@@ -714,13 +815,13 @@ func uint16Zero(value interface{}) uint16 {
return 0
}
-// intZero converts value to int or returns 0.
-func intZero(value interface{}) int {
+// intZero converts value to uint16 or returns 0.
+func intZero(value interface{}) uint16 {
switch v := value.(type) {
case float64:
- return int(v)
+ return uint16(v)
case int:
- return v
+ return uint16(v)
case nil:
}
return 0
@@ -739,7 +840,7 @@ func stringDefault(value interface{}, def string) string {
func (c *cloudflareProvider) nativeToRecord(domain string, cr cloudflare.DNSRecord) (*models.RecordConfig, error) {
// normalize cname,mx,ns records with dots to be consistent with our config format.
- if cr.Type == "CNAME" || cr.Type == "MX" || cr.Type == "NS" || cr.Type == "PTR" {
+ if cr.Type == "ALIAS" || cr.Type == "CNAME" || cr.Type == "MX" || cr.Type == "NS" || cr.Type == "PTR" {
if cr.Content != "." {
cr.Content = cr.Content + "."
}
@@ -812,21 +913,24 @@ func getProxyMetadata(r *models.RecordConfig) map[string]string {
// EnsureZoneExists creates a zone if it does not exist
func (c *cloudflareProvider) EnsureZoneExists(domain string) error {
- if c.domainIndex == nil {
- if err := c.fetchDomainList(); err != nil {
- return err
- }
+
+ c.Lock()
+ defer c.Unlock()
+ if err := c.cacheDomainList(); err != nil {
+ return err
}
+
if _, ok := c.domainIndex[domain]; ok {
return nil
}
var id string
id, err := c.createZone(domain)
printer.Printf("Added zone for %s to Cloudflare account: %s\n", domain, id)
+ clear(c.domainIndex) // clear the cache so that the next caller has to refresh it, thus loading the new ID.
return err
}
-// PrepareCloudflareTestWorkers creates Cloudflare Workers required for CF_WORKER_ROUTE tests.
+// PrepareCloudflareTestWorkers creates Cloudflare Workers required for CF_WORKER_ROUTE integration tests.
func PrepareCloudflareTestWorkers(prv providers.DNSServiceProvider) error {
cf, ok := prv.(*cloudflareProvider)
if ok {
@@ -842,3 +946,18 @@ func PrepareCloudflareTestWorkers(prv providers.DNSServiceProvider) error {
}
return nil
}
+
+func (c *cloudflareProvider) createTestWorker(workerName string) error {
+ wp := cloudflare.CreateWorkerParams{
+ ScriptName: workerName,
+ Script: `
+ addEventListener("fetch", (event) => {
+ event.respondWith(
+ new Response("Ok.", { status: 200 })
+ );
+ });`,
+ }
+
+ _, err := c.cfClient.UploadWorker(context.Background(), cloudflare.AccountIdentifier(c.accountID), wp)
+ return err
+}
diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go
index 8b8c708459..860d8cc5c9 100644
--- a/providers/cloudflare/rest.go
+++ b/providers/cloudflare/rest.go
@@ -2,32 +2,46 @@ package cloudflare
import (
"context"
+ "errors"
"fmt"
- "strconv"
"strings"
"golang.org/x/net/idna"
"github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
"github.com/cloudflare/cloudflare-go"
)
// get list of domains for account. Cache so the ids can be looked up from domain name
-func (c *cloudflareProvider) fetchDomainList() error {
- c.domainIndex = map[string]string{}
- c.nameservers = map[string][]string{}
+// The caller must do all locking.
+func (c *cloudflareProvider) cacheDomainList() error {
+ if c.domainIndex != nil {
+ return nil
+ }
+
+ //fmt.Printf("DEBUG: CLOUDFLARE POPULATING CACHE\n")
zones, err := c.cfClient.ListZones(context.Background())
if err != nil {
return fmt.Errorf("failed fetching domain list from cloudflare(%q): %s", c.cfClient.APIEmail, err)
}
+ c.domainIndex = map[string]string{}
+ c.nameservers = map[string][]string{}
+
for _, zone := range zones {
if encoded, err := idna.ToASCII(zone.Name); err == nil && encoded != zone.Name {
+ if _, ok := c.domainIndex[encoded]; ok {
+ fmt.Printf("WARNING: Zone %q appears twice in this cloudflare account\n", encoded)
+ }
c.domainIndex[encoded] = zone.ID
- c.nameservers[encoded] = append(c.nameservers[encoded], zone.NameServers...)
+ c.nameservers[encoded] = zone.NameServers
+ }
+ if _, ok := c.domainIndex[zone.Name]; ok {
+ fmt.Printf("WARNING: Zone %q appears twice in this cloudflare account\n", zone.Name)
}
c.domainIndex[zone.Name] = zone.ID
- c.nameservers[zone.Name] = append(c.nameservers[zone.Name], zone.NameServers...)
+ c.nameservers[zone.Name] = zone.NameServers
}
return nil
@@ -59,6 +73,15 @@ func (c *cloudflareProvider) createZone(domainName string) (string, error) {
return zone.ID, err
}
+func cfDnskeyData(rec *models.RecordConfig) *cfRecData {
+ return &cfRecData{
+ Algorithm: rec.DnskeyAlgorithm,
+ Flags: rec.DnskeyFlags,
+ Protocol: rec.DnskeyProtocol,
+ PublicKey: rec.DnskeyPublicKey,
+ }
+}
+
func cfDSData(rec *models.RecordConfig) *cfRecData {
return &cfRecData{
KeyTag: rec.DsKeyTag,
@@ -85,7 +108,7 @@ func cfSrvData(rec *models.RecordConfig) *cfRecData {
func cfCaaData(rec *models.RecordConfig) *cfRecData {
return &cfRecData{
Tag: rec.CaaTag,
- Flags: rec.CaaFlag,
+ Flags: uint16(rec.CaaFlag),
Value: rec.GetTargetField(),
}
}
@@ -107,6 +130,14 @@ func cfSshfpData(rec *models.RecordConfig) *cfRecData {
}
}
+func cfSvcbData(rec *models.RecordConfig) *cfRecData {
+ return &cfRecData{
+ Priority: rec.SvcPriority,
+ Target: cfTarget(rec.GetTargetField()),
+ Value: rec.SvcParams,
+ }
+}
+
func cfNaptrData(rec *models.RecordConfig) *cfNaptrRecData {
return &cfNaptrRecData{
Flags: rec.NaptrFlags,
@@ -163,11 +194,15 @@ func (c *cloudflareProvider) createRecDiff2(rec *models.RecordConfig, domainID s
} else if rec.Type == "SSHFP" {
cf.Data = cfSshfpData(rec)
cf.Name = rec.GetLabelFQDN()
+ } else if rec.Type == "DNSKEY" {
+ cf.Data = cfDnskeyData(rec)
} else if rec.Type == "DS" {
cf.Data = cfDSData(rec)
} else if rec.Type == "NAPTR" {
cf.Data = cfNaptrData(rec)
cf.Name = rec.GetLabelFQDN()
+ } else if rec.Type == "HTTPS" || rec.Type == "SVCB" {
+ cf.Data = cfSvcbData(rec)
}
resp, err := c.cfClient.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(domainID), cf)
if err != nil {
@@ -215,12 +250,17 @@ func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool,
} else if rec.Type == "SSHFP" {
r.Data = cfSshfpData(rec)
r.Name = rec.GetLabelFQDN()
+ } else if rec.Type == "DNSKEY" {
+ r.Data = cfDnskeyData(rec)
+ r.Content = ""
} else if rec.Type == "DS" {
r.Data = cfDSData(rec)
r.Content = ""
} else if rec.Type == "NAPTR" {
r.Data = cfNaptrData(rec)
r.Name = rec.GetLabelFQDN()
+ } else if rec.Type == "HTTPS" || rec.Type == "SVCB" {
+ r.Data = cfSvcbData(rec)
}
_, err := c.cfClient.UpdateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(domainID), r)
return err
@@ -238,6 +278,136 @@ func (c *cloudflareProvider) getUniversalSSL(domainID string) (bool, error) {
return result.Enabled, err
}
+func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*models.RecordConfig, error) {
+ rules, err := c.cfClient.GetEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(id), "http_request_dynamic_redirect")
+ if err != nil {
+ var e *cloudflare.NotFoundError
+ if errors.As(err, &e) {
+ return []*models.RecordConfig{}, nil
+ }
+ return nil, fmt.Errorf("failed fetching redirect rule list cloudflare: %s (%T)", err, err)
+ }
+
+ recs := []*models.RecordConfig{}
+ for _, pr := range rules.Rules {
+
+ var thisPr = pr
+ r := &models.RecordConfig{
+ Original: thisPr,
+ }
+
+ // Extract the valuables from the rule, use it to make the sr:
+ srName := pr.Description
+ srWhen := pr.Expression
+ srThen := pr.ActionParameters.FromValue.TargetURL.Expression
+ code := uint16(pr.ActionParameters.FromValue.StatusCode)
+
+ cfsingleredirect.MakeSingleRedirectFromAPI(r, code, srName, srWhen, srThen)
+ r.SetLabel("@", domain)
+
+ // Store the IDs
+ sr := r.CloudflareRedirect
+ sr.SRRRulesetID = rules.ID
+ sr.SRRRulesetRuleID = pr.ID
+
+ recs = append(recs, r)
+ }
+
+ return recs, nil
+}
+
+func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr models.CloudflareSingleRedirectConfig) error {
+
+ newSingleRedirectRulesActionParameters := cloudflare.RulesetRuleActionParameters{}
+ newSingleRedirectRule := cloudflare.RulesetRule{}
+ newSingleRedirectRules := []cloudflare.RulesetRule{}
+ newSingleRedirectRules = append(newSingleRedirectRules, newSingleRedirectRule)
+ newSingleRedirect := cloudflare.UpdateEntrypointRulesetParams{}
+
+ // Preserve query string if there isn't one in the replacement.
+ preserveQueryString := !strings.Contains(cfr.SRThen, "?")
+
+ newSingleRedirectRulesActionParameters.FromValue = &cloudflare.RulesetRuleActionParametersFromValue{}
+ // Redirect status code
+ newSingleRedirectRulesActionParameters.FromValue.StatusCode = uint16(cfr.Code)
+ // Incoming request expression
+ newSingleRedirectRules[0].Expression = cfr.SRWhen
+ // Redirect expression
+ newSingleRedirectRulesActionParameters.FromValue.TargetURL.Expression = cfr.SRThen
+ // Redirect name
+ newSingleRedirectRules[0].Description = cfr.SRName
+
+ // Rule action, should always be redirect in this case
+ newSingleRedirectRules[0].Action = "redirect"
+ // Phase should always be http_request_dynamic_redirect
+ newSingleRedirect.Phase = "http_request_dynamic_redirect"
+
+ // Assigns the values in the nested structs
+ newSingleRedirectRulesActionParameters.FromValue.PreserveQueryString = &preserveQueryString
+ newSingleRedirectRules[0].ActionParameters = &newSingleRedirectRulesActionParameters
+
+ // Get a list of current redirects so that the new redirect get appended to it
+ rules, err := c.cfClient.GetEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(domainID), "http_request_dynamic_redirect")
+ var e *cloudflare.NotFoundError
+ if err != nil && !errors.As(err, &e) {
+ return fmt.Errorf("failed fetching redirect rule list cloudflare: %s", err)
+ }
+ newSingleRedirect.Rules = newSingleRedirectRules
+ newSingleRedirect.Rules = append(newSingleRedirect.Rules, rules.Rules...)
+
+ _, err = c.cfClient.UpdateEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(domainID), newSingleRedirect)
+
+ return err
+}
+
+func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr models.CloudflareSingleRedirectConfig) error {
+
+ // This block should delete rules using the as is Cloudflare Golang lib in theory, need to debug why it isn't
+ // updatedRuleset := cloudflare.UpdateEntrypointRulesetParams{}
+ // updatedRulesetRules := []cloudflare.RulesetRule{}
+
+ // rules, err := c.cfClient.GetEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(domainID), "http_request_dynamic_redirect")
+ // if err != nil {
+ // return fmt.Errorf("failed fetching redirect rule list cloudflare: %s", err)
+ // }
+
+ // for _, rule := range rules.Rules {
+ // if rule.ID != cfr.SRRRulesetRuleID {
+ // updatedRulesetRules = append(updatedRulesetRules, rule)
+ // } else {
+ // printer.Printf("DEBUG: MATCH %v : %v\n", rule.ID, cfr.SRRRulesetRuleID)
+ // }
+ // }
+ // updatedRuleset.Rules = updatedRulesetRules
+ // _, err = c.cfClient.UpdateEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(domainID), updatedRuleset)
+
+ // Old Code
+
+ // rules, err := c.cfClient.GetEntrypointRuleset(context.Background(), cloudflare.ZoneIdentifier(domainID), "http_request_dynamic_redirect")
+ // if err != nil {
+ // return err
+ // }
+ //printer.Printf("DEBUG: CALLING API DeleteRulesetRule: SRRRulesetID=%v, cfr.SRRRulesetRuleID=%v\n", cfr.SRRRulesetID, cfr.SRRRulesetRuleID)
+
+ err := c.cfClient.DeleteRulesetRule(context.Background(), cloudflare.ZoneIdentifier(domainID), cloudflare.DeleteRulesetRuleParams{
+ RulesetID: cfr.SRRRulesetID,
+ RulesetRuleID: cfr.SRRRulesetRuleID},
+ )
+ // NB(tlim): Yuck. This returns an error even when it is successful. Dig into the JSON for the real status.
+ if strings.Contains(err.Error(), `"success": true,`) {
+ return nil
+ }
+
+ return err
+}
+
+func (c *cloudflareProvider) updateSingleRedirect(domainID string, oldrec, newrec *models.RecordConfig) error {
+ if err := c.deleteSingleRedirects(domainID, *oldrec.CloudflareRedirect); err != nil {
+ return err
+ }
+ return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect)
+}
+
func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) {
rules, err := c.cfClient.ListPageRules(context.Background(), id)
if err != nil {
@@ -255,16 +425,18 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R
value := pr.Actions[0].Value.(map[string]interface{})
var thisPr = pr
r := &models.RecordConfig{
- Type: "PAGE_RULE",
Original: thisPr,
- TTL: 1,
}
+
+ code := intZero(value["status_code"])
+
+ when := pr.Targets[0].Constraint.Value
+ then := value["url"].(string)
+ currentPrPrio := pr.Priority
+
+ cfsingleredirect.MakePageRule(r, currentPrPrio, code, when, then)
r.SetLabel("@", domain)
- r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
- pr.Targets[0].Constraint.Value,
- value["url"],
- pr.Priority,
- intZero(value["status_code"])))
+
recs = append(recs, r)
}
return recs, nil
@@ -274,30 +446,30 @@ func (c *cloudflareProvider) deletePageRule(recordID, domainID string) error {
return c.cfClient.DeletePageRule(context.Background(), domainID, recordID)
}
-func (c *cloudflareProvider) updatePageRule(recordID, domainID string, target string) error {
+func (c *cloudflareProvider) updatePageRule(recordID, domainID string, cfr models.CloudflareSingleRedirectConfig) error {
// maybe someday?
//c.apiProvider.UpdatePageRule(context.Background(), domainId, recordID, )
if err := c.deletePageRule(recordID, domainID); err != nil {
return err
}
- return c.createPageRule(domainID, target)
+ return c.createPageRule(domainID, cfr)
}
-func (c *cloudflareProvider) createPageRule(domainID string, target string) error {
- // from to priority code
- parts := strings.Split(target, ",")
- priority, _ := strconv.Atoi(parts[2])
- code, _ := strconv.Atoi(parts[3])
+func (c *cloudflareProvider) createPageRule(domainID string, cfr models.CloudflareSingleRedirectConfig) error {
+ priority := cfr.PRPriority
+ code := cfr.Code
+ prWhen := cfr.PRWhen
+ prThen := cfr.PRThen
pr := cloudflare.PageRule{
Status: "active",
Priority: priority,
Targets: []cloudflare.PageRuleTarget{
- {Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}},
+ {Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: prWhen}},
},
Actions: []cloudflare.PageRuleAction{
{ID: "forwarding_url", Value: &pageRuleFwdInfo{
StatusCode: code,
- URL: parts[1],
+ URL: prThen,
}},
},
}
@@ -358,21 +530,6 @@ func (c *cloudflareProvider) createWorkerRoute(domainID string, target string) e
return err
}
-func (c *cloudflareProvider) createTestWorker(workerName string) error {
- wp := cloudflare.CreateWorkerParams{
- ScriptName: workerName,
- Script: `
- addEventListener("fetch", (event) => {
- event.respondWith(
- new Response("Ok.", { status: 200 })
- );
- });`,
- }
-
- _, err := c.cfClient.UploadWorker(context.Background(), cloudflare.AccountIdentifier(c.accountID), wp)
- return err
-}
-
// https://github.com/dominikh/go-tools/issues/1137 which is a dup of
// https://github.com/dominikh/go-tools/issues/810
//
@@ -384,5 +541,5 @@ type pageRuleConstraint struct {
type pageRuleFwdInfo struct {
URL string `json:"url"`
- StatusCode int `json:"status_code"`
+ StatusCode uint16 `json:"status_code"`
}
diff --git a/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go b/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go
new file mode 100644
index 0000000000..a00ca4a5dc
--- /dev/null
+++ b/providers/cloudflare/rtypes/cfsingleredirect/cfsingleredirect.go
@@ -0,0 +1,40 @@
+package cfsingleredirect
+
+import (
+ "fmt"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol"
+)
+
+// SINGLEREDIRECT is the string name for this rType.
+const SINGLEREDIRECT = "CLOUDFLAREAPI_SINGLE_REDIRECT"
+
+func init() {
+ rtypecontrol.Register(SINGLEREDIRECT)
+}
+
+// FromRaw convert RecordConfig using data from a RawRecordConfig's parameters.
+func FromRaw(rc *models.RecordConfig, items []any) error {
+
+ // Validate types.
+ if err := rtypecontrol.PaveArgs(items, "siss"); err != nil {
+ return err
+ }
+
+ // Unpack the args:
+ var name, when, then string
+ var code uint16
+
+ name = items[0].(string)
+ code = items[1].(uint16)
+ if code != 301 && code != 302 {
+ return fmt.Errorf("code (%03d) is not 301 or 302", code)
+ }
+ when = items[2].(string)
+ then = items[3].(string)
+
+ makeSingleRedirectFromRawRec(rc, code, name, when, then)
+
+ return nil
+}
diff --git a/providers/cloudflare/rtypes/cfsingleredirect/convert.go b/providers/cloudflare/rtypes/cfsingleredirect/convert.go
new file mode 100644
index 0000000000..40ada49867
--- /dev/null
+++ b/providers/cloudflare/rtypes/cfsingleredirect/convert.go
@@ -0,0 +1,195 @@
+package cfsingleredirect
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+)
+
+// TranscodePRtoSR takes a PAGE_RULE record, stores transcoded versions of the fields, and makes the record a CLOUDFLAREAPI_SINGLE_REDDIRECT.
+func TranscodePRtoSR(rec *models.RecordConfig) error {
+ rec.Type = SINGLEREDIRECT // This record is now a CLOUDFLAREAPI_SINGLE_REDIRECT
+
+ // Extract the fields we're reading from:
+ sr := rec.CloudflareRedirect
+ code := sr.Code
+ prWhen := sr.PRWhen
+ prThen := sr.PRThen
+ srName := sr.PRDisplay
+
+ // Convert old-style patterns to new-style rules:
+ srWhen, srThen, err := makeRuleFromPattern(prWhen, prThen)
+ if err != nil {
+ return err
+ }
+
+ // Fix the RecordConfig
+ makeSingleRedirectFromConvert(rec,
+ sr.PRPriority,
+ prWhen, prThen,
+ code,
+ srName, srWhen, srThen)
+
+ return nil
+}
+
+// makeRuleFromPattern compile old-style patterns and replacements into new-style rules and expressions.
+func makeRuleFromPattern(pattern, replacement string) (string, string, error) {
+
+ var srWhen, srThen string
+ var err error
+
+ var phost, ppath string
+ origPattern := pattern
+ pattern, phost, ppath, err = normalizeURL(pattern)
+ _ = pattern
+ if err != nil {
+ return "", "", err
+ }
+ var rhost, rpath string
+ origReplacement := replacement
+ replacement, rhost, rpath, err = normalizeURL(replacement)
+ _ = rpath
+ if err != nil {
+ return "", "", err
+ }
+
+ // TODO(tlim): This could be a lot faster by not repeating itself so much.
+ // However I want to get it working before it is optimized.
+
+ // "pr" is Page Rule (old style)
+ // "sr" is Static Rule (new style)
+ // prWhen + prThen is the old-style matching pattern and replacement pattern.
+ // srWhen + srThen is the new-style matching rule and replacement expression.
+
+ if !strings.Contains(phost, `*`) && (ppath == `/` || ppath == "") {
+ // https://i.sstatic.net/ (No Wildcards)
+ srWhen = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, phost, "/")
+
+ } else if !strings.Contains(phost, `*`) && (ppath == `/*`) {
+ // https://i.stack.imgur.com/*
+ srWhen = fmt.Sprintf(`http.host eq "%s"`, phost)
+
+ } else if !strings.Contains(phost, `*`) && !strings.Contains(ppath, "*") {
+ // https://insights.stackoverflow.com/trends
+ srWhen = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, phost, ppath)
+
+ } else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && !strings.Contains(ppath, "*") {
+ // *stackoverflow.careers/ (wildcard at beginning only)
+ srWhen = fmt.Sprintf(`( http.host eq "%s" or ends_with(http.host, ".%s") ) and http.request.uri.path eq "%s"`, phost[1:], phost[1:], ppath)
+
+ } else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && ppath == "/*" {
+ // *stackoverflow.careers/* (wildcard at beginning and end)
+ srWhen = fmt.Sprintf(`http.host eq "%s" or ends_with(http.host, ".%s")`, phost[1:], phost[1:])
+
+ } else if strings.Contains(phost, `*`) && ppath == "/*" {
+ // meta.*yodeya.com/* (wildcard in host)
+ h := simpleGlobToRegex(phost)
+ srWhen = fmt.Sprintf(`http.host matches r###"%s"###`, h)
+
+ } else if !strings.Contains(phost, `*`) && strings.Count(ppath, `*`) == 1 && strings.HasSuffix(ppath, "*") {
+ // domain.tld/.well-known* (wildcard in path)
+ srWhen = fmt.Sprintf(`(starts_with(http.request.uri.path, "%s") and http.host eq "%s")`,
+ ppath[0:len(ppath)-1],
+ phost)
+
+ }
+
+ // replacement
+
+ if !strings.Contains(replacement, `$`) {
+ // https://stackexchange.com/ (no substitutions)
+ srThen = fmt.Sprintf(`concat("%s", "")`, replacement)
+
+ } else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && strings.Count(replacement, `$`) == 1 && len(rpath) > 3 && strings.HasSuffix(rpath, "/$2") {
+ // *stackoverflowenterprise.com/* -> https://www.stackoverflowbusiness.com/enterprise/$2
+ srThen = fmt.Sprintf(`concat("https://%s", "%s", http.request.uri.path)`,
+ rhost,
+ rpath[0:len(rpath)-3],
+ )
+
+ } else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && strings.Count(replacement, `$`) == 1 && len(rpath) > 3 && strings.HasSuffix(rpath, "/$2") {
+ // *stackoverflowenterprise.com/* -> https://www.stackoverflowbusiness.com/enterprise/$2
+ srThen = fmt.Sprintf(`concat("https://%s", "%s", http.request.uri.path)`,
+ rhost,
+ rpath[0:len(rpath)-3],
+ )
+
+ } else if strings.Count(replacement, `$`) == 1 && rpath == `/$1` {
+ // https://i.sstatic.net/$1 ($1 at end)
+ srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
+
+ } else if strings.Count(phost, `*`) == 1 && strings.Count(ppath, `*`) == 1 &&
+ strings.Count(replacement, `$`) == 1 && strings.HasSuffix(rpath, `/$2`) {
+ // https://careers.stackoverflow.com/$2
+ srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
+
+ } else if strings.Count(replacement, `$`) == 1 && strings.HasSuffix(replacement, `$1`) {
+ // https://social.domain.tld/.well-known$1
+ srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
+
+ } else if strings.Count(replacement, `$`) == 1 && strings.HasSuffix(replacement, `$1`) {
+ // https://social.domain.tld/.well-known$1
+ srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
+
+ }
+
+ // Not implemented
+
+ if srWhen == "" {
+ return "", "", fmt.Errorf("conversion not implemented for pattern: %s", origPattern)
+ }
+ if srThen == "" {
+ return "", "", fmt.Errorf("conversion not implemented for replacement: %s", origReplacement)
+ }
+
+ return srWhen, srThen, nil
+}
+
+// normalizeURL turns foo.com into https://foo.com and replaces HTTP with HTTPS.
+// It also returns an error if there is a port specified (like :8080)
+func normalizeURL(s string) (string, string, string, error) {
+ orig := s
+ if strings.HasPrefix(s, `http://`) {
+ s = "https://" + s[7:]
+ } else if !strings.HasPrefix(s, `https://`) {
+ s = `https://` + s
+ }
+
+ // Make sure it parses.
+ u, err := url.Parse(s)
+ if err != nil {
+ return "", "", "", err
+ }
+
+ // Make sure it doesn't have a port (https://example.com:8080)
+ _, port, _ := net.SplitHostPort(u.Host)
+ if port != "" {
+ return "", "", "", fmt.Errorf("unimplemented port: %q", orig)
+ }
+
+ return s, u.Host, u.Path, nil
+}
+
+// simpleGlobToRegex translates very simple Glob patterns into regexp-compatible expressions.
+// It only handles `.` and `*` currently. See singleredirect_test.go for supported patterns.
+func simpleGlobToRegex(g string) string {
+
+ if g == "" {
+ return `.*`
+ }
+
+ if !strings.HasSuffix(g, "*") {
+ g = g + `$`
+ }
+ if !strings.HasPrefix(g, "*") {
+ g = `^` + g
+ }
+
+ g = strings.ReplaceAll(g, `.`, `\.`)
+ g = strings.ReplaceAll(g, `*`, `.*`)
+ return g
+}
diff --git a/providers/cloudflare/rtypes/cfsingleredirect/convert_test.go b/providers/cloudflare/rtypes/cfsingleredirect/convert_test.go
new file mode 100644
index 0000000000..54e9bef7e2
--- /dev/null
+++ b/providers/cloudflare/rtypes/cfsingleredirect/convert_test.go
@@ -0,0 +1,350 @@
+package cfsingleredirect
+
+import (
+ "regexp"
+ "testing"
+
+ "github.com/gobwas/glob"
+)
+
+func Test_makeSingleDirectRule(t *testing.T) {
+ tests := []struct {
+ name string
+ //
+ pattern string
+ replace string
+ //
+ wantMatch string
+ wantExpr string
+ wantErr bool
+ }{
+ {
+ name: "000",
+ pattern: "example.com/",
+ replace: "foo.com",
+ wantMatch: `http.host eq "example.com" and http.request.uri.path eq "/"`,
+ wantExpr: `concat("https://foo.com", "")`,
+ wantErr: false,
+ },
+
+ /*
+ All the test-cases I could find in dnsconfig.js
+
+ Generated with this:
+
+ dnscontrol print-ir --pretty |grep '"target' |grep , | sed -e 's@"target":@@g' > /tmp/list
+ vim /tmp/list # removed the obvious duplicates
+ awk < /tmp/list -v q='"' -F, '{ print "{" ; print "name: " q NR q "," ; print "pattern: " $1 q "," ; print "replace: " q $2 "," ; print "wantMatch: `FIXME`," ; print "wantExpr: `FIXME`," ; print "wantErr: false," ; print "}," }' | pbcopy
+
+ */
+
+ {
+ name: "1",
+ pattern: "https://i-dev.sstatic.net/",
+ replace: "https://stackexchange.com/",
+ wantMatch: `http.host eq "i-dev.sstatic.net" and http.request.uri.path eq "/"`,
+ wantExpr: `concat("https://stackexchange.com/", "")`,
+ wantErr: false,
+ },
+ {
+ name: "2",
+ pattern: "https://i.stack.imgur.com/*",
+ replace: "https://i.sstatic.net/$1",
+ wantMatch: `http.host eq "i.stack.imgur.com"`,
+ wantExpr: `concat("https://i.sstatic.net", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "3",
+ pattern: "https://img.stack.imgur.com/*",
+ replace: "https://i.sstatic.net/$1",
+ wantMatch: `http.host eq "img.stack.imgur.com"`,
+ wantExpr: `concat("https://i.sstatic.net", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "4",
+ pattern: "https://insights.stackoverflow.com/",
+ replace: "https://survey.stackoverflow.co",
+ wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/"`,
+ wantExpr: `concat("https://survey.stackoverflow.co", "")`,
+ wantErr: false,
+ },
+ {
+ name: "5",
+ pattern: "https://insights.stackoverflow.com/trends",
+ replace: "https://trends.stackoverflow.co",
+ wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/trends"`,
+ wantExpr: `concat("https://trends.stackoverflow.co", "")`,
+ wantErr: false,
+ },
+ {
+ name: "6",
+ pattern: "https://insights.stackoverflow.com/trends/",
+ replace: "https://trends.stackoverflow.co",
+ wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/trends/"`,
+ wantExpr: `concat("https://trends.stackoverflow.co", "")`,
+ wantErr: false,
+ },
+ {
+ name: "7",
+ pattern: "https://insights.stackoverflow.com/survey/2021",
+ replace: "https://survey.stackoverflow.co/2021",
+ wantMatch: `http.host eq "insights.stackoverflow.com" and http.request.uri.path eq "/survey/2021"`,
+ wantExpr: `concat("https://survey.stackoverflow.co/2021", "")`,
+ wantErr: false,
+ },
+ {
+ name: "28",
+ pattern: "*stackoverflow.help/support/solutions/articles/36000241656-write-an-article",
+ replace: "https://stackoverflow.help/en/articles/4397209-write-an-article",
+ wantMatch: `( http.host eq "stackoverflow.help" or ends_with(http.host, ".stackoverflow.help") ) and http.request.uri.path eq "/support/solutions/articles/36000241656-write-an-article"`,
+ wantExpr: `concat("https://stackoverflow.help/en/articles/4397209-write-an-article", "")`,
+ wantErr: false,
+ },
+ {
+ name: "29",
+ pattern: "*stackoverflow.careers/*",
+ replace: "https://careers.stackoverflow.com/$2",
+ wantMatch: `http.host eq "stackoverflow.careers" or ends_with(http.host, ".stackoverflow.careers")`,
+ wantExpr: `concat("https://careers.stackoverflow.com", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "31",
+ pattern: "stackenterprise.com/*",
+ replace: "https://stackoverflow.co/teams/",
+ wantMatch: `http.host eq "stackenterprise.com"`,
+ wantExpr: `concat("https://stackoverflow.co/teams/", "")`,
+ wantErr: false,
+ },
+ {
+ name: "33",
+ pattern: "meta.*yodeya.com/*",
+ replace: "https://judaism.meta.stackexchange.com/$2",
+ wantMatch: `http.host matches r###"^meta\..*yodeya\.com$"###`,
+ wantExpr: `concat("https://judaism.meta.stackexchange.com", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "34",
+ pattern: "chat.*yodeya.com/*",
+ replace: "https://chat.stackexchange.com/?tab=site\u0026host=judaism.stackexchange.com",
+ wantMatch: `http.host matches r###"^chat\..*yodeya\.com$"###`,
+ wantExpr: `concat("https://chat.stackexchange.com/?tab=site&host=judaism.stackexchange.com", "")`,
+ wantErr: false,
+ },
+ {
+ name: "35",
+ pattern: "*yodeya.com/*",
+ replace: "https://judaism.stackexchange.com/$2",
+ wantMatch: `http.host eq "yodeya.com" or ends_with(http.host, ".yodeya.com")`,
+ wantExpr: `concat("https://judaism.stackexchange.com", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "36",
+ pattern: "meta.*seasonedadvice.com/*",
+ replace: "https://cooking.meta.stackexchange.com/$2",
+ wantMatch: `http.host matches r###"^meta\..*seasonedadvice\.com$"###`,
+ wantExpr: `concat("https://cooking.meta.stackexchange.com", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "70",
+ pattern: "collectivesonstackoverflow.co/*",
+ replace: "https://stackoverflow.com/collectives-on-stack-overflow",
+ wantMatch: `http.host eq "collectivesonstackoverflow.co"`,
+ wantExpr: `concat("https://stackoverflow.com/collectives-on-stack-overflow", "")`,
+ wantErr: false,
+ },
+ {
+ name: "71",
+ pattern: "*collectivesonstackoverflow.co/*",
+ replace: "https://stackoverflow.com/collectives-on-stack-overflow",
+ wantMatch: `http.host eq "collectivesonstackoverflow.co" or ends_with(http.host, ".collectivesonstackoverflow.co")`,
+ wantExpr: `concat("https://stackoverflow.com/collectives-on-stack-overflow", "")`,
+ wantErr: false,
+ },
+ {
+ name: "76",
+ pattern: "*stackexchange.ca/*",
+ replace: "https://stackexchange.com/$2",
+ wantMatch: `http.host eq "stackexchange.ca" or ends_with(http.host, ".stackexchange.ca")`,
+ wantExpr: `concat("https://stackexchange.com", http.request.uri.path)`,
+ wantErr: false,
+ },
+
+ // https://github.com/StackExchange/dnscontrol/issues/2313#issuecomment-2197296025
+ {
+ name: "pro-sumer1",
+ pattern: "domain.tld/.well-known*",
+ replace: "https://social.domain.tld/.well-known$1",
+ wantMatch: `(starts_with(http.request.uri.path, "/.well-known") and http.host eq "domain.tld")`,
+ wantExpr: `concat("https://social.domain.tld", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "pro-sumer2",
+ pattern: "domain.tld/users*",
+ replace: "https://social.domain.tld/users$1",
+ wantMatch: `(starts_with(http.request.uri.path, "/users") and http.host eq "domain.tld")`,
+ wantExpr: `concat("https://social.domain.tld", http.request.uri.path)`,
+ wantErr: false,
+ },
+ {
+ name: "pro-sumer3",
+ pattern: "domain.tld/@*",
+ replace: `https://social.domain.tld/@$1`,
+ wantMatch: `(starts_with(http.request.uri.path, "/@") and http.host eq "domain.tld")`,
+ wantExpr: `concat("https://social.domain.tld", http.request.uri.path)`,
+ wantErr: false,
+ },
+
+ {
+ name: "stackentwild",
+ pattern: "*stackoverflowenterprise.com/*",
+ replace: "https://www.stackoverflowbusiness.com/enterprise/$2",
+ wantMatch: `http.host eq "stackoverflowenterprise.com" or ends_with(http.host, ".stackoverflowenterprise.com")`,
+ wantExpr: `concat("https://www.stackoverflowbusiness.com", "/enterprise", http.request.uri.path)`,
+ wantErr: false,
+ },
+
+ //
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotMatch, gotExpr, err := makeRuleFromPattern(tt.pattern, tt.replace)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("makeSingleDirectRule() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if gotMatch != tt.wantMatch {
+ t.Errorf("makeSingleDirectRule() MATCH = %v\n want %v", gotMatch, tt.wantMatch)
+ }
+ if gotExpr != tt.wantExpr {
+ t.Errorf("makeSingleDirectRule() EXPR = %v\n want %v", gotExpr, tt.wantExpr)
+ }
+ //_ = gotType
+ })
+ }
+}
+
+func Test_normalizeURL(t *testing.T) {
+ tests := []struct {
+ name string
+ s string
+ want string
+ want1 string
+ want2 string
+ wantErr bool
+ }{
+ {
+ s: "foo.com",
+ want: "https://foo.com",
+ want1: "foo.com",
+ want2: "",
+ },
+ {
+ s: "http://foo.com",
+ want: "https://foo.com",
+ want1: "foo.com",
+ want2: "",
+ },
+ {
+ s: "https://foo.com",
+ want: "https://foo.com",
+ want1: "foo.com",
+ want2: "",
+ },
+
+ {
+ s: "foo.com/bar",
+ want: "https://foo.com/bar",
+ want1: "foo.com",
+ want2: "/bar",
+ },
+ {
+ s: "http://foo.com/bar",
+ want: "https://foo.com/bar",
+ want1: "foo.com",
+ want2: "/bar",
+ },
+ {
+ s: "https://foo.com/bar",
+ want: "https://foo.com/bar",
+ want1: "foo.com",
+ want2: "/bar",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, got1, got2, err := normalizeURL(tt.s)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("normalizeURL() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("normalizeURL() got = %v, want %v", got, tt.want)
+ }
+ if got1 != tt.want1 {
+ t.Errorf("normalizeURL() got1 = %v, want1 %v", got1, tt.want1)
+ }
+ if got2 != tt.want2 {
+ t.Errorf("normalizeURL() got2 = %v, want2 %v", got2, tt.want2)
+ }
+ })
+ }
+}
+
+func Test_simpleGlobToRegex(t *testing.T) {
+ tests := []struct {
+ name string
+ pattern string
+ want string
+ }{
+ {"1", `foo`, `^foo$`},
+ {"2", `fo.o`, `^fo\.o$`},
+ {"3", `*foo`, `.*foo$`},
+ {"4", `foo*`, `^foo.*`},
+ {"5", `f.oo*`, `^f\.oo.*`},
+ {"6", `f*oo*`, `^f.*oo.*`},
+ }
+
+ data := []string{
+ "bar",
+ "foo",
+ "foofoo",
+ "ONEfooTWO",
+ "fo",
+ "frankfodog",
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := simpleGlobToRegex(tt.pattern)
+ if got != tt.want {
+ t.Errorf("simpleGlobToRegex() = %v, want %v", got, tt.want)
+ }
+
+ // Make sure the regex compiles and gets the same result when matching against strings in data.
+ for i, d := range data {
+
+ rm, err := regexp.MatchString(got, d)
+ if err != nil {
+ t.Errorf("simpleGlobToRegex() = %003d can not compile: %v", i, err)
+ }
+
+ g := glob.MustCompile(tt.pattern)
+ gm := g.Match(d) // true
+
+ if gm != rm {
+ t.Errorf("simpleGlobToRegex() = %003d glob: %v '%v' regexp: %v '%v'", i, gm, tt.pattern, rm, got)
+ }
+
+ }
+ })
+
+ }
+}
diff --git a/providers/cloudflare/rtypes/cfsingleredirect/from.go b/providers/cloudflare/rtypes/cfsingleredirect/from.go
new file mode 100644
index 0000000000..f1bd3f71f0
--- /dev/null
+++ b/providers/cloudflare/rtypes/cfsingleredirect/from.go
@@ -0,0 +1,122 @@
+package cfsingleredirect
+
+import (
+ "fmt"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+)
+
+// MakePageRule updates a RecordConfig to be a PAGE_RULE using PAGE_RULE data.
+func MakePageRule(rc *models.RecordConfig, priority int, code uint16, when, then string) {
+ display := mkPageRuleBlob(priority, code, when, then)
+
+ rc.Type = "PAGE_RULE"
+ rc.TTL = 1
+ rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{
+ Code: code,
+ //
+ PRWhen: when,
+ PRThen: then,
+ PRPriority: priority,
+ PRDisplay: display,
+ }
+ rc.SetTarget(display)
+}
+
+// mkPageRuleBlob creates the 1,301,when,then string used in displays.
+func mkPageRuleBlob(priority int, code uint16, when, then string) string {
+ return fmt.Sprintf("%d,%03d,%s,%s", priority, code, when, then)
+}
+
+// makeSingleRedirectFromRawRec updates a RecordConfig to be a
+// SINGLEREDIRECT using the data from a RawRecord.
+func makeSingleRedirectFromRawRec(rc *models.RecordConfig, code uint16, name, when, then string) {
+ target := targetFromRaw(name, code, when, then)
+
+ rc.Type = SINGLEREDIRECT
+ rc.TTL = 1
+ rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{
+ Code: code,
+ //
+ PRWhen: "UNKNOWABLE",
+ PRThen: "UNKNOWABLE",
+ PRPriority: 0,
+ PRDisplay: "UNKNOWABLE",
+ //
+ SRName: name,
+ SRWhen: when,
+ SRThen: then,
+ SRDisplay: target,
+ }
+ rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
+}
+
+// targetFromRaw create the display text used for a normal Redirect.
+func targetFromRaw(name string, code uint16, when, then string) string {
+ return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)",
+ name,
+ code,
+ when,
+ then,
+ )
+}
+
+// MakeSingleRedirectFromAPI updatese a RecordConfig to be a SINGLEREDIRECT using data downloaded via the API.
+func MakeSingleRedirectFromAPI(rc *models.RecordConfig, code uint16, name, when, then string) {
+ // The target is the same as the name. It is the responsibility of the record creator to name it something diffable.
+ target := targetFromAPIData(name, code, when, then)
+
+ rc.Type = SINGLEREDIRECT
+ rc.TTL = 1
+ rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{
+ Code: code,
+ //
+ PRWhen: "UNKNOWABLE",
+ PRThen: "UNKNOWABLE",
+ PRPriority: 0,
+ PRDisplay: "UNKNOWABLE",
+ //
+ SRName: name,
+ SRWhen: when,
+ SRThen: then,
+ SRDisplay: target,
+ }
+ rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
+}
+
+// targetFromAPIData creates the display text used for a Redirect as received from Cloudflare's API.
+func targetFromAPIData(name string, code uint16, when, then string) string {
+ return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)",
+ name,
+ code,
+ when,
+ then,
+ )
+}
+
+// makeSingleRedirectFromConvert updates a RecordConfig to be a SINGLEREDIRECT using data from a PAGE_RULE conversion.
+func makeSingleRedirectFromConvert(rc *models.RecordConfig,
+ priority int,
+ prWhen, prThen string,
+ code uint16,
+ srName, srWhen, srThen string) {
+
+ srDisplay := targetFromConverted(priority, code, prWhen, prThen, srWhen, srThen)
+
+ rc.Type = SINGLEREDIRECT
+ rc.TTL = 1
+ sr := rc.CloudflareRedirect
+ sr.Code = code
+
+ sr.SRName = srName
+ sr.SRWhen = srWhen
+ sr.SRThen = srThen
+ sr.SRDisplay = srDisplay
+
+ rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
+}
+
+// targetFromConverted makes the display text used when a redirect was the result of converting a PAGE_RULE.
+func targetFromConverted(prPriority int, code uint16, prWhen, prThen, srWhen, srThen string) string {
+ return fmt.Sprintf("%d,%03d,%s,%s code=(%03d) when=(%s) then=(%s)", prPriority, code, prWhen, prThen, code, srWhen, srThen)
+}
diff --git a/providers/cloudns/api.go b/providers/cloudns/api.go
index f5f5ac35a0..00d4e51536 100644
--- a/providers/cloudns/api.go
+++ b/providers/cloudns/api.go
@@ -1,23 +1,30 @@
package cloudns
import (
+ "context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
- "time"
+ "sync"
+
+ "golang.org/x/time/rate"
)
// Api layer for ClouDNS
type cloudnsProvider struct {
- domainIndex map[string]string
- nameserversNames []string
- creds struct {
+ creds struct {
id string
password string
subid string
}
+
+ requestLimit *rate.Limiter
+
+ sync.Mutex // Protects all access to the following fields:
+ domainIndex map[string]string
+ nameserversNames []string
}
type requestParams map[string]string
@@ -67,76 +74,97 @@ type domainRecord struct {
DsAlgorithm string `json:"dsalgorithm,omitempty"`
DsDigestType string `json:"digest_type,omitempty"`
DsDigest string `json:"dsdigest,omitempty"`
+ LocLatDeg string `json:"lat_deg,omitempty"`
+ LocLatMin string `json:"lat_min,omitempty"`
+ LocLatSec string `json:"lat_sec,omitempty"`
+ LocLatDir string `json:"lat_dir,omitempty"`
+ LocLongDeg string `json:"long_deg,omitempty"`
+ LocLongMin string `json:"long_min,omitempty"`
+ LocLongSec string `json:"long_sec,omitempty"`
+ LocLongDir string `json:"long_dir,omitempty"`
+ LocAltitude string `json:"altitude,omitempty"`
+ LocSize string `json:"size,omitempty"`
+ LocHPrecision string `json:"h_precision,omitempty"`
+ LocVPrecision string `json:"v_precision,omitempty"`
}
type recordResponse map[string]domainRecord
-var allowedTTLValues = []uint32{}
+func (c *cloudnsProvider) fetchAvailableNameservers() ([]string, error) {
+ c.Lock()
+ defer c.Unlock()
-func (c *cloudnsProvider) fetchAvailableNameservers() error {
- c.nameserversNames = nil
+ if c.nameserversNames == nil {
- var bodyString, err = c.get("/dns/available-name-servers.json", requestParams{})
- if err != nil {
- return fmt.Errorf("failed fetching available nameservers list from ClouDNS: %s", err)
- }
+ var bodyString, err = c.get("/dns/available-name-servers.json", requestParams{})
+ if err != nil {
+ return nil, fmt.Errorf("failed fetching available nameservers list from ClouDNS: %s", err)
+ }
- var nr nameserverResponse
- json.Unmarshal(bodyString, &nr)
+ var nr nameserverResponse
+ json.Unmarshal(bodyString, &nr)
- for _, nameserver := range nr {
- if nameserver.Type == "premium" {
- c.nameserversNames = append(c.nameserversNames, nameserver.Name)
- }
+ for _, nameserver := range nr {
+ if nameserver.Type == "premium" {
+ c.nameserversNames = append(c.nameserversNames, nameserver.Name)
+ }
+ }
}
- return nil
+ return c.nameserversNames, nil
}
-func (c *cloudnsProvider) fetchAvailableTTLValues(domain string) error {
- allowedTTLValues = nil
+func (c *cloudnsProvider) fetchAvailableTTLValues(domain string) ([]uint32, error) {
+ allowedTTLValues := make([]uint32, 0)
params := requestParams{
"domain-name": domain,
}
var bodyString, err = c.get("/dns/get-available-ttl.json", params)
if err != nil {
- return fmt.Errorf("failed fetching available TTL values list from ClouDNS: %s", err)
+ return nil, fmt.Errorf("failed fetching available TTL values list from ClouDNS: %s", err)
}
json.Unmarshal(bodyString, &allowedTTLValues)
- return nil
+ return allowedTTLValues, nil
}
-func (c *cloudnsProvider) fetchDomainList() error {
- // FIXME(tlim): This should return nil if c.domainIndex != nil. Then all
- // the callers won't have to check if c.domainIndex == nil before calling.
-
- c.domainIndex = map[string]string{}
- rowsPerPage := 100
- page := 1
- for {
- var dr zoneResponse
- params := requestParams{
- "page": strconv.Itoa(page),
- "rows-per-page": strconv.Itoa(rowsPerPage),
- }
- endpoint := "/dns/list-zones.json"
- var bodyString, err = c.get(endpoint, params)
- if err != nil {
- return fmt.Errorf("failed fetching domain list from ClouDNS: %s", err)
- }
- json.Unmarshal(bodyString, &dr)
-
- for _, domain := range dr {
- c.domainIndex[domain.Name] = domain.Name
- }
- if len(dr) < rowsPerPage {
- break
+func (c *cloudnsProvider) fetchDomainIndex(name string) (string, bool, error) {
+ c.Lock()
+ defer c.Unlock()
+
+ if c.domainIndex == nil {
+ rowsPerPage := 100
+ page := 1
+ for {
+ var dr zoneResponse
+ params := requestParams{
+ "page": strconv.Itoa(page),
+ "rows-per-page": strconv.Itoa(rowsPerPage),
+ }
+ endpoint := "/dns/list-zones.json"
+ var bodyString, err = c.get(endpoint, params)
+ if err != nil {
+ return "", false, fmt.Errorf("failed fetching domain list from ClouDNS: %s", err)
+ }
+ json.Unmarshal(bodyString, &dr)
+
+ if c.domainIndex == nil {
+ c.domainIndex = map[string]string{}
+ }
+
+ for _, domain := range dr {
+ c.domainIndex[domain.Name] = domain.Name
+ }
+ if len(dr) < rowsPerPage {
+ break
+ }
+ page++
}
- page++
}
- return nil
+
+ index, ok := c.domainIndex[name]
+ return index, ok, nil
}
func (c *cloudnsProvider) createDomain(domain string) error {
@@ -196,6 +224,43 @@ func (c *cloudnsProvider) getRecords(id string) ([]domainRecord, error) {
return records, nil
}
+func (c *cloudnsProvider) isDnssecEnabled(id string) (bool, error) {
+ params := requestParams{"domain-name": id}
+
+ var bodyString, err = c.get("/dns/get-dnssec-ds-records.json", params)
+ if err != nil {
+ // DNSSEC disabled is indicated by an error fetching the DS records.
+ var errResp errorResponse
+ err = json.Unmarshal(bodyString, &errResp)
+ if err == nil {
+ if errResp.Description == "The DNSSEC is not active." {
+ return false, nil
+ }
+ return false, fmt.Errorf("failed fetching DS records from ClouDNS: %s", err)
+ }
+ }
+
+ return true, nil
+}
+
+func (c *cloudnsProvider) setDnssec(id string, enabled bool) error {
+ params := requestParams{"domain-name": id}
+
+ var endpoint string
+ if enabled {
+ endpoint = "/dns/activate-dnssec.json"
+ } else {
+ endpoint = "/dns/deactivate-dnssec.json"
+ }
+
+ var _, err = c.get(endpoint, params)
+ if err != nil {
+ return fmt.Errorf("failed setting DNSSEC at ClouDNS: %s", err)
+ }
+
+ return nil
+}
+
func (c *cloudnsProvider) get(endpoint string, params requestParams) ([]byte, error) {
client := &http.Client{}
req, _ := http.NewRequest("GET", "https://api.cloudns.net"+endpoint, nil)
@@ -214,9 +279,9 @@ func (c *cloudnsProvider) get(endpoint string, params requestParams) ([]byte, er
req.URL.RawQuery = q.Encode()
// ClouDNS has a rate limit (not documented) of 10 request/second
- // so we do a very primitive rate-limiting here - delay every request for 100ms - so max. 10 requests/second ...
- time.Sleep(100 * time.Millisecond)
+ c.requestLimit.Wait(context.Background())
resp, err := client.Do(req)
+
if err != nil {
return []byte{}, err
}
@@ -228,14 +293,16 @@ func (c *cloudnsProvider) get(endpoint string, params requestParams) ([]byte, er
err = json.Unmarshal(bodyString, &errResp)
if err == nil {
if errResp.Status == "Failed" {
- return bodyString, fmt.Errorf("ClouDNS API error: %s URL:%s%s ", errResp.Description, req.Host, req.URL.RequestURI())
+ // For debug only - req.URL.RequestURI() contains the authentication params:
+ // return bodyString, fmt.Errorf("ClouDNS API error: %s URL:%s%s ", errResp.Description, req.Host, req.URL.RequestURI())
+ return bodyString, fmt.Errorf("ClouDNS API error: %s", errResp.Description)
}
}
return bodyString, nil
}
-func fixTTL(ttl uint32) uint32 {
+func fixTTL(allowedTTLValues []uint32, ttl uint32) uint32 {
// if the TTL is larger than the largest allowed value, return the largest allowed value
if ttl > allowedTTLValues[len(allowedTTLValues)-1] {
return allowedTTLValues[len(allowedTTLValues)-1]
diff --git a/providers/cloudns/auditrecords.go b/providers/cloudns/auditrecords.go
index 20d7b1d1d9..3c285db8e1 100644
--- a/providers/cloudns/auditrecords.go
+++ b/providers/cloudns/auditrecords.go
@@ -19,8 +19,6 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2021-03-01
- a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2021-03-01
-
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2023-03-30
return a.Audit(records)
diff --git a/providers/cloudns/cloudnsProvider.go b/providers/cloudns/cloudnsProvider.go
index ab79ef1cb4..16af5263ae 100644
--- a/providers/cloudns/cloudnsProvider.go
+++ b/providers/cloudns/cloudnsProvider.go
@@ -10,6 +10,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns/dnsutil"
+ "golang.org/x/time/rate"
)
/*
@@ -22,6 +23,7 @@ Info required in `creds.json`:
// NewCloudns creates the provider.
func NewCloudns(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
c := &cloudnsProvider{}
+ c.requestLimit = rate.NewLimiter(10, 10)
c.creds.id, c.creds.password, c.creds.subid = m["auth-id"], m["auth-password"], m["sub-auth-id"]
@@ -29,21 +31,20 @@ func NewCloudns(m map[string]string, metadata json.RawMessage) (providers.DNSSer
return nil, fmt.Errorf("missing ClouDNS auth-id or sub-auth-id and auth-password")
}
- // Get a domain to validate authentication
- if err := c.fetchDomainList(); err != nil {
- return nil, err
- }
-
return c, nil
}
var features = providers.DocumentationNotes{
- //providers.CanUseDS: providers.Can(), // in ClouDNS we can add DS record just for a subdomain(child)
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
+ providers.CanUseDNAME: providers.Can(),
providers.CanUseDSForChildren: providers.Can(),
- providers.CanUseLOC: providers.Cannot(),
+ providers.CanUseLOC: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
@@ -54,20 +55,25 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "CLOUDNS"
+ const providerMaintainer = "@pragmaton"
fns := providers.DspFuncs{
Initializer: NewCloudns,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("CLOUDNS", fns, features)
- providers.RegisterCustomRecordType("CLOUDNS_WR", "CLOUDNS", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterCustomRecordType("CLOUDNS_WR", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// GetNameservers returns the nameservers for a domain.
func (c *cloudnsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
- if len(c.nameserversNames) == 0 {
- c.fetchAvailableNameservers()
+ names, err := c.fetchAvailableNameservers()
+ if err != nil {
+ return nil, err
}
- return models.ToNameservers(c.nameserversNames)
+
+ return models.ToNameservers(names)
}
// // GetDomainCorrections returns the corrections for a domain.
@@ -108,32 +114,38 @@ func (c *cloudnsProvider) GetNameservers(domain string) ([]*models.Nameserver, e
// }
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
-
- if c.domainIndex == nil {
- if err := c.fetchDomainList(); err != nil {
- return nil, err
- }
- }
- domainID, ok := c.domainIndex[dc.Name]
- if !ok {
- return nil, fmt.Errorf("'%s' not a zone in ClouDNS account", dc.Name)
+func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
+ domainID, ok, err := c.fetchDomainIndex(dc.Name)
+ if err != nil {
+ return nil, 0, err
+ } else if !ok {
+ return nil, 0, fmt.Errorf("'%s' not a zone in ClouDNS account", dc.Name)
}
// Get a list of available TTL values.
// The TTL list needs to be obtained for each domain, so get it first here.
- c.fetchAvailableTTLValues(dc.Name)
+ allowedTTLValues, err := c.fetchAvailableTTLValues(dc.Name)
+ if err != nil {
+ return nil, 0, err
+ }
+
// ClouDNS can only be specified from a specific TTL list, so change the TTL in advance.
for _, record := range dc.Records {
- record.TTL = fixTTL(record.TTL)
+ record.TTL = fixTTL(allowedTTLValues, record.TTL)
}
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ dnssecFixes, err := c.getDNSSECCorrections(dc)
if err != nil {
- return nil, err
+ return nil, 0, err
+ }
+
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ if err != nil {
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
+ corrections = append(corrections, dnssecFixes...)
// Deletes first so changing type works etc.
for _, m := range del {
@@ -162,7 +174,7 @@ func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
for _, m := range create {
req, err := toReq(m.Desired)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// ClouDNS does not require the trailing period to be specified when creating an NS record where the A or AAAA record exists in the zone.
@@ -197,7 +209,7 @@ func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
id := m.Existing.Original.(*domainRecord).ID
req, err := toReq(m.Desired)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// ClouDNS does not require the trailing period to be specified when updating an NS record where the A or AAAA record exists in the zone.
@@ -215,10 +227,38 @@ func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
+// getDNSSECCorrections returns corrections that update a domain's DNSSEC state.
+func (c *cloudnsProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ enabled, err := c.isDnssecEnabled(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ if enabled && dc.AutoDNSSEC == "off" {
+ return []*models.Correction{
+ {
+ Msg: "Disable DNSSEC",
+ F: func() error { err := c.setDnssec(dc.Name, false); return err },
+ },
+ }, nil
+ }
+
+ if !enabled && dc.AutoDNSSEC == "on" {
+ return []*models.Correction{
+ {
+ Msg: "Enable DNSSEC",
+ F: func() error { err := c.setDnssec(dc.Name, true); return err },
+ },
+ }, nil
+ }
+
+ return []*models.Correction{}, nil
+}
+
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (c *cloudnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
records, err := c.getRecords(domain)
@@ -234,11 +274,9 @@ func (c *cloudnsProvider) GetZoneRecords(domain string, meta map[string]string)
// EnsureZoneExists creates a zone if it does not exist
func (c *cloudnsProvider) EnsureZoneExists(domain string) error {
- if err := c.fetchDomainList(); err != nil {
+ if _, ok, err := c.fetchDomainIndex(domain); err != nil {
return err
- }
- // zone already exists
- if _, ok := c.domainIndex[domain]; ok {
+ } else if ok { // zone already exists
return nil
}
return c.createDomain(domain)
@@ -266,7 +304,7 @@ func toRc(domain string, r *domainRecord) *models.RecordConfig {
switch rtype := r.Type; rtype { // #rtype_variations
case "TXT":
rc.SetTargetTXT(r.Target)
- case "CNAME", "MX", "NS", "SRV", "ALIAS", "PTR":
+ case "CNAME", "DNAME", "MX", "NS", "SRV", "ALIAS", "PTR":
rc.SetTarget(dnsutil.AddOrigin(r.Target+".", domain))
case "CAA":
caaFlag, _ := strconv.ParseUint(r.CaaFlag, 10, 8)
@@ -299,6 +337,12 @@ func toRc(domain string, r *domainRecord) *models.RecordConfig {
case "CLOUD_WR":
rc.Type = "WR"
rc.SetTarget(r.Target)
+ case "LOC":
+ loc := fmt.Sprintf("%s %s %s %s %s %s %s %s %s %s %s %s",
+ r.LocLatDeg, r.LocLatMin, r.LocLatSec, r.LocLatDir,
+ r.LocLongDeg, r.LocLongMin, r.LocLongSec, r.LocLongDir,
+ r.LocAltitude, r.LocSize, r.LocHPrecision, r.LocVPrecision)
+ rc.SetTargetLOCString(r.Target, loc)
default:
rc.SetTarget(r.Target)
}
@@ -306,6 +350,15 @@ func toRc(domain string, r *domainRecord) *models.RecordConfig {
return rc
}
+func formatLocParam(param string) string {
+ param = strings.Split(param, "m")[0]
+ // API misbehaves with a parameter of "0.00" and treats it as the default, so convert to "0" for this case only
+ if param == "0.00" {
+ param = "0"
+ }
+ return param
+}
+
// toReq takes a RecordConfig and turns it into the native format used by the API.
func toReq(rc *models.RecordConfig) (requestParams, error) {
req := requestParams{
@@ -321,7 +374,7 @@ func toReq(rc *models.RecordConfig) (requestParams, error) {
}
switch rc.Type { // #rtype_variations
- case "A", "AAAA", "NS", "PTR", "TXT", "SOA", "ALIAS", "CNAME", "WR":
+ case "A", "AAAA", "NS", "PTR", "TXT", "SOA", "ALIAS", "CNAME", "WR", "DNAME":
// Nothing special.
case "CLOUDNS_WR":
req["record-type"] = "WR"
@@ -347,6 +400,20 @@ func toReq(rc *models.RecordConfig) (requestParams, error) {
req["algorithm"] = strconv.Itoa(int(rc.DsAlgorithm))
req["digest-type"] = strconv.Itoa(int(rc.DsDigestType))
req["record"] = rc.DsDigest
+ case "LOC":
+ parts := strings.Fields(rc.GetTargetCombined())
+ req["lat-deg"] = parts[0]
+ req["lat-min"] = parts[1]
+ req["lat-sec"] = parts[2]
+ req["lat-dir"] = parts[3]
+ req["long-deg"] = parts[4]
+ req["long-min"] = parts[5]
+ req["long-sec"] = parts[6]
+ req["long-dir"] = parts[7]
+ req["altitude"] = formatLocParam(parts[8])
+ req["size"] = formatLocParam(parts[9])
+ req["h-precision"] = formatLocParam(parts[10])
+ req["v-precision"] = formatLocParam(parts[11])
default:
return nil, fmt.Errorf("ClouDNS.toReq rtype %q unimplemented", rc.Type)
}
diff --git a/providers/cnr/auditrecords.go b/providers/cnr/auditrecords.go
new file mode 100644
index 0000000000..2712559a61
--- /dev/null
+++ b/providers/cnr/auditrecords.go
@@ -0,0 +1,21 @@
+package cnr
+
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
+
+// AuditRecords returns a list of errors corresponding to the records
+// that aren't supported by this provider. If all records are
+// supported, an empty list is returned.
+func AuditRecords(records []*models.RecordConfig) []error {
+ a := rejectif.Auditor{}
+
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-10-01
+
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-11-30
+
+ a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28
+
+ return a.Audit(records)
+}
diff --git a/providers/cnr/cnrProvider.go b/providers/cnr/cnrProvider.go
new file mode 100644
index 0000000000..5debd3f088
--- /dev/null
+++ b/providers/cnr/cnrProvider.go
@@ -0,0 +1,100 @@
+package cnr
+
+// Package CNR implements a registrar that uses the CNR api to set name servers. It will self register it's providers when imported.
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/StackExchange/dnscontrol/v4/providers"
+ cnrcl "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/apiclient"
+)
+
+// GoReleaser: version
+var (
+ version = "dev"
+)
+
+// CNRClient describes a connection to the CNR API.
+type CNRClient struct {
+ conf map[string]string
+ APILogin string
+ APIPassword string
+ APIEntity string
+ client *cnrcl.APIClient
+}
+
+var features = providers.DocumentationNotes{
+ // See providers/capabilities.go for the entire list of capabilities.
+ // The default for unlisted capabilities is 'Cannot'.
+ // --- Supported Features ---
+ providers.CanAutoDNSSEC: providers.Unimplemented("Ask for this feature."),
+ providers.CanConcur: providers.Can(),
+ providers.CanGetZones: providers.Can(),
+ providers.DocCreateDomains: providers.Can(),
+ providers.DocDualHost: providers.Can(),
+ providers.DocOfficiallySupported: providers.Cannot("Actively maintained provider module."),
+ // --- Supported record types ---
+ // providers.CanUseAKAMAICDN: providers.Cannot(), // can only be supported by Akamai EdgeDns provider
+ providers.CanUseAlias: providers.Cannot("Not supported. You may use CNAME records instead. An Alternative solution is planned."),
+ // providers.CanUseAzureAlias: providers.Cannot(), // can only be supported by Azure provider
+ providers.CanUseCAA: providers.Can(),
+ providers.CanUseDHCID: providers.Cannot("Ask for this feature."),
+ providers.CanUseDNAME: providers.Cannot("Ask for this feature."),
+ providers.CanUseDNSKEY: providers.Unimplemented("Ask for this feature."),
+ providers.CanUseDS: providers.Unimplemented("Ask for this feature."),
+ providers.CanUseDSForChildren: providers.Unimplemented("Ask for this feature."), // CanUseDS implies CanUseDSForChildren
+ providers.CanUseHTTPS: providers.Cannot("Managed via (Query|Add|Modify|Delete)WebFwd API call. Data not accessible via the resource records list. Hard to integrate this into DNSControl by that."),
+ providers.CanUseLOC: providers.Cannot("Ask for this feature."),
+ providers.CanUseNAPTR: providers.Can(),
+ providers.CanUsePTR: providers.Can(),
+ // providers.CanUseRoute53Alias: providers.Cannot(), // can only be supported by AWS Route53 provider
+ providers.CanUseSOA: providers.Cannot("The SOA record is managed on the DNSZone directly. Data only accessible via StatusDNSZone Request, not via the resource records list. Hard to integrate this into DNSControl by that."), // supported by bind, honstingde
+ providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"),
+ providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Cannot("Ask for this feature."),
+ providers.CanUseTLSA: providers.Can(),
+}
+
+func newProvider(conf map[string]string) (*CNRClient, error) {
+ api := &CNRClient{
+ conf: conf,
+ client: cnrcl.NewAPIClient(),
+ }
+ api.client.SetUserAgent("DNSControl", version)
+ api.APILogin, api.APIPassword, api.APIEntity = conf["apilogin"], conf["apipassword"], conf["apientity"]
+ if conf["debugmode"] == "2" {
+ api.client.EnableDebugMode()
+ }
+ if api.APIEntity != "OTE" && api.APIEntity != "LIVE" {
+ return nil, fmt.Errorf("wrong api system entity used. use \"OTE\" for OT&E system or \"LIVE\" for Live system")
+ }
+ if api.APIEntity == "OTE" {
+ api.client.UseOTESystem()
+ }
+ if api.APILogin == "" || api.APIPassword == "" {
+ return nil, fmt.Errorf("missing login credentials apilogin or apipassword")
+ }
+ api.client.SetCredentials(api.APILogin, api.APIPassword)
+ return api, nil
+}
+
+func newReg(conf map[string]string) (providers.Registrar, error) {
+ return newProvider(conf)
+}
+
+func newDsp(conf map[string]string, meta json.RawMessage) (providers.DNSServiceProvider, error) {
+ return newProvider(conf)
+}
+
+func init() {
+ const providerName = "CNR"
+ const providerMaintainer = "@KaiSchwarz-cnic"
+ fns := providers.DspFuncs{
+ Initializer: newDsp,
+ RecordAuditor: AuditRecords,
+ }
+ providers.RegisterRegistrarType(providerName, newReg)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+}
diff --git a/providers/cnr/domains.go b/providers/cnr/domains.go
new file mode 100644
index 0000000000..fed530407b
--- /dev/null
+++ b/providers/cnr/domains.go
@@ -0,0 +1,55 @@
+package cnr
+
+// EnsureZoneExists returns an error
+// * if access to dnszone is not allowed (not authorized) or
+// * if it doesn't exist and creating it fails
+func (n *CNRClient) EnsureZoneExists(domain string) error {
+ r := n.client.Request(map[string]interface{}{
+ "COMMAND": "StatusDNSZone",
+ "DNSZONE": domain,
+ })
+ code := r.GetCode()
+ if code == 545 {
+ command := map[string]interface{}{
+ "COMMAND": "AddDNSZone",
+ "DNSZONE": domain,
+ }
+ if n.APIEntity == "OTE" {
+ command["SOATTL"] = "33200"
+ command["SOASERIAL"] = "0000000000"
+ }
+ // Create the zone
+ r = n.client.Request(command)
+ if !r.IsSuccess() {
+ return n.GetCNRApiError("Failed to create not existing zone ", domain, r)
+ }
+ } else if code == 531 {
+ return n.GetCNRApiError("Not authorized to manage dnszone", domain, r)
+ } else if r.IsError() || r.IsTmpError() {
+ return n.GetCNRApiError("Error while checking status of dnszone", domain, r)
+ }
+ return nil
+}
+
+// ListZones lists all the
+func (n *CNRClient) ListZones() ([]string, error) {
+ var zones []string
+
+ // Basic
+
+ rs := n.client.RequestAllResponsePages(map[string]string{
+ "COMMAND": "QueryDNSZoneList",
+ })
+ for _, r := range rs {
+ if r.IsError() {
+ return nil, n.GetCNRApiError("Error while QueryDNSZoneList", "Basic", &r)
+ }
+ zoneColumn := r.GetColumn("DNSZONE")
+ if zoneColumn != nil {
+ //return nil, fmt.Errorf("failed getting DNSZONE BASIC column")
+ zones = append(zones, zoneColumn.GetData()...)
+ }
+ }
+
+ return zones, nil
+}
diff --git a/providers/cnr/error.go b/providers/cnr/error.go
new file mode 100644
index 0000000000..a51c6b0045
--- /dev/null
+++ b/providers/cnr/error.go
@@ -0,0 +1,12 @@
+package cnr
+
+import (
+ "fmt"
+
+ "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/response"
+)
+
+// GetCNRApiError returns an error including API error code and error description.
+func (n *CNRClient) GetCNRApiError(format string, objectid string, r *response.Response) error {
+ return fmt.Errorf(format+" %q. [%v %s]", objectid, r.GetCode(), r.GetDescription())
+}
diff --git a/providers/cnr/nameservers.go b/providers/cnr/nameservers.go
new file mode 100644
index 0000000000..d2f7e000ba
--- /dev/null
+++ b/providers/cnr/nameservers.go
@@ -0,0 +1,106 @@
+package cnr
+
+import (
+ "fmt"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+)
+
+var defaultNameservers = []*models.Nameserver{
+ {Name: "ns1.rrpproxy.net"},
+ {Name: "ns2.rrpproxy.net"},
+ {Name: "ns3.rrpproxy.net"},
+}
+
+var nsRegex = regexp.MustCompile(`ns([1-3]{1})[0-9]+\.rrpproxy\.net`)
+
+// GetNameservers gets the nameservers set on a domain.
+func (n *CNRClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ // NOTE: This information is taken over from HX and adapted to CNR... might be wrong...
+ // This is an interesting edge case. CNR expects you to SET the nameservers to ns[1-3].rrpproxy.net,
+ // but it will internally set it to (ns1xyz|ns2uvw|ns3asd).rrpproxy.net, where xyz/uvw/asd is a uniqueish number.
+ // In order to avoid endless loops, we will use the unique nameservers if present, or else the generic ones if not.
+ nss, err := n.getNameserversRaw(domain)
+ if err != nil {
+ return nil, err
+ }
+ toUse := []string{
+ defaultNameservers[0].Name,
+ defaultNameservers[1].Name,
+ defaultNameservers[2].Name,
+ }
+ for _, ns := range nss {
+ if matches := nsRegex.FindStringSubmatch(ns); len(matches) == 2 && len(matches[1]) == 1 {
+ idx := matches[1][0] - '1' // regex ensures proper range
+ toUse[idx] = matches[0]
+ }
+ }
+ return models.ToNameservers(toUse)
+}
+
+func (n *CNRClient) getNameserversRaw(domain string) ([]string, error) {
+ r := n.client.Request(map[string]interface{}{
+ "COMMAND": "StatusDomain",
+ "DOMAIN": domain,
+ })
+ code := r.GetCode()
+ if code != 200 {
+ return nil, n.GetCNRApiError("Could not get status for domain", domain, r)
+ }
+ nsColumn := r.GetColumn("NAMESERVER")
+ if nsColumn == nil {
+ fmt.Println("No nameservers found")
+ return []string{}, nil // No nameserver assigned
+ }
+ ns := nsColumn.GetData()
+ sort.Strings(ns)
+ return ns, nil
+}
+
+// GetRegistrarCorrections gathers corrections that would being n to match dc.
+func (n *CNRClient) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ nss, err := n.getNameserversRaw(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+ foundNameservers := strings.Join(nss, ",")
+
+ expected := []string{}
+ for _, ns := range dc.Nameservers {
+ name := strings.TrimRight(ns.Name, ".")
+ expected = append(expected, name)
+ }
+ sort.Strings(expected)
+ expectedNameservers := strings.Join(expected, ",")
+
+ if foundNameservers != expectedNameservers {
+ return []*models.Correction{
+ {
+ Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
+ F: n.updateNameservers(expected, dc.Name),
+ },
+ }, nil
+ }
+ return nil, nil
+}
+
+func (n *CNRClient) updateNameservers(ns []string, domain string) func() error {
+ return func() error {
+ cmd := map[string]interface{}{
+ "COMMAND": "ModifyDomain",
+ "DOMAIN": domain,
+ }
+ for idx, ns := range ns {
+ cmd[fmt.Sprintf("NAMESERVER%d", idx)] = ns
+ }
+ response := n.client.Request(cmd)
+ code := response.GetCode()
+ if code != 200 {
+ return fmt.Errorf("%d %s", code, response.GetDescription())
+ }
+ return nil
+ }
+}
diff --git a/providers/cnr/records.go b/providers/cnr/records.go
new file mode 100644
index 0000000000..5a931b5f1c
--- /dev/null
+++ b/providers/cnr/records.go
@@ -0,0 +1,380 @@
+package cnr
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/diff"
+ "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
+)
+
+// CNRRecord covers an individual DNS resource record.
+type CNRRecord struct {
+ // DomainName is the zone that the record belongs to.
+ DomainName string
+ // Host is the hostname relative to the zone: e.g. for a record for blog.example.org, domain would be "example.org" and host would be "blog".
+ // An apex record would be specified by either an empty host "" or "@".
+ // A SRV record would be specified by "_{service}._{protocol}.{host}": e.g. "_sip._tcp.phone" for _sip._tcp.phone.example.org.
+ Host string
+ // FQDN is the Fully Qualified Domain Name. It is the combination of the host and the domain name. It always ends in a ".". FQDN is ignored in CreateRecord, specify via the Host field instead.
+ Fqdn string
+ // Type is one of the following: A, AAAA, ANAME, CNAME, MX, NS, SRV, or TXT.
+ Type string
+ // Answer is either the IP address for A or AAAA records; the target for ANAME, CNAME, MX, or NS records; the text for TXT records.
+ // For SRV records, answer has the following format: "{weight} {port} {target}" e.g. "1 5061 sip.example.org".
+ Answer string
+ // TTL is the time this record can be cached for in seconds.
+ TTL uint32
+ // Priority is only required for MX and SRV records, it is ignored for all others.
+ Priority uint32
+}
+
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (n *CNRClient) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
+ records, err := n.getRecords(domain)
+ if err != nil {
+ return nil, err
+ }
+ actual := make([]*models.RecordConfig, len(records))
+ for i, r := range records {
+ actual[i] = toRecord(r, domain)
+ }
+
+ return actual, nil
+
+}
+
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *CNRClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
+ toReport, create, del, mod, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(actual)
+ if err != nil {
+ return nil, 0, err
+ }
+ // Start corrections with the reports
+ corrections := diff.GenerateMessageCorrections(toReport)
+
+ buf := &bytes.Buffer{}
+ // Print a list of changes. Generate an actual change that is the zone
+ changes := false
+ var builder strings.Builder
+ params := map[string]interface{}{}
+ delrridx := 0
+ addrridx := 0
+
+ for _, cre := range create {
+ changes = true
+ fmt.Fprintln(buf, cre)
+ newRecordString, err := n.createRecordString(cre.Desired, dc.Name)
+ if err != nil {
+ return corrections, 0, err
+ }
+ key := fmt.Sprintf("ADDRR%d", addrridx)
+ params[key] = newRecordString
+ fmt.Fprintf(&builder, "\033[32m+ %s = %s\033[0m\n", key, newRecordString)
+ addrridx++
+ }
+ for _, d := range del {
+ changes = true
+ fmt.Fprintln(buf, d)
+ key := fmt.Sprintf("DELRR%d", delrridx)
+ oldRecordString := n.deleteRecordString(d.Existing.Original.(*CNRRecord))
+ params[key] = oldRecordString
+ fmt.Fprintf(&builder, "\033[31m- %s = %s\033[0m\n", key, oldRecordString)
+ delrridx++
+ }
+ for _, chng := range mod {
+ changes = true
+ fmt.Fprintln(buf, chng)
+ // old record deletion
+ key := fmt.Sprintf("DELRR%d", delrridx)
+ oldRecordString := n.deleteRecordString(chng.Existing.Original.(*CNRRecord))
+ params[key] = oldRecordString
+ fmt.Fprintf(&builder, "\033[31m- %s = %s\033[0m\n", key, oldRecordString)
+ delrridx++
+ // new record creation
+ newRecordString, err := n.createRecordString(chng.Desired, dc.Name)
+ if err != nil {
+ return corrections, 0, err
+ }
+ key = fmt.Sprintf("ADDRR%d", addrridx)
+ params[key] = newRecordString
+ fmt.Fprintf(&builder, "\033[32m+ %s = %s\033[0m\n", key, newRecordString)
+ addrridx++
+ }
+
+ if changes {
+ msg := fmt.Sprintf("GENERATE_ZONE: %s\n%s", dc.Name, buf.String())
+ if n.isDebugOn() {
+ msg = fmt.Sprintf("GENERATE_ZONE: %s\n%sPROVIDER CNR, API COMMAND PARAMETERS:\n%s", dc.Name, buf.String(), builder.String())
+ }
+ corrections = append(corrections, &models.Correction{
+ Msg: msg,
+ F: func() error {
+ return n.updateZoneBy(params, dc.Name)
+ },
+ })
+ }
+
+ return corrections, actualChangeCount, nil
+}
+
+func toRecord(r *CNRRecord, origin string) *models.RecordConfig {
+ rc := &models.RecordConfig{
+ Type: r.Type,
+ TTL: r.TTL,
+ Original: r,
+ }
+ fqdn := r.Fqdn[:len(r.Fqdn)-1]
+ rc.SetLabelFromFQDN(fqdn, origin)
+
+ switch r.Type {
+ case "MX", "SRV":
+ if r.Priority > 65535 {
+ panic(fmt.Errorf("priority value out of range for %s record: %d", r.Type, r.Priority))
+ }
+ if r.Type == "MX" {
+ if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
+ panic(fmt.Errorf("unparsable MX record received from centralnic reseller API: %w", err))
+ }
+ } else {
+ // _service._proto.name. TTL Type Priority Weight Port Target.
+ // e.g. _sip._tcp.phone.example.org. 86400 IN SRV 5 6 7 sip.example.org.
+ // r.Anser covers the format "Priority Weight Port Target" and we've to remove the priority from the string
+ r.Answer = strings.TrimPrefix(r.Answer, fmt.Sprintf("%d ", r.Priority))
+ if err := rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Answer); err != nil {
+ panic(fmt.Errorf("unparsable SRV record received from centralnic reseller API: %w", err))
+ }
+ }
+ default: // "A", "AAAA", "ANAME", "CNAME", "NS", "TXT", "CAA", "TLSA", "PTR"
+ if err := rc.PopulateFromStringFunc(r.Type, r.Answer, r.Fqdn, txtutil.ParseQuoted); err != nil {
+ panic(fmt.Errorf("unparsable record received from centralnic reseller API: %w", err))
+ }
+ }
+ return rc
+}
+
+// updateZoneBy updates the zone with the provided changes.
+func (n *CNRClient) updateZoneBy(params map[string]interface{}, domain string) error {
+ zone := domain
+ cmd := map[string]interface{}{
+ "COMMAND": "ModifyDNSZone",
+ "DNSZONE": zone,
+ }
+ for key, val := range params {
+ cmd[key] = val
+ }
+ r := n.client.Request(cmd)
+ if !r.IsSuccess() {
+ return n.GetCNRApiError("Error while updating zone", zone, r)
+ }
+ return nil
+}
+
+// deleteRecordString constructs the record string based on the provided CNRRecord.
+func (n *CNRClient) getRecords(domain string) ([]*CNRRecord, error) {
+ var records []*CNRRecord
+
+ // Command to find out the total numbers of resource records for the zone
+ // so that the follow-up query can be done with the correct limit
+ cmd := map[string]interface{}{
+ "COMMAND": "QueryDNSZoneRRList",
+ "DNSZONE": domain,
+ "ORDERBY": "type",
+ "FIRST": "0",
+ "LIMIT": "1",
+ }
+ r := n.client.Request(cmd)
+
+ // Check if the request was successful
+ if !r.IsSuccess() {
+ if r.GetCode() == 545 {
+ // If dns zone does not exist create a new one automatically
+ if !isNoPopulate() {
+ err := n.EnsureZoneExists(domain)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ // Return specific error if the zone does not exist
+ return nil, n.GetCNRApiError("Use `dnscontrol create-domains` to create not-existing zone", domain, r)
+ }
+ }
+ // Return general error for any other issues
+ return nil, n.GetCNRApiError("Failed loading resource records for zone", domain, r)
+ }
+ totalRecords := r.GetRecordsTotalCount()
+ if totalRecords <= 0 {
+ return nil, nil
+ }
+ limitation := 100
+ totalRecords += limitation
+
+ // finally request all resource records available for the zone
+ cmd["LIMIT"] = fmt.Sprintf("%d", totalRecords)
+ cmd["WIDE"] = "1"
+ r = n.client.Request(cmd)
+
+ // Check if the request was successful
+ if !r.IsSuccess() {
+ // Return general error for any other issues
+ return nil, n.GetCNRApiError("Failed loading resource records for zone", domain, r)
+ }
+
+ // loop over the records array
+ rrs := r.GetRecords()
+ for i := 0; i < len(rrs); i++ {
+ data := rrs[i].GetData()
+ // fmt.Printf("Data: %+v\n", data)
+ if _, exists := data["NAME"]; !exists {
+ continue
+ }
+
+ if data["TYPE"] == "MX" {
+ tmp := strings.Split(data["CONTENT"], " ")
+ data["PRIO"] = tmp[0]
+ data["CONTENT"] = tmp[1]
+ }
+
+ // Parse the TTL string to an unsigned integer
+ ttl, err := strconv.ParseUint(data["TTL"], 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("invalid TTL value for domain %s: %s", domain, data["TTL"])
+ }
+
+ // Parse the TTL string to an unsigned integer
+ priority, _ := strconv.ParseUint(data["PRIO"], 10, 32)
+
+ // Add dot to Answer if supported by the record type
+ pattern := `^CNAME|MX|NS|SRV|PTR$`
+ re, err := regexp.Compile(pattern)
+ if err != nil {
+ return nil, fmt.Errorf("error compiling regex in getRecords: %s", err)
+ }
+ if re.MatchString(data["TYPE"]) && !strings.HasSuffix(data["CONTENT"], ".") {
+ data["CONTENT"] = fmt.Sprintf("%s.", data["CONTENT"])
+ }
+
+ // Only append domain if it's not already a fully qualified domain name
+ fqdn := fmt.Sprintf("%s.", domain)
+ if data["NAME"] != "@" && !strings.HasSuffix(data["NAME"], domain+".") {
+ fqdn = fmt.Sprintf("%s.%s.", data["NAME"], domain)
+ }
+
+ // Initialize a new CNRRecord
+ record := &CNRRecord{
+ DomainName: domain,
+ Host: data["NAME"],
+ Fqdn: fqdn,
+ Type: data["TYPE"],
+ Answer: data["CONTENT"],
+ TTL: uint32(ttl),
+ Priority: uint32(priority),
+ }
+ // fmt.Printf("Record: %+v\n", record)
+
+ // Append the record to the records slice
+ records = append(records, record)
+ }
+
+ // Return the slice of records
+ return records, nil
+}
+
+// Function to create record string from given RecordConfig for the ADDRR# API parameter
+func (n *CNRClient) createRecordString(rc *models.RecordConfig, domain string) (string, error) {
+ host := rc.GetLabel()
+ answer := ""
+
+ switch rc.Type { // #rtype_variations
+ case "A", "AAAA", "ANAME", "CNAME", "MX", "NS", "PTR":
+ answer = rc.GetTargetField()
+ if domain == host {
+ host = fmt.Sprintf(`%s.`, host)
+ }
+ case "SSHFP":
+ answer = fmt.Sprintf(`%v %v %s`, rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField())
+ if domain == host {
+ host = fmt.Sprintf(`%s.`, host)
+ }
+ case "NAPTR":
+ answer = fmt.Sprintf(`%v %v "%v" "%v" "%v" %v`, rc.NaptrOrder, rc.NaptrPreference, rc.NaptrFlags, rc.NaptrService, rc.NaptrRegexp, rc.GetTargetField())
+ if domain == host {
+ host = fmt.Sprintf(`%s.`, host)
+ }
+ case "TLSA":
+ answer = fmt.Sprintf(`%v %v %v %s`, rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType, rc.GetTargetField())
+ case "CAA":
+ answer = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
+ case "TXT":
+ answer = txtutil.EncodeQuoted(rc.GetTargetTXTJoined())
+ case "SRV":
+ if rc.GetTargetField() == "." {
+ return "", fmt.Errorf("SRV records with empty targets are not supported")
+ }
+ // _service._proto.name. TTL Type Priority Weight Port Target.
+ // e.g. _sip._tcp.phone.example.org. 86400 IN SRV 5 6 7 sip.example.org.
+ answer = fmt.Sprintf("%d %d %d %v", uint32(rc.SrvPriority), rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
+ default:
+ panic(fmt.Sprintf("createRecordString rtype %v unimplemented", rc.Type))
+ // We panic so that we quickly find any switch statements
+ // that have not been updated for a new RR type.
+ }
+
+ str := host + " " + fmt.Sprint(rc.TTL) + " "
+
+ if rc.Type != "NS" { // TODO
+ str += "IN "
+ }
+ str += rc.Type + " "
+ // Handle MX records which have priority
+ if rc.Type == "MX" {
+ str += fmt.Sprint(uint32(rc.MxPreference)) + " "
+ }
+ str += answer
+ return str, nil
+}
+
+// deleteRecordString constructs the record string based on the provided CNRRecord.
+func (n *CNRClient) deleteRecordString(record *CNRRecord) string {
+ // Initialize values slice
+ values := []string{
+ record.Host,
+ fmt.Sprintf("%v", record.TTL),
+ "IN",
+ record.Type,
+ }
+ if record.Type == "SRV" {
+ values = append(values, fmt.Sprintf("%d", record.Priority))
+ }
+ values = append(values, record.Answer)
+
+ // fmt.Printf("Values: %+v\n", values)
+
+ // Remove IN if the record type is "NS" TODO
+ if record.Type == "NS" {
+ values = append(values[:2], values[3:]...) // Skip over the "IN"
+ }
+
+ // Return the final string by joining the elements with spaces
+ return strings.Join(values, " ")
+}
+
+// Function to check the no-populate argument
+func isNoPopulate() bool {
+ for _, arg := range os.Args {
+ if arg == "--no-populate" {
+ return true
+ }
+ }
+ return false
+}
+
+// Function to check if debug mode is enabled
+func (n *CNRClient) isDebugOn() bool {
+ return n.conf["debugmode"] == "1" || n.conf["debugmode"] == "2"
+}
diff --git a/providers/cscglobal/api.go b/providers/cscglobal/api.go
index dd8224013b..161acccc46 100644
--- a/providers/cscglobal/api.go
+++ b/providers/cscglobal/api.go
@@ -478,7 +478,7 @@ func (client *providerClient) clearRequests(domain string) error {
if cscDebug {
printer.Printf("DEBUG: Clearing requests for %q\n", domain)
}
- var bodyString, err = client.get("/zones/edits?filter=zoneName==" + domain)
+ var bodyString, err = client.get(`/zones/edits?size=99999&filter=zoneName==` + domain)
if err != nil {
return err
}
@@ -486,10 +486,12 @@ func (client *providerClient) clearRequests(domain string) error {
var dr pagedZoneEditResponsePagedZoneEditResponse
json.Unmarshal(bodyString, &dr)
- // TODO(tlim): Properly handle paganation.
- if dr.Meta.Pages > 1 {
- return fmt.Errorf("cancelPendingEdits failed: Pages=%d", dr.Meta.Pages)
- }
+ // TODO(tlim): Ignore what's beyond the first page.
+ // It is unlikely that there are active jobs beyond the first page.
+ // If there are, the next edit will just wait.
+ //if dr.Meta.Pages > 1 {
+ // return fmt.Errorf("cancelPendingEdits failed: Pages=%d", dr.Meta.Pages)
+ //}
for i, ze := range dr.ZoneEdits {
if cscDebug {
@@ -645,7 +647,7 @@ func (client *providerClient) geturl(url string) ([]byte, error) {
// Default CSCGlobal rate limit is twenty requests per second
var backoff = time.Second
- const maxBackoff = time.Second * 15
+ const maxBackoff = time.Second * 25
retry:
resp, err := hclient.Do(req)
@@ -664,9 +666,9 @@ retry:
if string(bodyString) == "Requests exceeded API Rate limit." {
// a simple exponential back-off with a 3-minute max.
- if backoff > 10 {
+ if backoff > (time.Second * 10) {
// With this provider backups seem to be pretty common. Only
- // announce it when the problem gets really bad.
+ // announce it for long delays.
printer.Printf("Delaying %v due to ratelimit (CSCGLOBAL)\n", backoff)
}
time.Sleep(backoff)
diff --git a/providers/cscglobal/auditrecords.go b/providers/cscglobal/auditrecords.go
index 1ee9faf8c6..5cfee5a280 100644
--- a/providers/cscglobal/auditrecords.go
+++ b/providers/cscglobal/auditrecords.go
@@ -17,11 +17,9 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2022-08-08
- a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2022-06-10
-
a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2022-06-10
- a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2022-06-10
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-12-03
return a.Audit(records)
}
diff --git a/providers/cscglobal/cscglobalProvider.go b/providers/cscglobal/cscglobalProvider.go
index 4515a84cde..8b0d431f9b 100644
--- a/providers/cscglobal/cscglobalProvider.go
+++ b/providers/cscglobal/cscglobalProvider.go
@@ -25,7 +25,10 @@ type providerClient struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.DocOfficiallySupported: providers.Can(),
@@ -58,11 +61,14 @@ func newProvider(m map[string]string) (*providerClient, error) {
}
func init() {
- providers.RegisterRegistrarType("CSCGLOBAL", newReg)
+ const providerName = "CSCGLOBAL"
+ const providerMaintainer = "@mikenz"
+ providers.RegisterRegistrarType(providerName, newReg)
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("CSCGLOBAL", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
diff --git a/providers/cscglobal/dns.go b/providers/cscglobal/dns.go
index 4cf3cb5175..840dc81bd2 100644
--- a/providers/cscglobal/dns.go
+++ b/providers/cscglobal/dns.go
@@ -75,12 +75,11 @@ func (client *providerClient) GetNameservers(domain string) ([]*models.Nameserve
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (client *providerClient) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
- //txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+func (client *providerClient) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, int, error) {
- toReport, creates, dels, modifications, err := diff.NewCompat(dc).IncrementalDiff(foundRecords)
+ toReport, creates, dels, modifications, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(foundRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -93,15 +92,15 @@ func (client *providerClient) GetZoneRecordsCorrections(dc *models.DomainConfig,
var edits []zoneResourceRecordEdit
var descriptions []string
for _, del := range dels {
- edits = append(edits, makePurge(dc.Name, del))
+ edits = append(edits, makePurge(del))
descriptions = append(descriptions, del.String())
}
for _, cre := range creates {
- edits = append(edits, makeAdd(dc.Name, cre))
+ edits = append(edits, makeAdd(cre))
descriptions = append(descriptions, cre.String())
}
for _, m := range modifications {
- edits = append(edits, makeEdit(dc.Name, m))
+ edits = append(edits, makeEdit(m))
descriptions = append(descriptions, m.String())
}
if len(edits) > 0 {
@@ -124,15 +123,15 @@ func (client *providerClient) GetZoneRecordsCorrections(dc *models.DomainConfig,
corrections = append(corrections, c)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
-func makePurge(domainname string, cor diff.Correlation) zoneResourceRecordEdit {
+func makePurge(cor diff.Correlation) zoneResourceRecordEdit {
var existingTarget string
switch cor.Existing.Type {
case "TXT":
- existingTarget = strings.Join(cor.Existing.TxtStrings, "")
+ existingTarget = cor.Existing.GetTargetTXTJoined()
default:
existingTarget = cor.Existing.GetTargetField()
}
@@ -153,13 +152,13 @@ func makePurge(domainname string, cor diff.Correlation) zoneResourceRecordEdit {
return zer
}
-func makeAdd(domainname string, cre diff.Correlation) zoneResourceRecordEdit {
+func makeAdd(cre diff.Correlation) zoneResourceRecordEdit {
rec := cre.Desired
var recTarget string
switch rec.Type {
case "TXT":
- recTarget = strings.Join(rec.TxtStrings, "")
+ recTarget = rec.GetTargetTXTJoined()
default:
recTarget = rec.GetTargetField()
}
@@ -185,7 +184,7 @@ func makeAdd(domainname string, cre diff.Correlation) zoneResourceRecordEdit {
zer.NewWeight = rec.SrvWeight
zer.NewPort = rec.SrvPort
case "TXT":
- zer.NewValue = strings.Join(rec.TxtStrings, "")
+ zer.NewValue = rec.GetTargetTXTJoined()
default: // "A", "CNAME", "NS"
// Nothing to do.
}
@@ -193,7 +192,7 @@ func makeAdd(domainname string, cre diff.Correlation) zoneResourceRecordEdit {
return zer
}
-func makeEdit(domainname string, m diff.Correlation) zoneResourceRecordEdit {
+func makeEdit(m diff.Correlation) zoneResourceRecordEdit {
old, rec := m.Existing, m.Desired
// TODO: Assert that old.Type == rec.Type
// TODO: Assert that old.Name == rec.Name
@@ -201,8 +200,8 @@ func makeEdit(domainname string, m diff.Correlation) zoneResourceRecordEdit {
var oldTarget, recTarget string
switch old.Type {
case "TXT":
- oldTarget = strings.Join(old.TxtStrings, "")
- recTarget = strings.Join(rec.TxtStrings, "")
+ oldTarget = old.GetTargetTXTJoined()
+ recTarget = rec.GetTargetTXTJoined()
default:
oldTarget = old.GetTargetField()
recTarget = rec.GetTargetField()
diff --git a/providers/desec/convert.go b/providers/desec/convert.go
index 3b71b7316e..4d67c3e57d 100644
--- a/providers/desec/convert.go
+++ b/providers/desec/convert.go
@@ -35,7 +35,7 @@ func nativeToRecords(n resourceRecord, origin string) (rcs []*models.RecordConfi
return rcs
}
-func recordsToNative(rcs []*models.RecordConfig, origin string) []resourceRecord {
+func recordsToNative(rcs []*models.RecordConfig) []resourceRecord {
// Take a list of RecordConfig and return an equivalent list of resourceRecord.
// deSEC requires one resourceRecord for each label:key tuple, therefore we
// might collapse many RecordConfig into one resourceRecord.
diff --git a/providers/desec/desecProvider.go b/providers/desec/desecProvider.go
index d67fa73cb4..4c425f7ca2 100644
--- a/providers/desec/desecProvider.go
+++ b/providers/desec/desecProvider.go
@@ -4,12 +4,11 @@ import (
"bytes"
"encoding/json"
"fmt"
- "sort"
+ "strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns/dnsutil"
)
@@ -23,32 +22,30 @@ Info required in `creds.json`:
// NewDeSec creates the provider.
func NewDeSec(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
c := &desecProvider{}
- c.creds.token = m["auth-token"]
- if c.creds.token == "" {
+ c.token = strings.TrimSpace(m["auth-token"])
+ if c.token == "" {
return nil, fmt.Errorf("missing deSEC auth-token")
}
- if err := c.authenticate(); err != nil {
- return nil, fmt.Errorf("authentication failed")
- }
- //DomainIndex is used for corrections (minttl) and domain creation
- if err := c.initializeDomainIndex(); err != nil {
- return nil, err
- }
-
return c, nil
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can("deSEC always signs all records. When trying to disable, a notice is printed."),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Unimplemented("Apex aliasing is supported via new SVCB and HTTPS record types. For details, check the deSEC docs."),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Can(),
+ providers.CanUseDNSKEY: providers.Can(),
+ providers.CanUseHTTPS: providers.Can(),
providers.CanUseLOC: providers.Unimplemented(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Unimplemented(),
@@ -61,11 +58,14 @@ var defaultNameServerNames = []string{
}
func init() {
+ const providerName = "DESEC"
+ const providerMaintainer = "@D3luxee"
fns := providers.DspFuncs{
Initializer: NewDeSec,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("DESEC", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// GetNameservers returns the nameservers for a domain.
@@ -115,9 +115,13 @@ func (c *desecProvider) GetZoneRecords(domain string, meta map[string]string) (m
// EnsureZoneExists creates a zone if it does not exist
func (c *desecProvider) EnsureZoneExists(domain string) error {
- c.mutex.Lock()
- defer c.mutex.Unlock()
- if _, ok := c.domainIndex[domain]; ok {
+ _, ok, err := c.searchDomainIndex(domain)
+ if err != nil {
+ return err
+ }
+
+ if ok {
+ // Domain already exists
return nil
}
return c.createDomain(domain)
@@ -150,28 +154,26 @@ func PrepDesiredRecords(dc *models.DomainConfig, minTTL uint32) {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records)
-
- var minTTL uint32
- c.mutex.Lock()
- if ttl, ok := c.domainIndex[dc.Name]; !ok {
+func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
+ minTTL, ok, err := c.searchDomainIndex(dc.Name)
+ if err != nil {
+ return nil, 0, err
+ }
+ if !ok {
minTTL = 3600
- } else {
- minTTL = ttl
}
- c.mutex.Unlock()
+
PrepDesiredRecords(dc, minTTL)
- keysToUpdate, toReport, err := diff.NewCompat(dc).ChangedGroups(existing)
+ keysToUpdate, toReport, actualChangeCount, err := diff.NewCompat(dc).ChangedGroups(existing)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
if len(corrections) == 0 && len(keysToUpdate) == 0 {
- return nil, nil
+ return nil, 0, nil
}
desiredRecords := dc.Records.GroupedByKey()
@@ -202,7 +204,7 @@ func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exist
}
} else {
//it must be an update or create, both can be done with the same api call.
- ns := recordsToNative(desiredRecords[label], dc.Name)
+ ns := recordsToNative(desiredRecords[label])
if len(ns) > 1 {
panic("we got more than one resource record to create / modify")
}
@@ -217,19 +219,23 @@ func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exist
}
}
}
- msg := fmt.Sprintf("Changes:\n%s", buf)
- corrections = append(corrections,
- &models.Correction{
- Msg: msg,
- F: func() error {
- rc := rrs
- err := c.upsertRR(rc, dc.Name)
- if err != nil {
- return err
- }
- return nil
- },
- })
+
+ // If there are changes, upsert them.
+ if len(rrs) > 0 {
+ msg := fmt.Sprintf("Changes:\n%s", buf)
+ corrections = append(corrections,
+ &models.Correction{
+ Msg: msg,
+ F: func() error {
+ rc := rrs
+ err := c.upsertRR(rc, dc.Name)
+ if err != nil {
+ return err
+ }
+ return nil
+ },
+ })
+ }
// NB(tlim): This sort is just to make updates look pretty. It is
// cosmetic. The risk here is that there may be some updates that
@@ -237,16 +243,12 @@ func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exist
// However the code doesn't seem to have such situation. All tests
// pass. That said, if this breaks anything, the easiest fix might
// be to just remove the sort.
- sort.Slice(corrections, func(i, j int) bool { return diff.CorrectionLess(corrections, i, j) })
+ //sort.Slice(corrections, func(i, j int) bool { return diff.CorrectionLess(corrections, i, j) })
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// ListZones return all the zones in the account
func (c *desecProvider) ListZones() ([]string, error) {
- var domains []string
- for domain := range c.domainIndex {
- domains = append(domains, domain)
- }
- return domains, nil
+ return c.listDomainIndex()
}
diff --git a/providers/desec/protocol.go b/providers/desec/protocol.go
index c7c1c4aa2d..f36e3bcf5d 100644
--- a/providers/desec/protocol.go
+++ b/providers/desec/protocol.go
@@ -3,6 +3,7 @@ package desec
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -19,14 +20,9 @@ const apiBase = "https://desec.io/api/v1"
// Api layer for desec
type desecProvider struct {
- domainIndex map[string]uint32 //stores the minimum ttl of each domain. (key = domain and value = ttl)
- creds struct {
- tokenid string
- token string
- user string
- password string
- }
- mutex sync.Mutex
+ domainIndex map[string]uint32 //stores the minimum ttl of each domain. (key = domain and value = ttl)
+ domainIndexLock sync.Mutex
+ token string
}
type domainObject struct {
@@ -66,86 +62,105 @@ type nonFieldError struct {
Errors []string `json:"non_field_errors"`
}
-func (c *desecProvider) authenticate() error {
- endpoint := "/auth/account/"
- var _, resp, err = c.get(endpoint, "GET")
- //restricted tokens are valid, but get 403 on /auth/account
- //invalid tokens get 401
- if resp.StatusCode == 403 {
- return nil
- }
- if err != nil {
- return err
+// withDomainIndex checks if the domain index is initialized. If not, it's fetched from the deSEC API.
+// Next, the provided readFn function is executed to extract data from the domain index.
+func (c *desecProvider) withDomainIndex(readFn func(domainIndex map[string]uint32)) error {
+ // Lock index
+ c.domainIndexLock.Lock()
+ defer c.domainIndexLock.Unlock()
+
+ // Init index if needed
+ if c.domainIndex == nil {
+ printer.Debugf("Domain index not yet populated, fetching now\n")
+ var err error
+ c.domainIndex, err = c.fetchDomainIndex()
+ if err != nil {
+ return fmt.Errorf("failed to fetch domain index: %w", err)
+ }
}
+
+ // Execute handler on index
+ readFn(c.domainIndex)
return nil
}
-func (c *desecProvider) initializeDomainIndex() error {
- c.mutex.Lock()
- defer c.mutex.Unlock()
- if c.domainIndex != nil {
- return nil
- }
+
+// listDomainIndex lists all the available domains in the domain index
+func (c *desecProvider) listDomainIndex() (domains []string, err error) {
+ err = c.withDomainIndex(func(domainIndex map[string]uint32) {
+ domains = make([]string, 0, len(domainIndex))
+ for domain := range domainIndex {
+ domains = append(domains, domain)
+ }
+ })
+ return
+}
+
+// searchDomainIndex performs a lookup to the domain index for the TTL of the domain
+func (c *desecProvider) searchDomainIndex(domain string) (ttl uint32, found bool, err error) {
+ err = c.withDomainIndex(func(domainIndex map[string]uint32) {
+ ttl, found = domainIndex[domain]
+ })
+ return
+}
+
+func (c *desecProvider) fetchDomainIndex() (map[string]uint32, error) {
endpoint := "/domains/"
+ var domainIndex map[string]uint32
var bodyString, resp, err = c.get(endpoint, "GET")
if resp.StatusCode == 400 && resp.Header.Get("Link") != "" {
//pagination is required
- links := c.convertLinks(resp.Header.Get("Link"))
+ links := convertLinks(resp.Header.Get("Link"))
endpoint = links["first"]
printer.Debugf("initial endpoint %s\n", endpoint)
for endpoint != "" {
bodyString, resp, err = c.get(endpoint, "GET")
if err != nil {
- if resp.StatusCode == 404 {
- return nil
- }
- return fmt.Errorf("failed fetching domains: %s", err)
+ return nil, fmt.Errorf("failed fetching domains: %w", err)
}
- err = c.buildIndexFromResponse(bodyString)
+ domainIndex, err = appendDomainIndexFromResponse(domainIndex, bodyString)
if err != nil {
- return fmt.Errorf("failed fetching domains: %s", err)
+ return nil, fmt.Errorf("failed fetching domains: %w", err)
}
- links = c.convertLinks(resp.Header.Get("Link"))
+ links = convertLinks(resp.Header.Get("Link"))
endpoint = links["next"]
printer.Debugf("next endpoint %s\n", endpoint)
}
- printer.Debugf("Domain Index initilized with pagination (%d domains)\n", len(c.domainIndex))
- return nil //domainIndex was build using pagination without errors
+ printer.Debugf("Domain Index fetched with pagination (%d domains)\n", len(domainIndex))
+ return domainIndex, nil //domainIndex was build using pagination without errors
}
//no pagination required
if err != nil && resp.StatusCode != 400 {
- if resp.StatusCode == 404 {
- return nil
- }
- return fmt.Errorf("failed fetching domains: %s", err)
+ return nil, fmt.Errorf("failed fetching domains: %w", err)
}
- err = c.buildIndexFromResponse(bodyString)
- if err == nil {
- printer.Debugf("Domain Index initilized without pagination (%d domains)\n", len(c.domainIndex))
+ domainIndex, err = appendDomainIndexFromResponse(domainIndex, bodyString)
+ if err != nil {
+ return nil, err
}
- return err
+ printer.Debugf("Domain Index fetched without pagination (%d domains)\n", len(domainIndex))
+ return domainIndex, nil
}
-// buildIndexFromResponse takes the bodyString from initializeDomainIndex and builds the domainIndex
-func (c *desecProvider) buildIndexFromResponse(bodyString []byte) error {
- if c.domainIndex == nil {
- c.domainIndex = map[string]uint32{}
- }
+func appendDomainIndexFromResponse(domainIndex map[string]uint32, bodyString []byte) (map[string]uint32, error) {
var dr []domainObject
err := json.Unmarshal(bodyString, &dr)
if err != nil {
- return err
+ return nil, err
+ }
+
+ if domainIndex == nil {
+ domainIndex = make(map[string]uint32, len(dr))
}
for _, domain := range dr {
//deSEC allows different minimum ttls per domain
//we store the actual minimum ttl to use it in desecProvider.go GetDomainCorrections() to enforce the minimum ttl and avoid api errors.
- c.domainIndex[domain.Name] = domain.MinimumTTL
+ domainIndex[domain.Name] = domain.MinimumTTL
}
- return nil
+ return domainIndex, nil
}
-// Parses the Link Header into a map (https://github.com/desec-io/desec-tools/blob/master/fetch_zone.py#L13)
-func (c *desecProvider) convertLinks(links string) map[string]string {
+// Parses the Link Header into a map (https://github.com/desec-io/desec-tools/blob/main/fetch_zone.py#L13)
+func convertLinks(links string) map[string]string {
mapping := make(map[string]string)
printer.Debugf("Header: %s\n", links)
for _, link := range strings.Split(links, ", ") {
@@ -173,7 +188,7 @@ func (c *desecProvider) getRecords(domain string) ([]resourceRecord, error) {
var bodyString, resp, err = c.get(fmt.Sprintf(endpoint, domain), "GET")
if resp.StatusCode == 400 && resp.Header.Get("Link") != "" {
//pagination required
- links := c.convertLinks(resp.Header.Get("Link"))
+ links := convertLinks(resp.Header.Get("Link"))
endpoint = links["first"]
printer.Debugf("getRecords: initial endpoint %s\n", fmt.Sprintf(endpoint, domain))
for endpoint != "" {
@@ -182,14 +197,14 @@ func (c *desecProvider) getRecords(domain string) ([]resourceRecord, error) {
if resp.StatusCode == 404 {
return rrsNew, nil
}
- return rrsNew, fmt.Errorf("getRecords: failed fetching rrsets: %s", err)
+ return rrsNew, fmt.Errorf("getRecords: failed fetching rrsets: %w", err)
}
tmp, err := generateRRSETfromResponse(bodyString)
if err != nil {
- return rrsNew, fmt.Errorf("failed fetching records for domain %s (deSEC): %s", domain, err)
+ return rrsNew, fmt.Errorf("failed fetching records for domain %s (deSEC): %w", domain, err)
}
rrsNew = append(rrsNew, tmp...)
- links = c.convertLinks(resp.Header.Get("Link"))
+ links = convertLinks(resp.Header.Get("Link"))
endpoint = links["next"]
printer.Debugf("getRecords: next endpoint %s\n", endpoint)
}
@@ -198,7 +213,7 @@ func (c *desecProvider) getRecords(domain string) ([]resourceRecord, error) {
}
//no pagination
if err != nil {
- return rrsNew, fmt.Errorf("failed fetching records for domain %s (deSEC): %s", domain, err)
+ return rrsNew, fmt.Errorf("failed fetching records for domain %s (deSEC): %w", domain, err)
}
tmp, err := generateRRSETfromResponse(bodyString)
if err != nil {
@@ -238,7 +253,7 @@ func (c *desecProvider) createDomain(domain string) error {
var resp []byte
var err error
if resp, err = c.post(endpoint, "POST", byt); err != nil {
- return fmt.Errorf("failed domain create (deSEC): %v", err)
+ return fmt.Errorf("failed domain create (deSEC): %w", err)
}
dm := domainObject{}
err = json.Unmarshal(resp, &dm)
@@ -255,7 +270,7 @@ func (c *desecProvider) upsertRR(rr []resourceRecord, domain string) error {
endpoint := fmt.Sprintf("/domains/%s/rrsets/", domain)
byt, _ := json.Marshal(rr)
if _, err := c.post(endpoint, "PUT", byt); err != nil {
- return fmt.Errorf("failed create RRset (deSEC): %v", err)
+ return fmt.Errorf("failed create RRset (deSEC): %w", err)
}
return nil
}
@@ -265,7 +280,7 @@ func (c *desecProvider) upsertRR(rr []resourceRecord, domain string) error {
//func (c *desecProvider) deleteRR(domain, shortname, t string) error {
// endpoint := fmt.Sprintf("/domains/%s/rrsets/%s/%s/", domain, shortname, t)
// if _, _, err := c.get(endpoint, "DELETE"); err != nil {
-// return fmt.Errorf("failed delete RRset (deSEC): %v", err)
+// return fmt.Errorf("failed delete RRset (deSEC): %w", err)
// }
// return nil
//}
@@ -282,7 +297,7 @@ retry:
client := &http.Client{}
req, _ := http.NewRequest(method, endpoint, nil)
q := req.URL.Query()
- req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.creds.token))
+ req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.token))
req.URL.RawQuery = q.Encode()
@@ -302,14 +317,14 @@ retry:
wait, err := strconv.ParseInt(waitfor, 10, 64)
if err == nil {
if wait > 180 {
- return []byte{}, resp, fmt.Errorf("rate limiting exceeded")
+ return []byte{}, resp, errors.New("rate limiting exceeded")
}
- printer.Warnf("Rate limiting.. waiting for %s seconds", waitfor)
+ printer.Warnf("Rate limiting.. waiting for %s seconds\n", waitfor)
time.Sleep(time.Duration(wait+1) * time.Second)
goto retry
}
}
- printer.Warnf("Rate limiting.. waiting for 500 milliseconds")
+ printer.Warnf("Rate limiting.. waiting for 500 milliseconds\n")
time.Sleep(500 * time.Millisecond)
goto retry
}
@@ -317,12 +332,12 @@ retry:
var nfieldErrors []nonFieldError
err = json.Unmarshal(bodyString, &errResp)
if err == nil {
- return bodyString, resp, fmt.Errorf("%s", errResp.Detail)
+ return bodyString, resp, errors.New(errResp.Detail)
}
err = json.Unmarshal(bodyString, &nfieldErrors)
if err == nil && len(nfieldErrors) > 0 {
if len(nfieldErrors[0].Errors) > 0 {
- return bodyString, resp, fmt.Errorf("%s", nfieldErrors[0].Errors[0])
+ return bodyString, resp, errors.New(nfieldErrors[0].Errors[0])
}
}
return bodyString, resp, fmt.Errorf("HTTP status %s Body: %s, the API does not provide more information", resp.Status, bodyString)
@@ -346,7 +361,7 @@ retry:
}
q := req.URL.Query()
if endpoint != "/auth/login/" {
- req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.creds.token))
+ req.Header.Add("Authorization", fmt.Sprintf("Token %s", c.token))
}
req.Header.Set("Content-Type", "application/json")
@@ -369,14 +384,14 @@ retry:
wait, err := strconv.ParseInt(waitfor, 10, 64)
if err == nil {
if wait > 180 {
- return []byte{}, fmt.Errorf("rate limiting exceeded")
+ return []byte{}, errors.New("rate limiting exceeded")
}
- printer.Warnf("Rate limiting.. waiting for %s seconds", waitfor)
+ printer.Warnf("Rate limiting.. waiting for %s seconds\n", waitfor)
time.Sleep(time.Duration(wait+1) * time.Second)
goto retry
}
}
- printer.Warnf("Rate limiting.. waiting for 500 milliseconds")
+ printer.Warnf("Rate limiting.. waiting for 500 milliseconds\n")
time.Sleep(500 * time.Millisecond)
goto retry
}
@@ -389,7 +404,7 @@ retry:
err = json.Unmarshal(bodyString, &nfieldErrors)
if err == nil && len(nfieldErrors) > 0 {
if len(nfieldErrors[0].Errors) > 0 {
- return bodyString, fmt.Errorf("%s", nfieldErrors[0].Errors[0])
+ return bodyString, errors.New(nfieldErrors[0].Errors[0])
}
}
return bodyString, fmt.Errorf("HTTP status %s Body: %s, the API does not provide more information", resp.Status, bodyString)
diff --git a/providers/digitalocean/auditrecords.go b/providers/digitalocean/auditrecords.go
index 49263f45cd..01e7f50013 100644
--- a/providers/digitalocean/auditrecords.go
+++ b/providers/digitalocean/auditrecords.go
@@ -19,9 +19,9 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("TXT", MaxLengthDO) // Last verified 2021-03-01
- a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2021-03-01
- // Double-quotes not permitted in TXT strings. I have a hunch that
- // this is due to a broken parser on the DO side.
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2023-11-11
+
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-11-11
return a.Audit(records)
}
@@ -44,10 +44,9 @@ func MaxLengthDO(rc *models.RecordConfig) error {
// In other words, they're doing the checking on the API protocol
// encoded data instead of on on the resulting TXT record. Sigh.
- if len(rc.GetTargetField()) > 509 {
+ if len(rc.GetTargetRFC1035Quoted()) > 509 {
return fmt.Errorf("encoded txt too long")
}
- // FIXME(tlim): Try replacing GetTargetField() with (2 + (3*len(rc.TxtStrings) - 1))
return nil
}
diff --git a/providers/digitalocean/digitaloceanProvider.go b/providers/digitalocean/digitaloceanProvider.go
index 62ec0d7dd9..5f5ad3657a 100644
--- a/providers/digitalocean/digitaloceanProvider.go
+++ b/providers/digitalocean/digitaloceanProvider.go
@@ -10,7 +10,6 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/digitalocean/godo"
"github.com/miekg/dns/dnsutil"
@@ -71,7 +70,10 @@ retry:
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
@@ -80,11 +82,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "DIGITALOCEAN"
+ const providerMaintainer = "@Deraen"
fns := providers.DspFuncs{
Initializer: NewDo,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("DIGITALOCEAN", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// EnsureZoneExists creates a zone if it does not exist
@@ -167,14 +172,12 @@ func (api *digitaloceanProvider) GetZoneRecords(domain string, meta map[string]s
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *digitaloceanProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
+func (api *digitaloceanProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
ctx := context.Background()
- toReport, toCreate, toDelete, toModify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, toCreate, toDelete, toModify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -198,7 +201,7 @@ func (api *digitaloceanProvider) GetZoneRecordsCorrections(dc *models.DomainConf
corrections = append(corrections, corr)
}
for _, m := range toCreate {
- req := toReq(dc, m.Desired)
+ req := toReq(m.Desired)
corr := &models.Correction{
Msg: m.String(),
F: func() error {
@@ -216,7 +219,7 @@ func (api *digitaloceanProvider) GetZoneRecordsCorrections(dc *models.DomainConf
}
for _, m := range toModify {
id := m.Existing.Original.(*godo.DomainRecord).ID
- req := toReq(dc, m.Desired)
+ req := toReq(m.Desired)
corr := &models.Correction{
Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
F: func() error {
@@ -233,7 +236,7 @@ func (api *digitaloceanProvider) GetZoneRecordsCorrections(dc *models.DomainConf
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func getRecords(api *digitaloceanProvider, name string) ([]godo.DomainRecord, error) {
@@ -307,7 +310,7 @@ func toRc(domain string, r *godo.DomainRecord) *models.RecordConfig {
return t
}
-func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest {
+func toReq(rc *models.RecordConfig) *godo.DomainRecordEditRequest {
name := rc.GetLabel() // DO wants the short name or "@" for apex.
target := rc.GetTargetField() // DO uses the target field only for a single value
priority := 0 // DO uses the same property for MX and SRV priority
diff --git a/providers/dnsimple/auditrecords.go b/providers/dnsimple/auditrecords.go
index 0354a63eb1..93e48d5d98 100644
--- a/providers/dnsimple/auditrecords.go
+++ b/providers/dnsimple/auditrecords.go
@@ -11,9 +11,7 @@ import (
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
- a.Add("MX", rejectif.MxNull) // Last verified 2023-03
-
- a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2023-03
+ a.Add("TXT", rejectif.TxtLongerThan(1000)) // Last verified 2023-12
a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2023-03
diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go
index e1fc4277eb..860220cc6c 100644
--- a/providers/dnsimple/dnsimpleProvider.go
+++ b/providers/dnsimple/dnsimpleProvider.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "os"
"sort"
"strconv"
"strings"
@@ -12,14 +13,18 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple"
"golang.org/x/oauth2"
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
@@ -36,19 +41,22 @@ var features = providers.DocumentationNotes{
}
func init() {
- providers.RegisterRegistrarType("DNSIMPLE", newReg)
+ const providerName = "DNSIMPLE"
+ const providerMaintainer = "@onlyhavecans"
+ providers.RegisterRegistrarType(providerName, newReg)
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("DNSIMPLE", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
const stateRegistered = "registered"
var defaultNameServerNames = []string{
"ns1.dnsimple.com",
- "ns2.dnsimple.com",
+ "ns2.dnsimple-edge.net",
"ns3.dnsimple.com",
"ns4.dnsimple-edge.org",
}
@@ -90,13 +98,22 @@ func (c *dnsimpleProvider) GetZoneRecords(domain string, meta map[string]string)
r.Name = "@"
}
- if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" || r.Type == "NS" {
+ if r.Type == "CNAME" || r.Type == "ALIAS" || r.Type == "NS" {
+ r.Content += "."
+ } else if r.Type == "MX" && r.Content != "." {
r.Content += "."
}
// DNSimple adds TXT records that mirror the alias records.
// They manage them on ALIAS updates, so pretend they don't exist
- if r.Type == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") {
+ if r.Type == "TXT" && strings.HasPrefix(r.Content, `"ALIAS for `) {
+ continue
+ }
+ // This second check is the same of before, but it exists for compatibility purpose.
+ // Until Nov 2023 DNSimple did not normalize TXT records, and they used to store TXT records without quotes.
+ //
+ // This is a backward-compatible function to facilitate the TXT transition.
+ if r.Type == "TXT" && strings.HasPrefix(r.Content, `ALIAS for `) {
continue
}
@@ -120,7 +137,12 @@ func (c *dnsimpleProvider) GetZoneRecords(domain string, meta map[string]string)
case "SRV":
err = rec.SetTargetSRVPriorityString(uint16(r.Priority), r.Content)
case "TXT":
- err = rec.SetTargetTXT(r.Content)
+ // This is a backward-compatible function to facilitate the TXT transition.
+ if isQuotedTXT(r.Content) {
+ err = rec.PopulateFromStringFunc(r.Type, r.Content, domain, txtutil.ParseQuoted)
+ } else {
+ err = rec.SetTargetTXT(fmt.Sprintf("legacy: %s", r.Content))
+ }
default:
err = rec.PopulateFromString(r.Type, r.Content, domain)
}
@@ -138,17 +160,17 @@ func (c *dnsimpleProvider) GetZoneRecords(domain string, meta map[string]string)
return cleanedRecords, nil
}
-func (c *dnsimpleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
+func (c *dnsimpleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
removeOtherApexNS(dc)
dnssecFixes, err := c.getDNSSECCorrections(dc)
if err != nil {
- return nil, err
+ return nil, 0, err
}
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(actual)
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(actual)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -180,7 +202,7 @@ func (c *dnsimpleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ac
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func removeApexNS(records models.Records) models.Records {
@@ -255,6 +277,10 @@ func (c *dnsimpleProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*mod
// DNSimple calls
+// Initializes a new DNSimple API client.
+//
+// - if BaseURL is present, the provided BaseURL is used. Useful to switch to DNSimple sandbox site. It defaults to production otherwise.
+// - if "DNSIMPLE_DEBUG_HTTP" is set to "1", it enables the API client logging.
func (c *dnsimpleProvider) getClient() *dnsimpleapi.Client {
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AccountToken})
tc := oauth2.NewClient(context.Background(), ts)
@@ -266,6 +292,9 @@ func (c *dnsimpleProvider) getClient() *dnsimpleapi.Client {
if c.BaseURL != "" {
client.BaseURL = c.BaseURL
}
+ if os.Getenv("DNSIMPLE_DEBUG_HTTP") == "1" {
+ client.Debug = true
+ }
return client
}
@@ -533,7 +562,6 @@ func (c *dnsimpleProvider) deleteRecordFunc(recordID int64, domainName string) f
}
return nil
-
}
}
@@ -633,8 +661,10 @@ func newProvider(m map[string]string, _ json.RawMessage) (*dnsimpleProvider, err
return api, nil
}
-// remove all non-dnsimple NS records from our desired state.
-// if any are found, print a warning
+// utilities
+
+// Removes all non-dnsimple NS records from our desired state.
+// If any are found, print a warning.
func removeOtherApexNS(dc *models.DomainConfig) {
newList := make([]*models.RecordConfig, 0, len(dc.Records))
for _, rec := range dc.Records {
@@ -654,7 +684,7 @@ func removeOtherApexNS(dc *models.DomainConfig) {
dc.Records = newList
}
-// Return the correct combined content for all special record types, Target for everything else
+// Returns the correct combined content for all special record types, Target for everything else
// Using RecordConfig.GetTargetCombined returns priority in the string, which we do not allow
func getTargetRecordContent(rc *models.RecordConfig) string {
switch rtype := rc.Type; rtype {
@@ -671,13 +701,13 @@ func getTargetRecordContent(rc *models.RecordConfig) string {
case "SRV":
return fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
case "TXT":
- return rc.GetTargetTXTJoined()
+ return rc.GetTargetCombinedFunc(txtutil.EncodeQuoted)
default:
return rc.GetTargetField()
}
}
-// Return the correct priority for the record type, 0 for records without priority
+// Returns the correct priority for the record type, 0 for records without priority
func getTargetRecordPriority(rc *models.RecordConfig) int {
switch rtype := rc.Type; rtype {
case "MX":
@@ -699,7 +729,7 @@ func compileAttributeErrors(err *dnsimpleapi.ErrorResponse) error {
e := strings.Join(errors, "& ")
message += fmt.Sprintf(": %s %s", field, e)
}
- return fmt.Errorf(message)
+ return errors.New(message)
}
// Return true if the string ends in one of DNSimple's name server domains
@@ -712,3 +742,11 @@ func isDnsimpleNameServerDomain(name string) bool {
}
return false
}
+
+// Tests if the content is encoded, performing a naive check on the presence of quotes
+// at the beginning and end of the string.
+//
+// This is a backward-compatible function to facilitate the TXT transition.
+func isQuotedTXT(content string) bool {
+ return content[0:1] == `"` && content[len(content)-1:] == `"`
+}
diff --git a/providers/dnsmadeeasy/dnsMadeEasyProvider.go b/providers/dnsmadeeasy/dnsMadeEasyProvider.go
index 3e6f1eb31e..08f5655aae 100644
--- a/providers/dnsmadeeasy/dnsMadeEasyProvider.go
+++ b/providers/dnsmadeeasy/dnsMadeEasyProvider.go
@@ -8,12 +8,14 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
@@ -29,12 +31,15 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "DNSMADEEASY"
+ const providerMaintainer = "@vojtad"
fns := providers.DspFuncs{
Initializer: New,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("DNSMADEEASY", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// New creates a new API handle.
@@ -98,13 +103,11 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
// return api.GetZoneRecordsCorrections(dc, existingRecords)
// }
-func (api *dnsMadeEasyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
+func (api *dnsMadeEasyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
domainName := dc.Name
domain, err := api.findDomain(domainName)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, rec := range dc.Records {
@@ -117,9 +120,9 @@ func (api *dnsMadeEasyProvider) GetZoneRecordsCorrections(dc *models.DomainConfi
}
}
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -183,7 +186,7 @@ func (api *dnsMadeEasyProvider) GetZoneRecordsCorrections(dc *models.DomainConfi
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// EnsureZoneExists creates a zone if it does not exist
diff --git a/providers/dnsmadeeasy/restApi.go b/providers/dnsmadeeasy/restApi.go
index 02b570c57d..ac87e29f0b 100644
--- a/providers/dnsmadeeasy/restApi.go
+++ b/providers/dnsmadeeasy/restApi.go
@@ -207,7 +207,7 @@ func (restApi *dnsMadeEasyRestAPI) createRequest(request *apiRequest) (*http.Req
return req, nil
}
-// DNS Made Simple only allows 150 request / 5 minutes
+// DNS Made Easy only allows 150 request / 5 minutes
// backoff is the amount of time to sleep if a "Rate limit exceeded" error is received
// It is increased up to maxBackoff after each use
// It is reset after successful request
diff --git a/providers/dnsmadeeasy/types.go b/providers/dnsmadeeasy/types.go
index 1a4010d3a2..fcd7b767dc 100644
--- a/providers/dnsmadeeasy/types.go
+++ b/providers/dnsmadeeasy/types.go
@@ -140,7 +140,7 @@ func toRecordConfig(domain string, record *recordResponseDataEntry) *models.Reco
} else if record.Type == "CAA" {
value, unquoteErr := strconv.Unquote(record.Value)
if unquoteErr != nil {
- panic(err)
+ panic(unquoteErr)
}
err = rc.SetTargetCAA(uint8(record.IssuerCritical), record.CaaType, value)
} else {
diff --git a/providers/doh/api.go b/providers/doh/api.go
index f2d6d69217..25699f2453 100644
--- a/providers/doh/api.go
+++ b/providers/doh/api.go
@@ -31,6 +31,6 @@ func (c *dohProvider) getNameservers(domain string) ([]string, error) {
return ns, nil
}
-func (c *dohProvider) updateNameservers(ns []string, domain string) error {
+func (c *dohProvider) updateNameservers(domain string) error {
return fmt.Errorf("DNS-over-HTTPS 'Registrar' is read only, changes must be applied to %s manually", domain)
}
diff --git a/providers/doh/auditrecords.go b/providers/doh/auditrecords.go
deleted file mode 100644
index 5901140530..0000000000
--- a/providers/doh/auditrecords.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package doh
-
-import "github.com/StackExchange/dnscontrol/v4/models"
-
-// AuditRecords returns a list of errors corresponding to the records
-// that aren't supported by this provider. If all records are
-// supported, an empty list is returned.
-func AuditRecords(records []*models.RecordConfig) []error {
- return nil
-}
diff --git a/providers/doh/dohProvider.go b/providers/doh/dohProvider.go
index ad9ccd518d..2cc91e1680 100644
--- a/providers/doh/dohProvider.go
+++ b/providers/doh/dohProvider.go
@@ -17,8 +17,17 @@ Info required in `creds.json`:
- host DNS over HTTPS host (eg 9.9.9.9)
*/
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanConcur: providers.Cannot(),
+}
+
func init() {
- providers.RegisterRegistrarType("DNSOVERHTTPS", newDNSOverHTTPS)
+ const providerName = "DNSOVERHTTPS"
+ const providerMaintainer = "@mikenz"
+ providers.RegisterRegistrarType(providerName, newDNSOverHTTPS, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func newDNSOverHTTPS(m map[string]string) (providers.Registrar, error) {
@@ -54,7 +63,7 @@ func (c *dohProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*model
{
Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
F: func() error {
- return c.updateNameservers(expected, dc.Name)
+ return c.updateNameservers(dc.Name)
},
},
}, nil
diff --git a/providers/domainnameshop/dns.go b/providers/domainnameshop/dns.go
index e7619c11d8..6915b65f14 100644
--- a/providers/domainnameshop/dns.go
+++ b/providers/domainnameshop/dns.go
@@ -25,12 +25,12 @@ func (api *domainNameShopProvider) GetZoneRecords(domain string, meta map[string
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *domainNameShopProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (api *domainNameShopProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
// Merge TXT strings to one string
for _, rc := range dc.Records {
if rc.HasFormatIdenticalToTXT() {
- rc.SetTargetTXT(strings.Join(rc.TxtStrings, ""))
+ rc.SetTargetTXT(rc.GetTargetTXTJoined())
}
}
@@ -39,9 +39,9 @@ func (api *domainNameShopProvider) GetZoneRecordsCorrections(dc *models.DomainCo
record.TTL = fixTTL(record.TTL)
}
- toReport, create, delete, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, delete, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -65,7 +65,7 @@ func (api *domainNameShopProvider) GetZoneRecordsCorrections(dc *models.DomainCo
dnsR, err := api.fromRecordConfig(domainName, r.Desired)
if err != nil {
- return nil, err
+ return nil, 0, err
}
corr := &models.Correction{
@@ -81,7 +81,7 @@ func (api *domainNameShopProvider) GetZoneRecordsCorrections(dc *models.DomainCo
dnsR, err := api.fromRecordConfig(domainName, r.Desired)
if err != nil {
- return nil, err
+ return nil, 0, err
}
dnsR.ID = r.Existing.Original.(*domainNameShopRecord).ID
@@ -94,7 +94,7 @@ func (api *domainNameShopProvider) GetZoneRecordsCorrections(dc *models.DomainCo
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (api *domainNameShopProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
diff --git a/providers/domainnameshop/domainnameshopProvider.go b/providers/domainnameshop/domainnameshopProvider.go
index a3b9e1677b..ed92f38163 100644
--- a/providers/domainnameshop/domainnameshopProvider.go
+++ b/providers/domainnameshop/domainnameshopProvider.go
@@ -23,8 +23,11 @@ type domainNameShopProvider struct {
}
var features = providers.DocumentationNotes{
- providers.CanAutoDNSSEC: providers.Cannot(), // Maybe there is support for it
- providers.CanGetZones: providers.Unimplemented(), //
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanAutoDNSSEC: providers.Cannot(), // Maybe there is support for it
+ providers.CanGetZones: providers.Unimplemented(), //
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Unimplemented("Needs custom implementation"), // Can possibly be implemented, needs further research
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Unimplemented(), // Seems to support but needs to be implemented
@@ -44,12 +47,15 @@ var features = providers.DocumentationNotes{
// Register with the dnscontrol system.
// This establishes the name (all caps), and the function to call to initialize it.
func init() {
+ const providerName = "DOMAINNAMESHOP"
+ const providerMaintainer = "@SimenBai"
fns := providers.DspFuncs{
Initializer: newDomainNameShopProvider,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("DOMAINNAMESHOP", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// newDomainNameShopProvider creates a Domainnameshop specific DNS provider.
diff --git a/providers/dynadot/api.go b/providers/dynadot/api.go
new file mode 100644
index 0000000000..232fa2f3e1
--- /dev/null
+++ b/providers/dynadot/api.go
@@ -0,0 +1,135 @@
+package dynadot
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+// API layer for Dynadot
+
+type dynadotProvider struct {
+ key string
+}
+
+type requestParams map[string]string
+
+type header struct {
+ SuccessCode int `xml:"SuccessCode"`
+ Status string `xml:"Status"`
+ Error string `xml:"Error,omitempty"`
+}
+
+type addNsResponse struct {
+ XMLName xml.Name `xml:"AddNsResponse"`
+ AddNsHeader header `xml:"AddNsHeader"`
+}
+
+type setNsResponse struct {
+ XMLName xml.Name `xml:"SetNsResponse"`
+ SetNsHeader header `xml:"SetNsHeader"`
+}
+
+type getNsResponse struct {
+ XMLName xml.Name `xml:"GetNsResponse"`
+ NsContent nsContent `xml:"NsContent"`
+ GetNsHeader header `xml:"GetNsHeader"`
+}
+
+type nsContent struct {
+ Host []string `xml:"Host"`
+ NsName string `xml:"NsName"`
+}
+
+func (c *dynadotProvider) getNameservers(domain string) ([]string, error) {
+ var bodyString, err = c.get("get_ns", requestParams{"domain": domain})
+ if err != nil {
+ return []string{}, fmt.Errorf("failed NS list (Dynadot): %s", err)
+ }
+ var ns getNsResponse
+ xml.Unmarshal(bodyString, &ns)
+
+ if ns.GetNsHeader.SuccessCode != 0 {
+ return []string{}, fmt.Errorf("failed NS list (Dynadot): %s", ns.GetNsHeader.Error)
+ }
+
+ hosts := []string{}
+ hosts = append(hosts, ns.NsContent.Host...)
+ return hosts, nil
+}
+
+func (c *dynadotProvider) updateNameservers(ns []string, domain string) error {
+ if len(ns) > 13 {
+ return fmt.Errorf("failed NS update (Dynadot): only up to 13 nameservers are supported")
+ }
+
+ // Nameservers must first be added to the Dynadot account
+ for _, host := range ns {
+ b, err := c.get("add_ns", requestParams{"host": host})
+ if err != nil {
+ return fmt.Errorf("failed NS add (Dynadot): %s", err)
+ }
+ var resp addNsResponse
+ err = xml.Unmarshal(b, &resp)
+ if err != nil {
+ return fmt.Errorf("failed NS add (Dynadot): %s", err)
+ }
+
+ if resp.AddNsHeader.SuccessCode != 0 {
+ // No apparent way to get all existing entries on an account, so filter
+ if strings.Contains(resp.AddNsHeader.Error, "already exists") {
+ continue
+ }
+ return fmt.Errorf("failed NS add (Dynadot): %s", resp.AddNsHeader.Error)
+
+ }
+ }
+
+ rec := requestParams{}
+ rec["domain"] = domain
+ // supported prams: ns0 - ns12
+ for i, h := range ns {
+ rec[fmt.Sprintf("%s%d", "ns", i)] = h
+ }
+
+ b, err := c.get("set_ns", rec)
+ if err != nil {
+ return fmt.Errorf("failed NS set (Dynadot): %s", err)
+ }
+
+ var resp setNsResponse
+ err = xml.Unmarshal(b, &resp)
+ if err != nil {
+ return fmt.Errorf("failed NS add (Dynadot): %s", err)
+ }
+
+ if resp.SetNsHeader.SuccessCode != 0 {
+ return fmt.Errorf("failed NS add (Dynadot): %s", resp.SetNsHeader.Error)
+ }
+
+ return nil
+}
+
+func (c *dynadotProvider) get(command string, params requestParams) ([]byte, error) {
+ client := &http.Client{}
+ req, _ := http.NewRequest("GET", "https://api.dynadot.com/api3.xml", nil)
+ q := req.URL.Query()
+
+ q.Add("key", c.key)
+ q.Add("command", command)
+
+ for pName, pValue := range params {
+ q.Add(pName, pValue)
+ }
+
+ req.URL.RawQuery = q.Encode()
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return []byte{}, err
+ }
+
+ return io.ReadAll(resp.Body)
+}
diff --git a/providers/dynadot/dynadotProvider.go b/providers/dynadot/dynadotProvider.go
new file mode 100644
index 0000000000..23da0ec34a
--- /dev/null
+++ b/providers/dynadot/dynadotProvider.go
@@ -0,0 +1,71 @@
+package dynadot
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/providers"
+)
+
+/*
+
+Dynadot Registrator:
+
+Info required in `creds.json`:
+ - key API Key
+
+*/
+
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanConcur: providers.Cannot(),
+}
+
+func init() {
+ const providerName = "DYNADOT"
+ const providerMaintainer = "@e-im"
+ providers.RegisterRegistrarType(providerName, newDynadot, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+}
+
+func newDynadot(m map[string]string) (providers.Registrar, error) {
+ d := &dynadotProvider{}
+
+ d.key = m["key"]
+ if d.key == "" {
+ return nil, fmt.Errorf("missing Dynadot key")
+ }
+
+ return d, nil
+}
+
+func (c *dynadotProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ nss, err := c.getNameservers(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+ foundNameservers := strings.Join(nss, ",")
+
+ expected := []string{}
+ for _, ns := range dc.Nameservers {
+ name := strings.TrimRight(ns.Name, ".")
+ expected = append(expected, name)
+ }
+ sort.Strings(expected)
+ expectedNameservers := strings.Join(expected, ",")
+
+ if foundNameservers != expectedNameservers {
+ return []*models.Correction{
+ {
+ Msg: fmt.Sprintf("Update nameservers (%s) -> (%s)", foundNameservers, expectedNameservers),
+ F: func() error {
+ return c.updateNameservers(expected, dc.Name)
+ },
+ },
+ }, nil
+ }
+ return nil, nil
+}
diff --git a/providers/easyname/easynameProvider.go b/providers/easyname/easynameProvider.go
index 4fc9b848b6..3c4ec58f08 100644
--- a/providers/easyname/easynameProvider.go
+++ b/providers/easyname/easynameProvider.go
@@ -16,8 +16,17 @@ type easynameProvider struct {
domains map[string]easynameDomain
}
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanConcur: providers.Cannot(),
+}
+
func init() {
- providers.RegisterRegistrarType("EASYNAME", newEasyname)
+ const providerName = "EASYNAME"
+ const providerMaintainer = "@tresni"
+ providers.RegisterRegistrarType(providerName, newEasyname, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func newEasyname(m map[string]string) (providers.Registrar, error) {
diff --git a/providers/exoscale/exoscaleProvider.go b/providers/exoscale/exoscaleProvider.go
index 491e3e7be3..999fe25992 100644
--- a/providers/exoscale/exoscaleProvider.go
+++ b/providers/exoscale/exoscaleProvider.go
@@ -54,7 +54,10 @@ func NewExoscale(m map[string]string, metadata json.RawMessage) (providers.DNSSe
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Unimplemented(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
@@ -67,11 +70,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "EXOSCALE"
+ const providerMaintainer = "@pierre-emmanuelJ"
fns := providers.DspFuncs{
Initializer: NewExoscale,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("EXOSCALE", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// EnsureZoneExists creates a zone if it does not exist
@@ -181,18 +187,18 @@ func (c *exoscaleProvider) GetZoneRecords(domainName string, meta map[string]str
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *exoscaleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (c *exoscaleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
removeOtherNS(dc)
domain, err := c.findDomainByName(dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
domainID := *domain.ID
- toReport, create, toDelete, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, toDelete, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -222,7 +228,7 @@ func (c *exoscaleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// Returns a function that can be invoked to create a record in a zone.
diff --git a/providers/gandiv5/convert.go b/providers/gandiv5/convert.go
index 229ac0c3f1..46e3259325 100644
--- a/providers/gandiv5/convert.go
+++ b/providers/gandiv5/convert.go
@@ -7,6 +7,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/go-gandi/go-gandi/livedns"
)
@@ -29,10 +30,8 @@ func nativeToRecords(n livedns.DomainRecord, origin string) (rcs []*models.Recor
case "ALIAS":
rc.Type = "ALIAS"
err = rc.SetTarget(value)
- case "TXT":
- err = rc.SetTargetTXTfromRFC1035Quoted(value)
default:
- err = rc.PopulateFromString(rtype, value, origin)
+ err = rc.PopulateFromStringFunc(rtype, value, origin, txtutil.ParseQuoted)
}
if err != nil {
return nil, fmt.Errorf("unparsable record received from gandi: %w", err)
@@ -65,11 +64,11 @@ func recordsToNative(rcs []*models.RecordConfig, origin string) []livedns.Domain
RrsetType: r.Type,
RrsetTTL: int(r.TTL),
RrsetName: label,
- RrsetValues: []string{r.GetTargetCombined()},
+ RrsetValues: []string{r.GetTargetCombinedFunc(txtutil.EncodeQuoted)},
}
keys[key] = &zr
} else {
- zr.RrsetValues = append(zr.RrsetValues, r.GetTargetCombined())
+ zr.RrsetValues = append(zr.RrsetValues, r.GetTargetCombinedFunc(txtutil.EncodeQuoted))
if r.TTL != uint32(zr.RrsetTTL) {
printer.Warnf("All TTLs for a rrset (%v) must be the same. Using smaller of %v and %v.\n", key, r.TTL, zr.RrsetTTL)
diff --git a/providers/gandiv5/gandi_v5Provider.go b/providers/gandiv5/gandi_v5Provider.go
index d6fb5409fa..4220e6f5cb 100644
--- a/providers/gandiv5/gandi_v5Provider.go
+++ b/providers/gandiv5/gandi_v5Provider.go
@@ -24,10 +24,10 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/go-gandi/go-gandi"
"github.com/go-gandi/go-gandi/config"
+ "github.com/go-gandi/go-gandi/livedns"
"github.com/miekg/dns/dnsutil"
)
@@ -35,17 +35,23 @@ import (
// init registers the provider to dnscontrol.
func init() {
+ const providerName = "GANDI_V5"
+ const providerMaintainer = "@TomOnTime"
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("GANDI_V5", fns, features)
- providers.RegisterRegistrarType("GANDI_V5", newReg)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterRegistrarType(providerName, newReg)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// features declares which features and options are available.
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Can("Only on the bare domain. Otherwise CNAME will be substituted"),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot("Only supports DS records at the apex"),
@@ -55,7 +61,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(),
- providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"),
providers.DocOfficiallySupported: providers.Cannot(),
}
@@ -68,8 +73,10 @@ var features = providers.DocumentationNotes{
// gandiv5Provider is the gandiv5Provider handle used to store any client-related state.
type gandiv5Provider struct {
apikey string
+ token string
sharingid string
debug bool
+ apiurl string
}
// newDsp generates a DNS Service Provider client handle.
@@ -83,13 +90,15 @@ func newReg(conf map[string]string) (providers.Registrar, error) {
}
// newHelper generates a handle.
-func newHelper(m map[string]string, metadata json.RawMessage) (*gandiv5Provider, error) {
+func newHelper(m map[string]string, _ json.RawMessage) (*gandiv5Provider, error) {
api := &gandiv5Provider{}
api.apikey = m["apikey"]
- if api.apikey == "" {
- return nil, fmt.Errorf("missing Gandi apikey")
+ api.token = m["token"]
+ if (api.apikey == "") && (api.token == "") {
+ return nil, fmt.Errorf("missing Gandi personal access token (or apikey - deprecated)")
}
api.sharingid = m["sharing_id"]
+ api.apiurl = m["apiurl"]
debug, err := strconv.ParseBool(os.Getenv("GANDI_V5_DEBUG"))
if err == nil {
api.debug = debug
@@ -100,15 +109,24 @@ func newHelper(m map[string]string, metadata json.RawMessage) (*gandiv5Provider,
// Section 3: Domain Service Provider (DSP) related functions
+// newLiveDNSClient returns a client to the Gandi Domains API
+// It expects an API key, available from https://account.gandi.net/en/
+func newLiveDNSClient(client *gandiv5Provider) *livedns.LiveDNS {
+ g := gandi.NewLiveDNSClient(config.Config{
+ APIKey: client.apikey,
+ PersonalAccessToken: client.token,
+ SharingID: client.sharingid,
+ Debug: client.debug,
+ APIURL: client.apiurl,
+ })
+ return g
+}
+
// // ListZones lists the zones on this account.
// This no longer works. Until we can figure out why, we're removing this
// feature for Gandi.
// func (client *gandiv5Provider) ListZones() ([]string, error) {
-// g := gandi.NewLiveDNSClient(config.Config{
-// APIKey: client.apikey,
-// SharingID: client.sharingid,
-// Debug: client.debug,
-// })
+// g := newLiveDNSClient(client)
// listResp, err := g.ListDomains()
// if err != nil {
@@ -129,11 +147,7 @@ func newHelper(m map[string]string, metadata json.RawMessage) (*gandiv5Provider,
// GetZoneRecords gathers the DNS records and converts them to
// dnscontrol's format.
func (client *gandiv5Provider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
- g := gandi.NewLiveDNSClient(config.Config{
- APIKey: client.apikey,
- SharingID: client.sharingid,
- Debug: client.debug,
- })
+ g := newLiveDNSClient(client)
// Get all the existing records:
records, err := g.GetDomainRecords(domain)
@@ -176,9 +190,6 @@ func PrepDesiredRecords(dc *models.DomainConfig) {
printer.Warnf("Gandi does not support ttls > 30 days. Setting %s from %d to 2592000\n", rec.GetLabelFQDN(), rec.TTL)
rec.TTL = 2592000
}
- if rec.Type == "TXT" {
- rec.SetTarget("\"" + rec.GetTargetField() + "\"") // FIXME(tlim): Should do proper quoting.
- }
if rec.Type == "NS" && rec.GetLabel() == "@" {
if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") {
printer.Warnf("Gandi does not support changing apex NS records. Ignoring %s\n", rec.GetTargetField())
@@ -191,26 +202,21 @@ func PrepDesiredRecords(dc *models.DomainConfig) {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (client *gandiv5Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+func (client *gandiv5Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
if client.debug {
debugRecords("GenDC input", existing)
}
PrepDesiredRecords(dc)
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
- g := gandi.NewLiveDNSClient(config.Config{
- APIKey: client.apikey,
- SharingID: client.sharingid,
- Debug: client.debug,
- })
+ g := newLiveDNSClient(client)
// Gandi is a "ByLabel" API with the odd exception that changes must be
// done one label:rtype at a time.
- instructions, err := diff2.ByLabel(existing, dc, nil)
+ instructions, actualChangeCount, err := diff2.ByLabel(existing, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, inst := range instructions {
switch inst.Type {
@@ -238,7 +244,7 @@ func (client *gandiv5Provider) GetZoneRecordsCorrections(dc *models.DomainConfig
F: func() error {
res, err := g.CreateDomainRecord(domain, shortname, rtype, ttl, values)
if err != nil {
- return fmt.Errorf("%+v: %w", res, err)
+ return fmt.Errorf("%+v ret=%03d: %w", res, res.Code, err)
}
return nil
},
@@ -257,7 +263,7 @@ func (client *gandiv5Provider) GetZoneRecordsCorrections(dc *models.DomainConfig
F: func() error {
res, err := g.UpdateDomainRecordsByName(domain, shortname, ns)
if err != nil {
- return fmt.Errorf("%+v: %w", res, err)
+ return fmt.Errorf("%+v ret=%03d: %w", res, res.Code, err)
}
return nil
},
@@ -286,14 +292,14 @@ func (client *gandiv5Provider) GetZoneRecordsCorrections(dc *models.DomainConfig
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// debugRecords prints a list of RecordConfig.
func debugRecords(note string, recs []*models.RecordConfig) {
printer.Debugf(note)
for k, v := range recs {
- printer.Printf(" %v: %v %v %v %v\n", k, v.GetLabel(), v.Type, v.TTL, v.GetTargetCombined())
+ printer.Printf(" %v: %v %v %v %v\n", k, v.GetLabel(), v.Type, v.TTL, v.GetTargetDebug())
}
}
@@ -301,11 +307,7 @@ func debugRecords(note string, recs []*models.RecordConfig) {
// GetNameservers returns a list of nameservers for domain.
func (client *gandiv5Provider) GetNameservers(domain string) ([]*models.Nameserver, error) {
- g := gandi.NewLiveDNSClient(config.Config{
- APIKey: client.apikey,
- SharingID: client.sharingid,
- Debug: client.debug,
- })
+ g := newLiveDNSClient(client)
nameservers, err := g.GetDomainNS(domain)
if err != nil {
return nil, err
@@ -316,9 +318,11 @@ func (client *gandiv5Provider) GetNameservers(domain string) ([]*models.Nameserv
// GetRegistrarCorrections returns a list of corrections for this registrar.
func (client *gandiv5Provider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
gd := gandi.NewDomainClient(config.Config{
- APIKey: client.apikey,
- SharingID: client.sharingid,
- Debug: client.debug,
+ APIKey: client.apikey,
+ PersonalAccessToken: client.token,
+ SharingID: client.sharingid,
+ Debug: client.debug,
+ APIURL: client.apiurl,
})
existingNs, err := gd.GetNameServers(dc.Name)
diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go
index 8fd387ee20..ae976139f2 100644
--- a/providers/gcloud/gcloudProvider.go
+++ b/providers/gcloud/gcloudProvider.go
@@ -10,7 +10,7 @@ import (
"time"
"github.com/StackExchange/dnscontrol/v4/models"
- "github.com/StackExchange/dnscontrol/v4/pkg/diff"
+ "github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
@@ -23,14 +23,19 @@ import (
const selfLinkBasePath = "https://www.googleapis.com/compute/v1/projects/"
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDSForChildren: providers.Can(),
+ providers.CanUseHTTPS: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
@@ -48,11 +53,14 @@ func sPtr(s string) *string {
}
func init() {
+ const providerName = "GCLOUD"
+ const providerMaintainer = "@riyadhalnur"
fns := providers.DspFuncs{
Initializer: New,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("GCLOUD", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
type gcloudProvider struct {
@@ -60,9 +68,6 @@ type gcloudProvider struct {
project string
nameServerSet *string
zones map[string]*gdns.ManagedZone
- // For use with diff / NewComnpat()
- oldRRsMap map[string]map[key]*gdns.ResourceRecordSet
- zoneNameMap map[string]string
// provider metadata fields
Visibility string `json:"visibility"`
Networks []string `json:"networks"`
@@ -112,8 +117,6 @@ func New(cfg map[string]string, metadata json.RawMessage) (providers.DNSServiceP
client: dcli,
nameServerSet: nss,
project: cfg["project_id"],
- oldRRsMap: map[string]map[key]*gdns.ResourceRecordSet{},
- zoneNameMap: map[string]string{},
}
if len(metadata) != 0 {
err := json.Unmarshal(metadata, g)
@@ -207,9 +210,6 @@ type key struct {
func keyFor(r *gdns.ResourceRecordSet) key {
return key{Type: r.Type, Name: r.Name}
}
-func keyForRec(r *models.RecordConfig) key {
- return key{Type: r.Type, Name: r.GetLabelFQDN() + "."}
-}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (g *gcloudProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
@@ -218,7 +218,7 @@ func (g *gcloudProvider) GetZoneRecords(domain string, meta map[string]string) (
}
func (g *gcloudProvider) getZoneSets(domain string) (models.Records, error) {
- rrs, zoneName, err := g.getRecords(domain)
+ rrs, err := g.getRecords(domain)
if err != nil {
return nil, err
}
@@ -237,165 +237,168 @@ func (g *gcloudProvider) getZoneSets(domain string) (models.Records, error) {
}
}
- g.oldRRsMap[domain] = oldRRs
- g.zoneNameMap[domain] = zoneName
-
return existingRecords, err
}
-type msgs struct {
- Additions, Deletions []string
-}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (g *gcloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
-type orderedChanges struct {
- Change *gdns.Change
- Msgs msgs
-}
+ changes, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil)
+ if err != nil {
+ return nil, 0, err
+ }
+ if len(changes) == 0 {
+ return nil, 0, nil
+ }
+
+ var corrections []*models.Correction
+ batch := &gdns.Change{Kind: "dns#change"}
+ var accumlatedMsgs []string
+ var newMsgs []string
+ var newAdds, newDels *gdns.ResourceRecordSet
+
+ for _, change := range changes {
+
+ // Determine the work to be done.
+ n := change.Key.NameFQDN + "."
+ ty := change.Key.Type
+ switch change.Type {
+ case diff2.REPORT:
+ newMsgs = change.Msgs
+ newAdds = nil
+ newDels = nil
+ case diff2.CREATE:
+ newMsgs = change.Msgs
+ newAdds = mkRRSs(n, ty, change.New)
+ newDels = nil
+ case diff2.CHANGE:
+ newMsgs = change.Msgs
+ newAdds = mkRRSs(n, ty, change.New)
+ newDels = change.Old[0].Original.(*gdns.ResourceRecordSet)
+ case diff2.DELETE:
+ newMsgs = change.Msgs
+ newAdds = nil
+ newDels = change.Old[0].Original.(*gdns.ResourceRecordSet)
+ default:
+ return nil, 0, fmt.Errorf("GCLOUD unhandled change.TYPE %s", change.Type)
+ }
-type correctionValues struct {
- Change *gdns.Change
- Msgs string
-}
+ // If the work would overflow the current batch, process what we have so far and start a new batch.
+ if wouldOverfill(batch, newAdds, newDels) {
+ // Process what we have.
+ corrections = g.mkCorrection(corrections, accumlatedMsgs, batch, dc.Name)
-// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (g *gcloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+ // Start a new batch.
+ batch = &gdns.Change{Kind: "dns#change"}
+ accumlatedMsgs = nil
+ }
+
+ // Add the new work to the batch.
+ if newAdds != nil {
+ batch.Additions = append(batch.Additions, newAdds)
+ }
+ if newDels != nil {
+ batch.Deletions = append(batch.Deletions, newDels)
+ }
+ if len(newMsgs) != 0 {
+ accumlatedMsgs = append(accumlatedMsgs, newMsgs...)
+ }
- oldRRs, ok := g.oldRRsMap[dc.Name]
- if !ok {
- return nil, fmt.Errorf("oldRRsMap: no zone named %q", dc.Name)
}
- zoneName, ok := g.zoneNameMap[dc.Name]
- if !ok {
- return nil, fmt.Errorf("zoneNameMap: no zone named %q", dc.Name)
+
+ // Process the remaining work.
+ corrections = g.mkCorrection(corrections, accumlatedMsgs, batch, dc.Name)
+ return corrections, actualChangeCount, nil
+}
+
+// mkRRSs returns a gdns.ResourceRecordSet using the name, rType, and recs
+func mkRRSs(name, rType string, recs models.Records) *gdns.ResourceRecordSet {
+ if len(recs) == 0 { // NB(tlim): This is defensive. mkRRSs is never called with an empty list.
+ return nil
}
- // first collect keys that have changed
- toReport, create, toDelete, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
- if err != nil {
- return nil, fmt.Errorf("incdiff error: %w", err)
+ newRRS := &gdns.ResourceRecordSet{
+ Name: name,
+ Type: rType,
+ Kind: "dns#resourceRecordSet",
+ Ttl: int64(recs[0].TTL), // diff2 assures all TTLs in a ReceordSet are the same.
+ }
+
+ for _, r := range recs {
+ newRRS.Rrdatas = append(newRRS.Rrdatas, r.GetTargetCombinedFunc(txtutil.EncodeQuoted))
}
- // Start corrections with the reports
- corrections := diff.GenerateMessageCorrections(toReport)
- // Now generate all other corrections
+ return newRRS
+}
+
+// wouldOverfill returns true if adding this work would overflow the batch.
+func wouldOverfill(batch *gdns.Change, adds, dels *gdns.ResourceRecordSet) bool {
+ const batchMax = 1000
+ // Google used to document batchMax = 1000. As of 2024-01 the max isn't
+ // documented but testing shows it rejects if either Additions or Deletions
+ // are >3000. Setting this to 3001 makes the batchRecordswithOthers
+ // integration test fail.
+ // It is currently set to 1000 because (1) its the last documented max,
+ // (2) changes of more than 1000 RSets is rare; we'd rather be correct and
+ // working than broken and efficient.
- changedKeys := map[key]string{}
- for _, c := range create {
- changedKeys[keyForRec(c.Desired)] = fmt.Sprintln(c)
+ addCount := 0
+ if adds != nil {
+ addCount = len(adds.Rrdatas)
}
- for _, d := range toDelete {
- changedKeys[keyForRec(d.Existing)] = fmt.Sprintln(d)
+ delCount := 0
+ if dels != nil {
+ delCount = len(dels.Rrdatas)
}
- for _, m := range modify {
- changedKeys[keyForRec(m.Existing)] = fmt.Sprintln(m)
+
+ if (len(batch.Additions) + addCount) > batchMax { // Would additions push us over the limit?
+ return true
}
- if len(changedKeys) == 0 {
- return nil, nil
+ if (len(batch.Deletions) + delCount) > batchMax { // Would deletions push us over the limit?
+ return true
}
- chg := orderedChanges{Change: &gdns.Change{}, Msgs: msgs{}}
- // create slices of Deletions and Additions
- // that can be split into properly ordered batches
- // if necessary. Retain the string messages from
- // differ in the same order
- for ck, msg := range changedKeys {
- newRRs := &gdns.ResourceRecordSet{
- Name: ck.Name,
- Type: ck.Type,
- Kind: "dns#resourceRecordSet",
- }
- for _, r := range dc.Records {
- if keyForRec(r) == ck {
- newRRs.Rrdatas = append(newRRs.Rrdatas, r.GetTargetCombined())
- newRRs.Ttl = int64(r.TTL)
- }
- }
- if len(newRRs.Rrdatas) > 0 {
- // if we have Rrdatas because the key from differ
- // exists in normalized config,
- // check whether the key also has data in oldRRs.
- // if so, this is actually a modify operation, insert
- // the Addition and Deletion at the beginning of the slices
- // to ensure they are executed in the same batch
- if old, ok := oldRRs[ck]; ok {
- chg.Change.Additions = append([]*gdns.ResourceRecordSet{newRRs}, chg.Change.Additions...)
- chg.Change.Deletions = append([]*gdns.ResourceRecordSet{old}, chg.Change.Deletions...)
- chg.Msgs.Additions = append([]string{msg}, chg.Msgs.Additions...)
- chg.Msgs.Deletions = append([]string{""}, chg.Msgs.Deletions...)
- } else {
- // otherwise this is a pure Addition
- chg.Change.Additions = append(chg.Change.Additions, newRRs)
- chg.Msgs.Additions = append(chg.Msgs.Additions, msg)
- }
- } else {
- // there is no Rrdatas from normalized config for this key.
- // it must be a Deletion, use the ResourceRecordSet from
- // oldRRs
- if old, ok := oldRRs[ck]; ok {
- chg.Change.Deletions = append(chg.Change.Deletions, old)
- chg.Msgs.Deletions = append(chg.Msgs.Deletions, msg)
- }
- }
+ return false
+}
+
+func (g *gcloudProvider) mkCorrection(corrections []*models.Correction, accumulatedMsgs []string, batch *gdns.Change, origin string) []*models.Correction {
+ if len(accumulatedMsgs) == 0 && len(batch.Additions) == 0 && len(batch.Deletions) == 0 {
+ // Nothing to do!
+ return corrections
}
- // create a slice of Changes in batches of at most
- // 1000 Deletions and 1000 Additions per Change.
- // create a slice of strings that aligns with the batch
- // to output with each correction/Change
- const batchMax = 1000
- setBatchLen := func(len int) int {
- if len > batchMax {
- return batchMax
- }
- return len
- }
- chgSet := []correctionValues{}
- for len(chg.Change.Deletions) > 0 {
- b := setBatchLen(len(chg.Change.Deletions))
- chgSet = append(chgSet, correctionValues{Change: &gdns.Change{Deletions: chg.Change.Deletions[:b:b], Kind: "dns#change"}, Msgs: "\n" + strings.Join(chg.Msgs.Deletions[:b:b], "")})
- chg.Change.Deletions = chg.Change.Deletions[b:]
- chg.Msgs.Deletions = chg.Msgs.Deletions[b:]
- }
- for i := 0; len(chg.Change.Additions) > 0; i++ {
- b := setBatchLen(len(chg.Change.Additions))
- if len(chgSet) == i {
- chgSet = append(chgSet, correctionValues{Change: &gdns.Change{Additions: chg.Change.Additions[:b:b], Kind: "dns#change"}, Msgs: "\n" + strings.Join(chg.Msgs.Additions[:b:b], "")})
- } else {
- chgSet[i].Change.Additions = chg.Change.Additions[:b:b]
- chgSet[i].Msgs += strings.Join(chg.Msgs.Additions[:b:b], "")
- }
- chg.Change.Additions = chg.Change.Additions[b:]
- chg.Msgs.Additions = chg.Msgs.Additions[b:]
- }
- // create a Correction for each gdns.Change
- // that needs to be executed
- makeCorrection := func(chg *gdns.Change, msgs string) {
- runChange := func() error {
- retry:
- resp, err := g.client.Changes.Create(g.project, zoneName, chg).Do()
- var check *googleapi.ServerResponse
- if resp != nil {
- check = &resp.ServerResponse
- }
- if retryNeeded(check, err) {
- goto retry
- }
- if err != nil {
- return fmt.Errorf("runChange error: %w", err)
- }
- return nil
- }
- corrections = append(corrections,
- &models.Correction{
- Msg: strings.TrimSuffix(msgs, "\n"),
- F: runChange,
- })
+ corr := &models.Correction{}
+ if len(accumulatedMsgs) != 0 {
+ corr.Msg = strings.Join(accumulatedMsgs, "\n")
}
- for _, v := range chgSet {
- makeCorrection(v.Change, v.Msgs)
+ if (len(batch.Additions) + len(batch.Deletions)) != 0 {
+ corr.F = func() error { return g.process(origin, batch) }
}
- return corrections, nil
+ corrections = append(corrections, corr)
+ return corrections
+}
+
+// process calls the Google DNS API to process a Change and re-tries if needed.
+func (g *gcloudProvider) process(origin string, batch *gdns.Change) error {
+
+ zoneName, err := g.getZone(origin)
+ if err != nil || zoneName == nil {
+ return fmt.Errorf("zoneNameMap: no zone named %q", origin)
+ }
+
+retry:
+ resp, err := g.client.Changes.Create(g.project, zoneName.Name, batch).Do()
+ var check *googleapi.ServerResponse
+ if resp != nil {
+ check = &resp.ServerResponse
+ }
+ if retryNeeded(check, err) {
+ goto retry
+ }
+ if err != nil {
+ return fmt.Errorf("runChange error: %w", err)
+ }
+ return nil
}
func nativeToRecord(set *gdns.ResourceRecordSet, rec, origin string) (*models.RecordConfig, error) {
@@ -403,23 +406,18 @@ func nativeToRecord(set *gdns.ResourceRecordSet, rec, origin string) (*models.Re
r.SetLabelFromFQDN(set.Name, origin)
r.TTL = uint32(set.Ttl)
rtype := set.Type
- var err error
- switch rtype {
- case "TXT":
- err = r.SetTargetTXTs(models.ParseQuotedTxt(rec))
- default:
- err = r.PopulateFromString(rtype, rec, origin)
- }
+ r.Original = set
+ err := r.PopulateFromStringFunc(rtype, rec, origin, txtutil.ParseQuoted)
if err != nil {
return nil, fmt.Errorf("unparsable record %q received from GCLOUD: %w", rtype, err)
}
return r, nil
}
-func (g *gcloudProvider) getRecords(domain string) ([]*gdns.ResourceRecordSet, string, error) {
+func (g *gcloudProvider) getRecords(domain string) ([]*gdns.ResourceRecordSet, error) {
zone, err := g.getZone(domain)
if err != nil {
- return nil, "", err
+ return nil, err
}
pageToken := ""
sets := []*gdns.ResourceRecordSet{}
@@ -438,7 +436,7 @@ func (g *gcloudProvider) getRecords(domain string) ([]*gdns.ResourceRecordSet, s
goto retry
}
if err != nil {
- return nil, "", err
+ return nil, err
}
for _, rrs := range resp.Rrsets {
if rrs.Type == "SOA" {
@@ -450,7 +448,7 @@ func (g *gcloudProvider) getRecords(domain string) ([]*gdns.ResourceRecordSet, s
break
}
}
- return sets, zone.Name, nil
+ return sets, nil
}
func (g *gcloudProvider) EnsureZoneExists(domain string) error {
@@ -481,7 +479,7 @@ func (g *gcloudProvider) EnsureZoneExists(domain string) error {
mz.Name = strings.Replace(mz.Name, "zone-", "zone-"+g.Visibility+"-", 1)
}
if g.Networks != nil {
- mzn := make([]*gdns.ManagedZonePrivateVisibilityConfigNetwork, len(g.Networks))
+ mzn := make([]*gdns.ManagedZonePrivateVisibilityConfigNetwork, 0, len(g.Networks))
printer.Printf("for network(s) ")
for _, v := range g.Networks {
printer.Printf("%s ", v)
diff --git a/providers/gcore/convert.go b/providers/gcore/convert.go
index f3e4ebb5a3..d4b3731b6d 100644
--- a/providers/gcore/convert.go
+++ b/providers/gcore/convert.go
@@ -19,9 +19,14 @@ func nativeToRecords(n gcoreRRSetExtended, zoneName string) ([]*models.RecordCon
// Split G-Core's RRset into individual records
for _, value := range n.Records {
+ metadata, err := nativeMetadataToRecords(&n, &value)
+ if err != nil {
+ return nil, fmt.Errorf("unparsable record received from G-Core: %w", err)
+ }
rc := &models.RecordConfig{
TTL: uint32(n.TTL),
Original: n,
+ Metadata: metadata,
}
rc.SetLabelFromFQDN(recName, zoneName)
switch recType {
@@ -45,6 +50,11 @@ func nativeToRecords(n gcoreRRSetExtended, zoneName string) ([]*models.RecordCon
return nil, fmt.Errorf("unparsable record received from G-Core: %w", err)
}
+ case "SCVB": // GCore mistypes "SVCB" as "SCVB"
+ if err := rc.PopulateFromString("SVCB", value.ContentToString(), zoneName); err != nil {
+ return nil, fmt.Errorf("unparsable record received from G-Core: %w", err)
+ }
+
default: // "A", "AAAA", "CAA", "NS", "CNAME", "MX", "PTR", "SRV"
if err := rc.PopulateFromString(recType, value.ContentToString(), zoneName); err != nil {
return nil, fmt.Errorf("unparsable record received from G-Core: %w", err)
@@ -56,10 +66,13 @@ func nativeToRecords(n gcoreRRSetExtended, zoneName string) ([]*models.RecordCon
return rcs, nil
}
-func recordsToNative(rcs []*models.RecordConfig, expectedKey models.RecordKey) *dnssdk.RRSet {
+func recordsToNative(rcs []*models.RecordConfig, expectedKey models.RecordKey) (*dnssdk.RRSet, error) {
// Merge DNSControl records into G-Core RRsets
var result *dnssdk.RRSet
+ var resultRRSetFilters []dnssdk.RecordFilter = nil
+ var resultRRSetMeta map[string]any = nil
+ var resultRRSetMetaSourceRecord *models.RecordConfig = nil
for _, r := range rcs {
label := r.GetLabel()
@@ -72,6 +85,33 @@ func recordsToNative(rcs []*models.RecordConfig, expectedKey models.RecordKey) *
continue
}
+ rrsetFilters, rrsetMeta, recordMeta, err := recordsMetadataToNative(r.Metadata)
+ if err != nil {
+ return nil, err
+ }
+
+ if resultRRSetMeta == nil {
+ resultRRSetFilters = rrsetFilters
+ resultRRSetMeta = rrsetMeta
+ resultRRSetMetaSourceRecord = r
+ } else {
+ isRRSetFilterEqual, err := isListStructEqual(resultRRSetFilters, rrsetFilters)
+ if err != nil {
+ return nil, err
+ }
+ if !isRRSetFilterEqual {
+ return nil, fmt.Errorf("filter is not consistent between %s and %s in RRSet %s", resultRRSetMetaSourceRecord, r, expectedKey)
+ }
+
+ isRRSetMetaEqual, err := isStructEqual(resultRRSetMeta, rrsetMeta)
+ if err != nil {
+ return nil, err
+ }
+ if !isRRSetMetaEqual {
+ return nil, fmt.Errorf("metadata is not consistent between %s and %s in RRSet %s", resultRRSetMetaSourceRecord, r, expectedKey)
+ }
+ }
+
var rr dnssdk.ResourceRecord
switch key.Type {
case "CAA": // G-Core API don't need quotes around CAA with whitespace
@@ -81,19 +121,26 @@ func recordsToNative(rcs []*models.RecordConfig, expectedKey models.RecordKey) *
r.CaaTag,
r.GetTargetField(),
},
- Meta: nil,
+ Meta: recordMeta,
Enabled: true,
}
case "TXT": // Avoid double quoting for TXT records
rr = dnssdk.ResourceRecord{
- Content: convertTxtSliceToSdkAnySlice(r.TxtStrings),
- Meta: nil,
+ Content: convertTxtSliceToSdkAnySlice(r.GetTargetTXTJoined()),
+ Meta: recordMeta,
+ Enabled: true,
+ }
+ case "SVCB":
+ // GCore mistypes "SVCB" as "SCVB"
+ rr = dnssdk.ResourceRecord{
+ Content: dnssdk.ContentFromValue("SCVB", r.GetTargetCombined()),
+ Meta: recordMeta,
Enabled: true,
}
default:
rr = dnssdk.ResourceRecord{
Content: dnssdk.ContentFromValue(key.Type, r.GetTargetCombined()),
- Meta: nil,
+ Meta: recordMeta,
Enabled: true,
}
}
@@ -101,7 +148,6 @@ func recordsToNative(rcs []*models.RecordConfig, expectedKey models.RecordKey) *
if result == nil {
result = &dnssdk.RRSet{
TTL: int(r.TTL),
- Filters: nil,
Records: []dnssdk.ResourceRecord{rr},
}
} else {
@@ -116,14 +162,16 @@ func recordsToNative(rcs []*models.RecordConfig, expectedKey models.RecordKey) *
}
}
- return result
+ if result != nil {
+ result.Filters = resultRRSetFilters
+ result.Meta = resultRRSetMeta
+ }
+
+ return result, nil
}
-func convertTxtSliceToSdkAnySlice(records []string) []any {
- result := []any{}
- for _, record := range records {
- result = append(result, record)
- }
+func convertTxtSliceToSdkAnySlice(record string) []any {
+ result := []any{record}
return result
}
diff --git a/providers/gcore/convertMetadata.go b/providers/gcore/convertMetadata.go
new file mode 100644
index 0000000000..1b940e2675
--- /dev/null
+++ b/providers/gcore/convertMetadata.go
@@ -0,0 +1,402 @@
+package gcore
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+
+ dnssdk "github.com/G-Core/gcore-dns-sdk-go"
+)
+
+type gcoreFailoverMetadata struct {
+ Protocol *string `json:"protocol,omitempty"`
+ Port *int `json:"port,omitempty"`
+ Frequency *int `json:"frequency,omitempty"`
+ Timeout *int `json:"timeout,omitempty"`
+ Method *string `json:"method,omitempty"`
+ Command *string `json:"command,omitempty"`
+ URL *string `json:"url,omitempty"`
+ TLS *bool `json:"tls,omitempty"`
+ RegExp *string `json:"regexp,omitempty"`
+ HTTPStatusCode *int `json:"http_status_code,omitempty"`
+ Host *string `json:"host,omitempty"`
+}
+
+type gcoreMetadata struct {
+ // These fields only apply to record metadata
+ ASN []int `json:"asn,omitempty"`
+ Continents []string `json:"continents,omitempty"`
+ Countries []string `json:"countries,omitempty"`
+ LatLong *[2]float64 `json:"latlong,omitempty"`
+ Fallback *bool `json:"fallback,omitempty"`
+ Backup *bool `json:"backup,omitempty"`
+ Notes *string `json:"notes,omitempty"`
+ Weight *float64 `json:"weight,omitempty"`
+ IP []string `json:"ip,omitempty"`
+ // Failover only applies to RRSet metadata
+ Failover *gcoreFailoverMetadata `json:"failover,omitempty"`
+}
+
+func nativeMetadataToRecords(rrset *gcoreRRSetExtended, rec *dnssdk.ResourceRecord) (map[string]string, error) {
+ result := map[string]string{}
+
+ if len(rrset.Filters) > 0 {
+ result[metaFilters] = serializeRecordFilter(rrset.Filters)
+ }
+
+ // RRSet only supports failover field
+ if rrset.Meta != nil && rrset.Meta.Failover != nil {
+ failover := *rrset.Meta.Failover
+ if failover.Protocol != nil {
+ result[metaFailoverProtocol] = *failover.Protocol
+ }
+ if failover.Port != nil {
+ result[metaFailoverPort] = strconv.Itoa(*failover.Port)
+ }
+ if failover.Frequency != nil {
+ result[metaFailoverFrequency] = strconv.Itoa(*failover.Frequency)
+ }
+ if failover.Timeout != nil {
+ result[metaFailoverTimeout] = strconv.Itoa(*failover.Timeout)
+ }
+ if failover.Method != nil {
+ result[metaFailoverMethod] = *failover.Method
+ }
+ if failover.Command != nil {
+ result[metaFailoverCommand] = *failover.Command
+ }
+ if failover.URL != nil {
+ result[metaFailoverURL] = *failover.URL
+ }
+ if failover.TLS != nil {
+ result[metaFailoverTLS] = strconv.FormatBool(*failover.TLS)
+ }
+ if failover.RegExp != nil {
+ result[metaFailoverRegexp] = *failover.RegExp
+ }
+ if failover.HTTPStatusCode != nil {
+ result[metaFailoverHTTPStatusCode] = strconv.Itoa(*failover.HTTPStatusCode)
+ }
+ if failover.Host != nil {
+ result[metaFailoverHost] = *failover.Host
+ }
+ }
+
+ // ResourceRecord supports all fields except failover
+ if rec.Meta != nil {
+ // Convert GCore SDK's type to our metadata type
+ metaJSON, err := json.Marshal(rec.Meta)
+ if err != nil {
+ return nil, err
+ }
+
+ meta := gcoreMetadata{}
+ err = json.Unmarshal(metaJSON, &meta)
+ if err != nil {
+ return nil, err
+ }
+
+ if meta.ASN != nil {
+ // This is probably a ton of memory copies, but I cannot think of a better solution for now
+ asnArray := []string{}
+ for _, asn := range meta.ASN {
+ asnArray = append(asnArray, strconv.Itoa(asn))
+ }
+ result[metaASN] = strings.Join(asnArray, ",")
+ }
+ if meta.Continents != nil {
+ result[metaContinents] = strings.Join(meta.Continents, ",")
+ }
+ if meta.Countries != nil {
+ result[metaCountries] = strings.Join(meta.Countries, ",")
+ }
+ if meta.LatLong != nil {
+ result[metaLatitude] = strconv.FormatFloat(meta.LatLong[0], 'f', -1, 64)
+ result[metaLongitude] = strconv.FormatFloat(meta.LatLong[1], 'f', -1, 64)
+ }
+ if meta.Fallback != nil {
+ result[metaFallback] = strconv.FormatBool(*meta.Fallback)
+ }
+ if meta.Backup != nil {
+ result[metaBackup] = strconv.FormatBool(*meta.Backup)
+ }
+ if meta.Notes != nil {
+ result[metaNotes] = *meta.Notes
+ }
+ if meta.Weight != nil {
+ result[metaWeight] = strconv.FormatFloat(*meta.Weight, 'f', -1, 64)
+ }
+ if meta.IP != nil {
+ result[metaIP] = strings.Join(meta.IP, ",")
+ }
+ }
+
+ return result, nil
+}
+
+func recordsMetadataToNative(meta map[string]string) ([]dnssdk.RecordFilter, map[string]any, map[string]any, error) {
+ var rrsetFilters []dnssdk.RecordFilter = nil
+ rrsetMeta := gcoreMetadata{}
+ recordMeta := gcoreMetadata{}
+
+ for k, v := range meta {
+ // Copy string to avoid it later changed by other logic
+ vCopy := strings.Clone(v)
+ switch k {
+ case metaFilters:
+ var err error
+ rrsetFilters, err = parseRecordFilter(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ case metaFailoverProtocol:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ rrsetMeta.Failover.Protocol = &vCopy
+ case metaFailoverPort:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ value, err := strconv.Atoi(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ rrsetMeta.Failover.Port = &value
+ case metaFailoverFrequency:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ value, err := strconv.Atoi(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ rrsetMeta.Failover.Frequency = &value
+ case metaFailoverTimeout:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ value, err := strconv.Atoi(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ rrsetMeta.Failover.Timeout = &value
+ case metaFailoverMethod:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ rrsetMeta.Failover.Method = &vCopy
+ case metaFailoverCommand:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ rrsetMeta.Failover.Command = &vCopy
+ case metaFailoverURL:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ rrsetMeta.Failover.URL = &vCopy
+ case metaFailoverTLS:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ value, err := strconv.ParseBool(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ rrsetMeta.Failover.TLS = &value
+ case metaFailoverRegexp:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ rrsetMeta.Failover.RegExp = &vCopy
+ case metaFailoverHTTPStatusCode:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ value, err := strconv.Atoi(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ rrsetMeta.Failover.HTTPStatusCode = &value
+ case metaFailoverHost:
+ if rrsetMeta.Failover == nil {
+ rrsetMeta.Failover = &gcoreFailoverMetadata{}
+ }
+ rrsetMeta.Failover.Host = &vCopy
+ case metaASN:
+ // This is probably a ton of memory copies, but I cannot think of a better solution for now
+ asnArray := []int{}
+ for _, asn := range strings.Split(v, ",") {
+ if len(asn) > 0 {
+ value, err := strconv.Atoi(asn)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ asnArray = append(asnArray, value)
+ }
+ }
+ recordMeta.ASN = asnArray
+ case metaContinents:
+ continents := strings.Split(v, ",")
+ recordMeta.Continents = continents
+ case metaCountries:
+ countries := strings.Split(v, ",")
+ recordMeta.Countries = countries
+ case metaLatitude:
+ value, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ if recordMeta.LatLong == nil {
+ recordMeta.LatLong = &[2]float64{0, 0}
+ }
+ recordMeta.LatLong[0] = value
+ case metaLongitude:
+ value, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ if recordMeta.LatLong == nil {
+ recordMeta.LatLong = &[2]float64{0, 0}
+ }
+ recordMeta.LatLong[1] = value
+ case metaFallback:
+ value, err := strconv.ParseBool(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ recordMeta.Fallback = &value
+ case metaBackup:
+ value, err := strconv.ParseBool(v)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ recordMeta.Backup = &value
+ case metaNotes:
+ recordMeta.Notes = &vCopy
+ case metaWeight:
+ value, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ recordMeta.Weight = &value
+ case metaIP:
+ ips := strings.Split(v, ",")
+ recordMeta.IP = ips
+ }
+ }
+
+ // Convert metadata to the map[string]any type SDK wants
+ rrsetMetaSDK, err := convertToDnssdkMeta(rrsetMeta)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ recordMetaSDK, err := convertToDnssdkMeta(recordMeta)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ return rrsetFilters, rrsetMetaSDK, recordMetaSDK, nil
+}
+
+func serializeRecordFilter(f []dnssdk.RecordFilter) string {
+ result := []string{}
+
+ for _, v := range f {
+ filterString := v.Type
+
+ if v.Strict {
+ filterString += ",true"
+ } else {
+ filterString += ",false"
+ }
+
+ if v.Limit != 0 {
+ filterString += "," + strconv.Itoa(int(v.Limit))
+ }
+
+ result = append(result, filterString)
+ }
+
+ return strings.Join(result, ";")
+}
+
+func parseRecordFilter(filterString string) ([]dnssdk.RecordFilter, error) {
+ result := []dnssdk.RecordFilter{}
+
+ for _, s := range strings.Split(filterString, ";") {
+ var err error
+
+ fields := strings.Split(s, ",")
+ if len(fields) < 2 || len(fields) > 3 {
+ return nil, fmt.Errorf("filter %s has invalid format, correct format is \"type,strict[,limit]\"", s)
+ }
+
+ record := dnssdk.RecordFilter{}
+ record.Type = fields[0]
+
+ record.Strict, err = strconv.ParseBool(fields[1])
+ if err != nil {
+ return nil, err
+ }
+
+ if len(fields) == 3 {
+ limit, err := strconv.Atoi(fields[2])
+ if err != nil {
+ return nil, err
+ }
+ record.Limit = uint(limit)
+ }
+
+ result = append(result, record)
+ }
+
+ return result, nil
+}
+
+func isStructEqual[T any](a T, b T) (bool, error) {
+ aJSON, err := json.Marshal(a)
+ if err != nil {
+ return false, err
+ }
+ bJSON, err := json.Marshal(b)
+ if err != nil {
+ return false, err
+ }
+ return bytes.Equal(aJSON, bJSON), nil
+}
+
+func isListStructEqual[T any](a []T, b []T) (bool, error) {
+ if len(a) != len(b) {
+ return true, nil
+ }
+
+ for i := 0; i < len(a); i++ {
+ result, err := isStructEqual(a[i], b[i])
+ if err != nil {
+ return false, err
+ }
+
+ if !result {
+ return false, nil
+ }
+ }
+
+ return true, nil
+}
+
+func convertToDnssdkMeta(v any) (map[string]any, error) {
+ marshaled, err := json.Marshal(v)
+ if err != nil {
+ return nil, err
+ }
+ result := map[string]any{}
+ err = json.Unmarshal(marshaled, &result)
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+}
diff --git a/providers/gcore/gcoreExtend.go b/providers/gcore/gcoreExtend.go
index b0bfa43d7e..18a1fc7cb7 100644
--- a/providers/gcore/gcoreExtend.go
+++ b/providers/gcore/gcoreExtend.go
@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"log"
"net/http"
"path"
@@ -13,6 +13,14 @@ import (
dnssdk "github.com/G-Core/gcore-dns-sdk-go"
)
+type gcoreZone struct {
+ DNSSECEnabled bool `json:"dnssec_enabled"`
+}
+
+type gcoreDNSSECRequest struct {
+ Enabled bool `json:"enabled"`
+}
+
type gcoreRRSets struct {
RRSets []gcoreRRSetExtended `json:"rrsets"`
}
@@ -26,6 +34,7 @@ type gcoreRRSetExtended struct {
TTL int `json:"ttl"`
Records []dnssdk.ResourceRecord `json:"resource_records"`
Filters []dnssdk.RecordFilter `json:"filters"`
+ Meta *gcoreMetadata `json:"meta"`
}
func dnssdkDo(ctx context.Context, c *dnssdk.Client, apiKey string, method, uri string, bodyParams interface{}, dest interface{}) error {
@@ -69,7 +78,7 @@ func dnssdkDo(ctx context.Context, c *dnssdk.Client, apiKey string, method, uri
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= http.StatusMultipleChoices {
- all, _ := ioutil.ReadAll(resp.Body)
+ all, _ := io.ReadAll(resp.Body)
e := dnssdk.APIError{
StatusCode: resp.StatusCode,
}
@@ -103,3 +112,29 @@ func (c *gcoreProvider) dnssdkRRSets(domain string) (gcoreRRSets, error) {
return result, nil
}
+
+func (c *gcoreProvider) dnssdkGetDNSSEC(domain string) (bool, error) {
+ var result gcoreZone
+ url := fmt.Sprintf("/v2/zones/%s", domain)
+
+ err := dnssdkDo(c.ctx, c.provider, c.apiKey, http.MethodGet, url, nil, &result)
+ if err != nil {
+ return false, err
+ }
+
+ return result.DNSSECEnabled, nil
+}
+
+func (c *gcoreProvider) dnssdkSetDNSSEC(domain string, enabled bool) error {
+ var request gcoreDNSSECRequest
+ request.Enabled = enabled
+
+ url := fmt.Sprintf("/v2/zones/%s/dnssec", domain)
+
+ err := dnssdkDo(c.ctx, c.provider, c.apiKey, http.MethodPatch, url, request, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/providers/gcore/gcoreProvider.go b/providers/gcore/gcoreProvider.go
index 81d2fceb8e..3548635f11 100644
--- a/providers/gcore/gcoreProvider.go
+++ b/providers/gcore/gcoreProvider.go
@@ -8,6 +8,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/providers"
dnssdk "github.com/G-Core/gcore-dns-sdk-go"
@@ -41,17 +42,22 @@ func NewGCore(m map[string]string, metadata json.RawMessage) (providers.DNSServi
}
var features = providers.DocumentationNotes{
- providers.CanAutoDNSSEC: providers.Cannot(),
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
- providers.CanUseAlias: providers.Cannot(),
+ providers.CanConcur: providers.Cannot(),
+ providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Cannot(),
- providers.CanUsePTR: providers.Cannot(),
+ providers.CanUsePTR: providers.Can("G-Core supports PTR records only in rDNS zones"),
providers.CanUseSRV: providers.Can("G-Core doesn't support SRV records with empty targets"),
providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
+ providers.CanUseHTTPS: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
@@ -63,11 +69,14 @@ var defaultNameServerNames = []string{
}
func init() {
+ const providerName = "GCORE"
+ const providerMaintainer = "@xddxdd"
fns := providers.DspFuncs{
Initializer: NewGCore,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("GCORE", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// GetNameservers returns the nameservers for a domain.
@@ -129,20 +138,30 @@ func generateChangeMsg(updates []string) string {
// a list of functions to call to actually make the desired
// correction, and a message to output to the user when the change is
// made.
-func (c *gcoreProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+func (c *gcoreProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
// Make delete happen earlier than creates & updates.
var corrections []*models.Correction
var deletions []*models.Correction
var reports []*models.Correction
- changes, err := diff2.ByRecordSet(existing, dc, nil)
+ // Gcore auto uses ALIAS for apex zone CNAME records, just like CloudFlare
+ for _, rec := range dc.Records {
+ if rec.Type == "ALIAS" {
+ rec.Type = "CNAME"
+ }
+ }
+
+ changes, actualChangeCount, err := diff2.ByRecordSet(existing, dc, comparableFunc)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, change := range changes {
- record := recordsToNative(change.New, change.Key)
+ record, err := recordsToNative(change.New, change.Key)
+ if err != nil {
+ return nil, 0, err
+ }
// Copy all params to avoid overwrites
zone := dc.Name
@@ -179,7 +198,48 @@ func (c *gcoreProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exist
}
}
+ dnssecEnabled, err := c.dnssdkGetDNSSEC(dc.Name)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ if !dnssecEnabled && dc.AutoDNSSEC == "on" {
+ // Copy all params to avoid overwrites
+ zone := dc.Name
+ corrections = append(corrections, &models.Correction{
+ Msg: "Enable DNSSEC",
+ F: func() error {
+ return c.dnssdkSetDNSSEC(zone, true)
+ },
+ })
+ } else if dnssecEnabled && dc.AutoDNSSEC == "off" {
+ // Copy all params to avoid overwrites
+ zone := dc.Name
+ corrections = append(corrections, &models.Correction{
+ Msg: "Disable DNSSEC",
+ F: func() error {
+ return c.dnssdkSetDNSSEC(zone, false)
+ },
+ })
+ }
+
result := append(reports, deletions...)
result = append(result, corrections...)
- return result, nil
+ return result, actualChangeCount, nil
+}
+
+func comparableFunc(rec *models.RecordConfig) string {
+ if len(rec.Metadata) == 0 {
+ return ""
+ }
+
+ // json.Marshal always serialize all fields in alphabetical order,
+ // so the metadata string will be consistent between runs
+ result, err := json.Marshal(rec.Metadata)
+ if err != nil {
+ printer.Warnf("Cannot serialize metadata of record %s", rec)
+ return ""
+ }
+
+ return string(result)
}
diff --git a/providers/gcore/metaConstants.go b/providers/gcore/metaConstants.go
new file mode 100644
index 0000000000..0da384d647
--- /dev/null
+++ b/providers/gcore/metaConstants.go
@@ -0,0 +1,31 @@
+package gcore
+
+const (
+ // Applies to RRSet
+ metaFilters = "gcore_filters"
+
+ // Only applies to RRSet metadata
+ metaFailoverProtocol = "gcore_failover_protocol"
+ metaFailoverPort = "gcore_failover_port"
+ metaFailoverFrequency = "gcore_failover_frequency"
+ metaFailoverTimeout = "gcore_failover_timeout"
+ metaFailoverMethod = "gcore_failover_method"
+ metaFailoverCommand = "gcore_failover_command"
+ metaFailoverURL = "gcore_failover_url"
+ metaFailoverTLS = "gcore_failover_tls"
+ metaFailoverRegexp = "gcore_failover_regexp"
+ metaFailoverHTTPStatusCode = "gcore_failover_http_status_code"
+ metaFailoverHost = "gcore_failover_host"
+
+ // Only applies to record metadata
+ metaASN = "gcore_asn"
+ metaContinents = "gcore_continents"
+ metaCountries = "gcore_countries"
+ metaLatitude = "gcore_latitude"
+ metaLongitude = "gcore_longitude"
+ metaFallback = "gcore_fallback"
+ metaBackup = "gcore_backup"
+ metaNotes = "gcore_notes"
+ metaWeight = "gcore_weight"
+ metaIP = "gcore_ip"
+)
diff --git a/providers/hedns/hednsProvider.go b/providers/hedns/hednsProvider.go
index 0ee6d14855..5db74bc1e6 100644
--- a/providers/hedns/hednsProvider.go
+++ b/providers/hedns/hednsProvider.go
@@ -3,6 +3,7 @@ package hedns
import (
"crypto/sha1"
"encoding/json"
+ "errors"
"fmt"
"net/http"
"net/http/cookiejar"
@@ -42,18 +43,23 @@ Additionally
*/
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseDSForChildren: providers.Cannot(),
+ providers.CanUseHTTPS: providers.Can(),
providers.CanUseLOC: providers.Can(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSOA: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.CanUseTLSA: providers.Cannot(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
@@ -61,11 +67,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "HEDNS"
+ const providerMaintainer = "@rblenkinsopp"
fns := providers.DspFuncs{
Initializer: newHEDNSProvider,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("HEDNS", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
var defaultNameservers = []string{
@@ -112,13 +121,13 @@ func newHEDNSProvider(cfg map[string]string, _ json.RawMessage) (providers.DNSSe
sessionFilePath := cfg["session-file-path"]
if username == "" {
- return nil, fmt.Errorf("username must be provided")
+ return nil, errors.New("username must be provided")
}
if password == "" {
- return nil, fmt.Errorf("password must be provided")
+ return nil, errors.New("password must be provided")
}
if totpSecret != "" && totpValue != "" {
- return nil, fmt.Errorf("totp and totp-key must not be specified at the same time")
+ return nil, errors.New("totp and totp-key must not be specified at the same time")
}
// Perform the initial login
@@ -178,7 +187,7 @@ func (c *hednsProvider) GetNameservers(_ string) ([]*models.Nameserver, error) {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *hednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+func (c *hednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, int, error) {
// Get the SOA record to get the ZoneID, then remove it from the list.
zoneID := uint64(0)
@@ -191,16 +200,13 @@ func (c *hednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, recor
}
}
- // Normalize
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
return c.getDiff2DomainCorrections(dc, zoneID, prunedRecords)
}
-func (c *hednsProvider) getDiff2DomainCorrections(dc *models.DomainConfig, zoneID uint64, records models.Records) ([]*models.Correction, error) {
- changes, err := diff2.ByRecord(records, dc, nil)
+func (c *hednsProvider) getDiff2DomainCorrections(dc *models.DomainConfig, zoneID uint64, records models.Records) ([]*models.Correction, int, error) {
+ changes, actualChangeCount, err := diff2.ByRecord(records, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
var corrections []*models.Correction
@@ -238,7 +244,7 @@ func (c *hednsProvider) getDiff2DomainCorrections(dc *models.DomainConfig, zoneI
}
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// GetZoneRecords returns all the records for the given domain
@@ -277,7 +283,7 @@ func (c *hednsProvider) GetZoneRecords(domain string, meta map[string]string) (m
// Check we can find the zone records
if document.Find("#dns_main_content").Size() == 0 {
- return nil, fmt.Errorf("zone records listing failed")
+ return nil, errors.New("zone records listing failed")
}
// Load all the domain records
@@ -325,10 +331,8 @@ func (c *hednsProvider) GetZoneRecords(domain string, meta map[string]string) (m
// Convert to TXT record as SPF is deprecated
rc.Type = "TXT"
fallthrough
- case "TXT":
- err = rc.SetTargetTXTs(models.ParseQuotedTxt(data))
default:
- err = rc.PopulateFromString(rc.Type, data, domain)
+ err = rc.PopulateFromStringFunc(rc.Type, data, domain, txtutil.ParseQuoted)
}
if err != nil {
@@ -380,7 +384,7 @@ func (c *hednsProvider) authUsernameAndPassword() (authenticated bool, requiresT
document, err := c.parseResponseForDocumentAndErrors(response)
if err != nil {
if err.Error() == errorInvalidCredentials {
- err = fmt.Errorf("authentication failed with incorrect username or password")
+ err = errors.New("authentication failed with incorrect username or password")
}
if err.Error() == errorTotpTokenRequired {
return false, true, nil
@@ -398,7 +402,7 @@ func (c *hednsProvider) authUsernameAndPassword() (authenticated bool, requiresT
func (c *hednsProvider) auth2FA() (authenticated bool, err error) {
if c.TfaValue == "" && c.TfaSecret == "" {
- return false, fmt.Errorf("account requires two-factor authentication but neither totp or totp-key were provided")
+ return false, errors.New("account requires two-factor authentication but neither totp or totp-key were provided")
}
if c.TfaValue == "" && c.TfaSecret != "" {
@@ -422,9 +426,9 @@ func (c *hednsProvider) auth2FA() (authenticated bool, err error) {
if err != nil {
switch err.Error() {
case errorInvalidTotpToken:
- err = fmt.Errorf("invalid TOTP token value")
+ err = errors.New("invalid TOTP token value")
case errorTotpTokenReused:
- err = fmt.Errorf("TOTP token was reused within its period (30 seconds)")
+ err = errors.New("TOTP token was reused within its period (30 seconds)")
}
return false, err
}
@@ -463,7 +467,7 @@ func (c *hednsProvider) authenticate() error {
}
if !authenticated {
- err = fmt.Errorf("unknown authentication failure")
+ err = errors.New("unknown authentication failure")
} else {
if c.SessionFilePath != "" {
err = c.saveSessionFile()
@@ -563,7 +567,7 @@ func (c *hednsProvider) editZoneRecord(zoneID uint64, recordID uint64, rc *model
values.Set("Weight", strconv.FormatUint(uint64(rc.SrvWeight), 10))
values.Set("Port", strconv.FormatUint(uint64(rc.SrvPort), 10))
default:
- values.Set("Content", rc.GetTargetCombined())
+ values.Set("Content", rc.GetTargetCombinedFunc(txtutil.EncodeQuoted))
}
response, err := c.httpClient.PostForm(apiEndpoint, values)
@@ -652,7 +656,7 @@ func (c *hednsProvider) loadSessionFile() error {
for i, entry := range strings.Split(string(bytes), "\n") {
if i == 0 {
if entry != c.generateCredentialHash() {
- return fmt.Errorf("invalid credential hash in session file")
+ return errors.New("invalid credential hash in session file")
}
} else {
kv := strings.Split(entry, "=")
@@ -687,7 +691,7 @@ func (c *hednsProvider) parseResponseForDocumentAndErrors(response *http.Respons
return true
}
}
- err = fmt.Errorf(element.Text())
+ err = errors.New(element.Text())
return false
})
diff --git a/providers/hetzner/api.go b/providers/hetzner/api.go
index 52e3dc7f4a..8efcd449e2 100644
--- a/providers/hetzner/api.go
+++ b/providers/hetzner/api.go
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"strconv"
+ "sync"
"time"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
@@ -18,7 +19,8 @@ const (
type hetznerProvider struct {
apiKey string
- zones map[string]zone
+ mu sync.Mutex
+ cachedZones map[string]zone
requestRateLimiter requestRateLimiter
}
@@ -103,9 +105,17 @@ func (api *hetznerProvider) getAllRecords(domain string) ([]record, error) {
return records, nil
}
-func (api *hetznerProvider) getAllZones() error {
- if api.zones != nil {
- return nil
+func (api *hetznerProvider) resetZoneCache() {
+ api.mu.Lock()
+ defer api.mu.Unlock()
+ api.cachedZones = nil
+}
+
+func (api *hetznerProvider) getAllZones() (map[string]zone, error) {
+ api.mu.Lock()
+ defer api.mu.Unlock()
+ if api.cachedZones != nil {
+ return api.cachedZones, nil
}
var zones map[string]zone
page := 1
@@ -124,7 +134,7 @@ func (api *hetznerProvider) getAllZones() error {
response := getAllZonesResponse{}
url := fmt.Sprintf("/zones?per_page=100&page=%d", page)
if err := api.request(url, "GET", nil, &response, statusOK); err != nil {
- return fmt.Errorf("failed fetching zones: %w", err)
+ return nil, fmt.Errorf("failed fetching zones: %w", err)
}
if zones == nil {
zones = make(map[string]zone, response.Meta.Pagination.TotalEntries)
@@ -138,15 +148,16 @@ func (api *hetznerProvider) getAllZones() error {
}
page++
}
- api.zones = zones
- return nil
+ api.cachedZones = zones
+ return zones, nil
}
func (api *hetznerProvider) getZone(name string) (*zone, error) {
- if err := api.getAllZones(); err != nil {
+ zones, err := api.getAllZones()
+ if err != nil {
return nil, err
}
- z, ok := api.zones[name]
+ z, ok := zones[name]
if !ok {
return nil, fmt.Errorf("%q is not a zone in this HETZNER account", name)
}
@@ -213,18 +224,28 @@ func (api *hetznerProvider) request(endpoint string, method string, request inte
}
type requestRateLimiter struct {
+ mu sync.Mutex
delay time.Duration
lastRequest time.Time
+ resetAt time.Time
}
func (rrl *requestRateLimiter) delayRequest() {
- time.Sleep(time.Until(rrl.lastRequest.Add(rrl.delay)))
-
+ rrl.mu.Lock()
// When not rate-limited, include network/server latency in delay.
- rrl.lastRequest = time.Now()
+ next := rrl.lastRequest.Add(rrl.delay)
+ if next.After(rrl.resetAt) {
+ // Do not stack delays past the reset point.
+ next = rrl.resetAt
+ }
+ rrl.lastRequest = next
+ rrl.mu.Unlock()
+ time.Sleep(time.Until(next))
}
func (rrl *requestRateLimiter) handleResponse(resp *http.Response) (bool, error) {
+ rrl.mu.Lock()
+ defer rrl.mu.Unlock()
if resp.StatusCode == http.StatusTooManyRequests {
printer.Printf("Rate-Limited. Consider contacting the Hetzner Support for raising your quota. URL: %q, Headers: %q\n", resp.Request.URL, resp.Header)
@@ -264,5 +285,6 @@ func (rrl *requestRateLimiter) handleResponse(resp *http.Response) (bool, error)
// ... then spread requests evenly throughout the window.
rrl.delay = reset / time.Duration(remaining+1)
}
+ rrl.resetAt = time.Now().Add(reset)
return false, nil
}
diff --git a/providers/hetzner/hetznerProvider.go b/providers/hetzner/hetznerProvider.go
index 0c373c94cb..414b2ecfc6 100644
--- a/providers/hetzner/hetznerProvider.go
+++ b/providers/hetzner/hetznerProvider.go
@@ -7,13 +7,15 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Can(),
@@ -31,11 +33,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "HETZNER"
+ const providerMaintainer = "@das7pad"
fns := providers.DspFuncs{
Initializer: New,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("HETZNER", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// New creates a new API handle.
@@ -63,27 +68,27 @@ func (api *hetznerProvider) EnsureZoneExists(domain string) error {
}
}
- // reset zone cache
- api.zones = nil
- return api.createZone(domain)
+ if err = api.createZone(domain); err != nil {
+ return err
+ }
+ api.resetZoneCache()
+ return nil
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *hetznerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (api *hetznerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
domain := dc.Name
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
z, err := api.getZone(domain)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, m := range del {
@@ -133,7 +138,7 @@ func (api *hetznerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, e
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// GetNameservers returns the nameservers for a domain.
@@ -164,12 +169,13 @@ func (api *hetznerProvider) GetZoneRecords(domain string, meta map[string]string
// ListZones lists the zones on this account.
func (api *hetznerProvider) ListZones() ([]string, error) {
- if err := api.getAllZones(); err != nil {
+ zones, err := api.getAllZones()
+ if err != nil {
return nil, err
}
- zones := make([]string, 0, len(api.zones))
- for domain := range api.zones {
- zones = append(zones, domain)
+ domains := make([]string, 0, len(zones))
+ for domain := range zones {
+ domains = append(domains, domain)
}
- return zones, nil
+ return domains, nil
}
diff --git a/providers/hetzner/types.go b/providers/hetzner/types.go
index 80ad3c8e7f..61125a5d99 100644
--- a/providers/hetzner/types.go
+++ b/providers/hetzner/types.go
@@ -1,9 +1,8 @@
package hetzner
import (
- "strings"
-
"github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
)
type bulkCreateRecordsRequest struct {
@@ -58,12 +57,12 @@ func fromRecordConfig(in *models.RecordConfig, zone *zone) record {
r := record{
Name: in.GetLabel(),
Type: in.Type,
- Value: in.GetTargetCombined(),
+ Value: in.GetTargetCombinedFunc(txtutil.EncodeQuoted),
TTL: &in.TTL,
ZoneID: zone.ID,
}
- if r.Type == "TXT" && len(in.TxtStrings) == 1 {
+ if r.Type == "TXT" && (in.GetTargetTXTSegmentCount() == 1) {
// HACK: HETZNER rejects values that fit into 255 bytes w/o quotes,
// but do not fit w/ added quotes (via GetTargetCombined()).
// Sending the raw, non-quoted value works for the comprehensive
@@ -71,7 +70,7 @@ func fromRecordConfig(in *models.RecordConfig, zone *zone) record {
// The HETZNER validation does not provide helpful error messages.
// {"error":{"message":"422 Unprocessable Entity: missing: ; ","code":422}}
// Last checked: 2023-04-01
- valueNotQuoted := in.TxtStrings[0]
+ valueNotQuoted := in.GetTargetTXTSegmented()[0]
if len(valueNotQuoted) == 254 || len(valueNotQuoted) == 255 {
r.Value = valueNotQuoted
}
@@ -88,13 +87,9 @@ func toRecordConfig(domain string, r *record) (*models.RecordConfig, error) {
}
rc.SetLabel(r.Name, domain)
- value := r.Value
// HACK: Hetzner is inserting a trailing space after multiple, quoted values.
// NOTE: The actual DNS answer does not contain the space.
+ // NOTE: The txtutil.ParseQuoted parser handles this just fine.
// Last checked: 2023-04-01
- if r.Type == "TXT" && len(value) > 0 && value[len(value)-1] == ' ' {
- // Per RFC 1035 spaces outside quoted values are irrelevant.
- value = strings.TrimRight(value, " ")
- }
- return &rc, rc.PopulateFromString(r.Type, value, domain)
+ return &rc, rc.PopulateFromStringFunc(r.Type, r.Value, domain, txtutil.ParseQuoted)
}
diff --git a/providers/hexonet/auditrecords.go b/providers/hexonet/auditrecords.go
index 371bd49373..8a7f7021b8 100644
--- a/providers/hexonet/auditrecords.go
+++ b/providers/hexonet/auditrecords.go
@@ -13,6 +13,8 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-10-01
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-11-30
+
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28
return a.Audit(records)
diff --git a/providers/hexonet/error.go b/providers/hexonet/error.go
index 3c78841fe5..4ddd502802 100644
--- a/providers/hexonet/error.go
+++ b/providers/hexonet/error.go
@@ -3,7 +3,7 @@ package hexonet
import (
"fmt"
- "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v3/response"
+ "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/response"
)
// GetHXApiError returns an error including API error code and error description.
diff --git a/providers/hexonet/hexonetProvider.go b/providers/hexonet/hexonetProvider.go
index 1784f11909..3b24e1b0b7 100644
--- a/providers/hexonet/hexonetProvider.go
+++ b/providers/hexonet/hexonetProvider.go
@@ -5,9 +5,13 @@ import (
"encoding/json"
"fmt"
- "github.com/StackExchange/dnscontrol/v4/pkg/version"
"github.com/StackExchange/dnscontrol/v4/providers"
- hxcl "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v3/apiclient"
+ hxcl "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/apiclient"
+)
+
+// GoReleaser: version
+var (
+ version = "dev"
)
// HXClient describes a connection to the hexonet API.
@@ -19,14 +23,16 @@ type HXClient struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Unimplemented(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Cannot("Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us."),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Unimplemented(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"),
providers.CanUseTLSA: providers.Can(),
- providers.CantUseNOPURGE: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot("Actively maintained provider module."),
@@ -36,7 +42,7 @@ func newProvider(conf map[string]string) (*HXClient, error) {
api := &HXClient{
client: hxcl.NewAPIClient(),
}
- api.client.SetUserAgent("DNSControl", version.Banner())
+ api.client.SetUserAgent("DNSControl", version)
api.APILogin, api.APIPassword, api.APIEntity = conf["apilogin"], conf["apipassword"], conf["apientity"]
if conf["debugmode"] == "1" {
api.client.EnableDebugMode()
@@ -66,10 +72,13 @@ func newDsp(conf map[string]string, meta json.RawMessage) (providers.DNSServiceP
}
func init() {
+ const providerName = "HEXONET"
+ const providerMaintainer = "@KaiSchwarz-cnic"
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterRegistrarType("HEXONET", newReg)
- providers.RegisterDomainServiceProviderType("HEXONET", fns, features)
+ providers.RegisterRegistrarType(providerName, newReg)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
diff --git a/providers/hexonet/records.go b/providers/hexonet/records.go
index 3cb1846db7..7d164548ec 100644
--- a/providers/hexonet/records.go
+++ b/providers/hexonet/records.go
@@ -3,7 +3,6 @@ package hexonet
import (
"bytes"
"fmt"
- "regexp"
"strconv"
"strings"
@@ -57,12 +56,10 @@ func (n *HXClient) GetZoneRecords(domain string, meta map[string]string) (models
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records)
-
- toReport, create, del, mod, err := diff.NewCompat(dc).IncrementalDiff(actual)
+func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
+ toReport, create, del, mod, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(actual)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -79,7 +76,7 @@ func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual mod
rec := cre.Desired
recordString, err := n.createRecordString(rec, dc.Name)
if err != nil {
- return corrections, err
+ return corrections, 0, err
}
params[fmt.Sprintf("ADDRR%d", addrridx)] = recordString
addrridx++
@@ -88,7 +85,7 @@ func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual mod
changes = true
fmt.Fprintln(buf, d)
rec := d.Existing.Original.(*HXRecord)
- params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(rec, dc.Name)
+ params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(rec)
delrridx++
}
for _, chng := range mod {
@@ -96,10 +93,10 @@ func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual mod
fmt.Fprintln(buf, chng)
old := chng.Existing.Original.(*HXRecord)
new := chng.Desired
- params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(old, dc.Name)
+ params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(old)
newRecordString, err := n.createRecordString(new, dc.Name)
if err != nil {
- return corrections, err
+ return corrections, 0, err
}
params[fmt.Sprintf("ADDRR%d", addrridx)] = newRecordString
addrridx++
@@ -116,7 +113,7 @@ func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual mod
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func toRecord(r *HXRecord, origin string) *models.RecordConfig {
@@ -129,10 +126,6 @@ func toRecord(r *HXRecord, origin string) *models.RecordConfig {
rc.SetLabelFromFQDN(fqdn, origin)
switch rtype := r.Type; rtype {
- case "TXT":
- if err := rc.SetTargetTXTs(decodeTxt(r.Answer)); err != nil {
- panic(fmt.Errorf("unparsable TXT record received from hexonet api: %w", err))
- }
case "MX":
if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
panic(fmt.Errorf("unparsable MX record received from hexonet api: %w", err))
@@ -142,7 +135,7 @@ func toRecord(r *HXRecord, origin string) *models.RecordConfig {
panic(fmt.Errorf("unparsable SRV record received from hexonet api: %w", err))
}
default: // "A", "AAAA", "ANAME", "CNAME", "NS"
- if err := rc.PopulateFromString(rtype, r.Answer, r.Fqdn); err != nil {
+ if err := rc.PopulateFromStringFunc(rtype, r.Answer, r.Fqdn, txtutil.ParseQuoted); err != nil {
panic(fmt.Errorf("unparsable record received from hexonet api: %w", err))
}
}
@@ -242,7 +235,7 @@ func (n *HXClient) createRecordString(rc *models.RecordConfig, domain string) (s
case "CAA":
record.Answer = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, record.Answer)
case "TXT":
- record.Answer = encodeTxt(rc.TxtStrings)
+ record.Answer = txtutil.EncodeQuoted(rc.GetTargetTXTJoined())
case "SRV":
if rc.GetTargetField() == "." {
return "", fmt.Errorf("SRV records with empty targets are not supported (as of 2020-02-27, the API returns 'Invalid attribute value syntax')")
@@ -263,34 +256,6 @@ func (n *HXClient) createRecordString(rc *models.RecordConfig, domain string) (s
return str, nil
}
-func (n *HXClient) deleteRecordString(record *HXRecord, domain string) string {
+func (n *HXClient) deleteRecordString(record *HXRecord) string {
return record.Raw
}
-
-// encodeTxt encodes TxtStrings for sending in the CREATE/MODIFY API:
-func encodeTxt(txts []string) string {
- var r []string
- for _, txt := range txts {
- n := `"` + strings.Replace(txt, `"`, `\"`, -1) + `"`
- r = append(r, n)
- }
- return strings.Join(r, " ")
-}
-
-// finds a string surrounded by quotes that might contain an escaped quote character.
-var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`)
-
-// decodeTxt decodes the TXT record as received from hexonet api and
-// returns the list of strings.
-func decodeTxt(s string) []string {
-
- if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
- txtStrings := []string{}
- for _, t := range quotedStringRegexp.FindAllStringSubmatch(s, -1) {
- txtString := strings.Replace(t[1], `\"`, `"`, -1)
- txtStrings = append(txtStrings, txtString)
- }
- return txtStrings
- }
- return []string{s}
-}
diff --git a/providers/hexonet/records_test.go b/providers/hexonet/records_test.go
deleted file mode 100644
index 8b242931a3..0000000000
--- a/providers/hexonet/records_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package hexonet
-
-import (
- "strings"
- "testing"
-)
-
-var txtData = []struct {
- decoded []string
- encoded string
-}{
- {[]string{`simple`}, `"simple"`},
- {[]string{`changed`}, `"changed"`},
- {[]string{`with spaces`}, `"with spaces"`},
- {[]string{`with whitespace`}, `"with whitespace"`},
- {[]string{"one", "two"}, `"one" "two"`},
- {[]string{"eh", "bee", "cee"}, `"eh" "bee" "cee"`},
- {[]string{"o\"ne", "tw\"o"}, `"o\"ne" "tw\"o"`},
- {[]string{"dimple"}, `"dimple"`},
- {[]string{"fun", "two"}, `"fun" "two"`},
- {[]string{"eh", "bzz", "cee"}, `"eh" "bzz" "cee"`},
-}
-
-func TestEncodeTxt(t *testing.T) {
- // Test encoded the lists of strings into a string:
- for i, test := range txtData {
- enc := encodeTxt(test.decoded)
- if enc != test.encoded {
- t.Errorf("%v: txt\n data: []string{%v}\nexpected: %q\n got: %q",
- i, "`"+strings.Join(test.decoded, "`, `")+"`", test.encoded, enc)
- }
- }
-}
-
-func TestDecodeTxt(t *testing.T) {
- // Test decoded a string into the list of strings:
- for i, test := range txtData {
- data := test.encoded
- got := decodeTxt(data)
- wanted := test.decoded
- if len(got) != len(wanted) {
- t.Errorf("%v: txt\n decode: %v\nexpected: %q\n got: %q\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
- } else {
- for j := range got {
- if got[j] != wanted[j] {
- t.Errorf("%v: txt\n decode: %v\nexpected: %q\n got: %q\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
- }
- }
- }
- }
-}
diff --git a/providers/hostingde/hostingdeProvider.go b/providers/hostingde/hostingdeProvider.go
index 9410ee4b41..aa109007d0 100644
--- a/providers/hostingde/hostingdeProvider.go
+++ b/providers/hostingde/hostingdeProvider.go
@@ -16,8 +16,11 @@ import (
var defaultNameservers = []string{"ns1.hosting.de", "ns2.hosting.de", "ns3.hosting.de"}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Can(),
@@ -34,12 +37,15 @@ var features = providers.DocumentationNotes{
}
func init() {
- providers.RegisterRegistrarType("HOSTINGDE", newHostingdeReg)
+ const providerName = "HOSTINGDE"
+ const providerMaintainer = "@juliusrickert"
+ providers.RegisterRegistrarType(providerName, newHostingdeReg)
fns := providers.DspFuncs{
Initializer: newHostingdeDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("HOSTINGDE", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
type providerMeta struct {
@@ -117,7 +123,7 @@ func soaToString(s soaValues) string {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, int, error) {
var err error
// TTL must be between (inclusive) 1m and 1y (in fact, a little bit more)
@@ -134,12 +140,12 @@ func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
zone, err := hp.getZone(dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
- toReport, create, del, mod, err := diff.NewCompat(dc).IncrementalDiff(records)
+ toReport, create, del, mod, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(records)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -178,9 +184,10 @@ func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
}
defaultSoa := &hp.defaultSoa
- if defaultSoa == nil {
- defaultSoa = &soaValues{}
- }
+ // Commented out because this can not happen:
+ // if defaultSoa == nil {
+ // defaultSoa = &soaValues{}
+ // }
newSOA := soaValues{
Refresh: firstNonZero(desiredSoa.SoaRefresh, defaultSoa.Refresh, 86400),
@@ -218,7 +225,7 @@ func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
if existingAutoDNSSecEnabled && desiredAutoDNSSecEnabled {
currentDNSSecOptions, err := hp.getDNSSECOptions(zone.ZoneConfig.ID)
if err != nil {
- return nil, err
+ return nil, 0, err
}
if !currentDNSSecOptions.PublishKSK {
msg = append(msg, "Enabling publishKsk for AutoDNSSec")
@@ -239,7 +246,7 @@ func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
} else if existingAutoDNSSecEnabled && !desiredAutoDNSSecEnabled {
currentDNSSecOptions, err := hp.getDNSSECOptions(zone.ZoneConfig.ID)
if err != nil {
- return nil, err
+ return nil, 0, err
}
msg = append(msg, "Disable AutoDNSSEC")
zone.ZoneConfig.DNSSECMode = "off"
@@ -247,7 +254,7 @@ func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
// Remove auto dnssec keys from domain
DomainConfig, err := hp.getDomainConfig(dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, entry := range DomainConfig.DNSSecEntries {
for _, autoDNSKey := range currentDNSSecOptions.Keys {
@@ -260,7 +267,7 @@ func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
}
if !zoneChanged {
- return nil, nil
+ return nil, 0, nil
}
corrections = append(corrections, &models.Correction{
@@ -299,7 +306,7 @@ func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
corrections = append(corrections, correction)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func firstNonZero(items ...uint32) uint32 {
diff --git a/providers/hostingde/types.go b/providers/hostingde/types.go
index 0a1f924a5b..4f789ba2a9 100644
--- a/providers/hostingde/types.go
+++ b/providers/hostingde/types.go
@@ -193,8 +193,9 @@ func recordToNative(rc *models.RecordConfig) *record {
case "A", "AAAA", "ALIAS", "CAA", "CNAME", "DNSKEY", "DS", "NS", "NSEC", "NSEC3", "NSEC3PARAM", "PTR", "RRSIG", "SSHFP", "TSLA":
// Nothing special.
case "TXT":
- txtStrings := make([]string, len(rc.TxtStrings))
- copy(txtStrings, rc.TxtStrings)
+ // TODO(tlim): Move this to a function with unit tests.
+ txtStrings := make([]string, rc.GetTargetTXTSegmentCount())
+ copy(txtStrings, rc.GetTargetTXTSegmented())
// Escape quotes
for i := range txtStrings {
diff --git a/providers/huaweicloud/auditrecords.go b/providers/huaweicloud/auditrecords.go
new file mode 100644
index 0000000000..5b055393ca
--- /dev/null
+++ b/providers/huaweicloud/auditrecords.go
@@ -0,0 +1,18 @@
+package huaweicloud
+
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
+
+// AuditRecords returns a list of errors corresponding to the records
+// that aren't supported by this provider. If all records are
+// supported, an empty list is returned.
+func AuditRecords(records []*models.RecordConfig) []error {
+ a := rejectif.Auditor{}
+ a.Add("MX", rejectif.MxNull) // Last verified 2024-06-14
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2024-06-14
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2024-06-14
+
+ return a.Audit(records)
+}
diff --git a/providers/huaweicloud/convert.go b/providers/huaweicloud/convert.go
new file mode 100644
index 0000000000..e577b0ce59
--- /dev/null
+++ b/providers/huaweicloud/convert.go
@@ -0,0 +1,135 @@
+package huaweicloud
+
+import (
+ "fmt"
+ "slices"
+ "strconv"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model"
+)
+
+func getRRSetIDFromRecords(rcs models.Records) []string {
+ ids := []string{}
+ for _, r := range rcs {
+ if r.Original == nil {
+ continue
+ }
+ if r.Original.(*model.ShowRecordSetByZoneResp).Id == nil {
+ printer.Warnf("RecordSet ID is nil for record %+v\n", r)
+ continue
+ }
+ ids = append(ids, *r.Original.(*model.ShowRecordSetByZoneResp).Id)
+ }
+ slices.Sort(ids)
+ return slices.Compact(ids)
+}
+
+func nativeToRecords(n *model.ShowRecordSetByZoneResp, zoneName string) (models.Records, error) {
+ if n.Name == nil || n.Type == nil || n.Records == nil || n.Ttl == nil {
+ return nil, fmt.Errorf("missing required fields in Huaweicloud's RRset: %+v", n)
+ }
+ var rcs models.Records
+ recName := *n.Name
+ recType := *n.Type
+
+ // Split into records
+ for _, value := range *n.Records {
+ rc := &models.RecordConfig{
+ TTL: uint32(*n.Ttl),
+ Original: n,
+ Metadata: map[string]string{},
+ }
+ rc.SetLabelFromFQDN(recName, zoneName)
+ if err := rc.PopulateFromString(recType, value, zoneName); err != nil {
+ return nil, fmt.Errorf("unparsable record received from Huaweicloud: %w", err)
+ }
+ if n.Line != nil {
+ rc.Metadata[metaLine] = *n.Line
+ }
+ if n.Weight != nil {
+ rc.Metadata[metaWeight] = fmt.Sprintf("%d", *n.Weight)
+ }
+ if n.Description != nil {
+ rc.Metadata[metaKey] = *n.Description
+ }
+ rcs = append(rcs, rc)
+ }
+
+ return rcs, nil
+}
+
+func recordsToNative(rcs models.Records, expectedKey models.RecordKey) (*model.ShowRecordSetByZoneResp, error) {
+ // rcs length is guaranteed to be > 0
+ if len(rcs) == 0 {
+ return nil, fmt.Errorf("empty record set")
+ }
+ // line and weight should be the same for all records in the rrset
+ line := rcs[0].Metadata[metaLine]
+ weightStr := rcs[0].Metadata[metaWeight]
+ for _, r := range rcs {
+ if r.Metadata[metaLine] != line {
+ return nil, fmt.Errorf("all records in the rrset must have the same line %s", line)
+ }
+ if r.Metadata[metaWeight] != weightStr {
+ return nil, fmt.Errorf("all records in the rrset must have the same weight %s", weightStr)
+ }
+ }
+
+ // parse weight to int32
+ var weight *int32
+ if weightStr != "" {
+ weightInt, err := strconv.ParseInt(weightStr, 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse weight %s to int32", weightStr)
+ }
+ weightInt32 := int32(weightInt)
+ // weight should be 0-1000
+ if weightInt32 < 0 || weightInt32 > 1000 {
+ return nil, fmt.Errorf("weight must be between 0 and 1000")
+ }
+ weight = &weightInt32
+ }
+
+ resultTTL := int32(0)
+ resultVal := []string{}
+ name := expectedKey.NameFQDN + "."
+ key := rcs[0].Metadata[metaKey]
+ result := &model.ShowRecordSetByZoneResp{
+ Name: &name,
+ Type: &expectedKey.Type,
+ Ttl: &resultTTL,
+ Records: &resultVal,
+ Line: &line,
+ Weight: weight,
+ Description: &key,
+ }
+
+ for _, r := range rcs {
+ key := r.Key()
+ if key != expectedKey {
+ continue
+ }
+ val := r.GetTargetCombined()
+ // special case for empty TXT records
+ if key.Type == "TXT" && len(val) == 0 {
+ val = "\"\""
+ }
+
+ resultVal = append(resultVal, val)
+ if resultTTL == 0 {
+ resultTTL = int32(r.TTL)
+ }
+
+ // Check if all TTLs are the same
+ if int32(r.TTL) != resultTTL {
+ printer.Warnf("All TTLs for a rrset (%v) must be the same. Using smaller of %v and %v.\n", key, r.TTL, resultTTL)
+ if int32(r.TTL) < resultTTL {
+ resultTTL = int32(r.TTL)
+ }
+ }
+ }
+
+ return result, nil
+}
diff --git a/providers/huaweicloud/huaweicloudProvider.go b/providers/huaweicloud/huaweicloudProvider.go
new file mode 100644
index 0000000000..42f0d7b16a
--- /dev/null
+++ b/providers/huaweicloud/huaweicloudProvider.go
@@ -0,0 +1,174 @@
+package huaweicloud
+
+import (
+ "encoding/json"
+ "strings"
+ "time"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/StackExchange/dnscontrol/v4/providers"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/region"
+ dnssdk "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model"
+ dnsRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region"
+)
+
+// Support for Huawei Cloud DNS.
+// API Documentation: https://www.huaweicloud.com/intl/en-us/product/dns.html
+
+/*
+Huaweicloud API DNS provider:
+
+Info required in `creds.json`:
+ - KeyId
+ - SecretKey
+ - Region
+
+Record level metadata available:
+ - hw_line (refer below Huawei Cloud DNS API documentation for available lines, default "default_view")
+ (https://support.huaweicloud.com/intl/en-us/api-dns/en-us_topic_0085546214.html)
+ - hw_weight (0-1000, default "1")
+ - hw_rrset_key (default "")
+
+*/
+
+type huaweicloudProvider struct {
+ client *dnssdk.DnsClient
+ domainByZoneID map[string]string
+ zoneIDByDomain map[string]string
+ region *region.Region
+}
+
+const (
+ metaWeight = "hw_weight"
+ metaLine = "hw_line"
+ metaKey = "hw_rrset_key"
+ defaultWeight = "1"
+ defaultLine = "default_view"
+)
+
+// newHuaweicloud creates the provider.
+func newHuaweicloud(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
+ auth, err := basic.NewCredentialsBuilder().
+ WithAk(m["KeyId"]).
+ WithSk(m["SecretKey"]).
+ SafeBuild()
+ if err != nil {
+ return nil, err
+ }
+ region, err := dnsRegion.SafeValueOf(m["Region"])
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := dnssdk.DnsClientBuilder().
+ WithRegion(region).
+ WithCredential(auth).
+ SafeBuild()
+ if err != nil {
+ return nil, err
+ }
+
+ c := &huaweicloudProvider{
+ client: dnssdk.NewDnsClient(client),
+ region: region,
+ }
+
+ return c, nil
+}
+
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanAutoDNSSEC: providers.Unimplemented("No public api provided, but can be turned on manually in the console."),
+ providers.CanGetZones: providers.Can(),
+ providers.CanUseAlias: providers.Cannot(),
+ providers.CanUseCAA: providers.Can(),
+ providers.CanUseDS: providers.Cannot(),
+ providers.CanUseLOC: providers.Cannot(),
+ providers.CanUseNAPTR: providers.Cannot(),
+ providers.CanUsePTR: providers.Cannot(),
+ providers.CanUseSRV: providers.Can(),
+ providers.CanUseSSHFP: providers.Cannot(),
+ providers.CanUseTLSA: providers.Cannot(),
+ providers.CanUseHTTPS: providers.Cannot(),
+ providers.CanUseSVCB: providers.Cannot(),
+ providers.CanUseSOA: providers.Cannot(),
+ providers.DocCreateDomains: providers.Can(),
+ providers.DocDualHost: providers.Can(),
+ providers.DocOfficiallySupported: providers.Cannot(),
+}
+
+var defaultNameServerNames = []string{
+ // DNS server for regions in the Chinese mainland
+ "ns1.huaweicloud-dns.com.",
+ "ns1.huaweicloud-dns.cn.",
+ // DNS server for countries or regions outside the Chinese mainland
+ "ns1.huaweicloud-dns.net.",
+ "ns1.huaweicloud-dns.org.",
+}
+
+func init() {
+ const providerName = "HUAWEICLOUD"
+ const providerMaintainer = "@huihuimoe"
+ fns := providers.DspFuncs{
+ Initializer: newHuaweicloud,
+ RecordAuditor: AuditRecords,
+ }
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+}
+
+// huaweicloud has request limiting like above.
+// "The throttling threshold has been reached: policy user over ratelimit,limit:100,time:1 minute"
+func withRetry(f func() error) {
+ const maxRetries = 23
+ const sleepTime = 5 * time.Second
+ var currentRetry int
+ for {
+ err := f()
+ if err == nil {
+ return
+ }
+ if strings.Contains(err.Error(), "over ratelimit") {
+ currentRetry++
+ if currentRetry >= maxRetries {
+ return
+ }
+ printer.Printf("Huaweicloud rate limit exceeded. Waiting %s to retry.\n", sleepTime)
+ time.Sleep(sleepTime)
+ } else {
+ return
+ }
+ }
+}
+
+// GetNameservers returns the nameservers for a domain.
+func (c *huaweicloudProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ if err := c.getZones(); err != nil {
+ return nil, err
+ }
+
+ payload := &model.ShowPublicZoneNameServerRequest{
+ ZoneId: c.zoneIDByDomain[domain],
+ }
+ res, err := c.client.ShowPublicZoneNameServer(payload)
+ if err != nil {
+ return nil, err
+ }
+ nameservers := []string{}
+ if res.Nameservers != nil {
+ for _, record := range *res.Nameservers {
+ if record.Hostname != nil {
+ nameservers = append(nameservers, *record.Hostname)
+ }
+ }
+ }
+ if len(nameservers) != 0 {
+ return models.ToNameserversStripTD(nameservers)
+ }
+
+ return models.ToNameserversStripTD(defaultNameServerNames)
+}
diff --git a/providers/huaweicloud/listzones.go b/providers/huaweicloud/listzones.go
new file mode 100644
index 0000000000..1b7f06905d
--- /dev/null
+++ b/providers/huaweicloud/listzones.go
@@ -0,0 +1,95 @@
+package huaweicloud
+
+import (
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model"
+)
+
+// EnsureZoneExists creates a zone if it does not exist
+func (c *huaweicloudProvider) EnsureZoneExists(domain string) error {
+ if err := c.getZones(); err != nil {
+ return err
+ }
+
+ if _, ok := c.zoneIDByDomain[domain]; ok {
+ return nil
+ }
+
+ printer.Printf("Adding zone for %s to huaweicloud account in region %s\n", domain, c.region.Id)
+ createPayload := model.CreatePublicZoneRequest{
+ Body: &model.CreatePublicZoneReq{
+ Name: domain,
+ },
+ }
+ res, err := c.client.CreatePublicZone(&createPayload)
+ if err != nil {
+ return err
+ }
+ if res.Id == nil {
+ // clear cache
+ c.zoneIDByDomain = nil
+ c.domainByZoneID = nil
+ return nil
+ }
+ c.zoneIDByDomain[domain] = *res.Id
+ c.domainByZoneID[*res.Id] = domain
+ return nil
+}
+
+// ListZones returns all DNS zones managed by this provider.
+func (c *huaweicloudProvider) ListZones() ([]string, error) {
+ if err := c.getZones(); err != nil {
+ return nil, err
+ }
+ var zones []string
+ for i := range c.zoneIDByDomain {
+ zones = append(zones, i)
+ }
+ return zones, nil
+}
+
+func (c *huaweicloudProvider) getZones() error {
+ if c.zoneIDByDomain != nil {
+ return nil
+ }
+
+ var nextMarker *string
+ c.zoneIDByDomain = make(map[string]string)
+ c.domainByZoneID = make(map[string]string)
+
+ for {
+ listPayload := model.ListPublicZonesRequest{
+ Marker: nextMarker,
+ }
+ zonesRes, err := c.client.ListPublicZones(&listPayload)
+ if err != nil {
+ return err
+ }
+ // empty zones
+ if zonesRes.Zones == nil {
+ return nil
+ }
+ for _, zone := range *zonesRes.Zones {
+ // just a safety check
+ if zone.Name == nil || zone.Id == nil {
+ continue
+ }
+ domain := strings.TrimSuffix(*zone.Name, ".")
+ c.zoneIDByDomain[domain] = *zone.Id
+ c.domainByZoneID[*zone.Id] = domain
+ }
+
+ // if has next page, continue to get next page
+ if zonesRes.Links.Next != nil {
+ marker, err := parseMarkerFromURL(*zonesRes.Links.Next)
+ if err != nil {
+ return err
+ }
+ nextMarker = &marker
+ } else {
+ return nil
+ }
+ }
+}
diff --git a/providers/huaweicloud/records.go b/providers/huaweicloud/records.go
new file mode 100644
index 0000000000..8efb9da921
--- /dev/null
+++ b/providers/huaweicloud/records.go
@@ -0,0 +1,307 @@
+package huaweicloud
+
+import (
+ "fmt"
+ "net/url"
+ "slices"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model"
+)
+
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (c *huaweicloudProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
+ if err := c.getZones(); err != nil {
+ return nil, err
+ }
+ zoneID, ok := c.zoneIDByDomain[domain]
+ if !ok {
+ return nil, fmt.Errorf("zone %s not found", domain)
+ }
+ records, err := c.fetchZoneRecordsFromRemote(zoneID)
+ if err != nil {
+ return nil, err
+ }
+
+ // Convert rrsets to DNSControl's RecordConfig
+ existingRecords := []*models.RecordConfig{}
+ for _, rec := range *records {
+ if *rec.Type == "SOA" {
+ continue
+ }
+ nativeRecords, err := nativeToRecords(&rec, domain)
+ if err != nil {
+ return nil, err
+ }
+ existingRecords = append(existingRecords, nativeRecords...)
+ }
+
+ return existingRecords, nil
+}
+
+// GenerateDomainCorrections takes the desired and existing records
+// and produces a Correction list. The correction list is simply
+// a list of functions to call to actually make the desired
+// correction, and a message to output to the user when the change is
+// made.
+func (c *huaweicloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
+ if err := c.getZones(); err != nil {
+ return nil, 0, err
+ }
+ zoneID, ok := c.zoneIDByDomain[dc.Name]
+ if !ok {
+ return nil, 0, fmt.Errorf("zone %s not found", dc.Name)
+ }
+
+ addDefaultMeta(dc.Records)
+
+ // Make delete happen earlier than creates & updates.
+ var corrections []*models.Correction
+ var deletions []*models.Correction
+ var reports []*models.Correction
+
+ changes, actualChangeCount, err := diff2.ByRecordSet(existing, dc, genComparable)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ for _, change := range changes {
+ switch change.Type {
+ case diff2.REPORT:
+ reports = append(reports, &models.Correction{Msg: change.MsgsJoined})
+ case diff2.CREATE:
+ fallthrough
+ case diff2.CHANGE:
+ newRecordsColl := collectRecordsByLineAndWeightAndKey(change.New)
+ oldRecordsColl := collectRecordsByLineAndWeightAndKey(change.Old)
+ corrections = append(corrections, &models.Correction{
+ Msg: change.MsgsJoined,
+ F: func() error {
+ // delete old records if not exist in new records
+ for key, oldRecords := range oldRecordsColl {
+ if _, ok := newRecordsColl[key]; !ok {
+ rrsetIDOld := getRRSetIDFromRecords(oldRecords)
+ err := c.deleteRRSets(zoneID, rrsetIDOld)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ // modify or create new records
+ for key, newRecords := range newRecordsColl {
+ records, err := recordsToNative(newRecords, change.Key)
+ if err != nil {
+ return err
+ }
+ oldRecords := oldRecordsColl[key]
+ rrsetIDOld := getRRSetIDFromRecords(oldRecords)
+
+ if len(rrsetIDOld) == 1 {
+ // update existing rrset
+ err = c.updateRRSet(zoneID, rrsetIDOld[0], records)
+ if err != nil {
+ return err
+ }
+ } else {
+ // create new rrset or combine multiple rrsets into one
+ err := c.deleteRRSets(zoneID, rrsetIDOld)
+ if err != nil {
+ return err
+ }
+ err = c.createRRSet(zoneID, records)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ },
+ })
+ case diff2.DELETE:
+ rrsetsID := getRRSetIDFromRecords(change.Old)
+ deletions = append(deletions, &models.Correction{
+ Msg: change.MsgsJoined,
+ F: func() error {
+ return c.deleteRRSets(zoneID, rrsetsID)
+ },
+ })
+ default:
+ panic(fmt.Sprintf("unhandled change.Type %s", change.Type))
+ }
+ }
+
+ result := append(reports, deletions...)
+ result = append(result, corrections...)
+ return result, actualChangeCount, nil
+}
+
+func collectRecordsByLineAndWeightAndKey(records models.Records) map[string]models.Records {
+ recordsByLineAndWeight := make(map[string]models.Records)
+ for _, rec := range records {
+ line := rec.Metadata[metaLine]
+ weight := rec.Metadata[metaWeight]
+ rrsetKey := rec.Metadata[metaKey]
+ key := weight + "," + line + "," + rrsetKey
+ recordsByLineAndWeight[key] = append(recordsByLineAndWeight[key], rec)
+ }
+ return recordsByLineAndWeight
+}
+
+func addDefaultMeta(recs models.Records) {
+ for _, r := range recs {
+ if r.Metadata == nil {
+ r.Metadata = make(map[string]string)
+ }
+ if r.Metadata[metaLine] == "" {
+ r.Metadata[metaLine] = defaultLine
+ }
+ // apex ns should not have weight
+ isApexNS := r.Type == "NS" && r.Name == "@"
+ if !isApexNS && r.Metadata[metaWeight] == "" {
+ r.Metadata[metaWeight] = defaultWeight
+ }
+ }
+}
+
+func genComparable(rec *models.RecordConfig) string {
+ // apex ns
+ if rec.Type == "NS" && rec.Name == "@" {
+ return ""
+ }
+ weight := rec.Metadata[metaWeight]
+ line := rec.Metadata[metaLine]
+ key := rec.Metadata[metaKey]
+ if weight == "" {
+ weight = defaultWeight
+ }
+ if line == "" {
+ line = defaultLine
+ }
+ return "weight=" + weight + " line=" + line + " key=" + key
+}
+
+func (c *huaweicloudProvider) deleteRRSets(zoneID string, rrsets []string) error {
+ for _, rrset := range rrsets {
+ deletePayload := &model.DeleteRecordSetsRequest{
+ ZoneId: zoneID,
+ RecordsetId: rrset,
+ }
+ var err error
+ withRetry(func() error {
+ _, err = c.client.DeleteRecordSets(deletePayload)
+ return err
+ })
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *huaweicloudProvider) createRRSet(zoneID string, rc *model.ShowRecordSetByZoneResp) error {
+ createPayload := &model.CreateRecordSetWithLineRequest{
+ ZoneId: zoneID,
+ Body: &model.CreateRecordSetWithLineRequestBody{
+ Name: *rc.Name,
+ Type: *rc.Type,
+ Ttl: rc.Ttl,
+ Records: rc.Records,
+ Weight: rc.Weight,
+ Line: rc.Line,
+ Description: rc.Description,
+ },
+ }
+ var err error
+ withRetry(func() error {
+ _, err = c.client.CreateRecordSetWithLine(createPayload)
+ return err
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *huaweicloudProvider) updateRRSet(zoneID, rrsetID string, rc *model.ShowRecordSetByZoneResp) error {
+ updatePayload := &model.UpdateRecordSetsRequest{
+ ZoneId: zoneID,
+ RecordsetId: rrsetID,
+ Body: &model.UpdateRecordSetsReq{
+ Name: *rc.Name,
+ Type: *rc.Type,
+ Ttl: rc.Ttl,
+ Records: rc.Records,
+ Weight: rc.Weight,
+ Description: rc.Description,
+ },
+ }
+ var err error
+ withRetry(func() error {
+ _, err = c.client.UpdateRecordSets(updatePayload)
+ return err
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func parseMarkerFromURL(link string) (string, error) {
+ // Parse the marker params from the URL
+ // Example: https://dns.myhuaweicloud.com/v2/zones?marker=abcdefg
+ url, err := url.Parse(link)
+ if err != nil {
+ return "", err
+ }
+ marker := url.Query().Get("marker")
+ if marker == "" {
+ return "", fmt.Errorf("marker not found in URL %s", link)
+ }
+ return marker, nil
+}
+
+func (c *huaweicloudProvider) fetchZoneRecordsFromRemote(zoneID string) (*[]model.ShowRecordSetByZoneResp, error) {
+ var nextMarker *string
+ existingRecords := []model.ShowRecordSetByZoneResp{}
+ availableStatus := []string{"ACTIVE", "PENDING_CREATE", "PENDING_UPDATE"}
+
+ for {
+ payload := model.ShowRecordSetByZoneRequest{
+ ZoneId: zoneID,
+ Marker: nextMarker,
+ }
+ var res *model.ShowRecordSetByZoneResponse
+ var err error
+ withRetry(func() error {
+ res, err = c.client.ShowRecordSetByZone(&payload)
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+ if res.Recordsets == nil {
+ return &existingRecords, nil
+ }
+ for _, record := range *res.Recordsets {
+ if record.Records == nil {
+ continue
+ }
+ if !slices.Contains(availableStatus, *record.Status) {
+ continue
+ }
+ existingRecords = append(existingRecords, record)
+ }
+
+ // if has next page, continue to get next page
+ if res.Links.Next != nil {
+ marker, err := parseMarkerFromURL(*res.Links.Next)
+ if err != nil {
+ return nil, err
+ }
+ nextMarker = &marker
+ } else {
+ return &existingRecords, nil
+ }
+ }
+}
diff --git a/providers/internetbs/auditrecords.go b/providers/internetbs/auditrecords.go
deleted file mode 100644
index b9376c58c0..0000000000
--- a/providers/internetbs/auditrecords.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package internetbs
-
-import "github.com/StackExchange/dnscontrol/v4/models"
-
-// AuditRecords returns a list of errors corresponding to the records
-// that aren't supported by this provider. If all records are
-// supported, an empty list is returned.
-func AuditRecords(records []*models.RecordConfig) []error {
- return nil
-}
diff --git a/providers/internetbs/internetbsProvider.go b/providers/internetbs/internetbsProvider.go
index 84ba433539..a1224ff206 100644
--- a/providers/internetbs/internetbsProvider.go
+++ b/providers/internetbs/internetbsProvider.go
@@ -19,8 +19,17 @@ Info required in `creds.json`:
*/
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanConcur: providers.Cannot(),
+}
+
func init() {
- providers.RegisterRegistrarType("INTERNETBS", newInternetBs)
+ const providerName = "INTERNETBS"
+ const providerMaintainer = "@pragmaton"
+ providers.RegisterRegistrarType(providerName, newInternetBs, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func newInternetBs(m map[string]string) (providers.Registrar, error) {
diff --git a/providers/inwx/auditrecords.go b/providers/inwx/auditrecords.go
index fb15d2e71b..64dff42e24 100644
--- a/providers/inwx/auditrecords.go
+++ b/providers/inwx/auditrecords.go
@@ -11,8 +11,6 @@ import (
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
- a.Add("MX", rejectif.MxNull) // Last verified 2020-12-28
-
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28
a.Add("TXT", rejectif.TxtHasBackticks) // Last verified 2021-03-01
diff --git a/providers/inwx/inwxProvider.go b/providers/inwx/inwxProvider.go
index a00a60b03f..3c5ca53f06 100644
--- a/providers/inwx/inwxProvider.go
+++ b/providers/inwx/inwxProvider.go
@@ -10,7 +10,6 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/nrdcg/goinwx"
"github.com/pquerna/otp/totp"
@@ -42,16 +41,21 @@ var InwxSandboxDefaultNs = []string{"ns.ote.inwx.de", "ns2.ote.inwx.de"}
// features is used to let dnscontrol know which features are supported by INWX.
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Unimplemented("Supported by INWX but not implemented yet."),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Cannot("INWX does not support the ALIAS or ANAME record type."),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Unimplemented("DS records are only supported at the apex and require a different API call that hasn't been implemented yet."),
+ providers.CanUseHTTPS: providers.Can(),
providers.CanUseLOC: providers.Unimplemented(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can("PTR records with empty targets are not supported"),
providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported."),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
@@ -67,12 +71,15 @@ type inwxAPI struct {
// init registers the registrar and the domain service provider with dnscontrol.
func init() {
- providers.RegisterRegistrarType("INWX", newInwxReg)
+ const providerName = "INWX"
+ const providerMaintainer = "@patschi"
+ providers.RegisterRegistrarType(providerName, newInwxReg)
fns := providers.DspFuncs{
Initializer: newInwxDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("INWX", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// getOTP either returns the TOTPValue or uses TOTPKey and the current time to generate a valid TOTPValue.
@@ -181,7 +188,11 @@ func makeNameserverRecordRequest(domain string, rec *models.RecordConfig) *goinw
req.Content = content[:len(content)-1]
case "MX":
req.Priority = int(rec.MxPreference)
- req.Content = content[:len(content)-1]
+ if content == "." {
+ req.Content = content
+ } else {
+ req.Content = content[:len(content)-1]
+ }
case "SRV":
req.Priority = int(rec.SrvPriority)
req.Content = fmt.Sprintf("%d %d %v", rec.SrvWeight, rec.SrvPort, content[:len(content)-1])
@@ -213,12 +224,11 @@ func (api *inwxAPI) deleteRecord(RecordID int) error {
// checkRecords ensures that there is no single-quote inside TXT records which would be ignored by INWX.
func checkRecords(records models.Records) error {
+ // TODO(tlim) Remove this function. auditrecords.go takes care of this now.
for _, r := range records {
if r.Type == "TXT" {
- for _, target := range r.TxtStrings {
- if strings.ContainsAny(target, "`") {
- return fmt.Errorf("INWX TXT records do not support single-quotes in their target")
- }
+ if strings.ContainsAny(r.GetTargetTXTJoined(), "`") {
+ return fmt.Errorf("INWX TXT records do not support single-quotes in their target")
}
}
}
@@ -226,18 +236,15 @@ func checkRecords(records models.Records) error {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
-
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
+func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, int, error) {
err := checkRecords(dc.Records)
if err != nil {
- return nil, err
+ return nil, 0, err
}
- toReport, create, del, mod, err := diff.NewCompat(dc).IncrementalDiff(foundRecords)
+ toReport, create, del, mod, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(foundRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -265,7 +272,7 @@ func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundReco
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// getDefaultNameservers returns string map with default nameservers based on e.g. sandbox mode.
@@ -310,7 +317,11 @@ func (api *inwxAPI) GetZoneRecords(domain string, meta map[string]string) (model
"PTR": true,
}
if rtypeAddDot[record.Type] {
- record.Content = record.Content + "."
+ if record.Type == "MX" && record.Content == "." {
+ // null records don't need to be modified
+ } else {
+ record.Content = record.Content + "."
+ }
}
rc := &models.RecordConfig{
@@ -395,18 +406,24 @@ func (api *inwxAPI) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.
// fetchNameserverDomains returns the domains configured in INWX nameservers
func (api *inwxAPI) fetchNameserverDomains() error {
+ zones := map[string]int{}
request := &goinwx.NameserverListRequest{}
- request.PageLimit = 2147483647 // int32 max value, highest number API accepts
- info, err := api.client.Nameservers.ListWithParams(request)
- if err != nil {
- return err
- }
-
- api.domainIndex = map[string]int{}
- for _, domain := range info.Domains {
- api.domainIndex[domain.Domain] = domain.RoID
+ page := 1
+ for {
+ request.Page = page
+ info, err := api.client.Nameservers.ListWithParams(request)
+ if err != nil {
+ return err
+ }
+ for _, domain := range info.Domains {
+ zones[domain.Domain] = domain.RoID
+ }
+ if len(zones) >= info.Count {
+ break
+ }
+ page++
}
-
+ api.domainIndex = zones
return nil
}
diff --git a/providers/linode/linodeProvider.go b/providers/linode/linodeProvider.go
index de3b4cd523..24bbaa1e82 100644
--- a/providers/linode/linodeProvider.go
+++ b/providers/linode/linodeProvider.go
@@ -88,7 +88,10 @@ func NewLinode(m map[string]string, metadata json.RawMessage) (providers.DNSServ
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseCAA: providers.Can("Linode doesn't support changing the CAA flag"),
providers.CanUseLOC: providers.Cannot(),
providers.DocDualHost: providers.Cannot(),
@@ -96,12 +99,15 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "LINODE"
+ const providerMaintainer = "@koesie10"
// SRV support is in this provider, but Linode doesn't seem to support it properly
fns := providers.DspFuncs{
Initializer: NewLinode,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("LINODE", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// GetNameservers returns the nameservers for a domain.
@@ -125,7 +131,7 @@ func (api *linodeProvider) GetZoneRecords(domain string, meta map[string]string)
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *linodeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (api *linodeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
// Linode doesn't allow selecting an arbitrary TTL, only a set of predefined values
// We need to make sure we don't change it every time if it is as close as it's going to get
// The documentation says that it will always round up to the next highest value: 300 -> 300, 301 -> 3600.
@@ -136,17 +142,17 @@ func (api *linodeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
if api.domainIndex == nil {
if err := api.fetchDomainList(); err != nil {
- return nil, err
+ return nil, 0, err
}
}
domainID, ok := api.domainIndex[dc.Name]
if !ok {
- return nil, fmt.Errorf("'%s' not a zone in Linode account", dc.Name)
+ return nil, 0, fmt.Errorf("'%s' not a zone in Linode account", dc.Name)
}
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -168,11 +174,11 @@ func (api *linodeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
for _, m := range create {
req, err := toReq(dc, m.Desired)
if err != nil {
- return nil, err
+ return nil, 0, err
}
j, err := json.Marshal(req)
if err != nil {
- return nil, err
+ return nil, 0, err
}
corr := &models.Correction{
Msg: fmt.Sprintf("%s: %s", m.String(), string(j)),
@@ -194,11 +200,11 @@ func (api *linodeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
}
req, err := toReq(dc, m.Desired)
if err != nil {
- return nil, err
+ return nil, 0, err
}
j, err := json.Marshal(req)
if err != nil {
- return nil, err
+ return nil, 0, err
}
corr := &models.Correction{
Msg: fmt.Sprintf("%s, Linode ID: %d: %s", m.String(), id, string(j)),
@@ -209,7 +215,7 @@ func (api *linodeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (api *linodeProvider) getRecordsForDomain(domainID int, domain string) (models.Records, error) {
diff --git a/providers/loopia/auditrecords.go b/providers/loopia/auditrecords.go
index 0accc622c4..e8137f0637 100644
--- a/providers/loopia/auditrecords.go
+++ b/providers/loopia/auditrecords.go
@@ -1,7 +1,6 @@
package loopia
import (
- "fmt"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
)
@@ -14,20 +13,10 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-03-10: Loopia returns 404
- //Loopias TXT length limit appears to be 450 octets
- a.Add("TXT", TxtHasSegmentLen450orLonger)
+ // Loopias TXT length limit appears to be 450 octets
+ a.Add("TXT", rejectif.TxtLongerThan(450)) // Last verified 2023-03-10
a.Add("MX", rejectif.MxNull) // Last verified 2023-03-23
return a.Audit(records)
}
-
-// TxtHasSegmentLen450orLonger audits TXT records for strings that are >450 octets.
-func TxtHasSegmentLen450orLonger(rc *models.RecordConfig) error {
- for _, txt := range rc.TxtStrings {
- if len(txt) > 450 {
- return fmt.Errorf("%q txtstring length > 450", rc.GetLabel())
- }
- }
- return nil
-}
diff --git a/providers/loopia/loopiaProvider.go b/providers/loopia/loopiaProvider.go
index 96815a9b52..1d4908c330 100644
--- a/providers/loopia/loopiaProvider.go
+++ b/providers/loopia/loopiaProvider.go
@@ -25,7 +25,6 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns/dnsutil"
)
@@ -34,18 +33,24 @@ import (
// init registers the provider to dnscontrol.
func init() {
+ const providerName = "LOOPIA"
+ const providerMaintainer = "@systemcrash"
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("LOOPIA", fns, features)
- providers.RegisterRegistrarType("LOOPIA", newReg)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterRegistrarType(providerName, newReg)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// features declares which features and options are available.
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAKAMAICDN: providers.Cannot(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseAzureAlias: providers.Cannot(),
@@ -79,7 +84,7 @@ func newReg(conf map[string]string) (providers.Registrar, error) {
}
// newHelper generates a handle.
-func newHelper(m map[string]string, metadata json.RawMessage) (*APIClient, error) {
+func newHelper(m map[string]string, _ json.RawMessage) (*APIClient, error) {
if m["username"] == "" {
return nil, fmt.Errorf("missing Loopia API username")
}
@@ -265,28 +270,26 @@ func gatherAffectedLabels(groups map[models.RecordKey][]string) (labels map[stri
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *APIClient) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (c *APIClient) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
if c.Debug {
debugRecords("GenerateZoneRecordsCorrections input:\n", existingRecords)
}
- // Normalize
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
PrepDesiredRecords(dc)
var keysToUpdate map[models.RecordKey][]string
differ := diff.NewCompat(dc)
- toReport, create, del, modify, err := differ.IncrementalDiff(existingRecords)
+ toReport, create, del, modify, actualChangeCount, err := differ.IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
- keysToUpdate, _, err = differ.ChangedGroups(existingRecords)
+ keysToUpdate, _, _, err = differ.ChangedGroups(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, d := range create {
@@ -362,7 +365,7 @@ func (c *APIClient) GetZoneRecordsCorrections(dc *models.DomainConfig, existingR
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// debugRecords prints a list of RecordConfig.
diff --git a/providers/luadns/luadnsProvider.go b/providers/luadns/luadnsProvider.go
index 2359175052..aa240ab804 100644
--- a/providers/luadns/luadnsProvider.go
+++ b/providers/luadns/luadnsProvider.go
@@ -19,7 +19,10 @@ Info required in `creds.json`:
*/
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
@@ -33,11 +36,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "LUADNS"
+ const providerMaintainer = "@riku22"
fns := providers.DspFuncs{
Initializer: NewLuaDNS,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("LUADNS", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// NewLuaDNS creates the provider.
@@ -94,21 +100,21 @@ func (l *luadnsProvider) GetZoneRecords(domain string, meta map[string]string) (
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (l *luadnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+func (l *luadnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
checkNS(dc)
domainID, err := l.getDomainID(dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
var corrs []*models.Correction
- changes, err := diff2.ByRecord(records, dc, nil)
+ changes, actualChangeCount, err := diff2.ByRecord(records, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, change := range changes {
@@ -127,7 +133,7 @@ func (l *luadnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, reco
}
corrections = append(corrections, corrs...)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (l *luadnsProvider) makeCreateCorrection(newrec *models.RecordConfig, domainID uint32, msg string) []*models.Correction {
diff --git a/providers/msdns/auditrecords.go b/providers/msdns/auditrecords.go
index f031369f66..86e9169006 100644
--- a/providers/msdns/auditrecords.go
+++ b/providers/msdns/auditrecords.go
@@ -15,21 +15,19 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 20-0212-28
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2023-12-18
+
a.Add("TXT", rejectif.TxtHasBackticks) // Last verified 2023-02-02
a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-02-02
- a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2023-02-02
-
- a.Add("TXT", rejectif.TxtHasSegmentLen256orLonger) // Last verified 2023-02-02
+ a.Add("TXT", rejectif.TxtHasSemicolon) // Last verified 2023-12-18
a.Add("TXT", rejectif.TxtHasSingleQuotes) // Last verified 2023-02-02
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-02-02
- a.Add("TXT", rejectif.TxtIsExactlyLen255) // Last verified 2023-02-02
-
- a.Add("TXT", rejectif.TxtIsExactlyLen255) // Last verified 2023-02-02
+ a.Add("TXT", rejectif.TxtLongerThan(254)) // Last verified 2023-12-18
return a.Audit(records)
}
diff --git a/providers/msdns/corrections.go b/providers/msdns/corrections.go
index f58e2ba20c..57ad783b7a 100644
--- a/providers/msdns/corrections.go
+++ b/providers/msdns/corrections.go
@@ -5,20 +5,17 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
)
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (client *msdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
+func (client *msdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
- // Normalize
models.PostProcessRecords(foundRecords)
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
- changes, err := diff2.ByRecord(foundRecords, dc, nil)
+ changes, actualChangeCount, err := diff2.ByRecord(foundRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
var corr *models.Correction
@@ -69,7 +66,7 @@ func (client *msdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (client *msdnsProvider) deleteOneRecord(dnsserver, zonename string, oldrec *models.RecordConfig) error {
diff --git a/providers/msdns/msdnsProvider.go b/providers/msdns/msdnsProvider.go
index b8fffc5784..c543f7eb77 100644
--- a/providers/msdns/msdnsProvider.go
+++ b/providers/msdns/msdnsProvider.go
@@ -19,7 +19,10 @@ type msdnsProvider struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Cannot(),
providers.CanUseDS: providers.Unimplemented(),
@@ -37,11 +40,14 @@ var features = providers.DocumentationNotes{
//
// This establishes the name (all caps), and the function to call to initialize it.
func init() {
+ const providerName = "MSDNS"
+ const providerMaintainer = "@tlimoncelli"
fns := providers.DspFuncs{
Initializer: newDNS,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("MSDNS", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
diff --git a/providers/msdns/powershell.go b/providers/msdns/powershell.go
index 367deb5a51..3666a34248 100644
--- a/providers/msdns/powershell.go
+++ b/providers/msdns/powershell.go
@@ -8,11 +8,11 @@ import (
"os"
"github.com/StackExchange/dnscontrol/v4/models"
+ ps "github.com/StackExchange/dnscontrol/v4/pkg/powershell"
+ "github.com/StackExchange/dnscontrol/v4/pkg/powershell/backend"
+ "github.com/StackExchange/dnscontrol/v4/pkg/powershell/middleware"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/TomOnTime/utfutil"
- ps "github.com/bhendo/go-powershell"
- "github.com/bhendo/go-powershell/backend"
- "github.com/bhendo/go-powershell/middleware"
)
type psHandle struct {
@@ -316,7 +316,7 @@ func generatePSCreate(dnsserver, domain string, rec *models.RecordConfig) string
//case "WKS":
// fmt.Fprintf(&b, ` -Wks -InternetAddress -InternetProtocol {UDP | TCP} -Service `, rec.GetTargetField())
case "TXT":
- //printer.Printf("DEBUG TXT len = %v\n", rec.TxtStrings)
+ //printer.Printf("DEBUG TXT len = %v\n", rec.GetTargetTXTSegmentCount())
//printer.Printf("DEBUG TXT target = %q\n", rec.GetTargetField())
fmt.Fprintf(&b, ` -Txt -DescriptiveText %q`, rec.GetTargetTXTJoined())
//case "RT":
diff --git a/providers/mythicbeasts/auditrecords.go b/providers/mythicbeasts/auditrecords.go
index 7f7d242cb4..e778b6a5eb 100644
--- a/providers/mythicbeasts/auditrecords.go
+++ b/providers/mythicbeasts/auditrecords.go
@@ -10,6 +10,8 @@ import (
// supported, an empty list is returned.
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
- a.Add("TXT", rejectif.TxtHasDoubleQuotes)
- return nil
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2024-12-29
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2024-12-29
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-12-29
+ return a.Audit(records)
}
diff --git a/providers/mythicbeasts/mythicbeastsProvider.go b/providers/mythicbeasts/mythicbeastsProvider.go
index f5798c0123..a386f24500 100644
--- a/providers/mythicbeasts/mythicbeastsProvider.go
+++ b/providers/mythicbeasts/mythicbeastsProvider.go
@@ -7,7 +7,6 @@ import (
"encoding/json"
"fmt"
"io"
- "io/ioutil"
"net/http"
"strings"
@@ -31,13 +30,16 @@ type mythicBeastsProvider struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUsePTR: providers.Can(),
- providers.CanUseSSHFP: providers.Can(),
providers.CanUseSRV: providers.Can(),
+ providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Cannot("Requires domain registered through Web UI"),
providers.DocDualHost: providers.Can(),
@@ -45,11 +47,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "MYTHICBEASTS"
+ const providerMaintainer = "@tomfitzhenry"
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("MYTHICBEASTS", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
@@ -85,7 +90,7 @@ func (n *mythicBeastsProvider) GetZoneRecords(domain string, meta map[string]str
return nil, err
}
if got, want := resp.StatusCode, 200; got != want {
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("got HTTP %v, want %v: %v", got, want, string(body))
}
return zoneFileToRecords(resp.Body, domain)
@@ -109,11 +114,12 @@ func zoneFileToRecords(r io.Reader, origin string) (models.Records, error) {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (n *mythicBeastsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
- msgs, changes, err := diff2.ByZone(actual, dc, nil)
+func (n *mythicBeastsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
+ result, err := diff2.ByZone(actual, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
+ msgs, changes, actualChangeCount := result.Msgs, result.HasChanges, result.ActualChangeCount
var corrections []*models.Correction
if changes {
@@ -122,7 +128,7 @@ func (n *mythicBeastsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig
Msg: strings.Join(msgs, "\n"),
F: func() error {
var b strings.Builder
- for _, record := range dc.Records {
+ for _, record := range result.DesiredPlus {
switch rr := record.ToRR().(type) {
case *dns.SSHFP:
// "Hex strings [for SSHFP] must be in lower-case", per Mythic Beasts API docs.
@@ -138,7 +144,7 @@ func (n *mythicBeastsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig
return err
}
if got, want := resp.StatusCode, 200; got != want {
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("got HTTP %v, want %v: %v", got, want, string(body))
}
return nil
@@ -146,7 +152,7 @@ func (n *mythicBeastsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// GetNameservers returns the nameservers for a domain.
diff --git a/providers/namecheap/namecheapProvider.go b/providers/namecheap/namecheapProvider.go
index ddbc2a0273..057f95b3c8 100644
--- a/providers/namecheap/namecheapProvider.go
+++ b/providers/namecheap/namecheapProvider.go
@@ -26,29 +26,34 @@ type namecheapProvider struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUsePTR: providers.Cannot(),
providers.CanUseSRV: providers.Cannot("The namecheap web console allows you to make SRV records, but their api does not let you read or set them"),
providers.CanUseTLSA: providers.Cannot(),
- providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("Requires domain registered through their service"),
providers.DocDualHost: providers.Cannot("Doesn't allow control of apex NS records"),
providers.DocOfficiallySupported: providers.Cannot(),
}
func init() {
- providers.RegisterRegistrarType("NAMECHEAP", newReg)
+ const providerName = "NAMECHEAP"
+ const providerMaintainer = "@willpower232"
+ providers.RegisterRegistrarType(providerName, newReg)
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("NAMECHEAP", fns, features)
- providers.RegisterCustomRecordType("URL", "NAMECHEAP", "")
- providers.RegisterCustomRecordType("URL301", "NAMECHEAP", "")
- providers.RegisterCustomRecordType("FRAME", "NAMECHEAP", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterCustomRecordType("URL", providerName, "")
+ providers.RegisterCustomRecordType("URL301", providerName, "")
+ providers.RegisterCustomRecordType("FRAME", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
@@ -59,7 +64,7 @@ func newReg(conf map[string]string) (providers.Registrar, error) {
return newProvider(conf, nil)
}
-func newProvider(m map[string]string, metadata json.RawMessage) (*namecheapProvider, error) {
+func newProvider(m map[string]string, _ json.RawMessage) (*namecheapProvider, error) {
api := &namecheapProvider{}
api.APIUser, api.APIKEY = m["apiuser"], m["apikey"]
if api.APIKEY == "" || api.APIUser == "" {
@@ -231,7 +236,7 @@ func (n *namecheapProvider) GetZoneRecords(domain string, meta map[string]string
// }
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (n *namecheapProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
+func (n *namecheapProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
// namecheap does not allow setting @ NS with basic DNS
dc.Filter(func(r *models.RecordConfig) bool {
@@ -244,9 +249,9 @@ func (n *namecheapProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, a
return true
})
- toReport, create, delete, modify, err := diff.NewCompat(dc).IncrementalDiff(actual)
+ toReport, create, delete, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(actual)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -278,7 +283,7 @@ func (n *namecheapProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, a
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func toRecords(result *nc.DomainDNSGetHostsResult, origin string) ([]*models.RecordConfig, error) {
diff --git a/providers/namedotcom/auditrecords.go b/providers/namedotcom/auditrecords.go
index 7db65ad504..94d8442d25 100644
--- a/providers/namedotcom/auditrecords.go
+++ b/providers/namedotcom/auditrecords.go
@@ -18,9 +18,13 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28
- a.Add("TXT", MaxLengthNDC) // Last verified 2021-03-01
+ a.Add("TXT", MaxLengthNDC) // Last verified 2024-12-17
- a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-03-01
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2024-12-17
+
+ a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2024-12-17
+
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-12-17
return a.Audit(records)
}
@@ -29,18 +33,19 @@ func AuditRecords(records []*models.RecordConfig) []error {
// are longer than permitted by NDC. Sadly their
// length limit is undocumented. This seems to work.
func MaxLengthNDC(rc *models.RecordConfig) error {
- if len(rc.TxtStrings) == 0 {
+ txtStrings := rc.GetTargetTXTSegmented()
+ if len(txtStrings) == 0 {
return nil
}
sum := 2 // Count the start and end quote.
// Add the length of each segment.
- for _, segment := range rc.TxtStrings {
+ for _, segment := range txtStrings {
sum += len(segment) // The length of each segment
sum += strings.Count(segment, `"`) // Add 1 for any char to be escaped
}
// Add 3 (quote space quote) for each interior join.
- sum += 3 * (len(rc.TxtStrings) - 1)
+ sum += 3 * (len(txtStrings) - 1)
if sum > 512 {
return fmt.Errorf("encoded txt too long")
diff --git a/providers/namedotcom/namedotcomProvider.go b/providers/namedotcom/namedotcomProvider.go
index 9e10f5e137..a4f8c5fd03 100644
--- a/providers/namedotcom/namedotcomProvider.go
+++ b/providers/namedotcom/namedotcomProvider.go
@@ -21,7 +21,10 @@ type namedotcomProvider struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUsePTR: providers.Cannot("PTR records are not supported (See Link)", "https://www.name.com/support/articles/205188508-Reverse-DNS-records"),
@@ -62,10 +65,13 @@ func newProvider(conf map[string]string) (*namedotcomProvider, error) {
}
func init() {
- providers.RegisterRegistrarType("NAMEDOTCOM", newReg)
+ const providerName = "NAMEDOTCOM"
+ const providerMaintainer = "NEEDS VOLUNTEER"
+ providers.RegisterRegistrarType(providerName, newReg)
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("NAMEDOTCOM", fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
diff --git a/providers/namedotcom/records.go b/providers/namedotcom/records.go
index b9edb4ef8e..944f68d272 100644
--- a/providers/namedotcom/records.go
+++ b/providers/namedotcom/records.go
@@ -3,7 +3,6 @@ package namedotcom
import (
"errors"
"fmt"
- "regexp"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
@@ -27,7 +26,7 @@ func (n *namedotcomProvider) GetZoneRecords(domain string, meta map[string]strin
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (n *namedotcomProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
+func (n *namedotcomProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
checkNSModifications(dc)
for _, rec := range dc.Records {
@@ -36,9 +35,9 @@ func (n *namedotcomProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
}
}
- toReport, create, del, mod, err := diff.NewCompat(dc).IncrementalDiff(actual)
+ toReport, create, del, mod, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(actual)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -66,7 +65,7 @@ func (n *namedotcomProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
corrections = append(corrections, c)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func checkNSModifications(dc *models.DomainConfig) {
@@ -94,7 +93,7 @@ func toRecord(r *namecom.Record, origin string) *models.RecordConfig {
rc.SetLabelFromFQDN(fqdn, origin)
switch rtype := r.Type; rtype { // #rtype_variations
case "TXT":
- rc.SetTargetTXTs(decodeTxt(r.Answer))
+ rc.SetTargetTXT(r.Answer)
case "MX":
if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
panic(fmt.Errorf("unparsable MX record received from ndc: %w", err))
@@ -154,7 +153,7 @@ func (n *namedotcomProvider) createRecord(rc *models.RecordConfig, domain string
case "A", "AAAA", "ANAME", "CNAME", "MX", "NS":
// nothing
case "TXT":
- // record.Answer = encodeTxt(rc.TxtStrings)
+ record.Answer = rc.GetTargetTXTJoined()
case "SRV":
if rc.GetTargetField() == "." {
return errors.New("SRV records with empty targets are not supported (as of 2019-11-05, the API returns 'Parameter Value Error - Invalid Srv Format')")
@@ -170,24 +169,6 @@ func (n *namedotcomProvider) createRecord(rc *models.RecordConfig, domain string
return err
}
-// finds a string surrounded by quotes that might contain an escaped quote character.
-var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`)
-
-// decodeTxt decodes the TXT record as received from name.com and
-// returns the list of strings.
-func decodeTxt(s string) []string {
-
- if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
- txtStrings := []string{}
- for _, t := range quotedStringRegexp.FindAllStringSubmatch(s, -1) {
- txtString := strings.Replace(t[1], `\"`, `"`, -1)
- txtStrings = append(txtStrings, txtString)
- }
- return txtStrings
- }
- return []string{s}
-}
-
func (n *namedotcomProvider) deleteRecord(id int32, domain string) error {
request := &namecom.DeleteRecordRequest{
DomainName: domain,
diff --git a/providers/namedotcom/records_test.go b/providers/namedotcom/records_test.go
deleted file mode 100644
index 0424178fd8..0000000000
--- a/providers/namedotcom/records_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package namedotcom
-
-import (
- "strings"
- "testing"
-)
-
-var txtData = []struct {
- decoded []string
- encoded string
-}{
- {[]string{`simple`}, `simple`},
- {[]string{`changed`}, `changed`},
- {[]string{`with spaces`}, `with spaces`},
- {[]string{`with whitespace`}, `with whitespace`},
- {[]string{"one", "two"}, `"one""two"`},
- {[]string{"eh", "bee", "cee"}, `"eh""bee""cee"`},
- {[]string{"o\"ne", "tw\"o"}, `"o\"ne""tw\"o"`},
- {[]string{"dimple"}, `dimple`},
- {[]string{"fun", "two"}, `"fun""two"`},
- {[]string{"eh", "bzz", "cee"}, `"eh""bzz""cee"`},
-}
-
-// func TestEncodeTxt(t *testing.T) {
-// // Test encoded the lists of strings into a string:
-// for i, test := range txtData {
-// enc := encodeTxt(test.decoded)
-// if enc != test.encoded {
-// t.Errorf("%v: txt\n data: []string{%v}\nexpected: %s\n got: %s",
-// i, "`"+strings.Join(test.decoded, "`, `")+"`", test.encoded, enc)
-// }
-// }
-//}
-
-func TestDecodeTxt(t *testing.T) {
- // Test decoded a string into the list of strings:
- for i, test := range txtData {
- data := test.encoded
- got := decodeTxt(data)
- wanted := test.decoded
- if len(got) != len(wanted) {
- t.Errorf("%v: txt\n decode: %v\nexpected: `%v`\n got: `%v`\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
- } else {
- for j := range got {
- if got[j] != wanted[j] {
- t.Errorf("%v: txt\n decode: %v\nexpected: `%v`\n got: `%v`\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
- }
- }
- }
- }
-}
diff --git a/providers/netcup/netcupProvider.go b/providers/netcup/netcupProvider.go
index e55901772d..e3d336b7ed 100644
--- a/providers/netcup/netcupProvider.go
+++ b/providers/netcup/netcupProvider.go
@@ -10,7 +10,10 @@ import (
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Cannot(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUsePTR: providers.Cannot(),
@@ -21,11 +24,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "NETCUP"
+ const providerMaintainer = "@kordianbruck"
fns := providers.DspFuncs{
Initializer: New,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("NETCUP", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// New creates a new API handle.
@@ -68,12 +74,9 @@ func (api *netcupProvider) GetNameservers(domain string) ([]*models.Nameserver,
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *netcupProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (api *netcupProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
domain := dc.Name
- // no need for txtutil.SplitSingleLongTxt in function GetDomainCorrections
- // txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
// Setting the TTL is not supported for netcup
for _, r := range dc.Records {
r.TTL = 0
@@ -88,9 +91,9 @@ func (api *netcupProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
}
dc.Records = newRecords
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -130,5 +133,5 @@ func (api *netcupProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, ex
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
diff --git a/providers/netlify/netlifyProvider.go b/providers/netlify/netlifyProvider.go
index 2bdb1c1e8e..65b106e5f5 100644
--- a/providers/netlify/netlifyProvider.go
+++ b/providers/netlify/netlifyProvider.go
@@ -12,8 +12,11 @@ import (
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
@@ -30,13 +33,16 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "NETLIFY"
+ const providerMaintainer = "@SphericalKat"
fns := providers.DspFuncs{
Initializer: newNetlify,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("NETLIFY", fns, features)
- providers.RegisterCustomRecordType("NETLIFY", "NETLIFY", "")
- providers.RegisterCustomRecordType("NETLIFYv6", "NETLIFY", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterCustomRecordType(providerName, providerName, "")
+ providers.RegisterCustomRecordType("NETLIFYv6", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
type netlifyProvider struct {
@@ -138,18 +144,33 @@ func (n *netlifyProvider) GetZoneRecords(domain string, meta map[string]string)
return cleanRecords, nil
}
-// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (n *netlifyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(records)
+// ListZones returns all DNS zones managed by this provider.
+func (n *netlifyProvider) ListZones() ([]string, error) {
+ zones, err := n.getDNSZones()
if err != nil {
return nil, err
}
+
+ zoneNames := make([]string, len(zones))
+ for i, z := range zones {
+ zoneNames[i] = z.Name
+ }
+
+ return zoneNames, nil
+}
+
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *netlifyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, int, error) {
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(records)
+ if err != nil {
+ return nil, 0, err
+ }
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
zone, err := n.getZone(dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Deletes first so changing type works etc.
@@ -193,7 +214,7 @@ func (n *netlifyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, rec
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func toReq(rc *models.RecordConfig) *dnsRecordCreate {
diff --git a/providers/ns1/auditrecords.go b/providers/ns1/auditrecords.go
index d30fc86109..60a0827930 100644
--- a/providers/ns1/auditrecords.go
+++ b/providers/ns1/auditrecords.go
@@ -11,7 +11,7 @@ import (
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
- a.Add("TXT", rejectif.TxtHasMultipleSegments)
+ a.Add("TXT", rejectif.TxtIsEmpty)
return a.Audit(records)
}
diff --git a/providers/ns1/ns1Provider.go b/providers/ns1/ns1Provider.go
index 59a048cc35..c6dd83661d 100644
--- a/providers/ns1/ns1Provider.go
+++ b/providers/ns1/ns1Provider.go
@@ -9,6 +9,7 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/providers"
"gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
@@ -16,15 +17,23 @@ import (
)
var docNotes = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
+ providers.CanUseDNAME: providers.Can(),
providers.CanUseDS: providers.Can(),
providers.CanUseDSForChildren: providers.Can(),
+ providers.CanUseDHCID: providers.Can(),
+ providers.CanUseHTTPS: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
+ providers.CanUseSVCB: providers.Can(),
+ providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
@@ -34,12 +43,15 @@ var docNotes = providers.DocumentationNotes{
const clientRetries = 10
func init() {
+ const providerName = "NS1"
+ const providerMaintainer = "@costasd"
fns := providers.DspFuncs{
Initializer: newProvider,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("NS1", fns, providers.CanUseSRV, docNotes)
- providers.RegisterCustomRecordType("NS1_URLFWD", "NS1", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, providers.CanUseSRV, docNotes)
+ providers.RegisterCustomRecordType("NS1_URLFWD", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
type nsone struct {
@@ -186,7 +198,7 @@ func (n *nsone) getDomainCorrectionsDNSSEC(domain, toggleDNSSEC string) *models.
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (n *nsone) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (n *nsone) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
domain := dc.Name
@@ -195,9 +207,9 @@ func (n *nsone) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecor
corrections = append(corrections, dnssecCorrections)
}
- changes, err := diff2.ByRecordSet(existingRecords, dc, nil)
+ changes, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, change := range changes {
@@ -228,7 +240,7 @@ func (n *nsone) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecor
}
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (n *nsone) add(recs models.Records, domain string) error {
@@ -302,7 +314,7 @@ func buildRecord(recs models.Records, domain string, id string) *dns.Record {
if r.Type == "MX" {
rec.AddAnswer(&dns.Answer{Rdata: strings.Fields(fmt.Sprintf("%d %v", r.MxPreference, r.GetTargetField()))})
} else if r.Type == "TXT" {
- rec.AddAnswer(&dns.Answer{Rdata: r.TxtStrings})
+ rec.AddAnswer(&dns.Answer{Rdata: []string{r.GetTargetTXTJoined()}})
} else if r.Type == "CAA" {
rec.AddAnswer(&dns.Answer{
Rdata: []string{
@@ -327,8 +339,20 @@ func buildRecord(recs models.Records, domain string, id string) *dns.Record {
strconv.Itoa(int(r.DsDigestType)),
r.DsDigest}})
} else if r.Type == "NS1_URLFWD" {
+ printer.Warnf("NS1_URLFWD is deprecated and may stop working anytime now. Please avoid such records going forward.\n")
rec.Type = "URLFWD"
rec.AddAnswer(&dns.Answer{Rdata: strings.Fields(r.GetTargetField())})
+ } else if r.Type == "SVCB" || r.Type == "HTTPS" {
+ rec.AddAnswer(&dns.Answer{Rdata: []string{
+ strconv.Itoa(int(r.SvcPriority)),
+ r.GetTargetField(),
+ r.SvcParams}})
+ } else if r.Type == "TLSA" {
+ rec.AddAnswer(&dns.Answer{Rdata: []string{
+ strconv.Itoa(int(r.TlsaUsage)),
+ strconv.Itoa(int(r.TlsaSelector)),
+ strconv.Itoa(int(r.TlsaMatchingType)),
+ r.GetTargetField()}})
} else {
rec.AddAnswer(&dns.Answer{Rdata: strings.Fields(r.GetTargetField())})
}
@@ -366,6 +390,14 @@ func convert(zr *dns.ZoneRecord, domain string) ([]*models.RecordConfig, error)
if err := rec.SetTargetCAAStrings(xAns[0], xAns[1], xAns[2]); err != nil {
return nil, fmt.Errorf("unparsable %s record received from ns1: %w", rtype, err)
}
+ case "REDIRECT":
+ // NS1 returns REDIRECTs as records, but there is only one and dummy answer:
+ // "NS1 MANAGED RECORD"
+ // Redirects are managed via a different API endpoint https://api.nsone.net/v1/redirect
+ // It also involves cert management
+ // We may simpply ignore REDIRECTs for now until we support it
+ printer.Warnf("NS1_REDIRECT is NOT supported by dnscontrol and all existing redirects are ignored.\n")
+ continue
default:
if err := rec.PopulateFromString(rtype, ans, domain); err != nil {
return nil, fmt.Errorf("unparsable record received from ns1: %w", err)
diff --git a/providers/opensrs/auditrecords.go b/providers/opensrs/auditrecords.go
deleted file mode 100644
index 792b39a771..0000000000
--- a/providers/opensrs/auditrecords.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package opensrs
-
-import "github.com/StackExchange/dnscontrol/v4/models"
-
-// AuditRecords returns a list of errors corresponding to the records
-// that aren't supported by this provider. If all records are
-// supported, an empty list is returned.
-func AuditRecords(records []*models.RecordConfig) []error {
- return nil
-}
diff --git a/providers/opensrs/opensrsProvider.go b/providers/opensrs/opensrsProvider.go
index 08aebf76c5..3f9f41c8fc 100644
--- a/providers/opensrs/opensrsProvider.go
+++ b/providers/opensrs/opensrsProvider.go
@@ -12,8 +12,17 @@ import (
opensrs "github.com/philhug/opensrs-go/opensrs"
)
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanConcur: providers.Cannot(),
+}
+
func init() {
- providers.RegisterRegistrarType("OPENSRS", newReg)
+ const providerName = "OPENSRS"
+ const providerMaintainer = "@philhug"
+ providers.RegisterRegistrarType(providerName, newReg, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
var defaultNameServerNames = []string{
diff --git a/providers/oracle/auditrecords.go b/providers/oracle/auditrecords.go
index 629ff55e43..1f24276af8 100644
--- a/providers/oracle/auditrecords.go
+++ b/providers/oracle/auditrecords.go
@@ -1,10 +1,19 @@
package oracle
-import "github.com/StackExchange/dnscontrol/v4/models"
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
// AuditRecords returns a list of errors corresponding to the records
// that aren't supported by this provider. If all records are
// supported, an empty list is returned.
func AuditRecords(records []*models.RecordConfig) []error {
- return nil
+ a := rejectif.Auditor{}
+
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-08-21
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2024-08-21
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2024-08-21
+
+ return a.Audit(records)
}
diff --git a/providers/oracle/oracleProvider.go b/providers/oracle/oracleProvider.go
index 892e6cee8a..4f5b37dbb9 100644
--- a/providers/oracle/oracleProvider.go
+++ b/providers/oracle/oracleProvider.go
@@ -3,21 +3,24 @@ package oracle
import (
"context"
"encoding/json"
+ "fmt"
"strings"
"time"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v4/providers"
- "github.com/oracle/oci-go-sdk/v32/common"
- "github.com/oracle/oci-go-sdk/v32/dns"
- "github.com/oracle/oci-go-sdk/v32/example/helpers"
+ "github.com/oracle/oci-go-sdk/v65/common"
+ "github.com/oracle/oci-go-sdk/v65/dns"
+ "github.com/oracle/oci-go-sdk/v65/example/helpers"
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(), // should be supported, but getting 500s in tests
@@ -33,11 +36,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "ORACLE"
+ const providerMaintainer = "@kallsyms"
fns := providers.DspFuncs{
Initializer: New,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("ORACLE", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
type oracleProvider struct {
@@ -59,6 +65,12 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
return nil, err
}
+ // Set default retry policy to handle 429 automatically
+ defaultRetryPolicy := common.DefaultRetryPolicy()
+ client.SetCustomClientConfiguration(common.CustomClientConfiguration{
+ RetryPolicy: &defaultRetryPolicy,
+ })
+
return &oracleProvider{
client: client,
compartment: settings["compartment"],
@@ -81,6 +93,22 @@ func (o *oracleProvider) ListZones() ([]string, error) {
for i, zone := range listResp.Items {
zones[i] = *zone.Name
}
+
+ for listResp.OpcNextPage != nil {
+ listResp, err = o.client.ListZones(ctx, dns.ListZonesRequest{
+ CompartmentId: &o.compartment,
+ Page: listResp.OpcNextPage,
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ for _, zone := range listResp.Items {
+ zones = append(zones, *zone.Name)
+ }
+ }
+
return zones, nil
}
@@ -96,7 +124,7 @@ func (o *oracleProvider) EnsureZoneExists(domain string) error {
if err == nil {
return nil
}
- if err != nil && getResp.RawResponse.StatusCode != 404 {
+ if getResp.RawResponse.StatusCode != 404 {
return err
}
@@ -118,7 +146,7 @@ func (o *oracleProvider) EnsureZoneExists(domain string) error {
}
return true
}
- _, err = o.client.GetZone(ctx, dns.GetZoneRequest{
+ getResp, err = o.client.GetZone(ctx, dns.GetZoneRequest{
ZoneNameOrId: &domain,
CompartmentId: &o.compartment,
RequestMetadata: helpers.GetRequestMetadataWithCustomizedRetryPolicy(pollUntilAvailable),
@@ -144,7 +172,18 @@ func (o *oracleProvider) GetNameservers(domain string) ([]*models.Nameserver, er
nss[i] = *ns.Hostname
}
- return models.ToNameservers(nss)
+ nssNoStrip, err := models.ToNameservers(nss)
+
+ if err != nil {
+ nssStrip, err := models.ToNameserversStripTD(nss)
+ if err != nil {
+ return nil, fmt.Errorf("could not determine if trailing dots should be stripped or not")
+ }
+
+ return nssStrip, nil
+ }
+
+ return nssNoStrip, nil
}
func (o *oracleProvider) GetZoneRecords(zone string, meta map[string]string) (models.Records, error) {
@@ -202,9 +241,8 @@ func (o *oracleProvider) GetZoneRecords(zone string, meta map[string]string) (mo
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
var err error
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
// Ensure we don't emit changes for attempted modification of built-in apex NSs
for _, rec := range dc.Records {
@@ -218,15 +256,15 @@ func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis
continue
}
- if rec.TTL != 86400 {
+ if rec.GetLabel() == "@" && rec.TTL != 86400 {
printer.Warnf("Oracle Cloud forces TTL=86400 for NS records. Ignoring configured TTL of %d for %s\n", rec.TTL, recNS)
rec.TTL = 86400
}
}
- toReport, create, dels, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, dels, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -248,7 +286,6 @@ func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis
createRecords = append(createRecords, rec.Desired)
desc += rec.String() + "\n"
}
- desc = desc[:len(desc)-1]
}
if len(dels) > 0 {
@@ -256,7 +293,6 @@ func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis
deleteRecords = append(deleteRecords, rec.Existing)
desc += rec.String() + "\n"
}
- desc = desc[:len(desc)-1]
}
if len(modify) > 0 {
@@ -265,7 +301,6 @@ func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis
deleteRecords = append(deleteRecords, rec.Existing)
desc += rec.String() + "\n"
}
- desc = desc[:len(desc)-1]
}
// There were corrections. Send them as one big batch:
@@ -278,7 +313,7 @@ func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (o *oracleProvider) patch(createRecords, deleteRecords models.Records, domain string) error {
@@ -305,6 +340,7 @@ func (o *oracleProvider) patch(createRecords, deleteRecords models.Records, doma
batchEnd = len(ops)
}
patchReq.Items = ops[batchStart:batchEnd]
+
_, err := o.client.PatchZoneRecords(ctx, patchReq)
if err != nil {
return err
diff --git a/providers/ovh/ovhProvider.go b/providers/ovh/ovhProvider.go
index d3db32430b..006dae3cab 100644
--- a/providers/ovh/ovhProvider.go
+++ b/providers/ovh/ovhProvider.go
@@ -18,7 +18,10 @@ type ovhProvider struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Unimplemented(),
@@ -31,10 +34,10 @@ var features = providers.DocumentationNotes{
providers.DocOfficiallySupported: providers.Cannot(),
}
-func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error) {
+func newOVH(m map[string]string, _ json.RawMessage) (*ovhProvider, error) {
appKey, appSecretKey, consumerKey := m["app-key"], m["app-secret-key"], m["consumer-key"]
- c, err := ovh.NewClient(ovh.OvhEU, appKey, appSecretKey, consumerKey)
+ c, err := ovh.NewClient(getOVHEndpoint(m), appKey, appSecretKey, consumerKey)
if c == nil {
return nil, err
}
@@ -46,6 +49,22 @@ func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error)
return ovh, nil
}
+func getOVHEndpoint(params map[string]string) string {
+ if ep, ok := params["endpoint"]; ok && ep != "" {
+ switch strings.ToLower(ep) {
+ case "eu":
+ return ovh.OvhEU
+ case "ca":
+ return ovh.OvhCA
+ case "us":
+ return ovh.OvhUS
+ default:
+ return ep
+ }
+ }
+ return ovh.OvhEU
+}
+
func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
return newOVH(conf, metadata)
}
@@ -55,12 +74,15 @@ func newReg(conf map[string]string) (providers.Registrar, error) {
}
func init() {
+ const providerName = "OVH"
+ const providerMaintainer = "@masterzen"
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterRegistrarType("OVH", newReg)
- providers.RegisterDomainServiceProviderType("OVH", fns, features)
+ providers.RegisterRegistrarType(providerName, newReg)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
@@ -119,14 +141,23 @@ func (c *ovhProvider) GetZoneRecords(domain string, meta map[string]string) (mod
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *ovhProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
+func (c *ovhProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
- corrections, err := c.getDiff2DomainCorrections(dc, actual)
+ corrections, actualChangeCount, err := c.getDiff2DomainCorrections(dc, actual)
if err != nil {
- return nil, err
+ return nil, 0, err
}
- if len(corrections) > 0 {
+ // Only refresh zone if there's a real modification
+ reportOnlyCorrections := true
+ for _, c := range corrections {
+ if c.F != nil {
+ reportOnlyCorrections = false
+ break
+ }
+ }
+
+ if !reportOnlyCorrections {
corrections = append(corrections, &models.Correction{
Msg: "REFRESH zone " + dc.Name,
F: func() error {
@@ -135,14 +166,14 @@ func (c *ovhProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
-func (c *ovhProvider) getDiff2DomainCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
+func (c *ovhProvider) getDiff2DomainCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
- instructions, err := diff2.ByRecord(actual, dc, nil)
+ instructions, actualChangeCount, err := diff2.ByRecord(actual, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, inst := range instructions {
@@ -169,7 +200,7 @@ func (c *ovhProvider) getDiff2DomainCorrections(dc *models.DomainConfig, actual
panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
}
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func nativeToRecord(r *Record, origin string) (*models.RecordConfig, error) {
diff --git a/providers/ovh/ovhProvider_test.go b/providers/ovh/ovhProvider_test.go
new file mode 100644
index 0000000000..b5a07cea88
--- /dev/null
+++ b/providers/ovh/ovhProvider_test.go
@@ -0,0 +1,51 @@
+package ovh
+
+import (
+ "testing"
+
+ "github.com/ovh/go-ovh/ovh"
+)
+
+func Test_getOVHEndpoint(t *testing.T) {
+ tests := []struct {
+ name string
+ endpoint string
+ want string
+ }{
+ {
+ "default to EU", "", ovh.OvhEU,
+ },
+ {
+ "default to EU if omitted", "omitted", ovh.OvhEU,
+ },
+ {
+ "set to EU", "eu", ovh.OvhEU,
+ },
+ {
+ "set to CA", "ca", ovh.OvhCA,
+ },
+ {
+ "set to US", "us", ovh.OvhUS,
+ },
+ {
+ "case insensitive", "Eu", ovh.OvhEU,
+ },
+ {
+ "case insensitive ca", "CA", ovh.OvhCA,
+ },
+ {
+ "arbitratry", "https://blah", "https://blah",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ params := make(map[string]string)
+ if tt.endpoint != "" && tt.endpoint != "omitted" {
+ params["endpoint"] = tt.endpoint
+ }
+ if got := getOVHEndpoint(params); got != tt.want {
+ t.Errorf("getOVHEndpoint() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/providers/packetframe/packetframeProvider.go b/providers/packetframe/packetframeProvider.go
index 82e5565c25..2d012e978c 100644
--- a/providers/packetframe/packetframeProvider.go
+++ b/providers/packetframe/packetframeProvider.go
@@ -39,7 +39,10 @@ func newPacketframe(m map[string]string, metadata json.RawMessage) (providers.DN
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Unimplemented(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.DocDualHost: providers.Cannot(),
@@ -47,11 +50,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "PACKETFRAME"
+ const providerMaintainer = "@hamptonmoore"
fns := providers.DspFuncs{
Initializer: newPacketframe,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("PACKETFRAME", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// GetNameservers returns the nameservers for a domain.
@@ -100,23 +106,23 @@ func (api *packetframeProvider) GetZoneRecords(domain string, meta map[string]st
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *packetframeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (api *packetframeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
zone, err := api.getZone(dc.Name)
if err != nil {
- return nil, fmt.Errorf("no such zone %q in Packetframe account", dc.Name)
+ return nil, 0, fmt.Errorf("no such zone %q in Packetframe account", dc.Name)
}
- toReport, create, dels, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, dels, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
for _, m := range create {
- req, err := toReq(zone.ID, dc, m.Desired)
+ req, err := toReq(zone.ID, m.Desired)
if err != nil {
- return nil, err
+ return nil, 0, err
}
corr := &models.Correction{
Msg: m.String(),
@@ -150,7 +156,7 @@ func (api *packetframeProvider) GetZoneRecordsCorrections(dc *models.DomainConfi
continue
}
- req, _ := toReq(zone.ID, dc, m.Desired)
+ req, _ := toReq(zone.ID, m.Desired)
req.ID = original.ID
corr := &models.Correction{
Msg: m.String(),
@@ -162,10 +168,10 @@ func (api *packetframeProvider) GetZoneRecordsCorrections(dc *models.DomainConfi
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
-func toReq(zoneID string, dc *models.DomainConfig, rc *models.RecordConfig) (*domainRecord, error) {
+func toReq(zoneID string, rc *models.RecordConfig) (*domainRecord, error) {
req := &domainRecord{
Type: rc.Type,
TTL: int(rc.TTL),
diff --git a/providers/porkbun/api.go b/providers/porkbun/api.go
index 3145319af8..e16e649dbf 100644
--- a/providers/porkbun/api.go
+++ b/providers/porkbun/api.go
@@ -7,11 +7,14 @@ import (
"io"
"net/http"
"sort"
+ "strings"
"time"
+
+ "github.com/StackExchange/dnscontrol/v4/pkg/printer"
)
const (
- baseURL = "https://porkbun.com/api/json/v3"
+ baseURL = "https://api.porkbun.com/api/json/v3"
)
type porkbunProvider struct {
@@ -33,16 +36,27 @@ type domainRecord struct {
Content string `json:"content"`
TTL string `json:"ttl"`
Prio string `json:"prio"`
- Notes string `json:"notes"`
+ // Forwarding
+ Subdomain string `json:"subdomain"`
+ Location string `json:"location"`
+ IncludePath string `json:"includePath"`
+ Wildcard string `json:"wildcard"`
}
type recordResponse struct {
- Status string `json:"status"`
- Records []domainRecord `json:"records"`
+ Records []domainRecord `json:"records"`
+ Forwards []domainRecord `json:"forwards"`
+}
+
+type domainListRecord struct {
+ Domain string `json:"domain"`
+}
+
+type domainListResponse struct {
+ Domains []domainListRecord `json:"domains"`
}
type nsResponse struct {
- Status string `json:"status"`
Nameservers []string `json:"ns"`
}
@@ -58,9 +72,12 @@ func (c *porkbunProvider) post(endpoint string, params requestParams) ([]byte, e
client := &http.Client{}
req, _ := http.NewRequest("POST", baseURL+endpoint, bytes.NewBuffer(personJSON))
+ retrycnt := 0
+
// If request sending too fast, the server will fail with the following error:
// porkbun API error: Create error: We were unable to create the DNS record.
- time.Sleep(500 * time.Millisecond)
+retry:
+ time.Sleep(time.Second)
resp, err := client.Do(req)
if err != nil {
return []byte{}, err
@@ -68,6 +85,16 @@ func (c *porkbunProvider) post(endpoint string, params requestParams) ([]byte, e
bodyString, _ := io.ReadAll(resp.Body)
+ if resp.StatusCode == 202 || resp.StatusCode == 503 {
+ retrycnt++
+ if retrycnt == 5 {
+ return bodyString, fmt.Errorf("rate limiting exceeded")
+ }
+ printer.Warnf("Rate limiting.. waiting for %d second(s)\n", retrycnt*10)
+ time.Sleep(time.Second * time.Duration(retrycnt*10))
+ goto retry
+ }
+
// Got error from API ?
var errResp errorResponse
err = json.Unmarshal(bodyString, &errResp)
@@ -80,12 +107,6 @@ func (c *porkbunProvider) post(endpoint string, params requestParams) ([]byte, e
return bodyString, nil
}
-func (c *porkbunProvider) ping() error {
- params := requestParams{}
- _, err := c.post("/ping", params)
- return err
-}
-
func (c *porkbunProvider) createRecord(domain string, rec requestParams) error {
if _, err := c.post("/dns/create/"+domain, rec); err != nil {
return fmt.Errorf("failed create record (porkbun): %w", err)
@@ -116,7 +137,10 @@ func (c *porkbunProvider) getRecords(domain string) ([]domainRecord, error) {
}
var dr recordResponse
- json.Unmarshal(bodyString, &dr)
+ err = json.Unmarshal(bodyString, &dr)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing record list from porkbun: %w", err)
+ }
var records []domainRecord
for _, rec := range dr.Records {
@@ -128,6 +152,47 @@ func (c *porkbunProvider) getRecords(domain string) ([]domainRecord, error) {
return records, nil
}
+func (c *porkbunProvider) createURLForwardingRecord(domain string, rec requestParams) error {
+ if _, err := c.post("/domain/addUrlForward/"+domain, rec); err != nil {
+ return fmt.Errorf("failed create url forwarding record (porkbun): %w", err)
+ }
+ return nil
+}
+
+func (c *porkbunProvider) deleteURLForwardingRecord(domain string, recordID string) error {
+ params := requestParams{}
+ if _, err := c.post(fmt.Sprintf("/domain/deleteUrlForward/%s/%s", domain, recordID), params); err != nil {
+ return fmt.Errorf("failed delete url forwarding record (porkbun): %w", err)
+ }
+ return nil
+}
+
+func (c *porkbunProvider) modifyURLForwardingRecord(domain string, recordID string, rec requestParams) error {
+ if err := c.deleteURLForwardingRecord(domain, recordID); err != nil {
+ return err
+ }
+ if err := c.createURLForwardingRecord(domain, rec); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *porkbunProvider) getURLForwardingRecords(domain string) ([]domainRecord, error) {
+ params := requestParams{}
+ var bodyString, err = c.post("/domain/getUrlForwarding/"+domain, params)
+ if err != nil {
+ return nil, fmt.Errorf("failed fetching url forwarding record list from porkbun: %w", err)
+ }
+
+ var dr recordResponse
+ err = json.Unmarshal(bodyString, &dr)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing url forwarding record list from porkbun: %w", err)
+ }
+
+ return dr.Forwards, nil
+}
+
func (c *porkbunProvider) getNameservers(domain string) ([]string, error) {
params := requestParams{}
var bodyString, err = c.post(fmt.Sprintf("/domain/getNs/%s", domain), params)
@@ -136,10 +201,21 @@ func (c *porkbunProvider) getNameservers(domain string) ([]string, error) {
}
var ns nsResponse
- json.Unmarshal(bodyString, &ns)
+ err = json.Unmarshal(bodyString, &ns)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing nameserver list from porkbun: %w", err)
+ }
sort.Strings(ns.Nameservers)
- return ns.Nameservers, nil
+
+ var nameservers []string
+ for _, nameserver := range ns.Nameservers {
+ // Remove the trailing dot only if it exists.
+ // This provider seems to add the trailing dot to some domains but not others.
+ // The .DE domains seem to always include the dot, others don't.
+ nameservers = append(nameservers, strings.TrimSuffix(nameserver, "."))
+ }
+ return nameservers, nil
}
func (c *porkbunProvider) updateNameservers(ns []string, domain string) error {
@@ -150,3 +226,24 @@ func (c *porkbunProvider) updateNameservers(ns []string, domain string) error {
}
return nil
}
+
+func (c *porkbunProvider) listAllDomains() ([]string, error) {
+ params := requestParams{}
+ var bodyString, err = c.post("/domain/listAll", params)
+ if err != nil {
+ return nil, fmt.Errorf("failed listing all domains from porkbun: %w", err)
+ }
+
+ var dlr domainListResponse
+ err = json.Unmarshal(bodyString, &dlr)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing domain list from porkbun: %w", err)
+ }
+
+ var domains []string
+ for _, domain := range dlr.Domains {
+ domains = append(domains, domain.Domain)
+ }
+ sort.Strings(domains)
+ return domains, nil
+}
diff --git a/providers/porkbun/auditrecords.go b/providers/porkbun/auditrecords.go
index 6b575d67f8..af13ba4fb2 100644
--- a/providers/porkbun/auditrecords.go
+++ b/providers/porkbun/auditrecords.go
@@ -15,5 +15,7 @@ func AuditRecords(records []*models.RecordConfig) []error {
a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2022-11-19
+ a.Add("CAA", rejectif.CaaTargetContainsWhitespace) // Last verified 2024-11-11
+
return a.Audit(records)
}
diff --git a/providers/porkbun/listzones.go b/providers/porkbun/listzones.go
new file mode 100644
index 0000000000..95842b14f0
--- /dev/null
+++ b/providers/porkbun/listzones.go
@@ -0,0 +1,9 @@
+package porkbun
+
+func (c *porkbunProvider) ListZones() ([]string, error) {
+ zones, err := c.listAllDomains()
+ if err != nil {
+ return nil, err
+ }
+ return zones, err
+}
diff --git a/providers/porkbun/porkbunProvider.go b/providers/porkbun/porkbunProvider.go
index 18eec418e7..b5e1ea1400 100644
--- a/providers/porkbun/porkbunProvider.go
+++ b/providers/porkbun/porkbunProvider.go
@@ -11,12 +11,20 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/providers"
+
+ "github.com/miekg/dns/dnsutil"
)
const (
minimumTTL = 600
)
+const (
+ metaType = "type"
+ metaIncludePath = "includePath"
+ metaWildcard = "wildcard"
+)
+
// https://kb.porkbun.com/article/63-how-to-switch-to-porkbuns-nameservers
var defaultNS = []string{
"curitiba.ns.porkbun.com",
@@ -34,7 +42,7 @@ func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServ
}
// newPorkbun creates the provider.
-func newPorkbun(m map[string]string, metadata json.RawMessage) (*porkbunProvider, error) {
+func newPorkbun(m map[string]string, _ json.RawMessage) (*porkbunProvider, error) {
c := &porkbunProvider{}
c.apiKey, c.secretKey = m["api_key"], m["secret_key"]
@@ -43,19 +51,17 @@ func newPorkbun(m map[string]string, metadata json.RawMessage) (*porkbunProvider
return nil, fmt.Errorf("missing porkbun api_key or secret_key")
}
- // Validate authentication
- if err := c.ping(); err != nil {
- return nil, err
- }
-
return c, nil
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
- providers.CanUseCAA: providers.Unimplemented(), // CAA record for base domain is pinning to a fixed set once configure
+ providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseDSForChildren: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
@@ -71,12 +77,16 @@ var features = providers.DocumentationNotes{
}
func init() {
- providers.RegisterRegistrarType("PORKBUN", newReg)
+ const providerName = "PORKBUN"
+ const providerMaintainer = "@imlonghao"
+ providers.RegisterRegistrarType(providerName, newReg)
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("PORKBUN", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+ providers.RegisterCustomRecordType("PORKBUN_URLFWD", providerName, "")
}
// GetNameservers returns the nameservers for a domain.
@@ -84,8 +94,15 @@ func (c *porkbunProvider) GetNameservers(domain string) ([]*models.Nameserver, e
return models.ToNameservers(defaultNS)
}
+func genComparable(rec *models.RecordConfig) string {
+ if rec.Type == "PORKBUN_URLFWD" {
+ return fmt.Sprintf("type=%s includePath=%s wildcard=%s", rec.Metadata[metaType], rec.Metadata[metaIncludePath], rec.Metadata[metaWildcard])
+ }
+ return ""
+}
+
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
// Block changes to NS records for base domain
@@ -94,11 +111,26 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
// Make sure TTL larger than the minimum TTL
for _, record := range dc.Records {
record.TTL = fixTTL(record.TTL)
+ if record.Type == "PORKBUN_URLFWD" {
+ record.TTL = 0
+ if record.Metadata == nil {
+ record.Metadata = make(map[string]string)
+ }
+ if record.Metadata[metaType] == "" {
+ record.Metadata[metaType] = "temporary"
+ }
+ if record.Metadata[metaIncludePath] == "" {
+ record.Metadata[metaIncludePath] = "no"
+ }
+ if record.Metadata[metaWildcard] == "" {
+ record.Metadata[metaWildcard] = "yes"
+ }
+ }
}
- changes, err := diff2.ByRecord(existingRecords, dc, nil)
+ changes, actualChangeCount, err := diff2.ByRecord(existingRecords, dc, genComparable)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, change := range changes {
var corr *models.Correction
@@ -108,11 +140,14 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
case diff2.CREATE:
req, err := toReq(change.New[0])
if err != nil {
- return nil, err
+ return nil, 0, err
}
corr = &models.Correction{
Msg: change.Msgs[0],
F: func() error {
+ if change.New[0].Type == "PORKBUN_URLFWD" {
+ return c.createURLForwardingRecord(dc.Name, req)
+ }
return c.createRecord(dc.Name, req)
},
}
@@ -120,11 +155,14 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
id := change.Old[0].Original.(*domainRecord).ID
req, err := toReq(change.New[0])
if err != nil {
- return nil, err
+ return nil, 0, err
}
corr = &models.Correction{
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
F: func() error {
+ if change.New[0].Type == "PORKBUN_URLFWD" {
+ return c.modifyURLForwardingRecord(dc.Name, id, req)
+ }
return c.modifyRecord(dc.Name, id, req)
},
}
@@ -133,6 +171,9 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
corr = &models.Correction{
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
F: func() error {
+ if change.Old[0].Type == "PORKBUN_URLFWD" {
+ return c.deleteURLForwardingRecord(dc.Name, id)
+ }
return c.deleteRecord(dc.Name, id)
},
}
@@ -142,7 +183,7 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
corrections = append(corrections, corr)
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
@@ -151,9 +192,54 @@ func (c *porkbunProvider) GetZoneRecords(domain string, meta map[string]string)
if err != nil {
return nil, err
}
- existingRecords := make([]*models.RecordConfig, len(records))
+ forwards, err := c.getURLForwardingRecords(domain)
+ if err != nil {
+ return nil, err
+ }
+ existingRecords := make([]*models.RecordConfig, 0)
for i := range records {
- existingRecords[i] = toRc(domain, &records[i])
+ shouldSkip := false
+ if strings.HasSuffix(records[i].Content, ".porkbun.com") {
+ name := dnsutil.TrimDomainName(records[i].Name, domain)
+ if name == "@" {
+ name = ""
+ }
+ if records[i].Type == "ALIAS" {
+ for _, forward := range forwards {
+ if name == forward.Subdomain {
+ shouldSkip = true
+ break
+ }
+ }
+ }
+ if records[i].Type == "CNAME" {
+ for _, forward := range forwards {
+ if name == "*."+forward.Subdomain {
+ shouldSkip = true
+ break
+ }
+ }
+ }
+ }
+ if shouldSkip {
+ continue
+ }
+ existingRecords = append(existingRecords, toRc(domain, &records[i]))
+ }
+ for i := range forwards {
+ r := &forwards[i]
+ rc := &models.RecordConfig{
+ Type: "PORKBUN_URLFWD",
+ Original: r,
+ Metadata: map[string]string{
+ metaType: r.Type,
+ metaIncludePath: r.IncludePath,
+ metaWildcard: r.Wildcard,
+ },
+ }
+ rc.SetLabel(r.Subdomain, domain)
+ rc.SetTarget(r.Location)
+ existingRecords = append(existingRecords, rc)
}
return existingRecords, nil
}
@@ -218,6 +304,20 @@ func toRc(domain string, r *domainRecord) *models.RecordConfig {
// toReq takes a RecordConfig and turns it into the native format used by the API.
func toReq(rc *models.RecordConfig) (requestParams, error) {
+ if rc.Type == "PORKBUN_URLFWD" {
+ subdomain := rc.GetLabel()
+ if subdomain == "@" {
+ subdomain = ""
+ }
+ return requestParams{
+ "subdomain": subdomain,
+ "location": rc.GetTargetField(),
+ "type": rc.Metadata[metaType],
+ "includePath": rc.Metadata[metaIncludePath],
+ "wildcard": rc.Metadata[metaWildcard],
+ }, nil
+ }
+
req := requestParams{
"type": rc.Type,
"name": rc.GetLabel(),
diff --git a/providers/powerdns/auditrecords.go b/providers/powerdns/auditrecords.go
index 7b22a7d176..1903c5d22e 100644
--- a/providers/powerdns/auditrecords.go
+++ b/providers/powerdns/auditrecords.go
@@ -1,10 +1,18 @@
package powerdns
-import "github.com/StackExchange/dnscontrol/v4/models"
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
// AuditRecords returns a list of errors corresponding to the records
// that aren't supported by this provider. If all records are
// supported, an empty list is returned.
func AuditRecords(records []*models.RecordConfig) []error {
- return nil
+ a := rejectif.Auditor{}
+
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-11-11
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2023-11-11
+
+ return a.Audit(records)
}
diff --git a/providers/powerdns/convert.go b/providers/powerdns/convert.go
index 307955552f..591cb1882c 100644
--- a/providers/powerdns/convert.go
+++ b/providers/powerdns/convert.go
@@ -22,7 +22,7 @@ func toRecordConfig(domain string, r zones.Record, ttl int, name string, rtype s
switch rtype {
case "TXT":
- // PowerDNS API accepts long TXTs without requiring to split them
+ // PowerDNS API accepts long TXTs without requiring to split them.
// The API then returns them as they initially came in, e.g. "averylooooooo[...]oooooongstring" or "string" "string"
// So we need to strip away " and split into multiple string
// We can't use SetTargetRFC1035Quoted, it would split the long strings into multiple parts
diff --git a/providers/powerdns/convert_test.go b/providers/powerdns/convert_test.go
index a88fd29109..49779a933c 100644
--- a/providers/powerdns/convert_test.go
+++ b/providers/powerdns/convert_test.go
@@ -29,7 +29,8 @@ func TestToRecordConfig(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "large.example.com", recordConfig.NameFQDN)
- assert.Equal(t, largeContent, recordConfig.String())
+ assert.Equal(t, `"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"`,
+ recordConfig.String())
assert.Equal(t, uint32(5), recordConfig.TTL)
assert.Equal(t, "TXT", recordConfig.Type)
}
diff --git a/providers/powerdns/diff.go b/providers/powerdns/diff.go
index a48ef55deb..fdd2ee201d 100644
--- a/providers/powerdns/diff.go
+++ b/providers/powerdns/diff.go
@@ -3,19 +3,29 @@ package powerdns
import (
"context"
"fmt"
+ "strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+ "github.com/fatih/color"
"github.com/mittwald/go-powerdns/apis/zones"
)
-func (dsp *powerdnsProvider) getDiff2DomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
- changes, err := diff2.ByRecordSet(existing, dc, nil)
+func (dsp *powerdnsProvider) getDiff2DomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
+ changes, actualChangeCount, err := diff2.ByRecordSet(existing, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
var corrections []*models.Correction
+ var changeMsgs []string
+ var rrChangeSets []zones.ResourceRecordSet
+ var deleteMsgs []string
+ var rrDeleteSets []zones.ResourceRecordSet
+
+ // for pretty alignment, add an empty string
+ changeMsgs = append(changeMsgs, color.YellowString("Âą BATCHED CHANGE/CREATEs for %s", dc.Name))
+ deleteMsgs = append(deleteMsgs, color.RedString("- BATCHED DELETEs for %s", dc.Name))
for _, change := range changes {
labelName := canonical(change.Key.NameFQDN)
@@ -28,31 +38,44 @@ func (dsp *powerdnsProvider) getDiff2DomainCorrections(dc *models.DomainConfig,
labelTTL := int(change.New[0].TTL)
records := buildRecordList(change)
- corrections = append(corrections, &models.Correction{
- Msg: change.MsgsJoined,
- F: func() error {
- return dsp.client.Zones().AddRecordSetToZone(context.Background(), dsp.ServerName, dc.Name, zones.ResourceRecordSet{
- Name: labelName,
- Type: labelType,
- TTL: labelTTL,
- Records: records,
- ChangeType: zones.ChangeTypeReplace,
- })
- },
+ rrChangeSets = append(rrChangeSets, zones.ResourceRecordSet{
+ Name: labelName,
+ Type: labelType,
+ TTL: labelTTL,
+ Records: records,
+ // ChangeType is not needed since zone API sets it when calling Add
})
+ changeMsgs = append(changeMsgs, change.MsgsJoined)
case diff2.DELETE:
- corrections = append(corrections, &models.Correction{
- Msg: change.MsgsJoined,
- F: func() error {
- return dsp.client.Zones().RemoveRecordSetFromZone(context.Background(), dsp.ServerName, dc.Name, labelName, labelType)
- },
+ rrDeleteSets = append(rrDeleteSets, zones.ResourceRecordSet{
+ Name: labelName,
+ Type: labelType,
+ // ChangeType is not needed since zone API sets it when calling Remove
})
+ deleteMsgs = append(deleteMsgs, change.MsgsJoined)
default:
panic(fmt.Sprintf("unhandled change.Type %s", change.Type))
}
}
- return corrections, nil
+ // only append a Correction if there are any, otherwise causes an error when sending an empty rrset
+ if len(rrDeleteSets) > 0 {
+ corrections = append(corrections, &models.Correction{
+ Msg: strings.Join(deleteMsgs, "\n"),
+ F: func() error {
+ return dsp.client.Zones().RemoveRecordSetsFromZone(context.Background(), dsp.ServerName, canonical(dc.Name), rrDeleteSets)
+ },
+ })
+ }
+ if len(rrChangeSets) > 0 {
+ corrections = append(corrections, &models.Correction{
+ Msg: strings.Join(changeMsgs, "\n"),
+ F: func() error {
+ return dsp.client.Zones().AddRecordSetsToZone(context.Background(), dsp.ServerName, canonical(dc.Name), rrChangeSets)
+ },
+ })
+ }
+ return corrections, actualChangeCount, nil
}
// buildRecordList returns a list of records for the PowerDNS resource record set from a change
diff --git a/providers/powerdns/dns.go b/providers/powerdns/dns.go
index aeca212848..a57fd0db2a 100644
--- a/providers/powerdns/dns.go
+++ b/providers/powerdns/dns.go
@@ -20,7 +20,7 @@ func (dsp *powerdnsProvider) GetNameservers(string) ([]*models.Nameserver, error
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (dsp *powerdnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
- zone, err := dsp.client.Zones().GetZone(context.Background(), dsp.ServerName, domain)
+ zone, err := dsp.client.Zones().GetZone(context.Background(), dsp.ServerName, canonical(domain))
if err != nil {
return nil, err
}
@@ -45,20 +45,21 @@ func (dsp *powerdnsProvider) GetZoneRecords(domain string, meta map[string]strin
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (dsp *powerdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+func (dsp *powerdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
- corrections, err := dsp.getDiff2DomainCorrections(dc, existing)
+ corrections, actualChangeCount, err := dsp.getDiff2DomainCorrections(dc, existing)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// DNSSec corrections
dnssecCorrections, err := dsp.getDNSSECCorrections(dc)
if err != nil {
- return nil, err
+ return nil, 0, err
}
+ actualChangeCount += len(dnssecCorrections)
- return append(corrections, dnssecCorrections...), nil
+ return append(corrections, dnssecCorrections...), actualChangeCount, nil
}
// EnsureZoneExists creates a zone if it does not exist
diff --git a/providers/powerdns/powerdnsProvider.go b/providers/powerdns/powerdnsProvider.go
index 7fbbe3c859..4e4c49f71d 100644
--- a/providers/powerdns/powerdnsProvider.go
+++ b/providers/powerdns/powerdnsProvider.go
@@ -12,11 +12,15 @@ import (
)
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Can(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Can("Needs to be enabled in PowerDNS first", "https://doc.powerdns.com/authoritative/guides/alias.html"),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Can(),
+ providers.CanUseDHCID: providers.Can(),
providers.CanUseLOC: providers.Unimplemented("Normalization within the PowerDNS API seems to be buggy, so disabled", "https://github.com/PowerDNS/pdns/issues/10558"),
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
@@ -29,11 +33,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "POWERDNS"
+ const providerMaintainer = "@jpbede"
fns := providers.DspFuncs{
Initializer: newDSP,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("POWERDNS", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// powerdnsProvider represents the powerdnsProvider DNSServiceProvider.
diff --git a/providers/providers.go b/providers/providers.go
index e04ab35c4c..86ab34a573 100644
--- a/providers/providers.go
+++ b/providers/providers.go
@@ -72,6 +72,15 @@ func RegisterDomainServiceProviderType(name string, fns DspFuncs, pm ...Provider
unwrapProviderCapabilities(name, pm)
}
+var ProviderMaintainers = map[string]string{}
+
+func RegisterMaintainer(
+ providerName string,
+ gitHubUsername string,
+) {
+ ProviderMaintainers[providerName] = gitHubUsername
+}
+
// CreateRegistrar initializes a registrar instance from given credentials.
func CreateRegistrar(rType string, config map[string]string) (Registrar, error) {
var err error
@@ -162,8 +171,8 @@ func (n None) GetZoneRecords(domain string, meta map[string]string) (models.Reco
}
// GetZoneRecordsCorrections gets the records of a zone and returns them in RecordConfig format.
-func (n None) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
- return nil, nil
+func (n None) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, int, error) {
+ return nil, 0, nil
}
// GetDomainCorrections returns corrections to update a domain.
@@ -171,10 +180,16 @@ func (n None) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correctio
return nil, nil
}
+var featuresNone = DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ CanConcur: Can(),
+}
+
func init() {
RegisterRegistrarType("NONE", func(map[string]string) (Registrar, error) {
return None{}, nil
- })
+ }, featuresNone)
}
// CustomRType stores an rtype that is only valid for this DSP.
diff --git a/providers/realtimeregister/api.go b/providers/realtimeregister/api.go
new file mode 100644
index 0000000000..38a7f9f369
--- /dev/null
+++ b/providers/realtimeregister/api.go
@@ -0,0 +1,221 @@
+package realtimeregister
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+type realtimeregisterAPI struct {
+ apikey string
+ endpoint string
+ Zones map[string]*Zone //cache
+ ServiceType string
+}
+
+type Zones struct {
+ Entities []Zone `json:"entities"`
+}
+
+type Domain struct {
+ Nameservers []string `json:"ns"`
+}
+
+type Zone struct {
+ Name string `json:"name,omitempty"`
+ Service string `json:"service,omitempty"`
+ ID int `json:"id,omitempty"`
+ Records []Record `json:"records"`
+ Dnssec bool `json:"dnssec"`
+}
+
+type Record struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Content string `json:"content"`
+ Priority int `json:"prio,omitempty"`
+ TTL int `json:"ttl"`
+}
+
+const (
+ endpoint = "https://api.yoursrs.com/v2"
+ endpointSandbox = "https://api.yoursrs-ote.com/v2"
+)
+
+func (api *realtimeregisterAPI) request(method string, url string, body io.Reader) ([]byte, error) {
+ client := &http.Client{}
+ req, _ := http.NewRequest(
+ method,
+ url,
+ body,
+ )
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Authorization", "ApiKey "+api.apikey)
+
+ resp, err := client.Do(req)
+
+ if err != nil {
+ return nil, err
+ }
+
+ bodyString, _ := io.ReadAll(resp.Body)
+
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("realtime Register API error on request to %s: %d, %s", url, resp.StatusCode,
+ string(bodyString))
+ }
+
+ return bodyString, nil
+}
+
+func (api *realtimeregisterAPI) getZone(domain string) (*Zone, error) {
+ zones, err := api.getDomainZones(domain)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(zones.Entities) == 0 {
+ return nil, fmt.Errorf("zone %s does not exist", domain)
+ }
+
+ api.Zones[domain] = &zones.Entities[0]
+
+ return &zones.Entities[0], nil
+}
+
+func (api *realtimeregisterAPI) getDomainZones(domain string) (*Zones, error) {
+
+ url := fmt.Sprintf(api.endpoint+"/dns/zones?name=%s&service=%s", domain, api.ServiceType)
+
+ return api.getZones(url)
+}
+
+func (api *realtimeregisterAPI) getAllZones() ([]string, error) {
+ url := fmt.Sprintf(api.endpoint+"/dns/zones?service=%s&export=true&fields=id,name", api.ServiceType)
+
+ zones, err := api.getZones(url)
+ if err != nil {
+ return nil, err
+ }
+
+ zoneNames := make([]string, len(zones.Entities))
+
+ for i, zone := range zones.Entities {
+ zoneNames[i] = zone.Name
+ }
+
+ return zoneNames, nil
+}
+
+func (api *realtimeregisterAPI) getZones(url string) (*Zones, error) {
+ bodyBytes, err := api.request(
+ "GET",
+ url,
+ nil,
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ respData := &Zones{}
+ err = json.Unmarshal(bodyBytes, &respData)
+ if err != nil {
+ return nil, err
+ }
+
+ return respData, nil
+}
+
+func (api *realtimeregisterAPI) createZone(domain string) error {
+ zone := &Zone{
+ Records: []Record{},
+ Name: domain,
+ Service: api.ServiceType,
+ }
+
+ err := api.createOrUpdateZone(zone, api.endpoint+"/dns/zones")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (api *realtimeregisterAPI) zoneExists(domain string) (bool, error) {
+ if api.Zones[domain] != nil {
+ return true, nil
+ }
+ zones, err := api.getDomainZones(domain)
+ if err != nil {
+ return false, err
+ }
+ return len(zones.Entities) > 0, nil
+}
+
+func (api *realtimeregisterAPI) getDomainNameservers(domainName string) ([]string, error) {
+ respData, err := api.request(
+ "GET",
+ fmt.Sprintf(api.endpoint+"/domains/%s", domainName),
+ nil,
+ )
+ if err != nil {
+ return nil, err
+ }
+ domain := &Domain{}
+ err = json.Unmarshal(respData, &domain)
+ if err != nil {
+ return nil, err
+ }
+ return domain.Nameservers, nil
+}
+
+func (api *realtimeregisterAPI) updateZone(domain string, body *Zone) error {
+ return api.createOrUpdateZone(
+ body,
+ fmt.Sprintf(api.endpoint+"/dns/zones/%d/update", api.Zones[domain].ID),
+ )
+}
+
+func (api *realtimeregisterAPI) updateNameservers(domainName string, nameservers []string) error {
+ domain := &Domain{
+ Nameservers: nameservers,
+ }
+
+ bodyBytes, err := json.Marshal(domain)
+ if err != nil {
+ return err
+ }
+
+ _, err = api.request(
+ "POST",
+ fmt.Sprintf(api.endpoint+"/domains/%s/update", domainName),
+ bytes.NewReader(bodyBytes),
+ )
+
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (api *realtimeregisterAPI) createOrUpdateZone(body *Zone, url string) error {
+ bodyBytes, err := json.Marshal(body)
+
+ if err != nil {
+ return err
+ }
+
+ //Ugly hack for MX records with null target
+ requestBody := strings.Replace(string(bodyBytes), "\"prio\":-1", "\"prio\":0", -1)
+
+ _, err = api.request("POST", url, strings.NewReader(requestBody))
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/providers/realtimeregister/auditrecords.go b/providers/realtimeregister/auditrecords.go
new file mode 100644
index 0000000000..e8f681ff90
--- /dev/null
+++ b/providers/realtimeregister/auditrecords.go
@@ -0,0 +1,19 @@
+package realtimeregister
+
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+)
+
+// AuditRecords returns a list of errors corresponding to the records
+// that aren't supported by this provider. If all records are
+// supported, an empty list is returned.
+func AuditRecords(records []*models.RecordConfig) []error {
+ auditor := rejectif.Auditor{}
+
+ auditor.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2024-01-03
+
+ auditor.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-01-03
+
+ return auditor.Audit(records)
+}
diff --git a/providers/realtimeregister/realtimeregisterProvider.go b/providers/realtimeregister/realtimeregisterProvider.go
new file mode 100644
index 0000000000..47d9a4c4d4
--- /dev/null
+++ b/providers/realtimeregister/realtimeregisterProvider.go
@@ -0,0 +1,345 @@
+package realtimeregister
+
+import (
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+ "github.com/StackExchange/dnscontrol/v4/providers"
+ "github.com/miekg/dns/dnsutil"
+ "golang.org/x/exp/slices"
+)
+
+/*
+Realtime Register DNS provider
+
+Info required in `creds.json`:
+ - apikey
+ - premium: (0 for BASIC or 1 for PREMIUM)
+
+Additional settings available in `creds.json`:
+ - sandbox (set to 1 to use the sandbox API from realtime register)
+*/
+
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanAutoDNSSEC: providers.Can(),
+ providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
+ providers.CanUseAlias: providers.Can(),
+ providers.CanUseCAA: providers.Can(),
+ providers.CanUseDHCID: providers.Cannot(),
+ providers.CanUseDS: providers.Cannot("Only for subdomains"),
+ providers.CanUseDSForChildren: providers.Can(),
+ providers.CanUseLOC: providers.Can(),
+ providers.CanUseNAPTR: providers.Can(),
+ providers.CanUsePTR: providers.Cannot(),
+ providers.CanUseSOA: providers.Cannot(),
+ providers.CanUseSRV: providers.Can(),
+ providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseTLSA: providers.Can(),
+ providers.DocCreateDomains: providers.Can(),
+ providers.DocDualHost: providers.Cannot(),
+ providers.DocOfficiallySupported: providers.Cannot(),
+}
+
+// init registers the domain service provider with dnscontrol.
+func init() {
+ const providerName = "REALTIMEREGISTER"
+ const providerMaintainer = "@PJEilers"
+ fns := providers.DspFuncs{
+ Initializer: newRtrDsp,
+ RecordAuditor: AuditRecords,
+ }
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterRegistrarType(providerName, newRtrReg)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+}
+
+func newRtr(config map[string]string, _ json.RawMessage) (*realtimeregisterAPI, error) {
+ apikey := config["apikey"]
+ sandbox := config["sandbox"] == "1"
+
+ if apikey == "" {
+ return nil, fmt.Errorf("realtime register: apikey must be provided")
+ }
+
+ api := &realtimeregisterAPI{
+ apikey: apikey,
+ endpoint: getEndpoint(sandbox),
+ Zones: make(map[string]*Zone),
+ ServiceType: getServiceType(config["premium"] == "1"),
+ }
+
+ return api, nil
+}
+
+func newRtrDsp(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
+ return newRtr(config, metadata)
+}
+
+func newRtrReg(config map[string]string) (providers.Registrar, error) {
+ return newRtr(config, nil)
+}
+
+// GetNameservers Default name servers should not be included in the update
+func (api *realtimeregisterAPI) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ return []*models.Nameserver{}, nil
+}
+
+func (api *realtimeregisterAPI) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
+ response, err := api.getZone(domain)
+ if err != nil {
+ return nil, err
+ }
+ records := response.Records
+ recordConfigs := make([]*models.RecordConfig, len(records))
+ for i := range records {
+ recordConfigs[i] = toRecordConfig(domain, &records[i])
+ }
+
+ return recordConfigs, nil
+}
+
+func (api *realtimeregisterAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
+ result, err := diff2.ByZone(existing, dc, nil)
+ if err != nil {
+ return nil, 0, err
+ }
+ msgs, changes, actualChangeCount := result.Msgs, result.HasChanges, result.ActualChangeCount
+
+ var corrections []*models.Correction
+
+ if !changes {
+ return corrections, 0, nil
+ }
+
+ dnssec := api.Zones[dc.Name].Dnssec
+
+ if api.Zones[dc.Name].Dnssec && dc.AutoDNSSEC == "off" {
+ dnssec = false
+ corrections = append(corrections,
+ &models.Correction{
+ Msg: "Update DNSSEC on -> off",
+ F: func() error {
+ return nil
+ },
+ })
+ actualChangeCount++
+ }
+
+ if !api.Zones[dc.Name].Dnssec && dc.AutoDNSSEC == "on" {
+ dnssec = true
+ corrections = append(corrections,
+ &models.Correction{
+ Msg: "Update DNSSEC off -> on",
+ F: func() error {
+ return nil
+ },
+ })
+ actualChangeCount++
+ }
+
+ if changes {
+ corrections = append(corrections,
+ &models.Correction{
+ Msg: strings.Join(msgs, "\n"),
+ F: func() error {
+ records := make([]Record, len(result.DesiredPlus))
+ for i, r := range result.DesiredPlus {
+ records[i] = toRecord(r)
+ }
+ zone := &Zone{Records: records, Dnssec: dnssec}
+
+ err := api.updateZone(dc.Name, zone)
+ if err != nil {
+ return err
+ }
+ return nil
+ },
+ })
+ }
+
+ return corrections, actualChangeCount, nil
+}
+
+func (api *realtimeregisterAPI) ListZones() ([]string, error) {
+ zones, err := api.getAllZones()
+ if err != nil {
+ return nil, err
+ }
+ return zones, nil
+}
+
+func (api *realtimeregisterAPI) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ nameservers, err := api.getDomainNameservers(dc.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ expected := make([]string, len(dc.Nameservers))
+ for i, ns := range dc.Nameservers {
+ expected[i] = removeTrailingDot(ns.Name)
+ }
+
+ sort.Strings(nameservers)
+ sort.Strings(expected)
+
+ if !slices.Equal(nameservers, expected) {
+ return []*models.Correction{
+ {
+ Msg: fmt.Sprintf("Update nameservers %s -> %s",
+ strings.Join(nameservers, ","), strings.Join(expected, ",")),
+ F: func() error { return api.updateNameservers(dc.Name, expected) },
+ },
+ }, nil
+ }
+
+ return nil, nil
+}
+
+func toRecordConfig(domain string, record *Record) *models.RecordConfig {
+
+ recordConfig := &models.RecordConfig{
+ Type: record.Type,
+ TTL: uint32(record.TTL),
+ MxPreference: uint16(record.Priority),
+ SrvWeight: uint16(0),
+ SrvPort: uint16(0),
+ Original: record,
+ }
+
+ recordConfig.SetLabelFromFQDN(record.Name, domain)
+
+ switch rtype := record.Type; rtype { // #rtype_variations
+ case "TXT":
+ _ = recordConfig.SetTargetTXT(removeEscapeChars(record.Content))
+ case "NS", "ALIAS", "CNAME":
+ _ = recordConfig.SetTarget(dnsutil.AddOrigin(addTrailingDot(record.Content), domain))
+ case "MX":
+ content := record.Content
+ if content != "." {
+ content = addTrailingDot(content)
+ }
+ _ = recordConfig.SetTarget(dnsutil.AddOrigin(content, domain))
+ case "NAPTR":
+ _ = recordConfig.SetTargetNAPTRString(record.Content)
+ case "SRV":
+ parts := strings.Fields(record.Content)
+ weight, _ := strconv.ParseUint(parts[0], 10, 16)
+ port, _ := strconv.ParseUint(parts[1], 10, 16)
+ content := parts[2]
+ if content != "." {
+ content = addTrailingDot(content)
+ }
+ _ = recordConfig.SetTargetSRV(uint16(record.Priority), uint16(weight), uint16(port), content)
+ case "CAA":
+ _ = recordConfig.SetTargetCAAString(record.Content)
+ case "SSHFP":
+ _ = recordConfig.SetTargetSSHFPString(record.Content)
+ case "TLSA":
+ _ = recordConfig.SetTargetTLSAString(record.Content)
+ case "DS":
+ _ = recordConfig.SetTargetDSString(record.Content)
+ case "LOC":
+ _ = recordConfig.SetTargetLOCString(domain, record.Content)
+ default:
+ _ = recordConfig.SetTarget(record.Content)
+ }
+ return recordConfig
+}
+
+func toRecord(recordConfig *models.RecordConfig) Record {
+ record := &Record{
+ Type: recordConfig.Type,
+ Name: recordConfig.NameFQDN,
+ Content: removeTrailingDot(recordConfig.GetTargetField()),
+ TTL: int(recordConfig.TTL),
+ }
+
+ switch rtype := recordConfig.Type; rtype {
+ case "SRV":
+ if record.Content == "" {
+ record.Content = "."
+ }
+ record.Priority = int(recordConfig.SrvPriority)
+ record.Content = fmt.Sprintf("%d %d %s", recordConfig.SrvWeight, recordConfig.SrvPort, record.Content)
+ case "NAPTR", "SSHFP", "TLSA", "CAA":
+ record.Content = recordConfig.GetTargetCombined()
+ case "TXT":
+ record.Content = addEscapeChars(record.Content)
+ case "DS":
+ record.Content = fmt.Sprintf("%d %d %d %s", recordConfig.DsKeyTag, recordConfig.DsAlgorithm,
+ recordConfig.DsDigestType, strings.ToUpper(recordConfig.DsDigest))
+ case "MX":
+ if record.Content == "" {
+ record.Content = "."
+ record.Priority = -1
+ } else {
+ record.Priority = int(recordConfig.MxPreference)
+ }
+ case "LOC":
+ parts := strings.Fields(recordConfig.GetTargetCombined())
+ degrees1, _ := strconv.ParseUint(parts[0], 10, 32)
+ minutes1, _ := strconv.ParseUint(parts[1], 10, 32)
+ degrees2, _ := strconv.ParseUint(parts[4], 10, 32)
+ minutes2, _ := strconv.ParseUint(parts[5], 10, 32)
+ altitude, _ := strconv.ParseFloat(strings.Split(parts[8], "m")[0], 64)
+ size, _ := strconv.ParseFloat(strings.Split(parts[9], "m")[0], 64)
+ hp, _ := strconv.ParseFloat(strings.Split(parts[10], "m")[0], 64)
+ vp, _ := strconv.ParseFloat(strings.Split(parts[11], "m")[0], 64)
+ record.Content = fmt.Sprintf("%d %d %s %s %d %d %s %s %.2fm %.2fm %.2fm %.2fm",
+ degrees1, minutes1, parts[2], parts[3], degrees2, minutes2,
+ parts[6], parts[7], altitude, size, hp, vp,
+ )
+ }
+
+ return *record
+}
+
+func (api *realtimeregisterAPI) EnsureZoneExists(domain string) error {
+ exists, err := api.zoneExists(domain)
+ if err != nil {
+ return err
+ }
+ if exists {
+ return nil
+ }
+
+ return api.createZone(domain)
+}
+
+func removeTrailingDot(record string) string {
+ return strings.TrimSuffix(record, ".")
+}
+
+func addTrailingDot(record string) string {
+ return record + "."
+}
+
+func removeEscapeChars(name string) string {
+ return strings.Replace(strings.Replace(name, "\\\"", "\"", -1), "\\\\", "\\", -1)
+}
+
+func addEscapeChars(name string) string {
+ return strings.Replace(strings.Replace(name, "\\", "\\\\", -1), "\"", "\\\"", -1)
+}
+
+func getEndpoint(sandbox bool) string {
+ if sandbox {
+ return endpointSandbox
+ }
+ return endpoint
+}
+
+func getServiceType(premium bool) string {
+ if premium {
+ return "PREMIUM"
+ }
+ return "BASIC"
+}
diff --git a/providers/realtimeregister/realtimeregisterProvider_test.go b/providers/realtimeregister/realtimeregisterProvider_test.go
new file mode 100644
index 0000000000..9fe6dc09dc
--- /dev/null
+++ b/providers/realtimeregister/realtimeregisterProvider_test.go
@@ -0,0 +1,16 @@
+package realtimeregister
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestRemoveEscapeChars(t *testing.T) {
+ cleanedString := removeEscapeChars("\\\\\\\"")
+ assert.Equal(t, "\\\"", cleanedString)
+}
+
+func TestAddEscapeChars(t *testing.T) {
+ addedString := addEscapeChars("\\\"")
+ assert.Equal(t, "\\\\\\\"", addedString)
+}
diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go
index fd3e7dd1f9..05d3fdcb13 100644
--- a/providers/route53/route53Provider.go
+++ b/providers/route53/route53Provider.go
@@ -7,6 +7,7 @@ import (
"fmt"
"log"
"sort"
+ "strconv"
"strings"
"time"
"unicode/utf8"
@@ -26,12 +27,11 @@ import (
)
type route53Provider struct {
- client *r53.Client
- registrar *r53d.Client
- delegationSet *string
- zonesByID map[string]r53Types.HostedZone
- zonesByDomain map[string]r53Types.HostedZone
- originalRecords []r53Types.ResourceRecordSet
+ client *r53.Client
+ registrar *r53d.Client
+ delegationSet *string
+ zonesByID map[string]r53Types.HostedZone
+ zonesByDomain map[string]r53Types.HostedZone
}
func newRoute53Reg(conf map[string]string) (providers.Registrar, error) {
@@ -42,7 +42,7 @@ func newRoute53Dsp(conf map[string]string, metadata json.RawMessage) (providers.
return newRoute53(conf, metadata)
}
-func newRoute53(m map[string]string, metadata json.RawMessage) (*route53Provider, error) {
+func newRoute53(m map[string]string, _ json.RawMessage) (*route53Provider, error) {
optFns := []func(*config.LoadOptions) error{
// Route53 uses a global endpoint and route53domains
// currently only has a single regional endpoint in us-east-1
@@ -75,7 +75,10 @@ func newRoute53(m map[string]string, metadata json.RawMessage) (*route53Provider
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Cannot("R53 does not provide a generic ALIAS functionality. Use R53_ALIAS instead."),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
@@ -88,13 +91,16 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "ROUTE53"
+ const providerMaintainer = "@tresni"
fns := providers.DspFuncs{
Initializer: newRoute53Dsp,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("ROUTE53", fns, features)
- providers.RegisterRegistrarType("ROUTE53", newRoute53Reg)
- providers.RegisterCustomRecordType("R53_ALIAS", "ROUTE53", "")
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterRegistrarType(providerName, newRoute53Reg)
+ providers.RegisterCustomRecordType("R53_ALIAS", providerName, "")
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func withRetry(f func() error) {
@@ -263,7 +269,6 @@ func (r *route53Provider) getZoneRecords(zone r53Types.HostedZone) (models.Recor
if err != nil {
return nil, err
}
- r.originalRecords = records
var existingRecords = []*models.RecordConfig{}
for _, set := range records {
@@ -277,12 +282,10 @@ func (r *route53Provider) getZoneRecords(zone r53Types.HostedZone) (models.Recor
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
+func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
zone, err := r.getZone(dc)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// update zone_id to current zone.id if not specified by the user
@@ -298,9 +301,9 @@ func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
// Amazon Route53 is a "ByRecordSet" API.
// At each label:rtype pair, we either delete all records or UPSERT the desired records.
- instructions, err := diff2.ByRecordSet(existingRecords, dc, nil)
+ instructions, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
instructions = reorderInstructions(instructions)
var reports []*models.Correction
@@ -344,7 +347,7 @@ func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
for _, r := range inst.New {
rr := r53Types.ResourceRecord{
- Value: aws.String(r.GetTargetCombined()),
+ Value: aws.String(r.GetTargetCombinedFunc(txtutil.EncodeQuoted)),
}
rrset.ResourceRecords = append(rrset.ResourceRecords, rr)
i := int64(r.TTL)
@@ -400,10 +403,10 @@ func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
addCorrection(descBatchStr, req)
}
if err := batcher.Err(); err != nil {
- return nil, err
+ return nil, 0, err
}
- return append(reports, corrections...), nil
+ return append(reports, corrections...), actualChangeCount, nil
}
@@ -435,8 +438,9 @@ func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.R
Type: "R53_ALIAS",
TTL: 300,
R53Alias: map[string]string{
- "type": string(set.Type),
- "zone_id": aws.ToString(set.AliasTarget.HostedZoneId),
+ "type": string(set.Type),
+ "zone_id": aws.ToString(set.AliasTarget.HostedZoneId),
+ "evaluate_target_health": strconv.FormatBool(set.AliasTarget.EvaluateTargetHealth),
},
}
rc.SetLabelFromFQDN(unescape(set.Name), origin)
@@ -486,17 +490,10 @@ func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.R
}
}
- var err error
rc := &models.RecordConfig{TTL: uint32(aws.ToInt64(set.TTL))}
rc.SetLabelFromFQDN(unescape(set.Name), origin)
rc.Original = set
- switch rtypeString {
- case "TXT":
- err = rc.SetTargetTXTs(models.ParseQuotedTxt(val))
- default:
- err = rc.PopulateFromString(rtypeString, val, origin)
- }
- if err != nil {
+ if err := rc.PopulateFromStringFunc(rtypeString, val, origin, txtutil.ParseQuoted); err != nil {
return nil, fmt.Errorf("unparsable record type=%q received from ROUTE53: %w", rtypeString, err)
}
@@ -510,12 +507,16 @@ func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.R
func aliasToRRSet(zone r53Types.HostedZone, r *models.RecordConfig) *r53Types.ResourceRecordSet {
target := r.GetTargetField()
zoneID := getZoneID(zone, r)
+ evalTargetHealth, err := strconv.ParseBool(r.R53Alias["evaluate_target_health"])
+ if err != nil {
+ evalTargetHealth = false
+ }
rrset := &r53Types.ResourceRecordSet{
Type: r53Types.RRType(r.R53Alias["type"]),
AliasTarget: &r53Types.AliasTarget{
DNSName: &target,
HostedZoneId: aws.String(zoneID),
- EvaluateTargetHealth: false,
+ EvaluateTargetHealth: evalTargetHealth,
},
}
return rrset
diff --git a/providers/rwth/api.go b/providers/rwth/api.go
index 34230c2978..643854109d 100644
--- a/providers/rwth/api.go
+++ b/providers/rwth/api.go
@@ -66,7 +66,7 @@ func checkIsLockedSystemRecord(record *models.RecordConfig) error {
return nil
}
-func (api *rwthProvider) createRecord(domain string, record *models.RecordConfig) error {
+func (api *rwthProvider) createRecord(record *models.RecordConfig) error {
if err := checkIsLockedSystemRecord(record); err != nil {
return err
}
diff --git a/providers/rwth/auditrecords.go b/providers/rwth/auditrecords.go
index fee9c390cc..15d5116775 100644
--- a/providers/rwth/auditrecords.go
+++ b/providers/rwth/auditrecords.go
@@ -11,8 +11,6 @@ import (
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
- a.Add("TXT", rejectif.TxtHasMultipleSegments)
-
a.Add("TXT", rejectif.TxtHasTrailingSpace)
a.Add("TXT", rejectif.TxtIsEmpty)
diff --git a/providers/rwth/dns.go b/providers/rwth/dns.go
index 88b31e6e7e..b1433b05d6 100644
--- a/providers/rwth/dns.go
+++ b/providers/rwth/dns.go
@@ -5,7 +5,6 @@ import (
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
- "github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
)
// RWTHDefaultNs is the default DNS NS for this provider.
@@ -30,13 +29,12 @@ func (api *rwthProvider) GetNameservers(domain string) ([]*models.Nameserver, er
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *rwthProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
- txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+func (api *rwthProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
domain := dc.Name
- toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
+ toReport, create, del, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(existingRecords)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -45,7 +43,7 @@ func (api *rwthProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis
des := d.Desired
corrections = append(corrections, &models.Correction{
Msg: d.String(),
- F: func() error { return api.createRecord(dc.Name, des) },
+ F: func() error { return api.createRecord(des) },
})
}
for _, d := range del {
@@ -72,5 +70,5 @@ func (api *rwthProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
diff --git a/providers/rwth/rwthProvider.go b/providers/rwth/rwthProvider.go
index 9ba1a4f638..69468587fb 100644
--- a/providers/rwth/rwthProvider.go
+++ b/providers/rwth/rwthProvider.go
@@ -14,8 +14,11 @@ type rwthProvider struct {
// features is used to let dnscontrol know which features are supported by the RWTH DNS Admin.
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Unimplemented("Supported by RWTH but not implemented yet."),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Unimplemented("DS records are only supported at the apex and require a different API call that hasn't been implemented yet."),
@@ -32,11 +35,14 @@ var features = providers.DocumentationNotes{
// init registers the registrar and the domain service provider with dnscontrol.
func init() {
+ const providerName = "RWTH"
+ const providerMaintainer = "@mistererwin"
fns := providers.DspFuncs{
Initializer: New,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("RWTH", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// New allocates a DNS service provider.
diff --git a/providers/sakuracloud/api.go b/providers/sakuracloud/api.go
new file mode 100644
index 0000000000..4fe0aa806a
--- /dev/null
+++ b/providers/sakuracloud/api.go
@@ -0,0 +1,486 @@
+// NOTE: As the API documentation of Sakura Cloud is written in Japanese
+// and lacks further explanation, we have described the API data structures
+// in English in the structure comments.
+//
+// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/index.html
+
+package sakuracloud
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "html"
+ "io"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+// requestCommonServiceItem is the body structure of the request to create a zone or update zone data.
+//
+// Zone creation:
+//
+// POST /commonserviceitem
+//
+// {
+// "CommonServiceItem": {
+// "Name": "example.com",
+// "Status": {
+// "Zone": "example.com"
+// },
+// "Settings": {
+// "DNS": {
+// "ResourceRecordSets": []
+// }
+// },
+// "Provider": {
+// "Class": "dns"
+// },
+// }
+// }
+//
+// Zone update:
+//
+// PUT /commonserviceitem/:commonserviceitemid
+//
+// {
+// "CommonServiceItem": {
+// "Settings": {
+// "DNS": {
+// "ResourceRecordSets": [
+// {
+// "Name": "a",
+// "Type": "A",
+// "RData": "192.0.2.1",
+// "TTL": 600
+// },
+// ...
+// ]
+// }
+// }
+// }
+// }
+//
+// Reference:
+//
+// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#post_commonserviceitem
+// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#put_commonserviceitem_commonserviceitemid
+type requestCommonServiceItem struct {
+ CommonServiceItem commonServiceItem `json:"CommonServiceItem"`
+}
+
+// responseCommonServiceItems is the body structure of the success response to get a list of zones.
+//
+// Request:
+//
+// GET /commonserviceitem
+//
+// Response body structure:
+//
+// {
+// "From": 0,
+// "Count": 1,
+// "Total": 1,
+// "CommonServiceItems": [
+// {
+// "Index": 0,
+// "ID": "999999999999",
+// "Name": "example.com",
+// "Description": "",
+// "Settings": {
+// "DNS": {
+// "ResourceRecordSets": [
+// {
+// "Name": "a",
+// "Type": "A",
+// "RData": "192.0.2.1",
+// "TTL": 600
+// },
+// ...
+// ]
+// }
+// },
+// "SettingsHash": "ffffffffffffffffffffffffffffffff",
+// "Status": {
+// "Zone": "example.com",
+// "NS": [
+// "ns1.gslbN.sakura.ne.jp",
+// "ns2.gslbN.sakura.ne.jp"
+// ]
+// },
+// "ServiceClass": "cloud/dns",
+// "Availability": "available",
+// "CreatedAt": "2006-01-02T15:04:05+07:00",
+// "ModifiedAt": "2006-01-02T15:04:05+07:00",
+// "Provider": {
+// "ID": 9999999,
+// "Class": "dns",
+// "Name": "gslbN.sakura.ne.jp",
+// "ServiceClass": "cloud/dns"
+// },
+// "Icon": null,
+// "Tags": []
+// }
+// ],
+// "is_ok": true
+// }
+//
+// References:
+//
+// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#get_commonserviceitem
+type responseCommonServiceItems struct {
+ From int `json:"From"`
+ Count int `json:"Count"`
+ Total int `json:"Total"`
+ CommonServiceItems []commonServiceItem `json:"CommonServiceItems"`
+ IsOk bool `json:"is_ok"`
+}
+
+// responseCommonServiceItem is the body structure of the success response to get a zone or update zone data.
+//
+// Request:
+//
+// GET /commonserviceitem/:commonserviceitemid
+// PUT /commonserviceitem/:commonserviceitemid
+//
+// Response body structure:
+//
+// {
+// "CommonServiceItem": {
+// "ID": "999999999999",
+// "Name": "example.com",
+// "Description": "",
+// "Settings": {
+// "DNS": {
+// "ResourceRecordSets": [
+// {
+// "Name": "a",
+// "Type": "A",
+// "RData": "192.0.2.1",
+// "TTL": 600
+// },
+// ...
+// ]
+// }
+// },
+// "SettingsHash": "ffffffffffffffffffffffffffffffff",
+// "Status": {
+// "Zone": "example.com",
+// "NS": [
+// "ns1.gslbN.sakura.ne.jp",
+// "ns2.gslbN.sakura.ne.jp"
+// ]
+// },
+// "ServiceClass": "cloud/dns",
+// "Availability": "available",
+// "CreatedAt": "2006-01-02T15:04:05+07:00",
+// "ModifiedAt": "2006-01-02T15:04:05+07:00",
+// "Provider": {
+// "ID": 9999999,
+// "Class": "dns",
+// "Name": "gslbN.sakura.ne.jp",
+// "ServiceClass": "cloud/dns"
+// },
+// "Icon": null,
+// "Tags": []
+// },
+// "Success": true,
+// "is_ok": true
+// }
+//
+// References:
+//
+// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#get_commonserviceitem_commonserviceitemid
+// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#put_commonserviceitem_commonserviceitemid
+type responseCommonServiceItem struct {
+ CommonServiceItem commonServiceItem `json:"CommonServiceItem"`
+ Success bool `json:"Success"`
+ IsOk bool `json:"is_ok"`
+}
+
+// errorResponse is the body structure of an error response.
+//
+// Response body structure:
+//
+// {
+// "is_fatal": true,
+// "serial": "ffffffffffffffffffffffffffffffff",
+// "status": "401 Unauthorized",
+// "error_code": "unauthorized",
+// "error_msg": "error-unauthorized"
+// }
+type errorResponse struct {
+ IsFatal bool `json:"is_fatal"`
+ Serial string `json:"serial"`
+ Status string `json:"status"`
+ ErrorCode string `json:"error_code"`
+ ErrorMsg string `json:"error_msg"`
+}
+
+// commonServiceItem is a resource structure.
+type commonServiceItem struct {
+ ID string `json:"ID,omitempty"`
+ Name string `json:"Name,omitempty"`
+ Settings settings `json:"Settings"`
+ Status status `json:"Status,omitempty"`
+ ServiceClass string `json:"ServiceClass,omitempty"`
+ Provider provider `json:"Provider,omitempty"`
+}
+
+// settings is a resource setting.
+type settings struct {
+ DNS dNS `json:"DNS"`
+}
+
+// dNS is a set of dNS resources.
+type dNS struct {
+ ResourceRecordSets []domainRecord `json:"ResourceRecordSets"`
+}
+
+// domainRecord is a resource record.
+type domainRecord struct {
+ Name string `json:"Name"`
+ Type string `json:"Type"`
+ RData string `json:"RData"`
+ TTL uint32 `json:"TTL,omitempty"`
+}
+
+// status is the metadata of a zone.
+type status struct {
+ Zone string `json:"Zone,omitempty"`
+ NS []string `json:"NS,omitempty"`
+}
+
+// provider is the metadata of a service.
+type provider struct {
+ ID int `json:"ID,omitempty"`
+ Class string `json:"Class"`
+ Name string `json:"Name,omitempty"`
+ ServiceClass string `json:"ServiceClass,omitempty"`
+}
+
+// sakuracloudAPI has information about the API of the Sakura Cloud.
+type sakuracloudAPI struct {
+ accessToken string
+ accessTokenSecret string
+ baseURL url.URL
+ httpClient *http.Client
+ commonServiceItemMap map[string]*commonServiceItem
+}
+
+func NewSakuracloudAPI(accessToken, accessTokenSecret, endpoint string) (*sakuracloudAPI, error) {
+ baseURL, err := url.Parse(endpoint)
+ if err != nil {
+ return nil, fmt.Errorf("endpoint_url parse error: %w", err)
+ }
+
+ return &sakuracloudAPI{
+ accessToken: accessToken,
+ accessTokenSecret: accessTokenSecret,
+ baseURL: *baseURL,
+ httpClient: &http.Client{
+ Timeout: time.Minute,
+ },
+ }, nil
+}
+
+func (api *sakuracloudAPI) request(method, path string, data []byte) ([]byte, error) {
+ req, err := http.NewRequest(method, path, bytes.NewReader(data))
+ if err != nil {
+ return nil, err
+ }
+
+ req.SetBasicAuth(api.accessToken, api.accessTokenSecret)
+ req.Header.Add("Content-Type", "applicaiton/json; charset=UTF-8")
+ resp, err := api.httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode >= 400 {
+ var errResp errorResponse
+ err := json.Unmarshal(respBody, &errResp)
+ if err != nil {
+ return nil, err
+ }
+ // Since an error_msg uses HTML entities, unescape it.
+ return nil, fmt.Errorf("request failed: status: %s, serial: %s, error_code: %s, error_msg: %s", errResp.Status, errResp.Serial, errResp.ErrorCode, html.UnescapeString(errResp.ErrorMsg))
+ }
+
+ return respBody, nil
+}
+
+// getCommonServiceItems return all the zones in the account
+func (api *sakuracloudAPI) getCommonServiceItems() ([]*commonServiceItem, error) {
+ var items []*commonServiceItem
+
+ nextFrom := 0
+ count := 100
+ for {
+ u := api.baseURL.JoinPath("/commonserviceitem")
+
+ if nextFrom > 0 {
+ // The query string is similar to the flow-style YAML.
+ // {From: 0, Count: 10}
+ query := fmt.Sprintf("{From: %d, Count: %d}", nextFrom, count)
+ u.RawQuery = url.QueryEscape(query)
+ }
+
+ respBody, err := api.request(http.MethodGet, u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var respData responseCommonServiceItems
+ err = json.Unmarshal(respBody, &respData)
+ if err != nil {
+ return nil, err
+ }
+
+ if items == nil {
+ items = make([]*commonServiceItem, 0, respData.Total)
+ }
+
+ for _, item := range respData.CommonServiceItems {
+ items = append(items, &item)
+ }
+
+ count = respData.Count
+ nextFrom = respData.From + respData.Count
+ if nextFrom == respData.Total {
+ break
+ }
+ }
+
+ return items, nil
+}
+
+// GetCommonServiceItemMap return all the zones in the account
+func (api *sakuracloudAPI) GetCommonServiceItemMap() (map[string]*commonServiceItem, error) {
+ if api.commonServiceItemMap != nil {
+ return api.commonServiceItemMap, nil
+ }
+
+ items, err := api.getCommonServiceItems()
+ if err != nil {
+ return nil, err
+ }
+
+ api.commonServiceItemMap = make(map[string]*commonServiceItem, len(items))
+ for _, item := range items {
+ if item.ServiceClass != "cloud/dns" {
+ continue
+ }
+ api.commonServiceItemMap[item.Status.Zone] = item
+ }
+
+ return api.commonServiceItemMap, nil
+}
+
+// postCommonServiceItem submits a CommonServiceItem to the API and create the zone.
+func (api *sakuracloudAPI) postCommonServiceItem(reqItem requestCommonServiceItem) (*commonServiceItem, error) {
+ reqBody, err := json.Marshal(reqItem)
+ if err != nil {
+ return nil, err
+ }
+
+ u := api.baseURL.JoinPath("/commonserviceitem")
+ respBody, err := api.request(http.MethodPost, u.String(), reqBody)
+ if err != nil {
+ return nil, err
+ }
+
+ var respData responseCommonServiceItem
+ err = json.Unmarshal(respBody, &respData)
+ if err != nil {
+ return nil, err
+ }
+
+ return &respData.CommonServiceItem, nil
+}
+
+// CreateZone submits a CommonServiceItem to the API and create the zone.
+func (api *sakuracloudAPI) CreateZone(domain string) error {
+ reqItem := requestCommonServiceItem{
+ CommonServiceItem: commonServiceItem{
+ Name: domain,
+ Status: status{
+ Zone: domain,
+ },
+ Settings: settings{
+ DNS: dNS{
+ ResourceRecordSets: []domainRecord{},
+ },
+ },
+ Provider: provider{
+ Class: "dns",
+ },
+ },
+ }
+
+ item, err := api.postCommonServiceItem(reqItem)
+ if err != nil {
+ return err
+ }
+
+ api.commonServiceItemMap[domain] = item
+ return nil
+}
+
+// putCommonServiceItem submits a CommonServiceItem to the API and updates the zone data.
+func (api *sakuracloudAPI) putCommonServiceItem(id string, reqItem requestCommonServiceItem) (*commonServiceItem, error) {
+ reqBody, err := json.Marshal(reqItem)
+ if err != nil {
+ return nil, err
+ }
+
+ u := api.baseURL.JoinPath("/commonserviceitem/").JoinPath(id)
+ respBody, err := api.request(http.MethodPut, u.String(), reqBody)
+ if err != nil {
+ return nil, err
+ }
+
+ var respData responseCommonServiceItem
+ err = json.Unmarshal(respBody, &respData)
+ if err != nil {
+ return nil, err
+ }
+
+ return &respData.CommonServiceItem, nil
+}
+
+// UpdateZone submits a CommonServiceItem to the API and updates the zone data.
+func (api *sakuracloudAPI) UpdateZone(domain string, domainRecords []domainRecord) error {
+ drs := make([]domainRecord, 0, len(domainRecords)-2) // Removes 2 NS records.
+ for _, r := range domainRecords {
+ if r.Type == "NS" && r.Name == "@" {
+ continue
+ }
+ drs = append(drs, r)
+ }
+
+ reqItem := requestCommonServiceItem{
+ CommonServiceItem: commonServiceItem{
+ Settings: settings{
+ DNS: dNS{
+ ResourceRecordSets: drs,
+ },
+ },
+ },
+ }
+
+ item, err := api.putCommonServiceItem(api.commonServiceItemMap[domain].ID, reqItem)
+ if err != nil {
+ return err
+ }
+
+ api.commonServiceItemMap[domain] = item
+ return nil
+}
diff --git a/providers/sakuracloud/auditrecords.go b/providers/sakuracloud/auditrecords.go
new file mode 100644
index 0000000000..47ad061449
--- /dev/null
+++ b/providers/sakuracloud/auditrecords.go
@@ -0,0 +1,93 @@
+package sakuracloud
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
+ "github.com/miekg/dns"
+)
+
+// AuditRecords returns a list of errors corresponding to the records
+// that aren't supported by this provider. If all records are
+// supported, an empty list is returned.
+func AuditRecords(records []*models.RecordConfig) []error {
+ a := rejectif.Auditor{}
+
+ a.Add("MX", rejectif.MxNull) // Last verified 2024-08-09
+
+ a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2024-08-09
+
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2024-08-09
+
+ a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2024-08-09
+
+ a.Add("TXT", rejectif.TxtHasUnpairedDoubleQuotes) // Last verified 2024-08-09
+
+ a.Add("TXT", rejectif.TxtLongerThan(500)) // Last verified 2024-08-09
+
+ a.Add("CAA", rejectifCaaLongerThan(64)) // Last verified 2024-08-09
+
+ a.Add("NS", rejectifNsPointsToOrigin) // Last verified 2024-08-09
+
+ for _, t := range []string{"ALIAS", "CNAME", "HTTPS", "MX", "NS", "PTR", "SRV", "SVCB"} {
+ a.Add(t, rejectifTargetHasExample) // Last verified 2024-08-09
+ }
+
+ for _, t := range []string{"A", "AAAA", "ALIAS", "CAA", "CNAME", "HTTPS", "MX", "NS", "PTR", "SRV", "SVCB", "TXT"} {
+ a.Add(t, rejectifLabelHasExample) // Last verified 2024-08-09
+ }
+ return a.Audit(records)
+}
+
+// rejectifCaaLongerThan returns a function that audits CAA records
+// where the length of the property value is greater than maxLength.
+func rejectifCaaLongerThan(maxLength int) func(rc *models.RecordConfig) error {
+ return func(rc *models.RecordConfig) error {
+ m := maxLength
+ if len(rc.GetTargetField()) > m {
+ return fmt.Errorf("CAA record longer than %d octets (chars)", m)
+ }
+ return nil
+ }
+}
+
+// rejectifNsPointsToOrigin audits NS records that point to the origin.
+func rejectifNsPointsToOrigin(rc *models.RecordConfig) error {
+ originFQDN := strings.TrimPrefix(rc.GetLabelFQDN(), rc.GetLabel()+".") + "."
+ if originFQDN == rc.GetTargetField() {
+ return fmt.Errorf("NS record points to the origin: %s", rc.GetTargetField())
+ }
+ return nil
+}
+
+var labelExampleRe = regexp.MustCompile(`^example[0-9]?$`)
+
+func hasLabelExample(domain string) error {
+ for _, l := range dns.SplitDomainName(domain) {
+ if labelExampleRe.MatchString(l) {
+ return fmt.Errorf("label contains `example`: %s", domain)
+ }
+ }
+ return nil
+}
+
+// rejectifTargetHasExample returns a function that audits RDATA targets
+// containing the following labels:
+//
+// - example
+// - exampleN, where N is a numerical character
+func rejectifTargetHasExample(rc *models.RecordConfig) error {
+ return hasLabelExample(rc.GetTargetField())
+}
+
+// rejectifLabelHasExample returns a function that audits owner names
+// containing the following labels:
+//
+// - example
+// - exampleN, where N is a numerical character
+func rejectifLabelHasExample(rc *models.RecordConfig) error {
+ return hasLabelExample(rc.GetLabel())
+}
diff --git a/providers/sakuracloud/convert.go b/providers/sakuracloud/convert.go
new file mode 100644
index 0000000000..20f7677ff9
--- /dev/null
+++ b/providers/sakuracloud/convert.go
@@ -0,0 +1,47 @@
+package sakuracloud
+
+import (
+ "github.com/StackExchange/dnscontrol/v4/models"
+)
+
+const defaultTTL = uint32(3600)
+
+func toRc(domain string, r domainRecord) *models.RecordConfig {
+ rc := &models.RecordConfig{
+ Type: r.Type,
+ TTL: r.TTL,
+ Original: r,
+ }
+ if r.TTL == 0 {
+ rc.TTL = defaultTTL
+ }
+
+ rc.SetLabel(r.Name, domain)
+
+ switch r.Type {
+ case "TXT":
+ // TXT records are stored verbatim; no quoting/escaping to parse.
+ rc.SetTargetTXT(r.RData)
+ default:
+ rc.PopulateFromString(r.Type, r.RData, domain)
+ }
+
+ return rc
+}
+
+func toNative(rc *models.RecordConfig) domainRecord {
+ rr := domainRecord{
+ Name: rc.GetLabel(),
+ Type: rc.Type,
+ RData: rc.String(),
+ }
+ if rc.TTL != defaultTTL {
+ rr.TTL = rc.TTL
+ }
+
+ switch rc.Type {
+ case "TXT":
+ rr.RData = rc.GetTargetTXTJoined()
+ }
+ return rr
+}
diff --git a/providers/sakuracloud/listzones.go b/providers/sakuracloud/listzones.go
new file mode 100644
index 0000000000..e408c4f673
--- /dev/null
+++ b/providers/sakuracloud/listzones.go
@@ -0,0 +1,32 @@
+package sakuracloud
+
+import "github.com/StackExchange/dnscontrol/v4/pkg/printer"
+
+// ListZones return all the zones in the account
+func (s *sakuracloudProvider) ListZones() ([]string, error) {
+ itemMap, err := s.api.GetCommonServiceItemMap()
+ if err != nil {
+ return nil, err
+ }
+
+ var zones []string
+ for _, item := range itemMap {
+ zones = append(zones, item.Status.Zone)
+ }
+ return zones, nil
+}
+
+// EnsureZoneExists creates a zone if it does not exist
+func (s *sakuracloudProvider) EnsureZoneExists(domain string) error {
+ itemMap, err := s.api.GetCommonServiceItemMap()
+ if err != nil {
+ return err
+ }
+
+ if _, ok := itemMap[domain]; ok {
+ return nil
+ }
+
+ printer.Printf("Adding zone for %s to Sakura Cloud account\n", domain)
+ return s.api.CreateZone(domain)
+}
diff --git a/providers/sakuracloud/records.go b/providers/sakuracloud/records.go
new file mode 100644
index 0000000000..d29c642b75
--- /dev/null
+++ b/providers/sakuracloud/records.go
@@ -0,0 +1,92 @@
+package sakuracloud
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/pkg/diff2"
+)
+
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (s *sakuracloudProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
+ itemMap, err := s.api.GetCommonServiceItemMap()
+ if err != nil {
+ return nil, err
+ }
+
+ item, ok := itemMap[domain]
+ if !ok {
+ return nil, errNoExist{domain}
+ }
+
+ existingRecords := make([]*models.RecordConfig, 0, len(item.Status.NS)+len(item.Settings.DNS.ResourceRecordSets))
+
+ for _, ns := range item.Status.NS {
+ // CommonServiceItem.Status.NS fields do not end with a dot.
+ // Therefore, a dot is added at the end to make it an absolute domain name.
+ //
+ // "Status": {
+ // "Zone": "example.com",
+ // "NS": [
+ // "ns1.gslbN.sakura.ne.jp",
+ // "ns2.gslbN.sakura.ne.jp"
+ // ]
+ // },
+ rc := &models.RecordConfig{
+ Type: "NS",
+ TTL: defaultTTL,
+ Original: ns,
+ }
+ rc.SetLabel("@", domain)
+ if err := rc.PopulateFromString("NS", ns+".", domain); err != nil {
+ return nil, fmt.Errorf("unparsable record received: %w", err)
+ }
+ existingRecords = append(existingRecords, rc)
+ }
+
+ for _, dr := range item.Settings.DNS.ResourceRecordSets {
+ rc := toRc(domain, dr)
+ existingRecords = append(existingRecords, rc)
+ }
+ return existingRecords, nil
+}
+
+// GetZoneRecordsCorrections gets the records of a zone and returns them in RecordConfig format.
+func (s *sakuracloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, int, error) {
+ var corrections []*models.Correction
+
+ // The name servers for the Sakura cloud provider cannot be changed.
+ // These default TTL is 3600 and the default TTL of DNSControl is 300, so NS corrections can be found.
+ // To prevent this, match TTL of DNSControl to one of Sakura Cloud provider.
+ for _, rc := range dc.Records {
+ if rc.Type == "NS" && rc.Name == "@" {
+ rc.TTL = defaultTTL
+ }
+ }
+
+ result, err := diff2.ByZone(existing, dc, nil)
+ if err != nil {
+ return nil, 0, err
+ }
+ msgs, changes, actualChangeCount := result.Msgs, result.HasChanges, result.ActualChangeCount
+ if !changes {
+ return nil, actualChangeCount, nil
+ }
+ msg := strings.Join(msgs, "\n")
+
+ corrections = append(corrections,
+ &models.Correction{
+ Msg: msg,
+ F: func() error {
+ drs := make([]domainRecord, 0, len(result.DesiredPlus))
+ for _, rc := range result.DesiredPlus {
+ drs = append(drs, toNative(rc))
+ }
+ return s.api.UpdateZone(dc.Name, drs)
+ },
+ },
+ )
+
+ return corrections, actualChangeCount, nil
+}
diff --git a/providers/sakuracloud/sakuracloudProvider.go b/providers/sakuracloud/sakuracloudProvider.go
new file mode 100644
index 0000000000..bdf29a24b0
--- /dev/null
+++ b/providers/sakuracloud/sakuracloudProvider.go
@@ -0,0 +1,108 @@
+package sakuracloud
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/StackExchange/dnscontrol/v4/models"
+ "github.com/StackExchange/dnscontrol/v4/providers"
+)
+
+const defaultEndpoint = "https://secure.sakura.ad.jp/cloud/zone/is1a/api/cloud/1.1"
+
+var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
+ providers.CanAutoDNSSEC: providers.Cannot(),
+ providers.CanConcur: providers.Cannot(),
+ providers.CanGetZones: providers.Can(),
+ providers.CanUseAlias: providers.Can(),
+ providers.CanUseCAA: providers.Can(),
+ providers.CanUseDHCID: providers.Cannot(),
+ providers.CanUseDNAME: providers.Cannot(),
+ providers.CanUseDS: providers.Cannot(),
+ providers.CanUseDSForChildren: providers.Cannot(),
+ providers.CanUseHTTPS: providers.Can(),
+ providers.CanUseLOC: providers.Cannot(),
+ providers.CanUseNAPTR: providers.Cannot(),
+ providers.CanUsePTR: providers.Can(),
+ providers.CanUseSOA: providers.Cannot(),
+ providers.CanUseSRV: providers.Can(),
+ providers.CanUseSSHFP: providers.Cannot(),
+ providers.CanUseSVCB: providers.Can(),
+ providers.CanUseTLSA: providers.Cannot(),
+ providers.CanUseDNSKEY: providers.Cannot(),
+ providers.DocCreateDomains: providers.Can(),
+ providers.DocDualHost: providers.Cannot(),
+ providers.DocOfficiallySupported: providers.Cannot(),
+}
+
+func init() {
+ const providerName = "SAKURACLOUD"
+ const providerMaintainer = "@ttkzw"
+ fns := providers.DspFuncs{
+ Initializer: newSakuracloudDsp,
+ RecordAuditor: AuditRecords,
+ }
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
+}
+
+type sakuracloudProvider struct {
+ api *sakuracloudAPI
+}
+
+func newSakuracloudDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
+ return newSakuracloud(conf, metadata)
+}
+
+// newDSP initializes a Sakura Cloud DNSServiceProvider.
+func newSakuracloud(config map[string]string, _ json.RawMessage) (*sakuracloudProvider, error) {
+ // config -- the key/values from creds.json
+ accessToken := config["access_token"]
+ if accessToken == "" {
+ return nil, fmt.Errorf("access_token is required")
+ }
+
+ accessTokenSecret := config["access_token_secret"]
+ if accessTokenSecret == "" {
+ return nil, fmt.Errorf("access_token_secret is required")
+ }
+
+ endpoint := config["endpoint"]
+ if endpoint == "" {
+ endpoint = defaultEndpoint
+ }
+
+ api, err := NewSakuracloudAPI(accessToken, accessTokenSecret, endpoint)
+ if err != nil {
+ return nil, err
+ }
+ dsp := &sakuracloudProvider{
+ api: api,
+ }
+ return dsp, nil
+}
+
+type errNoExist struct {
+ domain string
+}
+
+func (e errNoExist) Error() string {
+ return fmt.Sprintf("Zone %s not found in your Sakura Cloud account", e.domain)
+}
+
+// GetNameservers returns the current nameservers for a domain.
+func (s *sakuracloudProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ itemMap, err := s.api.GetCommonServiceItemMap()
+ if err != nil {
+ return nil, err
+ }
+
+ item, ok := itemMap[domain]
+ if !ok {
+ return nil, errNoExist{domain}
+ }
+
+ return models.ToNameservers(item.Status.NS)
+}
diff --git a/providers/softlayer/softlayerProvider.go b/providers/softlayer/softlayerProvider.go
index 4238302b89..32f339d385 100644
--- a/providers/softlayer/softlayerProvider.go
+++ b/providers/softlayer/softlayerProvider.go
@@ -22,17 +22,23 @@ type softlayerProvider struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Unimplemented(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
}
func init() {
+ const providerName = "SOFTLAYER"
+ const providerMaintainer = "NEEDS VOLUNTEER"
fns := providers.DspFuncs{
Initializer: newReg,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("SOFTLAYER", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func newReg(conf map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
@@ -75,15 +81,15 @@ func (s *softlayerProvider) GetZoneRecords(domainName string, meta map[string]st
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (s *softlayerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
+func (s *softlayerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
domain, err := s.getDomain(&dc.Name)
if err != nil {
- return nil, err
+ return nil, 0, err
}
- toReport, create, deletes, modify, err := diff.NewCompat(dc).IncrementalDiff(actual)
+ toReport, create, deletes, modify, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(actual)
if err != nil {
- return nil, err
+ return nil, 0, err
}
// Start corrections with the reports
corrections := diff.GenerateMessageCorrections(toReport)
@@ -111,7 +117,7 @@ func (s *softlayerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, a
})
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
func (s *softlayerProvider) getDomain(name *string) (*datatypes.Dns_Domain, error) {
@@ -173,7 +179,10 @@ func (s *softlayerProvider) getExistingRecords(domain *datatypes.Dns_Domain) (mo
}
recConfig.SetLabel(fmt.Sprintf("%s.%s", service, strings.ToLower(protocol)), *domain.Name)
case "TXT":
- recConfig.TxtStrings = append(recConfig.TxtStrings, *record.Data)
+ // OLD: recConfig.TxtStrings = append(recConfig.TxtStrings, *record.Data)
+ recConfig.SetTargetTXTs(append(recConfig.GetTargetTXTSegmented(), *record.Data))
+ // NB(tlim) The above code seems too complex. Can it be simplied to this?
+ // recConfig.SetTargetTXT(*record.Data)
fallthrough
case "MX":
if record.MxPriority != nil {
diff --git a/providers/transip/auditrecords.go b/providers/transip/auditrecords.go
index 814e1c7923..281058d85a 100644
--- a/providers/transip/auditrecords.go
+++ b/providers/transip/auditrecords.go
@@ -10,10 +10,22 @@ import (
// supported, an empty list is returned.
func AuditRecords(records []*models.RecordConfig) []error {
a := rejectif.Auditor{}
- a.Add("MX", rejectif.MxNull) // Last verified 2023-01-28
- a.Add("TXT", rejectif.TxtHasBackticks) // Last verified 2023-01-28
- a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2023-01-28
- a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-01-28
+
+ a.Add("ALIAS", rejectif.LabelNotApex) // Last verified 2024-01-11
+
+ a.Add("MX", rejectif.MxNull) // Last verified 2023-12-04
+
+ a.Add("TXT", rejectif.TxtHasBackticks) // Last verified 2024-01-11
+
+ a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2024-01-11
+
+ a.Add("TXT", rejectif.TxtStartsOrEndsWithSpaces) // Last verified 2024-01-11
+
+ a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-01-11
+
+ a.Add("TXT", rejectif.TxtLongerThan(1024)) // Last verified 2024-01-11
+
+ a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2024-01-11
return a.Audit(records)
}
diff --git a/providers/transip/transipProvider.go b/providers/transip/transipProvider.go
index aadb707824..a83acc397f 100644
--- a/providers/transip/transipProvider.go
+++ b/providers/transip/transipProvider.go
@@ -29,18 +29,32 @@ type transipProvider struct {
}
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Can(),
+ providers.CanUseAKAMAICDN: providers.Cannot(),
providers.CanUseAlias: providers.Can(),
+ providers.CanUseAzureAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
+ providers.CanUseDHCID: providers.Cannot(),
+ providers.CanUseDNAME: providers.Cannot(),
providers.CanUseDS: providers.Cannot(),
providers.CanUseDSForChildren: providers.Cannot(),
+ providers.CanUseHTTPS: providers.Cannot(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUseNAPTR: providers.Can(),
+ providers.CanUsePTR: providers.Cannot(),
+ providers.CanUseRoute53Alias: providers.Cannot(),
+ providers.CanUseSOA: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
+ providers.CanUseSVCB: providers.Cannot(),
providers.CanUseTLSA: providers.Can(),
+ providers.CanUseDNSKEY: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(),
+ providers.DocDualHost: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
}
@@ -73,11 +87,14 @@ func NewTransip(m map[string]string, metadata json.RawMessage) (providers.DNSSer
}
func init() {
+ const providerName = "TRANSIP"
+ const providerMaintainer = "@blackshadev"
fns := providers.DspFuncs{
Initializer: NewTransip,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("TRANSIP", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
func (n *transipProvider) ListZones() ([]string, error) {
@@ -97,20 +114,20 @@ func (n *transipProvider) ListZones() ([]string, error) {
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (n *transipProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, curRecords models.Records) ([]*models.Correction, error) {
+func (n *transipProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, curRecords models.Records) ([]*models.Correction, int, error) {
removeOtherNS(dc)
- corrections, err := n.getCorrectionsUsingDiff2(dc, curRecords)
- return corrections, err
+ corrections, actualChangeCount, err := n.getCorrectionsUsingDiff2(dc, curRecords)
+ return corrections, actualChangeCount, err
}
-func (n *transipProvider) getCorrectionsUsingDiff2(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+func (n *transipProvider) getCorrectionsUsingDiff2(dc *models.DomainConfig, records models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
- instructions, err := diff2.ByRecordSet(records, dc, nil)
+ instructions, actualChangeCount, err := diff2.ByRecordSet(records, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, change := range instructions {
@@ -119,7 +136,7 @@ func (n *transipProvider) getCorrectionsUsingDiff2(dc *models.DomainConfig, reco
case diff2.DELETE:
oldEntries, err := recordsToNative(change.Old, true)
if err != nil {
- return corrections, err
+ return corrections, 0, err
}
correction := change.CreateCorrection(
wrapChangeFunction(
@@ -132,7 +149,7 @@ func (n *transipProvider) getCorrectionsUsingDiff2(dc *models.DomainConfig, reco
case diff2.CREATE:
newEntries, err := recordsToNative(change.New, false)
if err != nil {
- return corrections, err
+ return corrections, 0, err
}
correction := change.CreateCorrection(
wrapChangeFunction(
@@ -146,7 +163,7 @@ func (n *transipProvider) getCorrectionsUsingDiff2(dc *models.DomainConfig, reco
if canDirectApplyDNSEntries(change) {
newEntries, err := recordsToNative(change.New, false)
if err != nil {
- return corrections, err
+ return corrections, 0, err
}
correction := change.CreateCorrection(
wrapChangeFunction(
@@ -156,22 +173,9 @@ func (n *transipProvider) getCorrectionsUsingDiff2(dc *models.DomainConfig, reco
)
corrections = append(corrections, correction)
} else {
-
- oldEntries, err := recordsToNative(change.Old, true)
- if err != nil {
- return corrections, err
- }
- newEntries, err := recordsToNative(change.New, false)
- if err != nil {
- return corrections, err
- }
-
- deleteCorrection := wrapChangeFunction(oldEntries, func(rec domain.DNSEntry) error { return n.domains.RemoveDNSEntry(dc.Name, rec) })
- createCorrection := wrapChangeFunction(newEntries, func(rec domain.DNSEntry) error { return n.domains.AddDNSEntry(dc.Name, rec) })
corrections = append(
corrections,
- change.CreateCorrectionWithMessage("[1/2] delete", deleteCorrection),
- change.CreateCorrectionWithMessage("[2/2] create", createCorrection),
+ n.recreateRecordSet(dc, change)...,
)
}
case diff2.REPORT:
@@ -180,7 +184,43 @@ func (n *transipProvider) getCorrectionsUsingDiff2(dc *models.DomainConfig, reco
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
+}
+
+func (n *transipProvider) recreateRecordSet(dc *models.DomainConfig, change diff2.Change) []*models.Correction {
+ var corrections []*models.Correction
+
+ for _, rec := range change.Old {
+ if existsInRecords(rec, change.New) {
+ continue
+ }
+
+ nativeRec, _ := recordToNative(rec, true)
+ createCorrection := change.CreateCorrectionWithMessage("[1/2] delete", func() error { return n.domains.RemoveDNSEntry(dc.Name, nativeRec) })
+ corrections = append(corrections, createCorrection)
+ }
+
+ for _, rec := range change.New {
+ if existsInRecords(rec, change.Old) {
+ continue
+ }
+
+ nativeRec, _ := recordToNative(rec, false)
+ createCorrection := change.CreateCorrectionWithMessage("[2/2] create", func() error { return n.domains.AddDNSEntry(dc.Name, nativeRec) })
+ corrections = append(corrections, createCorrection)
+ }
+
+ return corrections
+}
+
+func existsInRecords(rec *models.RecordConfig, set models.Records) bool {
+ for _, existing := range set {
+ if rec.ToComparableNoTTL() == existing.ToComparableNoTTL() && rec.TTL == existing.TTL {
+ return true
+ }
+ }
+
+ return false
}
func recordsToNative(records models.Records, useOriginal bool) ([]domain.DNSEntry, error) {
@@ -276,6 +316,7 @@ func (n *transipProvider) GetNameservers(domainName string) ([]*models.Nameserve
return models.ToNameservers(nss)
}
+// recordToNative convrts RecordConfig TO Native.
func recordToNative(config *models.RecordConfig, useOriginal bool) (domain.DNSEntry, error) {
if useOriginal && config.Original != nil {
return config.Original.(domain.DNSEntry), nil
@@ -285,10 +326,11 @@ func recordToNative(config *models.RecordConfig, useOriginal bool) (domain.DNSEn
Name: config.Name,
Expire: int(config.TTL),
Type: config.Type,
- Content: getTargetRecordContent(config),
+ Content: config.GetTargetCombinedFunc(nil),
}, nil
}
+// nativeToRecord converts native to RecordConfig.
func nativeToRecord(entry domain.DNSEntry, origin string) (*models.RecordConfig, error) {
rc := &models.RecordConfig{
TTL: uint32(entry.Expire),
@@ -296,7 +338,7 @@ func nativeToRecord(entry domain.DNSEntry, origin string) (*models.RecordConfig,
Original: entry,
}
rc.SetLabel(entry.Name, origin)
- if err := rc.PopulateFromString(entry.Type, entry.Content, origin); err != nil {
+ if err := rc.PopulateFromStringFunc(entry.Type, entry.Content, origin, nil); err != nil {
return nil, fmt.Errorf("unparsable record received from TransIP: %w", err)
}
@@ -315,16 +357,3 @@ func removeOtherNS(dc *models.DomainConfig) {
}
dc.Records = newList
}
-
-func getTargetRecordContent(rc *models.RecordConfig) string {
- switch rtype := rc.Type; rtype {
- case "SSHFP":
- return fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField())
- case "DS":
- return fmt.Sprintf("%d %d %d %s", rc.DsKeyTag, rc.DsAlgorithm, rc.DsDigestType, rc.DsDigest)
- case "SRV":
- return fmt.Sprintf("%d %d %d %s", rc.SrvPriority, rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
- default:
- return models.StripQuotes(rc.GetTargetCombined())
- }
-}
diff --git a/providers/vultr/auditrecords.go b/providers/vultr/auditrecords.go
index e6aed47590..72653d0e59 100644
--- a/providers/vultr/auditrecords.go
+++ b/providers/vultr/auditrecords.go
@@ -17,8 +17,6 @@ func AuditRecords(records []*models.RecordConfig) []error {
// Needs investigation. Could be a dnscontrol issue or
// the provider doesn't support double quotes.
- a.Add("TXT", rejectif.TxtHasMultipleSegments)
-
a.Add("CAA", rejectif.CaaTargetContainsWhitespace) // Last verified 2023-01-19
return a.Audit(records)
diff --git a/providers/vultr/convert_test.go b/providers/vultr/convert_test.go
index f4d1802c6f..aca7fd9a90 100644
--- a/providers/vultr/convert_test.go
+++ b/providers/vultr/convert_test.go
@@ -65,7 +65,7 @@ func TestConversion(t *testing.T) {
t.Error("Error converting Vultr record", record)
}
- converted := toVultrRecord(dc, rc, "0")
+ converted := toVultrRecord(rc, "0")
if converted.Type != record.Type || converted.Name != record.Name || converted.Data != record.Data || (converted.Priority != record.Priority) || converted.TTL != record.TTL {
t.Error("Vultr record conversion mismatch", record, rc, converted)
diff --git a/providers/vultr/vultrProvider.go b/providers/vultr/vultrProvider.go
index 1a5b5b5772..25aa9143d3 100644
--- a/providers/vultr/vultrProvider.go
+++ b/providers/vultr/vultrProvider.go
@@ -26,7 +26,10 @@ Info required in `creds.json`:
*/
var features = providers.DocumentationNotes{
+ // The default for unlisted capabilities is 'Cannot'.
+ // See providers/capabilities.go for the entire list of capabilities.
providers.CanGetZones: providers.Can(),
+ providers.CanConcur: providers.Cannot(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
@@ -39,11 +42,14 @@ var features = providers.DocumentationNotes{
}
func init() {
+ const providerName = "VULTR"
+ const providerMaintainer = "@pgaskin"
fns := providers.DspFuncs{
Initializer: NewProvider,
RecordAuditor: AuditRecords,
}
- providers.RegisterDomainServiceProviderType("VULTR", fns, features)
+ providers.RegisterDomainServiceProviderType(providerName, fns, features)
+ providers.RegisterMaintainer(providerName, providerMaintainer)
}
// vultrProvider represents the Vultr DNSServiceProvider.
@@ -109,7 +115,7 @@ func (api *vultrProvider) GetZoneRecords(domain string, meta map[string]string)
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
-func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, curRecords models.Records) ([]*models.Correction, error) {
+func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, curRecords models.Records) ([]*models.Correction, int, error) {
var corrections []*models.Correction
for _, rec := range dc.Records {
@@ -118,7 +124,7 @@ func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, cur
// These rtypes are hostnames, therefore need to be converted (unlike, for example, an AAAA record)
t, err := idna.ToUnicode(rec.GetTargetField())
if err != nil {
- return nil, err
+ return nil, 0, err
}
rec.SetTarget(t)
default:
@@ -126,10 +132,9 @@ func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, cur
}
}
- changes, err := diff2.ByRecord(curRecords, dc, nil)
-
+ changes, actualChangeCount, err := diff2.ByRecord(curRecords, dc, nil)
if err != nil {
- return nil, err
+ return nil, 0, err
}
for _, change := range changes {
@@ -137,7 +142,7 @@ func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, cur
case diff2.REPORT:
corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined})
case diff2.CREATE:
- r := toVultrRecord(dc, change.New[0], "0")
+ r := toVultrRecord(change.New[0], "0")
corrections = append(corrections, &models.Correction{
Msg: change.Msgs[0],
F: func() error {
@@ -146,7 +151,7 @@ func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, cur
},
})
case diff2.CHANGE:
- r := toVultrRecord(dc, change.New[0], change.Old[0].Original.(govultr.DomainRecord).ID)
+ r := toVultrRecord(change.New[0], change.Old[0].Original.(govultr.DomainRecord).ID)
corrections = append(corrections, &models.Correction{
Msg: fmt.Sprintf("%s; Vultr RecordID: %v", change.Msgs[0], r.ID),
F: func() error {
@@ -166,7 +171,7 @@ func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, cur
}
}
- return corrections, nil
+ return corrections, actualChangeCount, nil
}
// GetNameservers gets the Vultr nameservers for a domain
@@ -271,7 +276,7 @@ func toRecordConfig(domain string, r govultr.DomainRecord) (*models.RecordConfig
}
// toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DomainRecordReq. #rtype_variations
-func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig, vultrID string) *govultr.DomainRecord {
+func toVultrRecord(rc *models.RecordConfig, vultrID string) *govultr.DomainRecord {
name := rc.GetLabel()
// Vultr uses a blank string to represent the apex domain.
if name == "@" {
@@ -314,7 +319,7 @@ func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig, vultrID str
// Vultr doesn't permit TXT strings to include double-quotes
// therefore, we don't have to escape interior double-quotes.
// Vultr's API requires the string to begin and end with double-quotes.
- r.Data = `"` + strings.Join(rc.TxtStrings, "") + `"`
+ r.Data = `"` + strings.Join(rc.GetTargetTXTSegmented(), "") + `"`
default:
}