---
title: "libdns Provider Adapter for Simple-Zone Providers"
version: v1alpha1
authors: "@mloiseleur"
creation-date: 2026-06-21
status: not-planned
---
libdns Provider Adapter for Simple-Zone Providers¶
Table of Contents¶
Summary¶
Add one generic in-tree provider that adapts libdns modules to the
ExternalDNS Provider interface, replacing several bespoke in-tree providers with thin shims over
maintained libdns modules. It ships in the single default binary — no build tags, no second image.
This is not a reversal of #4347 but a way
to advance it: remove bespoke simple-zone providers, offload their upkeep to the libdns ecosystem, and
still serve their users from one pod — while keeping the “no new in-tree providers” gate intact.
Motivation¶
Issue #4347 moves providers out of tree, with the webhook mechanism as the replacement. Two frictions recur:
- User experience. A webhook provider adds a second container and an unaffiliated third-party image
into a security-sensitive control loop. See
#6491: a TransIP
user on ~10 clusters pushed back on losing in-tree support with only a third-party webhook as the
alternative. - Maintainer cost. Providers leave because of bespoke code plus frequent SDK dependency bumps — the
cost #4347 calls out.
libdns is a small, stable interface set (RecordGetter, RecordAppender, RecordSetter,
RecordDeleter, ZoneLister) with 80+ maintained modules. One adapter serves many providers, and new
DNS vendors integrate by publishing their own libdns module — so the gate stays closed. As of 2026-06,
modules already exist for transip, scaleway, linode, dnsimple, gandi, godaddy, civo,
exoscale, and ovh (no module for ns1).
Goals¶
- One generic in-tree adapter from libdns modules to the
Providerinterface, in the single default
binary — no build tags, no separate image. - Replace bespoke simple-zone providers (
transip,scaleway,linode,dnsimple,gandi,godaddy,
civo,exoscale,ovh) with libdns shims, moving their per-provider upkeep upstream and advancing
#4347. - Keep the #4347 gate: new providers arrive as libdns modules, not in-tree code.
Non-Goals¶
- In-tree status for AWS, Azure, Google, or Cloudflare routing — they need the rich endpoint model
(set identifiers, weighted/latency/geo routing, provider-specific fields) libdns does not model. - Bundling all 80+ libdns modules; only a curated set is compiled in.
- Replacing the webhook mechanism; the two coexist.
Proposal¶
A generic provider at provider/libdns/ implementing the Provider interface on top of the libdns
interfaces. It imports no vendor SDK directly — only the curated set of libdns modules.
provider/libdns/
libdns.go // generic Provider impl (Records, ApplyChanges, AdjustEndpoints)
registry.go // name -> factory for the curated, compiled-in provider set
User Stories¶
- TransIP user (#6491). After in-tree
TransIP is removed, an operator keeps a first-party, single-pod setup via the libdns adapter over
libdns/transip— no second container, no third-party image. - New DNS vendor. A vendor publishes a libdns module and is usable through the adapter without adding
code to this repo or coupling to its release cycle. - Maintainer. Retire a bespoke provider by replacing it with a small libdns shim; its per-provider
upkeep moves upstream to the libdns module.
API¶
Selection mirrors --provider, with a sub-selector and a single JSON config blob (the pattern Caddy uses
for libdns modules), unmarshalled into the concrete provider struct by its shim:
No CRD changes; no changes to the Source, Plan, or Registry layers.
Behavior¶
The adapter implements Records and ApplyChanges. The TXT registry and ownership model are unchanged —
the registry sits above the provider, so TXT markers flow through as ordinary TXT endpoints.
Record Mapping¶
Every libdns record type exposes RR() → flat RR{Name, Type string, TTL time.Duration, Data string}.
The adapter works only in RR terms both directions; the module parses RR on write. No per-record-type
switch.
endpoint.Endpoint |
libdns | Notes |
|---|---|---|
DNSName (FQDN) |
zone + relative Name |
Names are zone-relative; use RelativeName/AbsoluteName. Needs zone discovery (below). |
Targets (N) |
N RR records |
Grouped back by (name, type) on read. |
RecordType |
RR.Type |
Direct string. |
RecordTTL (seconds) |
time.Duration |
Convert on each boundary. |
MX/SRV target ("10 host") |
RR.Data (zone-file value) |
Already ExternalDNS’s storage form; passes through. |
Applying Changes¶
Group plan.Changes by zone, then by (name, type) RRset:
Create+UpdateNew→SetRecords(its “these are the only records for this(name, type)“
semantics match a desired RRset exactly).Delete→DeleteRecords.- No
RecordSetter? Emulate viaDeleteRecords+AppendRecords.
Zone Discovery¶
libdns needs the zone as a separate argument, while ExternalDNS hands providers FQDNs. --domain-filter
is the primary zone source (works for every module, commonly set already); FQDNs match by longest suffix.
ZoneLister is an optional convenience: when implemented, the adapter can auto-discover zones and
--domain-filter becomes optional; otherwise --domain-filter is required. As of 2026-06, among in-scope
modules only transip and linode implement ZoneLister.
Provider Selection¶
The supported modules are one curated set compiled into the binary; the active one is chosen at runtime
via --libdns-provider. Registration is a package-level map (no init(), per the repo’s gochecknoinits
lint):
// registry.go
var registry = map[string]factory{
"transip": func(cfg json.RawMessage) (libdnsClient, error) { p := &transip.Provider{}; return p, json.Unmarshal(cfg, p) },
"scaleway": func(cfg json.RawMessage) (libdnsClient, error) { p := &scaleway.Provider{}; return p, json.Unmarshal(cfg, p) },
// ... rest of the curated set
}
Adding a provider to the set is one go.mod entry plus one map line.
Unsupported Endpoint Features¶
SetIdentifier drives provider-native routing — multiple records per (name, type), which flat libdns
backends cannot store. It cannot be silently dropped: the plan keys on (dnsName, setIdentifier), but a
flat backend reads back an empty identifier, so desired (non-empty) and current (empty) never match and
the record churns every reconcile under --update-events.
As a consequence, the adapter strips it (and warns) in AdjustEndpoints.
Drawbacks¶
- Dependency surface in the default binary — pure-HTTP modules (
gandi,godaddy) add almost
nothing, but SDK-wrapping ones (scaleway,ovh,civo,dnsimple) keep the vendor SDK as a
transitive dep. So #4347’s dependency churn is reduced (fewer direct provider deps, upkeep shared
upstream) but not eliminated — the cost of one binary over build tags or a second image. - Provider quirks still leak — libdns abstracts the API call, not provider behavior. Quirks like the
trailing-dot bug in #6491 are fixed upstream in the module, not here. - Reduced feature surface — no routing policies;
SetIdentifieris stripped inAdjustEndpoints.
Acceptable for the simple-zone tier (these providers have no routing), but must be documented. - Two integration paths — adapter vs. webhook; docs must steer users.
- Coverage gaps — providers without a module (e.g.
ns1) are not served.
Alternatives¶
Alternative 1: First-party generic libdns webhook image¶
An officially recognized, best-effort libdns webhook in a separate repo. Preferred by some maintainers in
the #6509 discussion.
- Pros: No in-tree libdns dependency; decoupled release cycle; surfaces the dependency to the user.
- Cons: Keeps the two-container UX #6491 objected to; removes no in-tree code; simple-tier users leave
the default distribution. - Recommendation: Viable in addition to the adapter, not instead.
Alternative 2: Status quo (webhook only)¶
Relying on the webhook, including the third-party orbit-online/external-dns-libdns-webhook.
- Pros: No work; functional today; one mechanism, no overlap.
- Cons: The third-party, low-activity image is the exact #6491 concern. “Webhook is a superset, libdns
degrades” does not apply here: the simple-zone tier has no routing/weighted/geo features, so the adapter
degrades nothing while removing in-tree code. - Recommendation: Not recommended as the only option for the simple-zone tier.
Alternative 3: Keep bespoke in-tree providers¶
- Pros: Best UX; no new abstraction.
- Cons: Reintroduces the maintenance and Dependabot burden #4347 removes; does not scale.
- Recommendation: Not recommended; contradicts #4347.