Skip to content

feat: DEP-0003 verified record withdrawal before address reuse - #17

Merged
reoring merged 20 commits into
mainfrom
work/20261018/4b-integration
Oct 18, 2026
Merged

reoring merged 20 commits into
mainfrom
work/20261018/4b-integration

Conversation

@reoring

@reoring reoring commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Implements DEP-0003 (deps/dep-0003-record-withdrawal.md, Draft → to be marked Accepted on merge): verified DNS withdrawal before a retired address is reused. Stacked on #16 (this branch contains it; merge #16 first or merge this and close #16).

What

  • Core API: RecordWithdrawal{zoneRef,name,address} (immutable, address label, no ownerReferences), RecordWithdrawalWaiver (create-RBAC is the authority; never sets Withdrawn=True), ZoneUnit.spec/status.withdrawals[] (keyed claimNamespace/claimName, ≤256), Provider.versions[].recordWithdrawal capability (external supported: true; route53/cloudflare false), ZoneUnit.spec.zone.zoneClassUID/zoneClassObservedGeneration fence so a Class policy edit cannot be consumed as old success.
  • Core: composition with the same Zone grants as A/AAAA RecordSets (wildcards need explicit grants), projection only for exact UID/generation + current Unit/Class binding, waiver binding, Zone deletion blocked while withdrawals are outstanding; admission for both kinds and the ERS retire-only state (CEL, no ERS webhook).
  • Endpoint app: the Gateway/Service source writers never drop a published pair: removed pairs move to EndpointRecordSet.spec.retiredTargets[]/retiredHostnames[] (snapshot associations, bounded); finalizer set at ERS create; Gateway deletion, rejected admission and cleanupForRoute retire instead of delete; the endpoint controller creates RecordWithdrawal claims for retired pairs (deterministic names, create-then-prune, crash-safe) and captures live+retired pairs on direct delete; retire-only ERS → Ready=False/Retiring.
  • External provider: probe.EvaluateWithdrawal (address absent from terminal A∪AAAA in every required view, both families; NXDOMAIN/NODATA count as absence), absentSince/notBefore from negative and retained positive cache horizons, a confirming post-horizon probe sets verifiedAt; reappearance/probe failure/policy change invalidate; waiver → OverrideAccepted; existing scheduler bounds; dns_external_withdrawals_pending{reason}.
  • Chart/Kustomize: CRDs, RBAC, admission parity.

Verification

  • task test — all packages ok (incl. envtest for composition/projection/waiver/zone-deletion gate; fake-resolver withdrawal tests); task controller-chart:check ok; go vet ./... clean; git diff --check clean.
  • Not yet proven live: the platform-side acceptance (two sshd hosts, one VIP) runs after the platform PR and this release land.

Make ZoneClass identityRef optional with Provider-version admission, preserve projected condition messages, gate cloud registrations and privileges, and support credentialless ZoneClass UI creation. Regenerate deepcopy and both CRD distributions; add deterministic admission, projection, chart and UI regressions.

Contract: dns-api PR #15 / 99d0753 docs/design/external-provider.md (ZoneClass Parameters, Conditions and Public Data, Packaging), core-api.md, provider-extension.md, controllers-webhooks.md (Provider Enablement), ui.md (Zone Classes), and test-policy.md. Platform PR #5331 / cd995cf0ed design/platform/onprem/README.md sections 4.3, 4.4 step 4, 4.7, and Phase 4.

Codegen: devbox run -- task generate manifests controller-chart:sync-crds completed. Tests, builds, linters and UI typecheck intentionally deferred to the primary integration agent.
Provider external.dns.appthrust.io/v1alpha1 (A/AAAA/CNAME, no identity), ZoneClass resolver policy and expectedNameservers, exact-set A/AAAA verification with bounded CNAME chains and wildcard witnesses, Cluster/Recursive views plus RD=0 authority checks, bounded scheduling (30-300s, jitter, backoff, 32 names/pass, 40 QPS/80 burst), UID/generation/policy-bound receipts, bookkeeping-only deletion, endpoint conversion handler, metrics. Design: docs/design/external-provider.md.
…8/external-integration

# Conflicts:
#	docs/design/controllers-webhooks.md
#	docs/design/core-api.md
#	docs/design/ui.md
…ernal-integration

# Conflicts:
#	docs/design/provider-extension.md
#	docs/design/test-policy.md
…ore admission accepts the manifest

The core webhook requires a description on every property and array item; the external Provider was rejected on install. The built-in-manifest validation test now covers external_extensions.yaml.
…gration

# Conflicts:
#	app/operator/config/external/external_extensions.yaml
#	deploy/charts/dns-api/templates/provider-external.yaml
#	deps/README.md
#	deps/dep-0003-record-withdrawal.md
#	docs/design/controllers-webhooks.md
#	docs/design/external-provider.md
#	internal/go/providers/external/controllers/zoneunit/controller.go
#	internal/go/providers/external/controllers/zoneunit/model.go
#	internal/go/providers/external/controllers/zoneunit/status.go
#	internal/go/providers/external/controllers/zoneunit/watches.go
#	internal/go/providers/external/manifest/manifest_test.go
#	internal/go/providers/external/manifest/provider.yaml
#	internal/go/providers/external/probe/metrics.go
#	internal/go/providers/external/probe/types.go
#	internal/go/providers/external/probe/wire.go
@reoring
reoring merged commit 8e28439 into main Oct 18, 2026
2 checks passed
reoring added a commit that referenced this pull request Nov 1, 2026
…ation to capture (#22)

DEP-0003 (#17) holds retirement capture whenever no publication Zone
resolves for a retired hostname/address pair. Any ERS that changes
hostname or address before a matching Zone exists therefore never
captures: the endpoint controller returns early on pending retirements,
observedGeneration freezes, active hostnames keep stale diagnostics, and
the gateway endpoint controller never releases the HTTPRoute finalizer.
On a platform without a DNS Provider this blocks every HTTPPublish from
ever reaching its terminal state and every owning Project from deleting.

Capture is now vacuous for a pair only after an explicit ZoneNotResolved
result and authoritative proof that the endpoint was never published:
no retained hostname Zone binding or generated-record reference/count,
no sticky WithdrawalProtected=True, an uncached read finding no
suffix-matching Zone, and an uncached source-label-bound read finding no
generated RecordSet. Everything else keeps capture pending and retains
finalizers: an unusable matching Zone, conversion/capability rejection,
any existing or deleting child, incomplete lists, a missing reader, or a
read error. A Zone that disappears after publication stays fail-closed.
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.

1 participant