Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Controllers and Webhooks](./controllers-webhooks.md): controller responsibilities, modules, acceptance boundary, and admission webhook.
- [Cloudflare Provider](./cloudflare-provider.md): Cloudflare identity, schema, ZoneClass parameters, provider data, reconciliation, deletion, and record mapping.
- [Route 53 Provider](./route53-provider.md): Route 53 identity, provider settings, and record-set provider schema.
- [External Provider](./external-provider.md): verify-only provider for customer-managed DNS: resolver policy, exact-set verification, conditions, scheduling bounds, deletion semantics, and conversion decision.
- [Gateway API Integration](./gateway-api-integration.md): `HTTPRoute` / `Gateway` DNS report, Zone selection, and generated `RecordSet` behavior.
- [UI](./ui.md): reusable DNS API UI package, screens, workflows, components, and mockup design.
- [Headlamp Desktop Plugin](./headlamp-plugin.md): Headlamp Desktop plugin package, distribution, and Desktop-specific behavior.
Expand Down
31 changes: 29 additions & 2 deletions docs/design/controllers-webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,31 @@ Admission validation must not reject `Zone`, `RecordSet`, or `ZoneUnit` solely b

All claim conditions include `observedGeneration`. If a resource moves out of a provider controller's scope, that controller stops updating `ZoneUnit.status`. Existing claim conditions can be recognized as old through `observedGeneration`.

When projecting a current provider condition from `ZoneUnit.status.zone` or `ZoneUnit.status.recordSets[]`, core preserves `Status`, `Reason`, and `Message`, including an empty Message. It must not replace a provider diagnostic with its reason string. This applies to both Accepted and Programmed on Zone and RecordSet and does not weaken UID/generation checks, composition acceptance, or the use of core-generated messages for stale/missing provider results. Providers must supply bounded, non-secret messages suitable for public claim status.

Provider implementation differences are controlled by common conditions, reasons, status ownership, support levels, and conformance tests. The core does not hide differences by providing one shared DNS controller implementation; providers give a consistent user experience by satisfying the same contract.

## Provider Enablement (required contract; not implemented at 87aa1dc)

Provider enablement is a runtime and permission boundary, not merely a switch for rendering Provider manifests. The required chart contract passes explicit boolean manager flags.

At the current revision only `--gateway-endpoint-controller-enabled` and `--service-endpoint-controller-enabled` exist as enable flags; `providers.*.enabled` gate manifests only, and identity controllers, identity webhooks, the Route 53 conversion handler, identity RBAC, and Secret read permission are always on (`app/operator/cmd/manager/main.go`, `deploy/charts/dns-api/templates/rbac.yaml`). The flags below are introduced with the external provider.

| Chart value | Manager flag (new) | Default | Enabled scope |
| --- | --- | --- | --- |
| `providers.route53.enabled` | `--route53-controller-enabled` | true | Route53Identity, Route 53 ZoneClass/ZoneUnit controllers, identity webhook, provider identity RBAC, and the Route 53 Provider manifest. |
| `providers.cloudflare.enabled` | `--cloudflare-controller-enabled` | true | CloudflareIdentity, Cloudflare ZoneClass/ZoneUnit controllers, identity webhook, identity/Secret reads and watches, and the Cloudflare Provider manifest. |
| `providers.external.enabled` | `--external-controller-enabled` | false | External ZoneClass/ZoneUnit controllers, Provider, endpoint conversion handler/capability/APIService, and conversion-create RBAC. No identity controller, identity webhook, or credential reads. |
| `providers.route53.endpoint.conversionAPI.enabled` | `--route53-endpoint-conversion-enabled` | true | Route 53 conversion handler/capability/APIService and conversion-create RBAC, only when Route 53 is enabled. |

The manager must not call disabled providers' controller or identity-webhook setup functions, register their conversion handlers, construct credential clients, or create their identity/Secret informers. Scheme/CRD types may remain available for reading existing objects. Provider-only RBAC and the corresponding ValidatingWebhookConfiguration entries are gated with registrations; leaving an unserved identity webhook in admission is not disabling a provider. Route 53 conversion requires both Route 53 flags. Cloudflare has no endpoint conversion handler in the current implementation; enablement must not invent an unserved capability.

Namespace reads and core/endpoint RBAC remain independent. Secret get/list/watch is granted only for an enabled credential consumer (Cloudflare in the built-in chart), not to the external provider or an all-disabled deployment. Mounted serving TLS certificates do not require Kubernetes Secret API read permission. Helm and Kustomize wiring must provide equivalent admission coverage, including ZoneClass status and enabled provider identity spec/status handlers. Existing `gatewayEndpoint.enabled` / `--gateway-endpoint-controller-enabled` and `serviceEndpoint.enabled` / `--service-endpoint-controller-enabled` continue to gate their own registrations and source-only permissions.

Core composition, endpoint translation, core CRDs, and core schema/status admission are retained when providers are disabled. External enablement requires its conversion API to be reachable through the existing webhook server with verified serving TLS and CA wiring; reject incompatible chart settings instead of publishing an unusable capability or using `insecureSkipTLSVerify`. Provider/controller-name/version flags select runtime bindings and cannot override a disabled flag. Changing flags does not complete cleanup of existing claims; operators must not disable a provider while relying on its finalizers or continuing verification.

These gates are the required design contract; manifest-only enable switches do not satisfy it.

## Implementation Modules

Implementation modules are controller ownership boundaries. One Kubernetes controller has one owning module, and one owning module owns at most one controller. The owning module contains the reconciler entrypoint, watch mapping, finalizer writes, status writes, and controller-specific planning. Shared helpers used by more than one controller must live in a non-controller module and must not own watches, finalizers, or status writes.
Expand Down Expand Up @@ -86,7 +109,7 @@ The webhook server runs as part of the controller-runtime manager and listens on

Webhook TLS certificates are issued by cert-manager. Tilt installs cert-manager as a Helm chart. dns-api manifests apply `Certificate`, `Issuer`, and `ValidatingWebhookConfiguration`. cert-manager CA injection sets the webhook `caBundle`.

Core validating webhook covers at least spec create/update for `Provider`, `ZoneClass`, `Zone`, `RecordSet`, and `ZoneUnit`, and status update for `Zone`, `RecordSet`, and `ZoneUnit`. Provider-specific validating webhooks cover provider-specific resources such as `Route53Identity` and `CloudflareIdentity`. The core webhook must not import provider API packages such as `api/route53` or `api/cloudflare`.
Core validating webhook covers at least spec create/update for `Provider`, `ZoneClass`, `Zone`, `RecordSet`, and `ZoneUnit`, and status update for `ZoneClass`, `Zone`, `RecordSet`, and `ZoneUnit`. Provider-specific validating webhooks cover enabled provider-specific resources such as `Route53Identity` and `CloudflareIdentity`; external has no identity admission handler. The core webhook must not import provider API packages such as `api/route53` or `api/cloudflare`.

Provider controllers do not fully trust the webhook. The webhook is the primary user feedback boundary, but controllers keep minimal safety checks. Controller-side validation failure is returned as `Accepted=False` or `Programmed=False`.

Expand Down Expand Up @@ -139,7 +162,11 @@ validate ZoneClass:

provider, providerVersion = resolve ProviderVersion(spec.provider)

validate spec.identityRef shape by providerVersion.identity.resource
if providerVersion.identity is present:
require spec.identityRef with nonempty name
validate spec.identityRef shape by providerVersion.identity.resource
else:
reject spec.identityRef
validate spec.parameters by providerVersion.zoneClass.schemas.parameters
validate providerVersion.zoneClass.validationRules
validate spec.allowedZones
Expand Down
18 changes: 9 additions & 9 deletions docs/design/core-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
- `provider.name`: `Provider.metadata.name`. Immutable. Because it references cluster-scoped `Provider`, it does not include namespace or kind.
- `provider.version`: `Provider.spec.versions[].name`. Immutable.
- `controllerName`: provider controller instance that reconciles `ZoneUnit` resources reachable through this class. Required and immutable.
- `identityRef`: provider identity reference. Required. `identityRef.name` references the provider identity resource declared by the selected Provider version in the same namespace as the `ZoneClass`.
- `identityRef`: optional provider identity reference. Required with a nonempty `name` if and only if the selected Provider version declares `identity`; forbidden otherwise. When present, it references the declared identity resource in the same namespace as the `ZoneClass`.
- `parameters`: provider-specific inline settings. Validated by the referenced Provider version `zoneClass.schemas.parameters`.
- `allowedZones`: namespace conditions for `Zone` resources that may use this `ZoneClass`.

`ZoneClass.spec.controllerName` uses the Gateway API-style domain-prefixed path form. It must include both a DNS subdomain prefix and a path part after `/`. Valid examples are `route53.dns.appthrust.io/controller` and `cloudflare.dns.appthrust.io/prod-a`; `route53.dns.appthrust.io` is invalid.

`ZoneClass.spec.identityRef` contains only `name`. It does not contain namespace, group, or kind. Cross-namespace identity references are not part of the initial design; identity resources live with the `ZoneClass` in the same platform namespace. The referenced Provider version declares the identity `group` and `kind`, so repeating them in every `ZoneClass` would create an unnecessary mismatch surface.
`ZoneClass.spec.identityRef`, when present, contains only `name`. It does not contain namespace, group, or kind. Cross-namespace identity references are not part of the initial design; identity resources live with the `ZoneClass` in the same platform namespace. The referenced Provider version declares the identity `group` and `kind`, so repeating them in every `ZoneClass` would create an unnecessary mismatch surface. Credentialless providers omit the field, rather than using an empty or sentinel reference. The Go field is `IdentityRef *LocalObjectReference` with JSON tag `identityRef,omitempty`; the CRD does not structurally require it, and core admission enforces the Provider-dependent requirement. Both generated CRD distributions, Go callers (`internal/go/providers/{route53,cloudflare}/controllers/{zoneclass,zoneunit}`), and the UI ZoneClass form are updated: the form omits `identityRef` and the identity picker step when the selected Provider version declares no `identity`. Route 53 and Cloudflare still require an identity.

`ZoneClass.status.conditions` contains `Accepted`. `Accepted=True` means `provider`, `identityRef`, `parameters`, and `allowedZones` are statically acceptable. `Accepted=False` reasons are `InvalidProvider`, `InvalidIdentityRef`, `InvalidParameters`, and `DeniedByPolicy`. `Accepted=Unknown` reasons are `IdentityNotResolved`, `ProviderStorageVersionNotResolved`, and `ReconcileError`.

Expand Down Expand Up @@ -53,19 +53,19 @@ spec:

`ZoneClass` is not reconciled by a core controller. The core validating webhook reads the referenced Provider version and validates `spec.identityRef`, `spec.parameters`, and `spec.allowedZones`.

Provider controllers reconcile only `ZoneClass` resources whose `spec.controllerName` matches their own controller name. If it does not match, they do not update status. They interpret `identityRef` according to the Provider version identity resource declaration, interpret provider-specific `parameters`, and set `ZoneClass.status.conditions[Accepted]` to `True` when the class is statically acceptable.
Provider controllers reconcile only `ZoneClass` resources whose `spec.controllerName` matches their own controller name. If it does not match, they do not update status. They interpret `identityRef` only when the Provider version declares an identity resource, interpret provider-specific `parameters`, and set `ZoneClass.status.conditions[Accepted]` to `True` when the class is statically acceptable. Credentialless providers perform no identity lookup.

## Zone

`Zone` is a namespace-scoped resource. It is the management unit corresponding to a hosted zone / zone on the DNS provider, connecting Kubernetes ownership boundaries with the provider-side object.
`Zone` is a namespace-scoped resource connecting Kubernetes ownership boundaries with a provider's zone scope. For DNS-writing providers it corresponds to a hosted zone / zone on the DNS provider. For the [external verification provider](external-provider.md#zone-semantics), it is a logical DNS suffix and need not be a separately hosted or delegated zone.

`Zone.spec` fields:

- `domainName`: zone apex. Only canonical ASCII values are accepted. Immutable.
- `domainName`: zone apex or logical suffix, according to the Provider contract. Only canonical ASCII values are accepted. Immutable.
- `zoneClassRef`: selected `ZoneClass`. If namespace is omitted, it defaults to the `Zone` namespace. Immutable.
- `provider.name`: `Provider.metadata.name`. Required and immutable.
- `provider.version`: `Provider.spec.versions[].name`. Required and immutable.
- `adoption`: specified when adopting an existing external zone. Omitted for creation. Mutable, but provider controllers stop changes that point to a different external zone after management starts.
- `adoption`: specified when adopting an existing external zone under a writing provider; omitted for creation. The external verification provider does not adopt DNS objects. Mutable, but provider controllers stop changes that point to a different external zone after management starts.
- `adoption`: provider-specific opaque object validated by the referenced Provider version `zone.schemas.adoption`.
- `allowedRecordSets`: conditions under which `RecordSet` resources from other namespaces may attach. If omitted, only same-namespace `RecordSet` resources are allowed. Each rule combines a namespace selector and record policy.

Expand Down Expand Up @@ -269,9 +269,9 @@ status:
- `IdentityNotResolved`: provider-specific identity object or its accepted state is not resolved enough to decide.
- `ProviderConversionUnavailable`: Provider conversion webhook is temporarily unavailable, timed out, or returned a retryable failure.

`Programmed` means the desired state has been reflected to the provider.
`Programmed` means the provider-specific completion criteria for the accepted desired state are satisfied.

`Programmed=True` does not mean DNS is resolvable. It means the last provider-side state observed by the controller matches the accepted desired state and there is no pending provider change. For Route 53, `Programmed=True` means no `pendingHostedZoneChange` exists in `ZoneUnit.status.provider.state` and the observed hosted zone matches `ZoneUnit.spec.zone` and `ZoneClass.spec.parameters`. Parent-zone NS delegation, public DNS resolvability, and resolver reachability are not guaranteed.
`Zone Programmed=True` is not a generic guarantee that records resolve. For Route 53, it means no `pendingHostedZoneChange` exists in `ZoneUnit.status.provider.state` and the observed hosted zone matches `ZoneUnit.spec.zone` and `ZoneClass.spec.parameters`; parent-zone delegation and DNS reachability are not guaranteed. For the external provider, it means the verifier is configured and any requested nameserver constraint is verified, independently of child record success. External RecordSet success requires the separate resolution predicate in [External Provider](external-provider.md#recordset-verification). Core performs no DNS queries.

`Programmed=False` reasons:

Expand Down Expand Up @@ -826,7 +826,7 @@ If a pre-applied `ZoneUnit` contains record-set ownership state whose owner clai
- `IdentityNotResolved`: provider-specific identity object or its accepted state is not resolved enough to decide.
- `ProviderConversionUnavailable`: Provider conversion webhook is temporarily unavailable, timed out, or returned a retryable failure.

`Programmed` means the desired state has been reflected to the provider. Provider-specific completion criteria are defined in provider sections. For Route 53, `Programmed=True` means the last observed RRSet matches the `ZoneUnit.spec.recordSets[]` item and there is no `pendingRecordSetChange` affecting that record identity.
`Programmed` means the provider-specific completion criteria for the accepted desired state are satisfied. For Route 53, `Programmed=True` means the last observed RRSet matches the `ZoneUnit.spec.recordSets[]` item and there is no `pendingRecordSetChange` affecting that record identity. For the external provider, it means the latest completed verification matches the expected records in every required resolver view, without DNS writes; it does not imply global resolver convergence or verified withdrawal after deletion.

`Programmed=False` reasons:

Expand Down
Loading
Loading