Skip to content

feat(): support dnszone classes. DNS Operations abstraction - #79

Open
fr6nco wants to merge 6 commits into
mainfrom
feat/support-dnszone-classes
Open

feat(): support dnszone classes. DNS Operations abstraction#79
fr6nco wants to merge 6 commits into
mainfrom
feat/support-dnszone-classes

Conversation

@fr6nco

@fr6nco fr6nco commented Aug 10, 2026

Copy link
Copy Markdown

Summary

This change makes downstream reconciliation class-aware so each controller instance only manages resources for its configured DNS zone class. It adds runtime flags for controller class and class type, with defaults that preserve current deployment behavior. It also refactors DNS operations behind a shared DNS handler and client interface so zone and recordset controllers use the same backend abstraction, including fake and PowerDNS implementations. Finally, it adds focused unit coverage for downstream DNSZone reconcile paths, including finalizer add, ensure, delete success, and delete failure retry behavior.

What changed

Added startup flags for selecting controller class and controller class type, then wired those into controller construction.
Introduced a shared DNS client interface and handler layer for zone and recordset operations.
Moved PowerDNS client code into the new DNS package layout and updated call sites.
Added fake DNS client implementation for deterministic tests.
Updated downstream reconcile logic to use class filtering and DNS abstraction methods.
Added unit tests for downstream DNSZone reconciliation and finalizer lifecycle.
Included small helper/normalization updates tied to status and nameserver handling.

Risks and rollout

  • Main risk is reconcile ownership changes when controller class values are misconfigured.
  • Rollout should set explicit class and class-type values per controller instance and verify only matching resources are reconciled.
  • Existing behavior is preserved by default values aligned to current infra expectations.

fr6nco added 4 commits August 7, 2026 17:36
…ments.

wip(): unified interfaces for zone and recordset management

Filters which DNSZone and DNSRecordSet resources to act on.
Unifies DNS controller implementation with defined interfaces to support multiple API enpdpoint types in the future (e.g. powerdns, hickory)
feat(): change default class name to match current infra deployment for backward compatibility.
@cla-assistant

cla-assistant Bot commented Aug 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@fr6nco fr6nco changed the title Feat/support dnszone classes feat(): support dnszone classes. DNS Operations abstraction Aug 10, 2026
@fr6nco
fr6nco requested a review from scotwells August 10, 2026 12:10
@fr6nco

fr6nco commented Aug 11, 2026

Copy link
Copy Markdown
Author

changeset tested on Staging @scotwells

@fr6nco
fr6nco requested review from 0xmc and mattdjenkinson August 12, 2026 07:14
@fr6nco

fr6nco commented Aug 13, 2026

Copy link
Copy Markdown
Author

still waiting very much on review @scotwells @mattdjenkinson @0xmc

@ecv

ecv commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Review of this branch on its own merits, separate from #82 which stacks on top of it. Verified by building e5c1f94, mutation-testing the new unit tests, and running all four deletion-path branches against the real reconciler. CI is green and e2e wall time is unchanged.

Blockers

The class gate sits above the deletion path, so a non-matching zone can never finish deleting. dnszone_downstream_controller.go:45 returns before both the finalizer-add and finalizer-remove branches. On main the equivalent checks lived inside the deletion branch and fell through to RemoveFinalizer.

deletion scenario main this branch
dnsZoneClassName: "" finalizer removed stranded
foreign class, class object exists finalizer removed stranded
matching class, class object missing stranded stranded (pre-existing)
matching class and class object removed removed

Create the second DNS class this PR exists to enable, delete one of its zones, and the object hangs in Terminating with no condition, event, or metric. Namespace deletion blocks behind it. "Defaults preserve current behaviour" holds for the single-class configuration in production today and fails for the multi-class configuration this PR is built for. Moving the gate below the DeletionTimestamp split fixes it, and the same reorder fixes the copy of this pattern that #82 adds to the recordset deletion path.

TestDNSZoneDownstreamReconcile_IgnoresDifferentClass passes with the feature deleted. It seeds a zone with class some-other-class but never seeds a DNSZoneClass of that name, so the reconcile short-circuits at the class-object Get on line 51, not at the gate. Nine behaviours were disabled one at a time:

behaviour disabled result
finalizer add killed
EnsureZone call killed
DeleteZone call killed
DeleteZone error propagation killed
finalizer removal killed
class-object Get killed
class gate removed entirely survived
class gate weakened to empty-check only survived
nameserver status patch survived

Four of the five behaviours the description claims are genuinely covered. Class filtering, the headline feature, is not. Seeding the DNSZoneClass object makes the un-gated build fail correctly.

--controller-class-type=fake is accepted in production. internal/dns/fake compiles into the shipped binary with no build tag, env check, or startup warning. It also fakes only the zone path, because cmd/main.go:264 constructs the PowerDNS recordset reconciler without a DNS field, so it falls back to NewFromEnv() regardless. An operator using it to dry-run a rollout gets zones that are never created, EnsureZone reporting success, no nameserver status, and record writes pushed to a backend where the zone does not exist. On delete, DeleteZone no-ops but the finalizer is still removed, leaving the real zone serving with nothing referencing it.

Majors

  • Only DNSZone is class-aware. Both record reconcilers still gate on zc.Spec.ControllerName and know nothing about --controller-class, so two instances would each process every record set. Feat/rework dnszone reconcile loop #82 closes this.
  • The class-object Get now gates finalizer-add too, and nothing watches DNSZoneClass. A downstream zone created before its class object sits with no finalizer until the resync.
  • zc.Spec.ControllerName is no longer consulted for zones. A class declaring controllerName: hickory is still claimed by the PowerDNS instance, because only the name is compared. The codebase now holds two disagreeing definitions of ownership.
  • One instance serves exactly one class name. The six e2e suites collapsed onto a shared powerdns-e2e class with concurrent: false, so they now share mutable global state and the ability to prove they do not interfere is gone.
  • The finalizer key is one shared constant. Provision a zone under class A, edit dnsZoneClassName to B, and A stops reconciling without deleting its copy. On deletion B removes the shared finalizer having cleaned only its own backend, and A's PowerDNS keeps the zone permanently.

Minors

  • internal/dns/client.go:24 puts PowerDNS RRSet primitives in the generic interface alongside the typed pair, and the typed pair is a return nil stub in both implementations. Feat/rework dnszone reconcile loop #82 gives them behaviour.
  • internal/dns/client.go:11 declares Init() and Shutdown(). Neither is ever called.
  • dnsrecordset_powerdns_controller.go:462 puts a class type in the class name field and leaves Type empty.
  • dnsrecordset_downstream_controller.go:31 adds a DNSHandler field that is never set and never read. Feat/rework dnszone reconcile loop #82 sets it.
  • internal/dns/pdns/client.go:129,183 narrows 2xx tolerance to exact status codes, so a proxy returning 202 becomes a permanent retry loop.
  • internal/dns/fake/client.go:61 returns a hard-coded empty nameserver list, which is why the nameserver status branch is untestable through this fake.
  • internal/dns/handler.go:41 reports an unknown class when only the type was unrecognised.
  • cmd/main.go:113 defaults --controller-class to datum-external-global-dns, Datum deployment wiring baked into the operator binary, coupled across two repos with nothing asserting it.
  • config/overlays/agent-powerdns-chainsaw/ is referenced by nothing.
  • All three config/overlays/agent-powerdns* patches append --controller-class to kind: StatefulSet, name: pdns-auth, the PowerDNS server container rather than the operator, so the flag may not reach the process that reads it. Worth confirming against the rendered manifest.

On the staging soak

This commit has run in staging since 2026-08-10, which is weaker evidence than it looks. Staging has exactly one class, whose name matches the flag default. That is precisely the configuration in which none of the above can fire.

@ecv ecv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving to unblock the stack. The findings in my previous comment are accepted risk rather than resolved, and I want that on the record here.

Three of them are worth carrying forward as follow-ups rather than losing: the class gate sitting above the deletion path, the class-filtering test that passes with the feature deleted, and the unguarded fake backend. None destroy data, and none can fire in the current single-class production configuration. They arrive with the second class, which is what this branch exists to enable.

The class gate reorder is a small change and fixes the same pattern in both this PR and #82.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants