## Summary
- Add support for DNScale DNS hosting service (https://dnscale.eu)
- Full CRUD operations for DNS records via DNScale REST API
- Support for common record types: A, AAAA, CNAME, MX, TXT, NS, CAA,
PTR, SRV, SSHFP, TLSA, ALIAS, HTTPS, SVCB
- Automatic zone management with EnsureZoneExists
- Integration test profile included
## Limitations
- Apex NS records are managed automatically by DNScale
- Empty TXT records not supported
- TXT records with leading/trailing whitespace not supported
- TXT records with unescaped double quotes not supported
- Null MX records not supported
## Test plan
- [x] Integration tests pass (all record types)
- [x] Documentation added
---------
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
## Summary
Fixes#3965
Investigation revealed that DigitalOcean's SOA support is too limited to
be useful in DNSControl:
- While the API documentation mentions SOA records exist, only the TTL
can be modified
- The SOA record cannot be deleted
- This makes it impractical to manage via DNSControl
Changes made:
- Disabled `CanUseSOA` capability in the DigitalOcean provider (with
comment "despite docs")
- Updated provider documentation to remove SOA from supported record
types
- Added clarification explaining why SOA is not supported
- Updated the provider feature matrix table to reflect SOA is not
supported
## Test plan
- [x] Reviewed the DigitalOcean API documentation
- [x] Confirmed the limitation matches the test failures from #3963
- [x] Updated all relevant documentation files
- [x] Provider capability correctly set to `Cannot()`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
ClouDNS uses the same API for DNS and registrar-related operations.
Following the pattern I saw in other provider/registrar combos (autodns,
cscglobal), I implemented the registry call in the `cloudnsProvider`
object type, and when used as a registry it creates an additional
instance of the Provider.
_Incidentally this causes rate-limiting functionality not to function
optimally because each of the instances—the provider and registrar—have
their own requestLimit object, causing the rate limit to be tracked and
applied within each separately. I'm going to follow up with a PR that
will improve the API rate-limit implementation for ClouDNS._
The ClouDNS nameserver API responds with different structures apparently
depending on the number of name servers that the domain has. I observed
three on the same domain:
1. When there are 2 domain names for non-ClouDNS name servers, the
response is an array of name server strings `["abc.ns.cloudflare.net",
"def.ns.cloudflare.net"]`
2. When there are 4 ClouDNS name servers, the response is an object with
string keys containing the 4 entries
`{"1":"pns1.cloudns.net","2":"pns2.cloudns.net","3":"pns3.cloudns.net","4":"pns4.cloudns.net"}`
3. When there are 5 name servers, the response is an object with string
keys containing 8 entries included 3 blank values
`{"1":"pns1.cloudns.net","2":"pns2.cloudns.net","3":"pns3.cloudns.net","4":"pns4.cloudns.net","5":"abc.ns.cloudflare.net","6":"","7":"","8":""}`
This covers all of those cases, plus the possible case where the array
contains blank strings.
The new `setNameservers` API call needed to send multiple copies of the
same key `nameservers[]` in the query value, which is supported by the
http query values object, but was not possible with the existing call
pattern because the `get` function was built to accept a string map
instead of a query values object. So I refactored `get` into a wrapper
function that converts the map into a query values object, which calls
the new `getWithQuery` function with the query values object.
---------
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
<!--
## Before submiting a pull request
Please make sure you've run the following commands from the root
directory.
bin/generate-all.sh
(this runs commands like "go generate", fixes formatting, and so on)
## 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.
!-->
Prior to this it would only work if you did
```js
NewDnsProvider("bind","BIND" {"stuff": "stuff"})
```
But it wouldn't support omitting the optional provider type and
specifying the extra object.
```js
NewDnsProvider("bind", {"stuff": "stuff"})
```
Fixes#3946
<!--
## Before submiting a pull request
Please make sure you've run the following commands from the root
directory.
bin/generate-all.sh
(this runs commands like "go generate", fixes formatting, and so on)
## 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.
!-->
Support `URL` and `URL301`, mark `PORKBUN_URLFWD` as DEPRECATED, maybe
delete after one year or shorter.
Manually tested, and integration test mostly passed except #3950close#3793
## Summary
This PR clarifies the DigitalOcean provider's capabilities by replacing
question marks (`❔`) with explicit feature flags in both the provider
code and documentation tables.
## Changes
- Updated `providers/digitalocean/digitaloceanProvider.go` to explicitly
declare support or lack of support for 15 additional record types and
features
- Updated `documentation/provider/index.md` to reflect these explicit
capabilities in the provider feature tables
- All changes based on DigitalOcean's API documentation
## Features Now Explicitly Documented
**Supported:**
- SOA records (Can)
- CAA records (already documented, maintained)
- SRV records (already documented, maintained)
- Concurrency (Can)
- Get Zones (Can)
**Not Supported (per DigitalOcean docs):**
- ALIAS, DNAME records
- DNSSEC-related: DNSKEY, DS, AutoDNSSEC
- DHCID, HTTPS, LOC, NAPTR, PTR, SMIMEA, SSHFP, SVCB, TLSA records
## Testing was done
These are documentation/capability declaration changes only - no
functional code changes. Unit tests ran locally without errors and the
docs generated without errors.
The capabilities system will now properly validate configurations
against DigitalOcean's actual limitations.
## Something to consider
The `commands/types/dnscontrol.d.ts` file changes during documentation
generation, but I haven't added that to this PR because it seems
unrelated. Claude also thinks that this file is why the
`check-git-status` GHA is failing. Should I go ahead and add this file
to prevent this from tripping up future PRs?
🤖 PR summary initially generated with [Claude
Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
<!--
## Before submiting a pull request
Please make sure you've run the following commands from the root
directory.
bin/generate-all.sh
(this runs commands like "go generate", fixes formatting, and so on)
## 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.
!-->
# Issue
The "fmt" command doesn't work like Terraform fmt, gofmt, and others. It
should:
* Default should DTRT: Format the existing dnsconfig.js file in place
* Not output a filename if no changes were made
* Don't modify the file's timestamp if no changes were made
* Accept stdin and stdout
# Resolution
All that.
# Potential breaking change
The default for '-o' is now dnsconfig.js instead of stdout. This may
break shell scripts that redirect stdout instead of using '-o' to
specify an output filename. If you use ">" instead of "-o", please
switch to "-o".
Fixes https://github.com/StackExchange/dnscontrol/issues/3890
# Issue
`no_ns` is undocumented.
# Resolution
Add it to the nameserver examples page and the `D()` page.
---------
Co-authored-by: Jeffrey Cafferata <jeffrey@jcid.nl>
Co-authored-by: ecraven <craven@gmx.net>
Co-authored-by: Peter <peter@nexoid.at>
## Context
Filenames for providers are not consistent.
https://github.com/StackExchange/dnscontrol/issues/3584#issuecomment-3633894582
## Changes
Rename files:
```
R documentation/provider/azure_dns.md -> documentation/provider/azuredns.md
R documentation/provider/azure_private_dns.md -> documentation/provider/azureprivatedns.md
R documentation/provider/bunny_dns.md -> documentation/provider/bunnydns.md
R documentation/provider/gandi_v5.md -> documentation/provider/gandiv5.md
R documentation/provider/hetzner_v2.md -> documentation/provider/hetznerv2.md
R providers/gandiv5/gandi_v5Provider.go -> providers/gandiv5/gandiv5Provider.go
```
## Verify
- `go build` still works.
- docs generation still works for me locally.
- I see. the Azure docs in the test site for the docs again after
f5767f4
---------
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
Either setting these new options OR using --notify on the commandline
will send notification for any specific execution.
Fixes#3906
In a second commit (feel free to remove) I added some logging to
indicate that notifications were enabled or not (useful when testing
each case).
I've manually tested combinations of the various options, for both
preview and push:
* --notify set or unset on commandline
* notify_on_* set to "true" or "ASDAS" or "false" or not mentioned
All seem to provide the correct logging line - invalid boolean values
are considered false (not an error).
I've made an attempt at documentation of the options, not sure if you
want it elsewhere as well.
<!--
## Before submiting a pull request
Please make sure you've run the following commands from the root
directory.
bin/generate-all.sh
(this runs commands like "go generate", fixes formatting, and so on)
## 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.
!-->
## Done
- [docs] Refresh digital ocean provider docs by clarifying which record
types are supported, linking to the digital ocean docs, and calling out
other relevant internal docs.
- spelling correction
- update docs for chicks-net as maintainer for digital ocean
## Meta
(Automated in `.just/gh-process.just`.)
# Issue
* New record type: "RP" (supported by BIND and GANDI_V5)
* Cloudflare: CF_REDIRECT/CF_TEMP_REDIRECT now generate
CF_SINGLE_REDIRECT records. All PAGE_RULE-based code is removed.
PAGE_RULEs are deprecated at Cloudflare. (be careful when upgrading!)
* New "v2" RecordConfig: RP and CF_SINGLE_REDIRECT are the only record
types that use this method. It shifts most of the work out of JavaScript
and into the Go code, making new record types easier to make, easier to
test, and easier to use by providers. This opens the door to new things
like a potential code-generator for rtypes. Converting existing rtypes
will happen over the next year.
* When only the TTL changes (MODIFY-TTL), the output lists the TTL
change first, not at the end of the line where it is visually lost.
* CF_REDIRECT/CF_TEMP_REDIRECT generate different rule "names". They
will be updated the first time you "push" with this release. The order
of the rules may also change. If you rules depend on a particular order,
be very careful with this upgrade!
Refactoring:
* New "v2" RecordConfig: Record types using this new method simply
package the parameters from dnsconfig.js statements like
CF_REDIRECT(foo,bar) and send them (raw) to the Go code. The Go code
does all processing, validation, etc. and turns them into RecordConfig
that store all the rdata in `RecordConfig.F`. No more adding fields to
RecordConfig for each new record type!
* RecordConfig.IsModernType() returns true if the record uses the new v2
record mechanism.
* PostProcess is now a method on DnsConfig and DomainConfig.
* DOC: How to create new rtypes using the v2 method (incomplete)
Other things:
* Integration tests for CF "full proxy" are removed. This feature
doesn't exist any more.
* DEV: Debugger tips now includes VSCode advice
* TESTING: The names of testgroup's can now have extra spaces to make
data align better
* CF_TEMP_REDIRECT/CF_REDIRECT is now a "builder" that generates
CLOUDFLAREAPI_SINGLE_REDIRECT records.
* And more!
# Resolution
---------
Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
<!--
## Before submiting a pull request
Please make sure you've run the following commands from the root
directory.
bin/generate-all.sh
(this runs commands like "go generate", fixes formatting, and so on)
## 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.
!-->
https://github.com/StackExchange/dnscontrol/issues/420
Please create the GitHub label 'provider-ALIDNS'
---------
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
The PR follows https://github.com/StackExchange/dnscontrol/pull/3542
Found some bugs when running intergration tests locally again, and the
PR is an attempt to fix them:
- When updating/creating HTTPS/SRV records, Vercel API only reads from
the corresponding struct (either `srv` or `https`). If we provide a
`value`, the Vercel API will reject with an error.
- The PR makes `Value` "nil-able", and sets `Value` to nil when dealing
with `SRV` or `HTTPS` records.
- When updating a record, currently, we treat the empty SVC param as
omitting the field. But with Vercel's API, omitting a field means not
updating the field. We need to explicitly make the field an empty string
to create/update an empty SVC param, and the PR does that.
- Vercel implements an unknown `ech=` parameter validation process for
HTTPS records. The validation process is unknown, undocumented, thus I
can't implement a `rejectif` for `AuditRecord`.
- Let's make this a known caveat, describe it in the provider docs, skip
these intergration tests, and move on.
Please tag this PR w/ `provider-VERCEL`.
# Issue
The previous fix had backwards compatibility issues and treated
uppercase Unicode incorrectly.
# Resolution
* Don't call strings.ToUpper() on Unicode strings. Only call it on the
output of ToASCII.
* Fix BIND's "filenameformat" to be more compatible (only breaks if you
had uppercase unicode in a domain name... which you probably didn't)
* Change IDN to ASCII in most places (Thanks for the suggestion,
@KaiSchwarz-cnic!)
* Update BIND documentation
Made a few mistakes when creating the initial version of the docs back
in #3542
Fix typos, adjust a few wordings, descriptions, etc.
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
## Summary
This PR adds a new domain modifier `IGNORE_EXTERNAL_DNS()` that
automatically detects and ignores DNS records managed by Kubernetes
[external-dns](https://github.com/kubernetes-sigs/external-dns)
controller.
**Related Issue:** This addresses the feature request discussed in
StackExchange/dnscontrol#935 (Idea: Ownership system), where
@tlimoncelli indicated openness to accepting a PR for this
functionality.
## Problem
When running DNSControl alongside Kubernetes external-dns, users face a
challenge:
- **external-dns** dynamically creates DNS records based on Kubernetes
Ingress/Service resources
- Users cannot use `IGNORE()` because they cannot predict which record
names external-dns will create
- Using `NO_PURGE()` is too broad - it prevents DNSControl from cleaning
up any orphaned records
The fundamental issue is that `IGNORE()` requires static patterns known
at config-time, but external-dns creates records dynamically at runtime.
## Solution
`IGNORE_EXTERNAL_DNS()` solves this by detecting external-dns managed
records at runtime:
```javascript
D("example.com", REG_CHANGEME, DnsProvider(DSP_MY_PROVIDER),
IGNORE_EXTERNAL_DNS(), // Automatically ignore external-dns managed records
A("@", "1.2.3.4"),
CNAME("www", "@")
);
```
### How It Works
external-dns uses a TXT record registry to track ownership. For each
managed record, it creates a TXT record like:
- `a-myapp.example.com` → TXT containing
`heritage=external-dns,external-dns/owner=...`
- `cname-api.example.com` → TXT containing
`heritage=external-dns,external-dns/owner=...`
This PR:
1. Scans existing TXT records for the `heritage=external-dns` marker
2. Parses the TXT record name prefix (e.g., `a-`, `cname-`) to determine
the managed record type
3. Automatically adds those records to the ignore list during diff
operations
## Changes
| File | Purpose |
|------|---------|
| `models/domain.go` | Add `IgnoreExternalDNS` field to DomainConfig |
| `pkg/js/helpers.js` | Add `IGNORE_EXTERNAL_DNS()` JavaScript helper |
| `pkg/diff2/externaldns.go` | Core detection logic for external-dns TXT
records |
| `pkg/diff2/externaldns_test.go` | Unit tests for detection logic |
| `pkg/diff2/handsoff.go` | Integrate external-dns detection into
handsoff() |
| `pkg/diff2/diff2.go` | Pass IgnoreExternalDNS flag to handsoff() |
| `commands/types/dnscontrol.d.ts` | TypeScript definitions for IDE
support |
| `documentation/.../IGNORE_EXTERNAL_DNS.md` | User documentation |
## Design Philosophy
This follows DNSControl's pattern of convenience builders (like
`M365_BUILDER`, `SPF_BUILDER`, `DKIM_BUILDER`) that make complex
operations simple. Just as those builders abstract away implementation
details, `IGNORE_EXTERNAL_DNS()` abstracts away the complexity of
detecting external-dns managed records.
## Testing
All unit tests pass:
```
go test ./pkg/diff2/... -v # Tests detection logic
go test ./pkg/js/... # Tests JS helpers
go build ./... # Builds successfully
```
## Caveats Documented
- Only supports TXT registry (the default for external-dns)
- Requires external-dns to use default naming conventions
- May need updates if external-dns changes its registry format
---------
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
Closes https://github.com/StackExchange/dnscontrol/issues/3787
This PR is adding a `HETZNER_V2` provider for the "new" Hetzner DNS API.
Testing:
- The integration tests are passing.
- Manual testing:
- `preview` (see diff for existing zone)
- `preview --populate-on-preview` (see full diff for newly created zone)
- `push` (see full diff; no diff after push)
- `push` (see full diff; no diff after push to newly created zone --
i.e. single pass and done)
```js
var REG_NONE = NewRegistrar('none')
var DSP = NewDnsProvider('HETZNER_V2')
D('testing-2025-11-14-7.dev', REG_NONE, DnsProvider(DSP),
A('@', '127.0.0.1')
)
```
<details>
```
# push for newly created zone
CONCURRENTLY checking for 1 zone(s)
SERIALLY checking for 0 zone(s)
Waiting for concurrent checking(s) to complete...DONE
******************** Domain: testing-2025-11-14-7.dev
1 correction (HETZNER_V2)
#1: Ensuring zone "testing-2025-11-14-7.dev" exists in "HETZNER_V2"
SUCCESS!
CONCURRENTLY gathering records of 1 zone(s)
SERIALLY gathering records of 0 zone(s)
Waiting for concurrent gathering(s) to complete...DONE
******************** Domain: testing-2025-11-14-7.dev
4 corrections (HETZNER_V2)
#1: ± MODIFY-TTL testing-2025-11-14-7.dev NS helium.ns.hetzner.de. ttl=(3600->300)
± MODIFY-TTL testing-2025-11-14-7.dev NS hydrogen.ns.hetzner.com. ttl=(3600->300)
± MODIFY-TTL testing-2025-11-14-7.dev NS oxygen.ns.hetzner.com. ttl=(3600->300)
SUCCESS!
#2: + CREATE testing-2025-11-14-7.dev A 127.0.0.1 ttl=300
SUCCESS!
Done. 5 corrections.
```
</details>
Feedback for @jooola and @LKaemmerling:
- The SDK was very useful in getting 80% there! Nice! 🎉
- Footgun:
- The `result` values are not "up-to-date" after waiting for an
`Action`, e.g. `Zone.AuthoritativeNameservers.Assigned` is not set when
`Client.Zone.Create()` returns and the following "wait" will not update
it.
- Taking a step back here: Waiting for an `Action` with a separate SDK
call does not seem very natural to me. Does the SDK-user need to know
that you are processing operations asynchronous? (Which seems like an
implementation detail to me, something that the SDK could abstrct over.)
Can `Client.Zone.Create()` return the final `Zone` instead of the
intermediate result?
- Features missing compared to the DNS Console, in priority order:
- It is no longer possible to remove your provided name servers from the
root/apex. Use-case: dual-home/multi-home zone with fewer than three
servers from Hetzner. I'm operating one of these and cannot migrate over
until this is fixed.
- Performance regression due to lack of bulk create/modify. E.g. [one of
the test
suites](a71b89e5a2/integrationTest/integration_test.go (L619))
spends about 4.5 minutes on making creating 100 record-sets and then
another 4 minutes for deleting them in sequence again. With your async
API, these are `create 2*100 + delete 2*100 = 400` API calls.
Previously, these were `create 1 + delete 100 = 101` API calls. Are you
planning on adding batch processing again?
- Usability nits
- Compared to other record-set based APIs, upserts for record-sets are
missing. This applies to records of a record-set and the ttl of the
record-set (see separate SDK calls for the cases `diff2.CREATE` vs
`diff2.CHANGE` and two calls in `diff2.CHANGE` for updating the TTL vs
records).
- Some SDK methods return an `Action` (e.g. `Zone.ChangeRRSetTTL()`),
others wrap the `Action` in a struct (`Client.Zone.CreateRRSet()`) --
even when the struct has a single field (`ZoneRRSetDeleteResult`).
---------
Co-authored-by: "Jonas L." <jooola@users.noreply.github.com>
Co-authored-by: "Lukas Kämmerling" <LKaemmerling@users.noreply.github.com>
Co-authored-by: Tom Limoncelli <6293917+tlimoncelli@users.noreply.github.com>
# Issue
Fixes https://github.com/StackExchange/dnscontrol/issues/3842
CC @das7pad
# Resolution
Convert domain.Name to IDN earlier in the pipeline. Hack the --domains
processing to convert everything to IDN.
* Domain names are now stored 3 ways: The original input from
dnsconfig.js, canonical IDN format (`xn--...`), and Unicode format. All
are downcased. Providers that haven't been updated will receive the IDN
format instead of the original input format. This might break some
providers but only for users with unicode in their D("domain.tld").
PLEASE TEST YOUR PROVIDER.
* BIND filename formatting options have been added to access the new
formats.
# Breaking changes
* BIND zonefiles may change. The default used the name input in the D()
statement. It now defaults to the IDN name + "!tag" if there is a tag.
* Providers that are not IDN-aware may break (hopefully only if they
weren't processing IDN already)
---------
Co-authored-by: Jakob Ackermann <das7pad@outlook.com>