diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index b8b76bd40..e3f4dd6b1 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -52,7 +52,7 @@ jobs: Write-Host "Integration test providers: $Providers" echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT env: - PROVIDERS: "['AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','HEDNS','HEXONET','HUAWEICLOUD','INWX','JOKER','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']" + PROVIDERS: "['AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','HEDNS','HETZNER_V2','HEXONET','HUAWEICLOUD','INWX','JOKER','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']" ENV_CONTEXT: ${{ toJson(env) }} VARS_CONTEXT: ${{ toJson(vars) }} SECRETS_CONTEXT: ${{ toJson(secrets) }} @@ -87,6 +87,7 @@ jobs: GANDI_V5_DOMAIN: ${{ vars.GANDI_V5_DOMAIN }} GCLOUD_DOMAIN: ${{ vars.GCLOUD_DOMAIN }} HEDNS_DOMAIN: ${{ vars.HEDNS_DOMAIN }} + HETZNER_V2_DOMAIN: ${{ vars.HETZNER_V2_DOMAIN }} HEXONET_DOMAIN: ${{ vars.HEXONET_DOMAIN }} HUAWEICLOUD_DOMAIN: ${{ vars.HUAWEICLOUD_DOMAIN }} JOKER_DOMAIN: ${{ vars.JOKER_DOMAIN }} @@ -154,6 +155,8 @@ jobs: HEDNS_TOTP_SECRET: ${{ secrets.HEDNS_TOTP_SECRET }} HEDNS_USERNAME: ${{ secrets.HEDNS_USERNAME }} # + HETZNER_V2_API_TOKEN: ${{ secrets.HETZNER_V2_API_TOKEN }} + # HEXONET_ENTITY: ${{ secrets.HEXONET_ENTITY }} HEXONET_PW: ${{ secrets.HEXONET_PW }} HEXONET_UID: ${{ secrets.HEXONET_UID }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 68ec15037..c2938e051 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -39,7 +39,7 @@ changelog: regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$" order: 1 - title: 'Provider-specific changes:' - regexp: "(?i)((adguardhome|akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cnr|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|huaweicloud|inwx|joker|linode|loopia|luadns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|vultr).*:)+.*" + regexp: "(?i)((adguardhome|akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cnr|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi|gcloud|gcore|hedns|hetzner|hetznerv2|hexonet|hostingde|huaweicloud|inwx|joker|linode|loopia|luadns|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)]*:+.*$" diff --git a/OWNERS b/OWNERS index ea320dec8..3cc1ee6fe 100644 --- a/OWNERS +++ b/OWNERS @@ -25,6 +25,7 @@ providers/gcloud @riyadhalnur providers/gcore @xddxdd providers/hedns @rblenkinsopp providers/hetzner @das7pad +providers/hetznerv2 @das7pad providers/hexonet @KaiSchwarz-cnic providers/hostingde @juliusrickert providers/huaweicloud @huihuimoe diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index 4fdc07d67..d7d5dca11 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -137,7 +137,8 @@ * [Gandi_v5](provider/gandi_v5.md) * [Gcore](provider/gcore.md) * [Google Cloud DNS](provider/gcloud.md) -* [Hetzner DNS Console](provider/hetzner.md) +* [Hetzner DNS API](provider/hetzner_v2.md) +* [Hetzner DNS Console (legacy)](provider/hetzner.md) * [HEXONET](provider/hexonet.md) * [hosting.de](provider/hostingde.md) * [Huawei Cloud DNS](provider/huaweicloud.md) diff --git a/documentation/provider/hetzner_v2.md b/documentation/provider/hetzner_v2.md new file mode 100644 index 000000000..d8788d886 --- /dev/null +++ b/documentation/provider/hetzner_v2.md @@ -0,0 +1,54 @@ +## Configuration + +To use this provider, add an entry to `creds.json` with `TYPE` set to `HETZNER_V2` +along with a [Hetzner API Token](https://docs.hetzner.cloud/reference/cloud#getting-started). + +Example: + +{% code title="creds.json" %} +```json +{ + "hetzner_v2": { + "TYPE": "HETZNER_V2", + "api_token": "your-api-token" + } +} +``` +{% endcode %} + +## Metadata + +This provider does not recognize any special metadata fields unique to Hetzner DNS API. + +## Usage + +An example configuration: + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_HETZNER = NewDnsProvider("hetzner_v2"); + +D("example.com", REG_NONE, DnsProvider(DSP_HETZNER), + A("test", "1.2.3.4"), +); +``` +{% endcode %} + +## Activation + +Create a new API Key in the +[Hetzner Console](https://docs.hetzner.cloud/reference/cloud#getting-started). + +## Caveats + +### NS + +Removing the Hetzner provided NS records at the root is not possible. + +### SOA + +Hetzner DNS API does not allow changing the SOA record via their API. +There is an alternative method using an import of a full BIND file, but this + approach does not play nice with incremental changes or ignored records. +At this time you cannot update SOA records via DNSControl. diff --git a/documentation/provider/index.md b/documentation/provider/index.md index 911144f12..1a56c28ad 100644 --- a/documentation/provider/index.md +++ b/documentation/provider/index.md @@ -52,6 +52,7 @@ Jump to a table: | [`GCORE`](gcore.md) | ❌ | ✅ | ❌ | | [`HEDNS`](hedns.md) | ❌ | ✅ | ❌ | | [`HETZNER`](hetzner.md) | ❌ | ✅ | ❌ | +| [`HETZNER_V2`](hetzner_v2.md) | ❌ | ✅ | ❌ | | [`HEXONET`](hexonet.md) | ❌ | ✅ | ✅ | | [`HOSTINGDE`](hostingde.md) | ❌ | ✅ | ✅ | | [`HUAWEICLOUD`](huaweicloud.md) | ❌ | ✅ | ❌ | @@ -112,6 +113,7 @@ Jump to a table: | [`GCORE`](gcore.md) | ✅ | ✅ | ✅ | ✅ | | [`HEDNS`](hedns.md) | ❔ | ✅ | ✅ | ✅ | | [`HETZNER`](hetzner.md) | ✅ | ✅ | ✅ | ✅ | +| [`HETZNER_V2`](hetzner_v2.md) | ✅ | ✅ | ✅ | ✅ | | [`HEXONET`](hexonet.md) | ❔ | ✅ | ✅ | ❔ | | [`HOSTINGDE`](hostingde.md) | ❔ | ✅ | ✅ | ✅ | | [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ✅ | ✅ | ✅ | @@ -169,6 +171,7 @@ Jump to a table: | [`GCORE`](gcore.md) | ✅ | ❔ | ❌ | ✅ | ❔ | | [`HEDNS`](hedns.md) | ✅ | ❔ | ✅ | ✅ | ❌ | | [`HETZNER`](hetzner.md) | ❌ | ❔ | ❌ | ❌ | ❌ | +| [`HETZNER_V2`](hetzner_v2.md) | ❌ | ❔ | ❌ | ✅ | ❌ | | [`HEXONET`](hexonet.md) | ❌ | ❔ | ❔ | ✅ | ❔ | | [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ❌ | ✅ | ✅ | | [`HUAWEICLOUD`](huaweicloud.md) | ❌ | ❔ | ❌ | ❌ | ❌ | @@ -223,6 +226,7 @@ Jump to a table: | [`GCORE`](gcore.md) | ❔ | ❌ | ✅ | ✅ | | [`HEDNS`](hedns.md) | ❔ | ✅ | ✅ | ✅ | | [`HETZNER`](hetzner.md) | ❔ | ❌ | ✅ | ❔ | +| [`HETZNER_V2`](hetzner_v2.md) | ❔ | ❌ | ✅ | ✅ | | [`HEXONET`](hexonet.md) | ❔ | ❔ | ✅ | ❔ | | [`HOSTINGDE`](hostingde.md) | ❔ | ❌ | ✅ | ❔ | | [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ❌ | ✅ | ❌ | @@ -276,6 +280,7 @@ Jump to a table: | [`GCORE`](gcore.md) | ✅ | ✅ | ❔ | ❌ | ❌ | | [`HEDNS`](hedns.md) | ✅ | ✅ | ❔ | ✅ | ❌ | | [`HETZNER`](hetzner.md) | ✅ | ❔ | ❔ | ❌ | ✅ | +| [`HETZNER_V2`](hetzner_v2.md) | ✅ | ✅ | ❔ | ❌ | ✅ | | [`HEXONET`](hexonet.md) | ✅ | ❔ | ❔ | ❔ | ✅ | | [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ❔ | ✅ | ✅ | | [`HUAWEICLOUD`](huaweicloud.md) | ✅ | ❌ | ❔ | ❌ | ❌ | @@ -320,6 +325,7 @@ Jump to a table: | [`GCORE`](gcore.md) | ✅ | ❔ | ❌ | | [`HEDNS`](hedns.md) | ❌ | ❔ | ❌ | | [`HETZNER`](hetzner.md) | ❌ | ❔ | ✅ | +| [`HETZNER_V2`](hetzner_v2.md) | ❌ | ❔ | ✅ | | [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ✅ | | [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ❔ | ❌ | | [`INWX`](inwx.md) | ✅ | ❔ | ❔ | diff --git a/go.mod b/go.mod index 8c3fa5482..6b2894dd6 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/miekg/dns v1.1.68 github.com/mittwald/go-powerdns v0.6.7 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 - github.com/nrdcg/goinwx v0.11.0 + github.com/nrdcg/goinwx v0.12.0 github.com/ovh/go-ovh v1.9.0 github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d github.com/pkg/errors v0.9.1 @@ -66,6 +66,7 @@ require ( github.com/fbiville/markdown-table-formatter v0.3.0 github.com/google/go-cmp v0.7.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/hetznercloud/hcloud-go/v2 v2.30.0 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.174 github.com/kylelemons/godebug v1.1.0 github.com/luadns/luadns-go v0.3.0 @@ -97,8 +98,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect github.com/aws/smithy-go v1.23.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.0 // indirect github.com/boombuler/barcode v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deepmap/oapi-codegen v1.9.1 // indirect @@ -126,10 +129,15 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/peterhellberg/link v1.2.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/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect @@ -147,6 +155,7 @@ require ( go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/go.sum b/go.sum index 4b5b7cf3d..a508646c3 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= @@ -90,6 +92,8 @@ github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7 h1:Jk7u 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.18 h1:RvyTDU0VmnUBd3Qm2i6irEXtCR2KRIxnRlD8l+5z/DY= github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.18/go.mod h1:a6n4wXFHbMW0iJFxHIJR4PkgG5krP52nOVCBU0m+Obw= +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= @@ -229,6 +233,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 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/hetznercloud/hcloud-go/v2 v2.30.0 h1:fgAUtCCw4PbJNSs9XPLHVu0//dTNMbPq8P/48ovmdG8= +github.com/hetznercloud/hcloud-go/v2 v2.30.0/go.mod h1:zv7x2kM7xyJ5mW/+y4HbfxQYhk8TE57ypTa1hofsYdw= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.174 h1:FBlx7E5rl8doUTbizt+DXR0zU05Mu2oEYvc/2GMB7pc= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.174/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY= @@ -246,6 +252,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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= @@ -300,6 +308,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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= @@ -307,8 +317,8 @@ github.com/nicholas-fedor/shoutrrr v0.12.0 h1:8mwJdfU+uBEybSymwQJMGl/grG7lvVUKbV github.com/nicholas-fedor/shoutrrr v0.12.0/go.mod h1:WYiRalR4C43Qmd2zhPWGIFIxu633NB1hDM6Ap/DQcsA= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= -github.com/nrdcg/goinwx v0.11.0 h1:GER0SE3POub7rxARt3Y3jRy1OON1hwF1LRxHz5xsFBw= -github.com/nrdcg/goinwx v0.11.0/go.mod h1:0BXSC0FxVtU4aTjX0Zw3x0DK32tjugLzeNIAGtwXvPQ= +github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4= +github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -333,7 +343,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 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/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= @@ -426,6 +444,10 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/integrationTest/profiles.json b/integrationTest/profiles.json index 8c85a2975..4d84672a8 100644 --- a/integrationTest/profiles.json +++ b/integrationTest/profiles.json @@ -176,6 +176,11 @@ "api_key": "$HETZNER_API_KEY", "domain": "$HETZNER_DOMAIN" }, + "HETZNER_V2": { + "TYPE": "HETZNER_V2", + "api_token": "$HETZNER_V2_API_TOKEN", + "domain": "$HETZNER_V2_DOMAIN" + }, "HEXONET": { "TYPE": "HEXONET", "apientity": "$HEXONET_ENTITY", diff --git a/pkg/zonerecs/zonerecords.go b/pkg/zonerecs/zonerecords.go index 7904ee99c..84876bad0 100644 --- a/pkg/zonerecs/zonerecords.go +++ b/pkg/zonerecs/zonerecords.go @@ -19,7 +19,7 @@ func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]* models.CanonicalizeTargets(existingRecords, dc.Name) models.CanonicalizeTargets(dc.Records, dc.Name) - // Copy dc so that any corrections code that wants to + // Copy dc so that any correction code that wants to // modify the records may. For example, if the provider only // supports certain TTL values, it will adjust the ones in // dc.Records. diff --git a/providers/_all/all.go b/providers/_all/all.go index 92b403e0d..74355f5f8 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -30,6 +30,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v4/providers/gcore" _ "github.com/StackExchange/dnscontrol/v4/providers/hedns" _ "github.com/StackExchange/dnscontrol/v4/providers/hetzner" + _ "github.com/StackExchange/dnscontrol/v4/providers/hetznerv2" _ "github.com/StackExchange/dnscontrol/v4/providers/hexonet" _ "github.com/StackExchange/dnscontrol/v4/providers/hostingde" _ "github.com/StackExchange/dnscontrol/v4/providers/huaweicloud" diff --git a/providers/hetznerv2/auditrecords.go b/providers/hetznerv2/auditrecords.go new file mode 100644 index 000000000..dae136cee --- /dev/null +++ b/providers/hetznerv2/auditrecords.go @@ -0,0 +1,12 @@ +package hetznerv2 + +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(_ []*models.RecordConfig) []error { + return nil +} diff --git a/providers/hetznerv2/hetznerv2Provider.go b/providers/hetznerv2/hetznerv2Provider.go new file mode 100644 index 000000000..7c9535b75 --- /dev/null +++ b/providers/hetznerv2/hetznerv2Provider.go @@ -0,0 +1,264 @@ +package hetznerv2 + +import ( + "context" + "encoding/json" + "errors" + + "github.com/hetznercloud/hcloud-go/v2/hcloud" + "golang.org/x/net/idna" + + "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/pkg/version" + "github.com/StackExchange/dnscontrol/v4/pkg/zonecache" + "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.CanConcur: providers.Can(), + providers.CanGetZones: providers.Can(), + providers.CanOnlyDiff1Features: providers.Can(), + providers.CanUseAlias: providers.Cannot(), + providers.CanUseCAA: providers.Can(), + providers.CanUseDS: providers.Can(), + 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.CanUseSVCB: providers.Can(), + providers.CanUseHTTPS: providers.Can(), + providers.CanUseSSHFP: providers.Cannot(), + providers.CanUseTLSA: providers.Can(), + providers.DocCreateDomains: providers.Can(), + providers.DocOfficiallySupported: providers.Cannot(), + providers.DocDualHost: providers.Can(), +} + +func init() { + const providerName = "HETZNER_V2" + const providerMaintainer = "@das7pad" + fns := providers.DspFuncs{ + Initializer: New, + RecordAuditor: AuditRecords, + } + providers.RegisterDomainServiceProviderType(providerName, fns, features) + providers.RegisterMaintainer(providerName, providerMaintainer) +} + +// New creates a new API handle. +func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) { + apiToken := settings["api_token"] + if apiToken == "" { + return nil, errors.New("missing HETZNER_V2 api_token") + } + + h := &hetznerv2Provider{ + client: hcloud.NewClient( + hcloud.WithToken(apiToken), + hcloud.WithApplication("dnscontrol", version.Version()), + ), + } + h.zoneCache = zonecache.New(h.fetchAllZones) + return h, nil +} + +type hetznerv2Provider struct { + zoneCache zonecache.ZoneCache[*hcloud.Zone] + client *hcloud.Client +} + +// fetchAllZones is used by the zonecache.ZoneCache. +func (h *hetznerv2Provider) fetchAllZones() (map[string]*hcloud.Zone, error) { + flat, err := h.client.Zone.All(context.Background()) + if err != nil { + return nil, err + } + zones := make(map[string]*hcloud.Zone, len(flat)) + for _, z := range flat { + zones[z.Name] = z + } + return zones, nil +} + +// EnsureZoneExists creates a zone if it does not exist +func (h *hetznerv2Provider) EnsureZoneExists(domain string, _ map[string]string) error { + encoded, err := idna.ToASCII(domain) + if err != nil { + return err + } + if ok, err2 := h.zoneCache.HasZone(encoded); err2 != nil || ok { + return err2 + } + result, _, err := h.client.Zone.Create(context.Background(), hcloud.ZoneCreateOpts{ + Name: encoded, + Mode: hcloud.ZoneModePrimary, + }) + if err != nil { + return err + } + err = h.client.Action.WaitFor(context.Background(), result.Action) + if err != nil { + return err + } + z, _, err := h.client.Zone.GetByID(context.Background(), result.Zone.ID) + if err != nil { + return err + } + h.zoneCache.SetZone(encoded, z) + return nil +} + +// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. +func (h *hetznerv2Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) { + encoded, err := idna.ToASCII(dc.Name) + if err != nil { + return nil, 0, err + } + + z, err := h.zoneCache.GetZone(encoded) + if err != nil { + return nil, 0, err + } + + // Hetzner Cloud has a "ByRecordSet" API for DNS. + // At each label:rtype pair, we either delete all records or UPSERT the desired records. + instructions, actualChangeCount, err := diff2.ByRecordSet(existingRecords, dc, nil) + if err != nil { + return nil, 0, err + } + + var reports []*models.Correction + for _, instruction := range instructions { + switch instruction.Type { + case diff2.REPORT: + reports = append(reports, &models.Correction{ + Msg: instruction.MsgsJoined, + }) + continue + case diff2.CREATE: + first := instruction.New[0] + ttl := int(first.TTL) + opts := hcloud.ZoneRRSetCreateOpts{ + Name: first.Name, + Type: hcloud.ZoneRRSetType(first.Type), + TTL: &ttl, + } + for _, r := range instruction.New { + opts.Records = append(opts.Records, hcloud.ZoneRRSetRecord{ + Value: r.GetTargetCombinedFunc(txtutil.EncodeQuoted), + }) + } + reports = append(reports, &models.Correction{ + F: func() error { + _, _, err2 := h.client.Zone.CreateRRSet(context.Background(), z, opts) + return err2 + }, + Msg: instruction.MsgsJoined, + }) + case diff2.CHANGE: + rrSet := instruction.Old[0].Original.(*hcloud.ZoneRRSet) + reports = append(reports, &models.Correction{ + F: func() error { + if instruction.New[0].TTL != instruction.Old[0].TTL { + ttl := int(instruction.New[0].TTL) + opts := hcloud.ZoneRRSetChangeTTLOpts{TTL: &ttl} + _, _, err2 := h.client.Zone.ChangeRRSetTTL(context.Background(), rrSet, opts) + if err2 != nil { + return err2 + } + } + + opts := hcloud.ZoneRRSetSetRecordsOpts{} + for _, r := range instruction.New { + opts.Records = append(opts.Records, hcloud.ZoneRRSetRecord{ + Value: r.GetTargetCombinedFunc(txtutil.EncodeQuoted), + }) + } + _, _, err2 := h.client.Zone.SetRRSetRecords(context.Background(), rrSet, opts) + return err2 + }, + Msg: instruction.MsgsJoined, + }) + case diff2.DELETE: + reports = append(reports, &models.Correction{ + F: func() error { + rc := instruction.Old[0].Original.(*hcloud.ZoneRRSet) + _, _, err2 := h.client.Zone.DeleteRRSet(context.Background(), rc) + return err2 + }, + Msg: instruction.MsgsJoined, + }) + } + } + + return reports, actualChangeCount, nil +} + +// GetNameservers returns the nameservers for a domain. +func (h *hetznerv2Provider) GetNameservers(domain string) ([]*models.Nameserver, error) { + encoded, err := idna.ToASCII(domain) + if err != nil { + return nil, err + } + z, err := h.zoneCache.GetZone(encoded) + if err != nil { + return nil, err + } + return models.ToNameserversStripTD(z.AuthoritativeNameservers.Assigned) +} + +// GetZoneRecords gets the records of a zone and returns them in RecordConfig format. +func (h *hetznerv2Provider) GetZoneRecords(domain string, _ map[string]string) (models.Records, error) { + encoded, err := idna.ToASCII(domain) + if err != nil { + return nil, err + } + z, err := h.zoneCache.GetZone(encoded) + if err != nil { + return nil, err + } + opts := hcloud.ZoneRRSetListOpts{} + opts.PerPage = 100 + records, err := h.client.Zone.AllRRSetsWithOpts(context.Background(), z, opts) + if err != nil { + return nil, err + } + existingRecords := make([]*models.RecordConfig, 0, len(records)) + for _, rrSet := range records { + if rrSet.Type == hcloud.ZoneRRSetTypeSOA { + // SOA records are not available for editing, hide them. + continue + } + base := models.RecordConfig{ + Type: string(rrSet.Type), + Original: rrSet, + } + base.SetLabel(rrSet.Name, z.Name) + if rrSet.TTL != nil { + base.TTL = uint32(*rrSet.TTL) + } else { + base.TTL = uint32(z.TTL) + } + + for _, r := range rrSet.Records { + rc := base + if err = rc.PopulateFromStringFunc(rc.Type, r.Value, z.Name, txtutil.ParseQuoted); err != nil { + return nil, err + } + existingRecords = append(existingRecords, &rc) + } + } + return existingRecords, nil +} + +// ListZones lists the zones on this account. +func (h *hetznerv2Provider) ListZones() ([]string, error) { + return h.zoneCache.GetZoneNames() +} diff --git a/providers/inwx/auditrecords.go b/providers/inwx/auditrecords.go index 7a2853d19..95ca7b771 100644 --- a/providers/inwx/auditrecords.go +++ b/providers/inwx/auditrecords.go @@ -10,12 +10,8 @@ import ( // supported, an empty list is returned. func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} - - a.Add("TXT", rejectif.TxtHasBackticks) // Last verified 2021-03-01 - + a.Add("TXT", rejectif.TxtHasBackticks) // Last verified 2021-03-01 a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2021-03-01 - - a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-03-01 - + a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-03-01 return a.Audit(records) } diff --git a/providers/inwx/dnssec.go b/providers/inwx/dnssec.go index 7053644f0..c9ccfabda 100644 --- a/providers/inwx/dnssec.go +++ b/providers/inwx/dnssec.go @@ -10,25 +10,22 @@ const ( // testing shows 'AUTO' is what to expect if the domain has automatic // DNSSEC enabled. - // AutoDNSSEC is the status for DNSSEC enabled with automatic management + // AutoDNSSECStatus is the status for DNSSEC enabled with automatic management AutoDNSSECStatus = "AUTO" - // ManualDNSSEC is the status for DNSSEC enabled with manual management + // ManualDNSSECStatus is the status for DNSSEC enabled with manual management ManualDNSSECStatus = "MANUAL" ) // DNSSecStatus returns domain dnssec status func (api *inwxAPI) DNSSecStatus(domain string) (string, error) { - resp, err := api.client.Dnssec.Info([]string{domain}) if err != nil { return "", err } - // domain has no DNSSEC configuration if len(resp.Data) == 0 { return "", nil } - return resp.Data[0].DNSSecStatus, nil } @@ -40,16 +37,12 @@ func (api *inwxAPI) enableAutoDNSSEC(domain string) error { if err != nil { return err } - err = api.client.Dnssec.Enable(domain) - return err } // disableAutoDNSSEC disables automatic management of DNSSEC func (api *inwxAPI) disableAutoDNSSEC(domain string) error { - err := api.client.Dnssec.Disable(domain) - return err } diff --git a/providers/inwx/inwxProvider.go b/providers/inwx/inwxProvider.go index 3a4eb4097..ef5461eee 100644 --- a/providers/inwx/inwxProvider.go +++ b/providers/inwx/inwxProvider.go @@ -22,6 +22,9 @@ import ( /* INWX Registrar and DNS provider +Based on this great INWX API implementation: +https://github.com/nrdcg/goinwx + Info required in `creds.json`: - username - password @@ -34,7 +37,6 @@ Either of the following settings is required when two factor authentication is e Additional settings available in `creds.json`: - sandbox (set to 1 to use the sandbox API from INWX) - */ // InwxProductionDefaultNs contains the default INWX nameservers. @@ -182,10 +184,10 @@ func makeNameserverRecordRequest(domain string, rec *models.RecordConfig) *goinw switch rType := rec.Type; rType { /* - INWX is a little bit special for CNAME,NS,MX and SRV records: + INWX is a little bit special for CNAME, NS, MX and SRV records: The API will not accept any target with a final dot but will instead always add this final dot internally. - Records with empty targets (i.e. records with target ".") + Records with empty targets (i.e., records with target ".") are allowed. */ case "CNAME", "NS", "ALIAS": @@ -219,14 +221,14 @@ func (api *inwxAPI) createRecord(domain string, rec *models.RecordConfig) error } // updateRecord is used by GetDomainCorrections to update an existing record. -func (api *inwxAPI) updateRecord(RecordID int, rec *models.RecordConfig) error { +func (api *inwxAPI) updateRecord(RecordID string, rec *models.RecordConfig) error { req := makeNameserverRecordRequest("", rec) err := api.client.Nameservers.UpdateRecord(RecordID, req) return err } // deleteRecord is used by GetDomainCorrections to delete a record. -func (api *inwxAPI) deleteRecord(RecordID int) error { +func (api *inwxAPI) deleteRecord(RecordID string) error { return api.client.Nameservers.DeleteRecord(RecordID) } @@ -244,7 +246,8 @@ func (api *inwxAPI) AutoDnssecToggle(dc *models.DomainConfig, corrections []*mod } if dnssecStatus == ManualDNSSECStatus && dc.AutoDNSSEC != "" { - return corrections, fmt.Errorf("INWX: Domain %s has manual DNSSEC enabled. Disable it before using AUTODNSSEC_ON/AUTODNSSEC_OFF", dc.Name) + return corrections, fmt.Errorf("INWX: Domain %s has manual DNSSEC enabled. Disable it before using "+ + "AUTODNSSEC_ON/AUTODNSSEC_OFF", dc.Name) } if dnssecStatus != AutoDNSSECStatus && dc.AutoDNSSEC == "on" { @@ -289,23 +292,25 @@ func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundReco return nil, 0, err } for _, change := range changes { - changeMsgs := change.MsgsJoined + changeMessage := change.MsgsJoined switch change.Type { case diff2.REPORT: - corrections = append(corrections, &models.Correction{Msg: changeMsgs}) + corrections = append(corrections, &models.Correction{Msg: changeMessage}) case diff2.CHANGE: oldRec := change.Old[0] newRec := change.New[0] if isNullMX(newRec) || isNullMX(oldRec) { - // changing to or from a Null MX has to be delete then create + // changing to or from a Null MX has to be deleted then create deletes = append(deletes, &models.Correction{ - Msg: color.RedString("- DELETE %s %s %s ttl=%d", oldRec.GetLabelFQDN(), oldRec.Type, oldRec.ToComparableNoTTL(), oldRec.TTL), + Msg: color.RedString("- DELETE %s %s %s ttl=%d", oldRec.GetLabelFQDN(), oldRec.Type, + oldRec.ToComparableNoTTL(), oldRec.TTL), F: func() error { return api.deleteRecord(oldRec.Original.(goinwx.NameserverRecord).ID) }, }) deferred = append(deferred, &models.Correction{ - Msg: color.GreenString("+ CREATE %s %s %s ttl=%d", newRec.GetLabelFQDN(), newRec.Type, newRec.ToComparableNoTTL(), newRec.TTL), + Msg: color.GreenString("+ CREATE %s %s %s ttl=%d", newRec.GetLabelFQDN(), newRec.Type, + newRec.ToComparableNoTTL(), newRec.TTL), F: func() error { return api.createRecord(dc.Name, newRec) }, @@ -313,7 +318,7 @@ func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundReco } else { recID := oldRec.Original.(goinwx.NameserverRecord).ID corrections = append(corrections, &models.Correction{ - Msg: changeMsgs, + Msg: changeMessage, F: func() error { return api.updateRecord(recID, newRec) }, @@ -322,7 +327,7 @@ func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundReco } case diff2.CREATE: creates = append(creates, &models.Correction{ - Msg: changeMsgs, + Msg: changeMessage, F: func() error { return api.createRecord(dc.Name, change.New[0]) }, @@ -330,7 +335,7 @@ func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundReco case diff2.DELETE: recID := change.Old[0].Original.(goinwx.NameserverRecord).ID deletes = append(deletes, &models.Correction{ - Msg: changeMsgs, + Msg: changeMessage, F: func() error { return api.deleteRecord(recID) }, }) default: @@ -343,7 +348,7 @@ func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundReco return corrections, actualChangeCount, nil } -// getDefaultNameservers returns string map with default nameservers based on e.g. sandbox mode. +// getDefaultNameservers returns a string map with default nameservers based on e.g. sandbox mode. func (api *inwxAPI) getDefaultNameservers() []string { if api.sandbox { return InwxSandboxDefaultNs