diff --git a/README.md b/README.md index b129ede..e92d74c 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,101 @@ -## datum dns-operator - -Kubernetes operator for managing DNS zones and records, with a pluggable backend architecture. This repository provides: -- Custom resources to model zones, recordsets, and zone classes -- Controllers for two runtime roles: - - "downstream" agent that programs a DNS backend (PowerDNS supported) - - "replicator" that mirrors resources from an upstream cluster to a downstream cluster and synthesizes status -- Kustomize overlays to deploy either role - -### CRDs -- **`DNSZoneClass`** (cluster-scoped) - - `spec.controllerName`: selects backend controller (e.g., "powerdns") - - `spec.nameServerPolicy`: currently supports `Static` with `servers: []` - - `spec.defaults.defaultTTL`: optional default TTL for zones - -- **`DNSZone`** (namespaced) - - `spec.domainName`: required zone FQDN (e.g., `example.com`) - - `spec.dnsZoneClassName`: optional reference to a `DNSZoneClass` - - `status.nameservers`: authoritative nameservers (derived from class policy) - - `status.conditions`: `Accepted`, `Programmed` - -- **`DNSRecordSet`** (namespaced) - - `spec.dnsZoneRef`: `LocalObjectReference` to a `DNSZone` in the same namespace - - `spec.recordType`: one of `A, AAAA, CNAME, TXT, MX, SRV, CAA, NS, SOA, PTR, TLSA, HTTPS, SVCB` - - `spec.records[]`: owners with typed fields per record type (or `raw` strings). TTL per-owner optional. - - `status.conditions`: `Accepted`, `Programmed` - -### Controllers and Roles - -- **Downstream role** (`--role=downstream`) - - `DNSZoneReconciler`: when `DNSZone.spec.dnsZoneClassName` references a class with `controllerName: powerdns`, ensures the zone exists in PowerDNS and honors static nameserver policy. - - `DNSRecordSetReconciler`: for PowerDNS-backed zones, applies recordsets to PDNS using an authoritative mode that REPLACEs desired owners and DELETEs extraneous owners of the same type. Requeues while the zone is not ready. - -- **Replicator role** (`--role=replicator`) - - Multicluster manager discovers one or many upstream clusters (single-cluster or Milo discovery) and mirrors `DNSZone`/`DNSRecordSet` into a configured downstream cluster using a mapped-namespace strategy. - - `DNSZoneReplicator`: - - Mirrors upstream `spec` into a downstream shadow object - - Ensures an operator-managed upstream `DNSRecordSet` named `soa` exists (typed SOA targeting `@`) for PowerDNS-backed zones - - Updates upstream `status`: sets `Accepted=True` and currently treats `Programmed=True` optimistically; fills `status.nameservers` from `DNSZoneClass` when `Static` policy is set - - `DNSRecordSetReplicator`: - - Mirrors upstream `spec` into a downstream shadow object - - Updates upstream `status`: `Accepted` reflects `DNSZone` presence; `Programmed=True` once downstream shadow ensured - -### Backends -- **PowerDNS (Authoritative)** - - Enabled when `DNSZoneClass.spec.controllerName: powerdns` - - The downstream agent uses environment variables to connect: - - `PDNS_API_URL` (default `http://127.0.0.1:8081`) - - `PDNS_API_KEY` or `PDNS_API_KEY_FILE` - - Recordset translation supports typed fields for all declared RR types and sensible normalization of names and quoting for TXT/targets. - -### Deployment Overlays - -- `config/agent/` - - Namespace: `dns-agent-system` - - Runs the operator with `--role=downstream` - - Merges a `pdns` sidecar container into the controller Deployment to run PowerDNS alongside the manager - - Mounts a shared `emptyDir` to exchange an auto-generated API key, and sets `PDNS_API_KEY_FILE` in the manager - - Provides a `Service` exposing PDNS ports 53/udp, 53/tcp, and 8081/tcp - - ConfigMap `server-config` wired to `--server-config` - -- `config/overlays/replicator/` - - Namespace: `dns-replicator-system` - - Runs the operator with `--role=replicator` - - Requires a Secret `downstream-kubeconfig` containing key `kubeconfig` to target the downstream cluster - - ConfigMap `server-config` sets discovery mode (defaults to `single`) and points `downstreamResourceManagement.kubeconfigPath` to `/downstream/kubeconfig` - -### Quickstart: Agent with embedded PowerDNS -1. Install CRDs and default manifests: - - `kubectl apply -k config/agent` -2. Create a `DNSZoneClass` for PowerDNS with static nameservers, for example: -```yaml -apiVersion: dns.networking.miloapis.com/v1alpha1 -kind: DNSZoneClass -metadata: - name: powerdns -spec: - controllerName: powerdns - nameServerPolicy: - mode: Static - static: - servers: ["ns1.example.net.", "ns2.example.net."] -``` -3. Create a `DNSZone` and a `DNSRecordSet`: -```yaml -apiVersion: dns.networking.miloapis.com/v1alpha1 -kind: DNSZone -metadata: - name: example-com - namespace: default -spec: - domainName: example.com - dnsZoneClassName: powerdns ---- -apiVersion: dns.networking.miloapis.com/v1alpha1 -kind: DNSRecordSet -metadata: - name: www-a - namespace: default -spec: - dnsZoneRef: - name: example-com - recordType: A - records: - - name: www - a: - content: ["192.0.2.10", "192.0.2.11"] - ttl: 300 +# DNS + +Manage authoritative DNS the Kubernetes way. Declare a domain and its records as +ordinary Kubernetes resources, and the DNS service programs them into an +authoritative DNS backend and publishes them to a globally distributed serving +layer — no zone files, no backend API calls, no manual nameserver wiring. + +You manage DNS through native Kubernetes resources, so it works with `kubectl` +and any Kubernetes client, and inherits your platform's identity, RBAC, and audit +controls. + +This repository provides the **DNS operator**, the control-plane component that +reconciles those resources and programs the backend. See the +[Architecture Overview](docs/architecture/README.md) for how the operator fits +into the wider service. + +## What it does + +- **Zones and records as resources** — Model domains with `DNSZone` and records + with `DNSRecordSet`, covering `A`, `AAAA`, `CNAME`, `ALIAS`, `TXT`, `MX`, + `SRV`, `CAA`, `NS`, `SOA`, `PTR`, `TLSA`, `HTTPS`, and `SVCB` types. +- **Pluggable backends** — A cluster-scoped `DNSZoneClass` selects the backend + and nameserver policy, keeping backend choice out of individual zones. + [PowerDNS](https://doc.powerdns.com/authoritative/) is supported today. +- **Multi-tenant by design** — Each tenant authors DNS in their own control + plane; the operator discovers and serves many control planes from one shared + authoritative backend, with per-domain ownership accounting. +- **Automatic zone bootstrap** — Default `SOA` and `NS` records are created for + every zone from its nameserver policy, without clobbering user-authored apex + records. +- **Clear status** — `Accepted` and `Programmed` conditions report whether a + zone or record is valid and actually serving, mirrored back from the + authoritative backend. + +## How it works + +Users declare `DNSZone` and `DNSRecordSet` resources in their own control plane. +A **replicator** mirrors that desired state into a shared authoritative cluster, +where a **downstream agent** programs it into the DNS backend. The authoritative +data is then replicated to a read-only serving layer that answers live queries. + +For the full picture — components, control planes, and the serving layer — see +the [Architecture Overview](docs/architecture/README.md). + +## Documentation + +**Architecture** +- [Architecture Overview](docs/architecture/README.md) — System design and core + concepts +- [Deployment Topology](docs/architecture/topology.md) — Roles, control planes, + and the serving layer +- [Replication Model](docs/architecture/replication.md) — How desired state and + status move between clusters +- [API Reference](docs/architecture/api-reference.md) — Full resource schema and + conditions + +**Guides** +- [Service Catalog](config/components/service-catalog/README.md) — DNS as a + billable platform service + +## Deploying + +The operator runs in one of two roles, deployed with the Kustomize overlays in +[`config/`](config). See [Deployment Topology](docs/architecture/topology.md) for +how the roles fit together. + +### Agent with embedded PowerDNS + +Runs the operator as a downstream agent alongside PowerDNS and a storage backend +— the quickest way to a working DNS service: + +```sh +kubectl apply -k config/overlays/agent-powerdns ``` -### Quickstart: Replicator (upstream → downstream) -1. Create Secret on the replicator namespace containing the downstream kubeconfig (`data.kubeconfig`): -```bash +Then create a `DNSZoneClass`, `DNSZone`, and `DNSRecordSet` (see +[`config/samples`](config/samples) and the +[API Reference](docs/architecture/api-reference.md)). + +### Replicator (upstream → downstream) + +Runs the operator as a replicator that mirrors DNS resources from tenant control +planes into a downstream authoritative cluster: + +```sh +# Provide the downstream cluster kubeconfig kubectl -n dns-replicator-system create secret generic downstream-kubeconfig \ --from-file=kubeconfig=/path/to/downstream/kubeconfig -``` -2. Deploy replicator overlay: -```bash + kubectl apply -k config/overlays/replicator ``` -3. Create `DNSZoneClass` (cluster-scoped), `DNSZone` and `DNSRecordSet` on the upstream cluster. The replicator will mirror them into the downstream cluster and update upstream `status` conditions. - -### Conditions -- `Accepted`: resource is valid and has required dependencies (e.g., `DNSRecordSet` sees its `DNSZone`) -- `Programmed`: desired state is realized (shadow exists downstream; for downstream agent, recordsets applied to backend) - -### Configuration CRD (server config) -- `kind: DNSOperator` (internal config consumed by the binary via `--server-config`) - - `discovery.mode`: `single` or `milo` - - `downstreamResourceManagement.kubeconfigPath`: path inside the Pod to the downstream kubeconfig - - `controllers.dnsRecordSetPowerDNS.maxConcurrentReconciles`: concurrent reconciles for the PowerDNS recordset controller (default: 4) - - `controllers.dnsRecordSetPowerDNS.rateLimiterBaseDelay`: exponential backoff base delay (default: `1s`) - - `controllers.dnsRecordSetPowerDNS.rateLimiterMaxDelay`: exponential backoff max delay (default: `30s`) - -### Development -- Build: `make docker-build` (see `Makefile`) -- Generate code/manifests: `make generate` and `make manifests` -- Local e2e: see `test/e2e/chainsaw-test.yaml` and sample manifests under `config/samples/` + +The replicator mirrors upstream `DNSZone` / `DNSRecordSet` resources downstream +and synthesizes their status back upstream. See +[Replication Model](docs/architecture/replication.md). + +## Development + +- **Build:** `make docker-build` (see the [`Makefile`](Makefile)) +- **Generate code / manifests:** `make generate` and `make manifests` +- **End-to-end tests:** see `test/e2e/` and the samples under `config/samples/` diff --git a/docs/Taskfile.yaml b/docs/Taskfile.yaml new file mode 100644 index 0000000..5bcbe52 --- /dev/null +++ b/docs/Taskfile.yaml @@ -0,0 +1,56 @@ +version: '3' + +# Renders the architecture diagrams from PlantUML sources using Docker, +# mirroring the datum-cloud/enhancements convention. Each diagram is committed +# next to the document that embeds it. The shared brand theme +# (datum-theme*.puml) is an include, not a diagram, so it is excluded. +# +# The whole docs/architecture tree is mounted so relative includes (e.g. +# ../datum-theme.puml from backends/) resolve, and each PNG is written back +# beside its source. + +vars: + ARCH_DIR: "{{.USER_WORKING_DIR}}/docs/architecture" + OUTPUT_FORMAT: "png" + PLANTUML_IMAGE: "plantuml/plantuml:latest" + +tasks: + diagrams: + desc: Render all architecture diagrams from PlantUML + cmds: + - task: diagrams:render + + diagrams:render: + desc: Render PlantUML diagrams to PNG using Docker + cmds: + - | + set -e + cd "{{.ARCH_DIR}}" + find . -name '*.puml' ! -name 'datum-theme*.puml' | while read -r puml; do + echo "Rendering: $puml" + docker run --rm -v "$(pwd)":/data "{{.PLANTUML_IMAGE}}" \ + -t{{.OUTPUT_FORMAT}} -o "/data/$(dirname "$puml")" "/data/$puml" + done + + diagrams:validate: + desc: Validate PlantUML syntax using Docker + cmds: + - | + set -e + cd "{{.ARCH_DIR}}" + find . -name '*.puml' ! -name 'datum-theme*.puml' | while read -r puml; do + echo "Validating: $puml" + docker run --rm -v "$(pwd)":/data "{{.PLANTUML_IMAGE}}" \ + -syntax "/data/$puml" + done + + diagrams:clean: + desc: Remove generated diagram PNGs + cmds: + - | + set -e + cd "{{.ARCH_DIR}}" + find . -name '*.puml' ! -name 'datum-theme*.puml' | while read -r puml; do + rm -f "${puml%.puml}.{{.OUTPUT_FORMAT}}" + done + echo "Removed generated diagrams" diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..9fda5ab --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,157 @@ +# DNS Service Architecture + +The DNS service provides multi-tenant, Kubernetes-native authoritative DNS. +Platform users declare zones and records as ordinary Kubernetes resources in +their own control plane; the service carries that desired state down to an +authoritative DNS backend and publishes the resulting records to a globally +distributed serving layer. + +The **DNS operator** is the control-plane component at the heart of the service. +It reconciles the user's resources and programs the backend. The DNS backend, the +serving layer, and a shared state store make up the rest of the system, and this +document describes how the operator and those components fit together. + +## How It Works + +Users author three kinds of resources: a `DNSZoneClass` that names the backend +and nameserver policy, a `DNSZone` for each domain, and `DNSRecordSet` resources +for the records within a zone. These resources live in the user's own **tenant +control plane**, so they inherit the platform's identity, RBAC, and audit +controls. + +The operator runs in one of two roles that split the work across trust +boundaries: + +- A **replicator** watches tenant control planes, mirrors the desired state into + a shared authoritative cluster, and synthesizes status back up so users can + see whether their DNS is live. +- A **downstream agent** runs next to the DNS backend, translates records into + backend calls, and owns the authoritative zone data. + +A read-only **serving layer** then replicates the authoritative data and answers +live DNS queries close to end users. + +This separation keeps tenants off the DNS backend, hides the backend technology +and cluster topology, and gives the authoritative data a single writer. + +## System Context + +

+ System Context — DNS Service +

+ +The DNS service exists so platform users and other platform services can make the +domains they own resolvable on the internet. Platform capabilities depend on it: +networking and ingress publish records to expose workloads, and TLS certificate +issuance validates domain control through DNS. A domain earns authority through +the platform's **Domains** capability, which handles ownership and verification; +the DNS service then serves that domain's records. + +Users and platform services author zones and records in a tenant control plane, +which holds the desired state. The operator reads that state, programs the +authoritative backend, and writes status back to the tenant control plane. +Recursive resolvers query the serving layer directly over standard DNS, so the +wider internet can reach the user's applications. The +[Technology Stack](#technology-stack) section lists the open-source components +that make up the service. + +## Core Concepts + +### Zones, Records, and Zone Classes + +- **[`DNSZone`](./api-reference.md#dnszone)** models a single domain. Its status + reports the authoritative nameservers and readiness. +- **[`DNSRecordSet`](./api-reference.md#dnsrecordset)** models records of one type + within a zone, across one or more owner names (A, AAAA, CNAME, ALIAS, TXT, MX, + SRV, CAA, NS, SOA, PTR, TLSA, HTTPS, SVCB). +- **[`DNSZoneClass`](./api-reference.md#dnszoneclass)** is a cluster-scoped policy, + analogous to a `StorageClass`, that selects the **backend** (via + `controllerName`) and the **nameserver policy** for every zone that references + it. The class is the seam that keeps backend choice out of individual zones. + +See the [API Reference](./api-reference.md) for the full resource schema. + +### Backends + +A `DNSZoneClass` selects a backend by name through `spec.controllerName`. The +downstream agent acts only on zones whose class names a backend it implements, so +one deployment can host several classes and several backends side by side. +**PowerDNS** is the backend the operator implements today. The record-translation +and zone-management logic sit behind a narrow backend interface, so you can add +authoritative servers without changing the reconcilers. + +See [DNS Backends](./backends/README.md) for the backend model and the +[PowerDNS backend](./backends/powerdns.md) for that backend's architecture. + +### Multi-Tenancy via Control Plane Discovery + +The operator isolates tenants by control plane rather than by namespace +convention. The replicator **discovers** the control planes it serves and +reconciles each one independently. Two discovery modes exist: + +- **`single`** — the operator serves one upstream cluster, the cluster it runs + in. This mode suits self-contained deployments and development. +- **`milo`** — the operator discovers per-project control planes from a platform + control plane and connects to each one. This mode serves the multi-tenant + platform. + +Because the desired state lives in the tenant's own control plane, each tenant +sees only its own zones and records, and the operator never exposes its +authoritative cluster to tenants. + +### Status Synthesis and Conditions + +Every zone and record resource carries two conditions that the operator sets: + +- **`Accepted`** — the resource is valid and its dependencies are satisfied. +- **`Programmed`** — the backend has realized the desired state. + +> [!NOTE] +> +> The one-shot `DNSZoneDiscovery` uses `Accepted` and `Discovered` instead of +> `Programmed`. + +The replicator mirrors realized status from the authoritative cluster back to the +tenant control plane. A user watching a `DNSZone` therefore sees `Programmed=True` +only after the zone actually serves. See [Replication Model](./replication.md) +for how the operator synthesizes status across the two clusters. + +## Technology Stack + +| Component | Technology | Purpose | +|-----------|------------|---------| +| **API model** | [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) | Zones, records, and zone classes as native Kubernetes objects | +| **Controller runtime** | [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) + [multicluster-runtime](https://github.com/kubernetes-sigs/multicluster-runtime) | Reconciliation across one or many control planes | +| **Authoritative backend** | [PowerDNS Authoritative Server](https://doc.powerdns.com/authoritative/) | Serves authoritative zone data | +| **State replication** | [LightningStream](https://github.com/PowerDNS/lightningstream) + object storage | Replicates the authoritative LMDB store to the serving layer | + +## API Resources + +The operator serves these resources under `dns.networking.miloapis.com/v1alpha1`: + +| Resource | Scope | Description | +|----------|-------|-------------| +| `DNSZoneClass` | Cluster | Selects backend and nameserver policy for zones | +| `DNSZone` | Namespaced | A single authoritative domain | +| `DNSRecordSet` | Namespaced | Records of one type for one or more owner names in a zone | +| `DNSZoneDiscovery` | Namespaced | One-shot snapshot of a zone's live records | + +See the [API Reference](./api-reference.md) for complete field documentation. + +## Learn More + +- [Deployment Topology](./topology.md) — Roles, control planes, and the serving + layer +- [Replication Model](./replication.md) — Shadow objects, namespace mapping, and + status synthesis +- [DNS Backends](./backends/README.md) — Backend model and available backends, + including the [PowerDNS backend](./backends/powerdns.md) +- [API Reference](./api-reference.md) — Full resource schema and conditions + +## References + +- [PowerDNS Authoritative Server](https://doc.powerdns.com/authoritative/) +- [Kubernetes Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) +- [multicluster-runtime](https://github.com/kubernetes-sigs/multicluster-runtime) +- [LightningStream](https://github.com/PowerDNS/lightningstream) +- [C4 model](https://c4model.com) — notation used for the diagrams in these docs diff --git a/docs/architecture/api-reference.md b/docs/architecture/api-reference.md new file mode 100644 index 0000000..0440641 --- /dev/null +++ b/docs/architecture/api-reference.md @@ -0,0 +1,147 @@ +# API Reference + +This reference documents the user-facing API — the resources platform users +create and read to manage DNS. The operator serves them under the API +group/version **`dns.networking.miloapis.com/v1alpha1`**. Generated CRDs live in +[`config/crd/bases`](../../config/crd/bases); runnable samples live in +[`config/samples`](../../config/samples). + +| Resource | Scope | Purpose | +|----------|-------|---------| +| [`DNSZoneClass`](#dnszoneclass) | Cluster | Selects backend and nameserver policy | +| [`DNSZone`](#dnszone) | Namespaced | A single authoritative domain | +| [`DNSRecordSet`](#dnsrecordset) | Namespaced | Records of one type, for one or more owner names | +| [`DNSZoneDiscovery`](#dnszonediscovery) | Namespaced | One-shot snapshot of live records | + +## DNSZoneClass + +Cluster-scoped policy, analogous to a `StorageClass`. Every `DNSZone` references +a class, which determines the backend and how the operator assigns authoritative +nameservers. + +| Field | Type | Description | +|-------|------|-------------| +| `spec.controllerName` | string | Backend selector (e.g. `powerdns`). The downstream agent only acts on zones whose class it implements. | +| `spec.nameServerPolicy.mode` | string | Nameserver assignment mode. `Static` is currently supported. | +| `spec.nameServerPolicy.static.servers` | []string | Authoritative nameservers advertised for zones using this class. | +| `spec.defaults.defaultTTL` | int64 | Optional default TTL applied to zones. | +| `status.conditions` | []Condition | `Accepted`, `Programmed`. | + +```yaml +apiVersion: dns.networking.miloapis.com/v1alpha1 +kind: DNSZoneClass +metadata: + name: powerdns +spec: + controllerName: powerdns + nameServerPolicy: + mode: Static + static: + servers: ["ns1.example.net.", "ns2.example.net."] +``` + +## DNSZone + +Namespaced. Models a single domain. + +| Field | Type | Description | +|-------|------|-------------| +| `spec.domainName` | string | Required FQDN (e.g. `example.com`). Immutable once set. | +| `spec.dnsZoneClassName` | string | Reference to a `DNSZoneClass`. | +| `status.nameservers` | []string | Authoritative nameservers, derived from the class policy. | +| `status.recordCount` | int | Number of record sets in the zone. | +| `status.conditions` | []Condition | `Accepted`, `Programmed`. | +| `status.domainRef` | object | Link to the owning `Domain`, exposing its name and assigned nameservers, when present. | + +```yaml +apiVersion: dns.networking.miloapis.com/v1alpha1 +kind: DNSZone +metadata: + name: example-com + namespace: default +spec: + domainName: example.com + dnsZoneClassName: powerdns +``` + +## DNSRecordSet + +Namespaced. Models records of one record type within a zone, across one or more +owner names. Each entry in `spec.records` sets one owner name and carries exactly +one typed field matching `spec.recordType`; the backend groups entries that share +an owner name into a single RRset. + +| Field | Type | Description | +|-------|------|-------------| +| `spec.dnsZoneRef` | LocalObjectReference | The `DNSZone` in the same namespace. | +| `spec.recordType` | string | One of `A, AAAA, ALIAS, CNAME, TXT, MX, SRV, CAA, NS, SOA, PTR, TLSA, HTTPS, SVCB`. | +| `spec.records[].name` | string | Owner name; `@` for the zone apex. | +| `spec.records[].ttl` | int64 | Optional per-owner TTL. | +| `spec.records[].` | object | Typed record content for the entry. Each typed field's `content` is a single value (e.g. `a.content: "192.0.2.10"`); other types use their own fields (`mx.preference`/`mx.exchange`, `srv.*`, `soa.*`). | +| `status.conditions` | []Condition | `Accepted`, `Programmed`. | +| `status.recordSets[]` | []object | Per-owner-name realized status, including per-record `Programmed`. | + +Each entry holds a single value. To give one owner name several addresses, add +one entry per value; the backend groups them into one RRset: + +```yaml +apiVersion: dns.networking.miloapis.com/v1alpha1 +kind: DNSRecordSet +metadata: + name: www-a + namespace: default +spec: + dnsZoneRef: + name: example-com + recordType: A + records: + - name: www + a: + content: "192.0.2.10" + ttl: 300 + - name: www + a: + content: "192.0.2.11" + ttl: 300 +``` + +### Multi-owner conflict resolution + +When several `DNSRecordSet` resources target the same zone, owner name, and +record type, the agent programs a **single** owner (chosen by oldest creation +timestamp, then name). The agent marks the others `Programmed=False` with reason +`NotOwner`, so conflicting records never silently overwrite each other. + +## DNSZoneDiscovery + +Namespaced, write-once. Snapshots a zone's live records via DNS queries; performs +no backend writes. Useful for onboarding or verifying an existing domain. + +| Field | Type | Description | +|-------|------|-------------| +| `spec.dnsZoneRef` | LocalObjectReference | The `DNSZone` to snapshot. | +| `status.conditions` | []Condition | `Accepted`, `Discovered`. | +| `status.recordSets[]` | []object | Discovered records, grouped by record type. | + +## Conditions + +Every DNS resource reports status through standard Kubernetes conditions: + +| Condition | Meaning | +|-----------|---------| +| `Accepted` | The resource is valid and its dependencies are satisfied. | +| `Programmed` | The backend has realized the desired state. | +| `Discovered` | (`DNSZoneDiscovery` only) The live-record snapshot completed. | + +Common reasons include `Pending`, `Programmed`, `DNSZoneInUse` (domain already +claimed by another zone), `NotOwner` (a conflicting record set owns the name), +and `PDNSError` (the backend rejected the change). See +[Replication Model](./replication.md#status-synthesis) for how the operator +synthesizes conditions across clusters. + +> [!NOTE] +> +> This reference covers the user-facing API. The `DNSOperator` object that +> configures the operator binary is deployment configuration, not part of the +> served API — see [Deployment Topology → Operator +> Configuration](./topology.md#operator-configuration). diff --git a/docs/architecture/backends/README.md b/docs/architecture/backends/README.md new file mode 100644 index 0000000..6a01485 --- /dev/null +++ b/docs/architecture/backends/README.md @@ -0,0 +1,67 @@ +# DNS Backends + +A **backend** is the authoritative DNS server that the downstream agent programs +and that answers queries. The operator keeps backend choice out of individual +zones: a `DNSZoneClass` names a backend, and every `DNSZone` that references the +class uses it. This design lets the platform offer several backends and lets you +add new ones without changing the DNS API. + +## Backend Model + +The downstream agent talks to a backend through a Go client. A narrow interface +covers the two record operations — replace the records for one owner name and +type, and delete them — so the record reconciler stays backend-agnostic. Zone +operations (create, read, delete) are methods on the backend's concrete client +rather than part of that interface. Either way, the reconcilers call the +backend's Go client and never its native API directly. + +Three properties hold for every backend: + +- **Selection by name.** A `DNSZoneClass` sets `spec.controllerName` (for + example, `powerdns`). The agent acts only on zones whose class names a backend + it implements and ignores every other class. One deployment can therefore host + several classes and several backends at once. +- **Single writer.** For each (zone, record type, owner name) tuple, the agent + programs exactly one owner. When several `DNSRecordSet` resources target the + same tuple, the agent picks one owner and marks the rest `Programmed=False` + with reason `NotOwner`, so conflicting records never overwrite each other. +- **Authoritative reconciliation.** The agent treats the desired state as + authoritative. It replaces the owners a zone should have and deletes owners of + the same type that no longer belong, so the backend converges on the declared + records. + +Because these properties live in the agent rather than in any one backend, every +backend behaves consistently from the user's point of view. A user picks a +`DNSZoneClass`; the choice of authoritative server behind it stays an operator +concern. + +## Available Backends + +| Backend | `controllerName` | Status | Documentation | +|---------|------------------|--------|---------------| +| [PowerDNS](./powerdns.md) | `powerdns` | Implemented | [PowerDNS backend](./powerdns.md) | + +The platform expects to offer more backends over time. Each new backend adds a +row here and a companion page that documents its specifics. + +## Adding a Backend + +Adding a backend is an operator task, not an API change. At a high level: + +1. Implement the record interface and the zone-management client for the target + authoritative server. +2. Choose a `controllerName` and register the backend under it. +3. Package the server and its dependencies in a deployment overlay, following the + pattern in [`config/agent`](../../../config/agent). +4. Add a companion page under this directory and a row to + [Available Backends](#available-backends). + +The API types, replicator, zone-class model, and status conditions stay +unchanged, so existing zones and clients keep working. + +## Related + +- [Architecture Overview](../README.md) — Zone classes and the backend concept +- [Deployment Topology](../topology.md) — Where a backend and its serving layer + run +- [API Reference](../api-reference.md#dnszoneclass) — `DNSZoneClass` schema diff --git a/docs/architecture/backends/powerdns-backend.png b/docs/architecture/backends/powerdns-backend.png new file mode 100644 index 0000000..a0e025d Binary files /dev/null and b/docs/architecture/backends/powerdns-backend.png differ diff --git a/docs/architecture/backends/powerdns-backend.puml b/docs/architecture/backends/powerdns-backend.puml new file mode 100644 index 0000000..c695446 --- /dev/null +++ b/docs/architecture/backends/powerdns-backend.puml @@ -0,0 +1,43 @@ +@startuml powerdns-backend +!$NEW_C4_STYLE = 1 +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Deployment.puml +!include ../datum-theme.puml + +LAYOUT_LEFT_RIGHT() + +title Deployment View — PowerDNS Backend + +Deployment_Node(rcp, "Replication Control Plane", "Kubernetes cluster") { + Container(replicator, "DNS Operator (replicator)", "controller-runtime", "Mirrors desired state into the authoritative cluster") +} + +Deployment_Node(auth, "Authoritative Cluster", "Kubernetes cluster") { + Container(api, "Kubernetes API", "control plane", "Holds shadow DNSZone / DNSRecordSet") + Container(agent, "DNS Operator (downstream agent)", "controller-runtime", "Reconciles shadows and programs PowerDNS") + Deployment_Node(pod, "pdns-auth", "StatefulSet pod") { + Container(pdns, "PowerDNS Authoritative", "pdns-auth + LMDB", "Writer and source of truth") + Container(pub, "LightningStream", "sidecar", "Snapshots LMDB to object storage") + } +} + +ContainerDb(store, "Object Storage", "S3-compatible", "Durable authoritative state") + +Deployment_Node(edge, "Serving Layer", "Edge nodes") { + Deployment_Node(node, "Serving Node", "DaemonSet pod") { + Container(spdns, "PowerDNS (read-only)", "pdns-auth + LMDB", "Answers DNS queries") + Container(recv, "LightningStream", "sidecar (receive)", "Pulls snapshots from object storage") + Container(rec, "Recursor", "pdns-recursor", "Expands ALIAS at query time") + } +} + +Person_Ext(resolver, "Recursive Resolver", "Resolves names for the internet") + +Rel(replicator, api, "Writes shadow DNS resources", "apply") +Rel(agent, api, "Watches shadows, writes status", "watch") +Rel(agent, pdns, "Programs zones / records", "HTTP API + X-API-Key") +Rel(pub, store, "Writes snapshots") +Rel(recv, store, "Pulls snapshots") +Rel(resolver, spdns, "DNS query", "UDP/TCP :53") + +SHOW_LEGEND() +@enduml diff --git a/docs/architecture/backends/powerdns.md b/docs/architecture/backends/powerdns.md new file mode 100644 index 0000000..fef0c1d --- /dev/null +++ b/docs/architecture/backends/powerdns.md @@ -0,0 +1,133 @@ +# PowerDNS Backend + +[PowerDNS Authoritative Server](https://doc.powerdns.com/authoritative/) is the +backend the operator implements today. A zone opts in by referencing a +`DNSZoneClass` with `controllerName: powerdns`. This page documents how the +PowerDNS backend translates records, programs zones, stores data, and serves +queries. For the properties every backend shares, see the +[backend model](./README.md#backend-model). + +The deployment view below shows where each component runs: the downstream agent +and the PowerDNS writer in the authoritative cluster (fed shadow resources by the +replicator through the cluster's Kubernetes API), and the read-only serving nodes +at the edge, replicating through shared object storage. + +

+ Deployment View — PowerDNS Backend +

+ +## Record Translation + +The PowerDNS client translates each typed `DNSRecordSet` entry into the +presentation format PowerDNS expects. Translation handles the details that DNS +record types require: + +- Synthesizes a date-based `SOA` serial (`YYYYMMDD01`) when a record omits it. +- Encodes `SVCB` and `HTTPS` service parameters. +- Quotes and escapes `TXT` content. +- Qualifies owner names against the zone and removes duplicate values. + +## Zone and Record Programming + +The client drives PowerDNS through its HTTP API, authenticating with an API key +in the `X-API-Key` header. For a zone, the agent ensures the zone exists and +applies the nameserver policy from its `DNSZoneClass`. For a record set, the +agent replaces the desired owners and deletes extraneous owners of the same +type. When PowerDNS rejects a change, the agent reports the failure on the +resource with reason `PDNSError` and a human-readable message. + +## Storage and Serving + +PowerDNS stores authoritative zone data in an embedded **LMDB** database. To +serve queries close to end users, the deployment replicates that database rather +than the query path: + +1. A [LightningStream](https://github.com/PowerDNS/lightningstream) sidecar + snapshots the LMDB store to shared, S3-compatible object storage. +2. Each serving node runs a read-only PowerDNS with a LightningStream sidecar + that pulls the snapshots and answers queries. + +Serving nodes hold only their local replica, so the deployment can add nodes +anywhere object storage is reachable. A serving node can also run a local +recursor to expand `ALIAS` records at query time. See +[Deployment Topology](../topology.md#authoritative-serving-and-state-replication) +for how the serving layer fits the wider system. + +## Propagation and Timing + +A record change reaches end users through four stages. Each stage adds delay, and +a different setting governs each one, so it helps to reason about them separately. + +| Stage | What happens | What governs the delay | +|-------|--------------|------------------------| +| 1. Reconcile | The agent programs the change into the writer's PowerDNS through the HTTP API. | Controller queue latency, normally seconds. On a backend error the agent retries with exponential backoff, from `rateLimiterBaseDelay` (1s) up to `rateLimiterMaxDelay` (30s). | +| 2. Authoritative write | The writer's PowerDNS serves the change. It reads LMDB directly (`zone-cache-refresh-interval=0`), so no cache stands between the write and the answer. | Effectively immediate on the writer. | +| 3. Replicate to serving nodes | A LightningStream sidecar snapshots the LMDB to object storage; each serving node lists object storage, downloads new snapshots, and merges them. | The writer's `lmdb_poll_interval` plus each node's `storage_poll_interval` (see [LightningStream intervals](#lightningstream-intervals)), plus object-storage upload and download time — typically a few seconds. | +| 4. Resolver caching | Recursive resolvers and clients cache the answer until its TTL expires. They also cache a *missing* name for the zone's negative-cache TTL (the `SOA` minimum). | The record's TTL, and — for a newly added name — the negative-cache TTL of any earlier lookup. | + +Stages 1–3 move a change from the API to every authoritative serving node, +typically within a few seconds. Stage 4 dominates what end users observe, because +a resolver keeps serving a cached answer until the TTL expires regardless of how +fast the authoritative servers update. + +### LightningStream intervals + +LightningStream drives stage 3. The deployment runs it with default intervals, so +a change reaches every serving node within a few seconds of becoming +authoritative on the writer, bounded mainly by object-storage latency: + +| Setting | Default | Effect on propagation | +|---------|---------|-----------------------| +| `lmdb_poll_interval` | `1s` | How often the writer's sidecar checks LMDB for changes to snapshot. | +| `storage_poll_interval` | `1s` | How often a serving node lists object storage for new snapshots. | +| `storage_force_snapshot_interval` | `4h` | Writes a snapshot even with no changes, so an idle instance does not look stale. | +| `storage_retry_interval` | `5s` | Retry delay after a failed snapshot upload or download. | + +Lowering the poll intervals shortens propagation at the cost of more frequent +storage listings; raising them does the reverse. These intervals govern only how +fast a change reaches the serving nodes — end users still see it no sooner than +their cached TTL allows. + +Two practical consequences follow: + +- **To make a change take effect quickly, lower the record's TTL before you make + it.** Set a short TTL, wait for the old TTL to expire everywhere, change the + record, then raise the TTL again. The serving pipeline itself adds only seconds + to minutes; the TTL sets the ceiling. +- **A brand-new name can appear slowly if something looked it up first**, because + resolvers cached the negative answer for the zone's negative-cache TTL. The + operator's default `SOA` sets this value (see + [Replication Model](../replication.md#default-soa-and-ns-records)). + +## Configuration + +The agent connects to PowerDNS through environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `PDNS_API_URL` | `http://127.0.0.1:8081` | PowerDNS HTTP API endpoint. | +| `PDNS_API_KEY` | — | API key (or use `PDNS_API_KEY_FILE`). | +| `PDNS_API_KEY_FILE` | — | Path to a file that contains the API key. | + +The PowerDNS record-set controller adds its own tuning under +`controllers.dnsRecordSetPowerDNS` in the [`DNSOperator` server +config](../topology.md#operator-configuration): + +| Field | Default | Description | +|-------|---------|-------------| +| `controllers.dnsRecordSetPowerDNS.maxConcurrentReconciles` | `4` | Concurrent reconciles for the PowerDNS record-set controller. | +| `controllers.dnsRecordSetPowerDNS.rateLimiterBaseDelay` | `1s` | Exponential backoff base delay. | +| `controllers.dnsRecordSetPowerDNS.rateLimiterMaxDelay` | `30s` | Exponential backoff max delay. | + +The [`config/agent`](../../../config/agent) base bundles PowerDNS, the recursor, +and LightningStream alongside the agent and wires the API key through a shared +volume; the runnable [`config/overlays/agent-powerdns`](../../../config/overlays/agent-powerdns) +overlay adds the namespace and a storage backend. See +[Deployment Topology](../topology.md) for the deployment shape. + +## Related + +- [DNS Backends](./README.md) — The backend model that every backend shares +- [API Reference](../api-reference.md#dnszoneclass) — `DNSZoneClass` schema +- [Deployment Topology](../topology.md#operator-configuration) — The generic + `DNSOperator` server config diff --git a/docs/architecture/container-view.png b/docs/architecture/container-view.png new file mode 100644 index 0000000..599fbb2 Binary files /dev/null and b/docs/architecture/container-view.png differ diff --git a/docs/architecture/container-view.puml b/docs/architecture/container-view.puml new file mode 100644 index 0000000..7829e1c --- /dev/null +++ b/docs/architecture/container-view.puml @@ -0,0 +1,38 @@ +@startuml container-view +!$NEW_C4_STYLE = 1 +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!include datum-theme.puml + +LAYOUT_LEFT_RIGHT() + +title Container View — DNS Service + +Person(user, "Platform User", "Declares zones and records") +Person(resolver, "Recursive Resolver", "Resolves names for the internet") + +System_Boundary(tenant, "Tenant Control Planes") { + Container(cp, "Project Control Plane", "Kubernetes API", "Holds user-authored DNSZone / DNSRecordSet / DNSZoneClass") +} + +System_Boundary(replication, "Replication Control Plane") { + Container(replicator, "DNS Operator (replicator)", "Go / controller-runtime", "Discovers control planes, mirrors desired state, synthesizes status") +} + +System_Boundary(authoritative, "Authoritative Cluster") { + Container(agent, "DNS Operator (downstream agent)", "Go / controller-runtime", "Programs zones and records") + Container(pdns, "Authoritative Backend", "DNS server", "Source of truth for authoritative zone data") +} + +System_Boundary(edge, "Serving Layer") { + Container(serving, "Authoritative Serving Nodes", "DNS servers", "Answer public DNS queries") +} + +Rel(user, cp, "Creates / edits DNS resources", "kubectl / API") +Rel(replicator, cp, "Watches spec, writes status", "watch / patch") +Rel(replicator, agent, "Mirrors desired state as shadow objects") +Rel(agent, pdns, "Programs zones / records") +Rel(pdns, serving, "Replicates authoritative zone data", "backend-specific") +Rel_U(resolver, serving, "DNS query", "UDP/TCP :53") + +SHOW_LEGEND() +@enduml diff --git a/docs/architecture/datum-theme.puml b/docs/architecture/datum-theme.puml new file mode 100644 index 0000000..a1171be --- /dev/null +++ b/docs/architecture/datum-theme.puml @@ -0,0 +1,55 @@ +' Datum Cloud C4 Theme (Modern Style) +' Based on brand guidelines: https://www.datum.net/brand/color +' +' IMPORTANT: Set !$NEW_C4_STYLE = 1 BEFORE including C4 libraries in your diagram. +' This theme should be included AFTER C4 library includes. + +' Brand Colors +!$DATUM_MIDNIGHT_FJORD = "#0C1D31" +!$DATUM_AURORA_MOSS = "#E6F59F" +!$DATUM_CANYON_CLAY = "#BF9595" +!$DATUM_PINE_FORGE = "#4D6356" +!$DATUM_BLUSH_QUARTZ = "#ECD0D0" +!$DATUM_GLACIER_MIST = "#F6F6F5" +!$DATUM_GLACIER_MIST_DARK = "#E8E7E4" + +' Element Background Colors - Internal (vibrant pastels) +!$DATUM_BG_PERSON = "#F5D5D5" +!$DATUM_BG_SYSTEM = "#D5E5F5" +!$DATUM_BG_CONTAINER = "#D5F5E0" +!$DATUM_BG_COMPONENT = "#F5F0D5" + +' Element Background Colors - External (muted/grayed) +!$DATUM_BG_EXT_PERSON = "#E8E0E0" +!$DATUM_BG_EXT_SYSTEM = "#E0E8EC" +!$DATUM_BG_EXT_CONTAINER = "#E0ECE4" +!$DATUM_BG_EXT_COMPONENT = "#ECE8E0" + +' Person elements - Canyon Clay accents, pink background +UpdateElementStyle("person", $bgColor=$DATUM_BG_PERSON, $borderColor=$DATUM_CANYON_CLAY, $fontColor=$DATUM_MIDNIGHT_FJORD) +UpdateElementStyle("external_person", $bgColor=$DATUM_BG_EXT_PERSON, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE) + +' System elements - blue background +UpdateElementStyle("system", $bgColor=$DATUM_BG_SYSTEM, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD) +UpdateElementStyle("external_system", $bgColor=$DATUM_BG_EXT_SYSTEM, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE) + +' Container elements - green background +UpdateElementStyle("container", $bgColor=$DATUM_BG_CONTAINER, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD) +UpdateElementStyle("external_container", $bgColor=$DATUM_BG_EXT_CONTAINER, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE) + +' Component elements - yellow background +UpdateElementStyle("component", $bgColor=$DATUM_BG_COMPONENT, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD) +UpdateElementStyle("external_component", $bgColor=$DATUM_BG_EXT_COMPONENT, $borderColor=$DATUM_PINE_FORGE, $fontColor=$DATUM_PINE_FORGE) + +' Boundary styling - subtle Glacier Mist background +UpdateBoundaryStyle("system", $bgColor=$DATUM_GLACIER_MIST, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD) +UpdateBoundaryStyle("container", $bgColor=$DATUM_GLACIER_MIST, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD) +UpdateBoundaryStyle("enterprise", $bgColor=$DATUM_GLACIER_MIST, $borderColor=$DATUM_MIDNIGHT_FJORD, $fontColor=$DATUM_MIDNIGHT_FJORD) + +' Relationship styling +UpdateRelStyle("", $textColor=$DATUM_MIDNIGHT_FJORD, $lineColor=$DATUM_MIDNIGHT_FJORD) + +' Legend inherits from element styles via skinparam +skinparam LegendBackgroundColor $DATUM_GLACIER_MIST +skinparam LegendBorderColor $DATUM_MIDNIGHT_FJORD +skinparam LegendFontColor $DATUM_MIDNIGHT_FJORD diff --git a/docs/architecture/replication-flow.png b/docs/architecture/replication-flow.png new file mode 100644 index 0000000..83c9d44 Binary files /dev/null and b/docs/architecture/replication-flow.png differ diff --git a/docs/architecture/replication-flow.puml b/docs/architecture/replication-flow.puml new file mode 100644 index 0000000..8b7c0a5 --- /dev/null +++ b/docs/architecture/replication-flow.puml @@ -0,0 +1,25 @@ +@startuml replication-flow +!$NEW_C4_STYLE = 1 +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Dynamic.puml +!include datum-theme.puml + +title Replication Flow — Desired State Down, Realized Status Up + +System_Boundary(up, "Upstream Control Plane") { + Container(uz, "DNSZone / DNSRecordSet", "Kubernetes resource", "User-authored desired state") +} + +System_Boundary(down, "Authoritative Cluster") { + Container(sz, "Shadow DNSZone / DNSRecordSet", "Kubernetes resource", "Operator-managed mirror") + Container(agent, "Downstream Agent", "controller-runtime", "Programs the backend") + ContainerDb(be, "DNS Backend", "PowerDNS", "Authoritative zone data") +} + +RelIndex(1, uz, sz, "Replicator mirrors spec", "down") +RelIndex(2, sz, agent, "Agent reconciles shadow") +RelIndex(3, agent, be, "Programs zones / records", "HTTP API") +RelIndex(4, be, sz, "Realized status") +RelIndex(5, sz, uz, "Replicator mirrors status", "up") + +SHOW_LEGEND() +@enduml diff --git a/docs/architecture/replication.md b/docs/architecture/replication.md new file mode 100644 index 0000000..f68e960 --- /dev/null +++ b/docs/architecture/replication.md @@ -0,0 +1,97 @@ +# Replication Model + +The replicator bridges two trust boundaries: the **upstream** tenant control +plane, where users author DNS resources, and the **downstream** authoritative +cluster, where the DNS backend runs. It mirrors desired state down, realized +status up, and never gives tenants access to the authoritative cluster. + +This document describes how that bridge works. For where each side runs, see +[Deployment Topology](./topology.md). + +## Upstream and Downstream + +- **Upstream** — a tenant / project control plane (or, in `single` mode, the + local cluster) holding the user-facing `DNSZone` and `DNSRecordSet`. +- **Downstream** — the shared authoritative cluster where the DNS backend and the + downstream agent run. + +For every upstream resource, the replicator maintains a **shadow object** +downstream. The downstream agent programs the backend from the shadow and writes +realized status onto it; the replicator mirrors that status back upstream. A +change on either side wakes the replicator via cross-cluster watches. + +

+ Replication Flow — desired state down, realized status up +

+ +## Shadow Objects and Namespace Mapping + +Kubernetes does not support cross-cluster ownership references, so the replicator +uses a **mapped-namespace** strategy to place and track shadows: + +- Each upstream namespace maps to a downstream namespace named from the upstream + namespace's UID (`ns-`), keeping tenant namespaces collision-free without + leaking their names. +- The shadow keeps the **same object name** as its upstream source. +- **Provenance annotations** (`meta.datumapis.com/upstream-*`) record the source + cluster, group, kind, name, and namespace on every shadow. +- An **anchor** object downstream stands in for the missing cross-cluster owner + reference, so garbage collection cleans up shadows when the upstream resource + is deleted. + +Deletion flows downstream-first: the replicator removes the shadow (and its +backend state) before releasing the upstream finalizer, so a deleted zone stops +resolving before it disappears from the tenant's view. + +## Status Synthesis + +The replicator computes the two conditions users watch, combining upstream +validity with downstream reality: + +| Condition | Set to `True` when | +|-----------|--------------------| +| `Accepted` | The resource is valid, its dependencies exist, and (for zones) the operator knows the authoritative nameservers | +| `Programmed` | The backend has realized the desired state — for a zone, its default `SOA` and `NS` record sets exist; for a record set, the backend has applied it | + +The replicator mirrors record-set status straight from the downstream shadow, +including per-owner results, so a user sees exactly what the backend realized. +Zone status also reports the authoritative nameservers and a record count. + +## Zone Ownership Accounting + +Because many tenant control planes feed one authoritative cluster, two tenants +could request the same domain. The replicator guards against that collision with +an **ownership ledger** in the authoritative cluster, keyed by domain name. The +first zone to claim a domain wins. The replicator holds a later claimant's zone +with `Accepted=False` (reason `DNSZoneInUse`) and emits a warning event, rather +than overwriting the incumbent. The replicator releases ownership when the owning +zone is deleted. + +## Default SOA and NS Records + +A zone is not authoritative until it has `SOA` and `NS` records. Once the +operator knows the zone's nameservers (from its `DNSZoneClass` nameserver +policy), the replicator ensures two operator-managed record sets exist for the +apex (`@`): + +- an **`NS`** set that lists the authoritative nameservers, and +- an **`SOA`** set whose primary nameserver is the first of those, with a + hostmaster address derived from the domain and default refresh, retry, and + expire values. + +The replicator creates these sets only when they are missing, so it never +overwrites user-authored apex records. The operator synthesizes a date-based +`SOA` serial (`YYYYMMDD01`) when a record omits one. + +## Discovery of Zone Records + +`DNSZoneDiscovery` is a one-shot, read-only companion resource. The replicator +resolves the referenced zone's live records over DNS and writes them into the +resource's status. Use it to snapshot existing DNS during onboarding or +verification; it performs no mirroring and no backend writes. + +## Related + +- [Deployment Topology](./topology.md) — Where upstream, downstream, and serving + layers run +- [API Reference](./api-reference.md) — Conditions and resource schema diff --git a/docs/architecture/system-context.png b/docs/architecture/system-context.png new file mode 100644 index 0000000..bbc388c Binary files /dev/null and b/docs/architecture/system-context.png differ diff --git a/docs/architecture/system-context.puml b/docs/architecture/system-context.puml new file mode 100644 index 0000000..a668617 --- /dev/null +++ b/docs/architecture/system-context.puml @@ -0,0 +1,30 @@ +@startuml system-context +!$NEW_C4_STYLE = 1 +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml +!include datum-theme.puml + +LAYOUT_LEFT_RIGHT() + +title System Context — DNS Service + +Person(user, "Platform User", "Owns domains and publishes DNS so their applications are reachable") + +System_Boundary(dns, "DNS Service") { + System(operator, "DNS Operator", "Reconciles zones and records into an authoritative backend") + System(serving, "Authoritative Serving Layer", "Answers DNS queries") +} + +System_Ext(domains, "Domains", "Domain ownership and verification") +System_Ext(services, "Platform Services", "Networking, ingress, and TLS certificate issuance") +System_Ext(cp, "Tenant Control Planes", "Kubernetes API where users and platform services author DNS resources") +Person_Ext(resolver, "Recursive Resolver", "Resolves names for the internet") + +Rel_D(user, cp, "Declares zones and records", "kubectl / API") +Rel_D(services, cp, "Publish records to expose workloads and validate domain control") +Rel(domains, cp, "Supply verified domains that zones are created for") +Rel(operator, cp, "Watches desired state, writes status", "watch / patch") +Rel(operator, serving, "Programs authoritative zones") +Rel_U(resolver, serving, "Resolves published records", "UDP/TCP :53") + +SHOW_LEGEND() +@enduml diff --git a/docs/architecture/topology.md b/docs/architecture/topology.md new file mode 100644 index 0000000..fe72850 --- /dev/null +++ b/docs/architecture/topology.md @@ -0,0 +1,152 @@ +# Deployment Topology + +The DNS operator — the service's control-plane component — is a single binary +that runs in one of two **roles**, which you select with `--role`. A complete +deployment composes those roles across a few control planes and a serving fleet. +This document describes the components a deployment needs, independent of any +specific cloud or cluster technology. + +> [!NOTE] +> +> This is a generic reference topology. A given environment may collapse or +> expand these tiers — for example, running everything in one cluster for +> development (`discovery.mode: single`) or spreading the serving layer across +> many regions in production. + +## Container View + +

+ Container View — DNS Service +

+ +## Roles + +### Replicator + +**Runs in:** the replication control plane, one deployment per platform. + +**Responsibilities:** +- Discover the tenant control planes to serve (see [Discovery](#discovery)). +- Mirror each `DNSZone` / `DNSRecordSet` into the authoritative cluster as a + **shadow object**. +- Ensure a default `SOA` and `NS` record set exist for each zone. +- Account for zone ownership so two tenants cannot claim the same domain. +- Synthesize `Accepted` / `Programmed` status back onto the tenant's resources. + +The replicator holds credentials for the authoritative cluster and for each +control plane it discovers; tenants never receive access to the authoritative +cluster. See [Replication Model](./replication.md) for the internals. + +### Downstream Agent + +**Runs in:** the authoritative cluster, co-located with the DNS backend. + +**Responsibilities:** +- Ensure zones exist in the backend for classes it implements, applying the + class's nameserver policy. +- Translate `DNSRecordSet` resources into authoritative record sets in the + backend, resolving multi-owner conflicts to a single writer. +- Report realized status (`Programmed`, per-record results) on the shadow + objects, which the replicator mirrors upstream. + +The agent is the **only writer** to the DNS backend. It acts only on zones whose +`DNSZoneClass.spec.controllerName` matches a backend it implements (`powerdns` +today) and ignores every other class. See [DNS Backends](./backends/README.md) +for the backend model and the servers the agent can program. + +> [!NOTE] +> +> In `single` discovery mode the replicator serves the cluster it runs in, so you +> can point the replicator and agent at one cluster for a self-contained DNS +> service with no separate control plane. Run the two roles as separate +> deployments; select each with `--role`. + +## Control Planes and Clusters + +| Tier | What runs there | Trust boundary | +|------|-----------------|----------------| +| **Tenant / project control planes** | User-authored `DNSZone`, `DNSRecordSet`, `DNSZoneClass` | Owned by tenants; isolated per project | +| **Platform control plane** | Registry of project control planes used for discovery | Platform-operated | +| **Replication control plane** | DNS operator (replicator role) | Platform-operated; holds cross-cluster credentials | +| **Authoritative cluster** | DNS operator (agent role) + DNS backend | Platform-operated; single writer of authoritative data | +| **Serving layer** | Read-only authoritative servers | Platform-operated; internet-facing | + +## Discovery + +The replicator's `discovery.mode` (in the +[`DNSOperator` server config](#operator-configuration)) selects how the +replicator finds upstream control planes: + +- **`single`** — the operator serves exactly one upstream cluster: the cluster it + runs in. The operator performs no external discovery. +- **`milo`** — the operator queries a **platform control plane** for the set of + project control planes and connects to each one using a connection template. + The operator picks up new projects as they appear. + +Discovery decouples the number of tenants from the operator's deployment: adding +a tenant control plane requires no change to the DNS operator. + +## Authoritative Serving and State Replication + +The authoritative cluster holds the source of truth, but it does not necessarily +answer public queries itself. To scale and distribute serving, a backend can +replicate its authoritative zone data to a separate **serving layer** of +read-only nodes that answer queries close to end users. A simpler backend may +instead answer queries directly from the authoritative server, with no separate +serving layer at all. + +Where a serving layer exists, each node holds only its own replica, so you can +add nodes close to users, and an anycast address typically fronts them so +resolvers reach the nearest one. + +**How** a backend replicates its data and serves it is backend-specific — zone +transfers, shared storage, and clustered databases are all valid approaches. For +the PowerDNS mechanism — LMDB snapshots shipped through LightningStream to object +storage, with a local recursor for `ALIAS` expansion — see [PowerDNS Backend → +Storage and Serving](./backends/powerdns.md#storage-and-serving). + +Each zone's `NS` and `SOA` records advertise nameserver names from the +`DNSZoneClass` nameserver policy, and those names point at wherever the backend +answers queries. + +## Supporting Components + +- **Service catalog** — a `Service` registration publishes DNS into the platform + service catalog and billing surface. See + [`config/components/service-catalog`](../../config/components/service-catalog/README.md). +- **Mutating webhook** — stamps display annotations (FQDNs, record values) onto + record sets at admission, so downstream consumers render human-readable names + without re-deriving them. + +## Operator Configuration + +You configure the operator binary with a `DNSOperator` object passed via +`--server-config`. The API does not serve this object; it configures a running +instance. Sample: +[`config/agent/server-config.yaml`](../../config/agent/server-config.yaml). + +| Field | Default | Description | +|-------|---------|-------------| +| `discovery.mode` | `single` | `single` (local cluster) or `milo` (discover project control planes). | +| `discovery.internalServiceDiscovery` | `false` | Use internal service addresses when connecting to discovered control planes. | +| `discovery.discoveryKubeconfigPath` | — | Kubeconfig for the platform control plane used for discovery. | +| `discovery.projectKubeconfigPath` | — | Connection template for discovered project control planes. | +| `downstreamResourceManagement.kubeconfigPath` | — | Kubeconfig for the authoritative (downstream) cluster. | +| `downstreamResourceManagement.dnsZoneAccountingNamespace` | `datum-downstream-dnszone-accounting` | Namespace holding the zone ownership ledger. | + +Each backend contributes its own `controllers.` tuning and connection +settings. For the PowerDNS controller options and environment variables, see +[PowerDNS Backend → Configuration](./backends/powerdns.md#configuration). + +## Deployment Overlays + +The repository ships Kustomize bases and overlays for each role: + +| Path | Role | Notes | +|------|------|-------| +| [`config/overlays/agent-powerdns`](../../config/overlays/agent-powerdns) | Downstream agent | Runnable agent + PowerDNS: builds on the `config/agent` base and adds the namespace and a storage backend | +| [`config/agent`](../../config/agent) | Downstream agent (base) | Manager, PowerDNS, and config; consumed by the overlay above rather than applied directly | +| [`config/overlays/replicator`](../../config/overlays/replicator) | Replicator | Wires downstream cluster credentials and discovery mode | +| [`config/milo`](../../config/milo) | Component | Installs platform integration resources into tenant control planes | + +See the [project README](../../README.md#deploying) for quickstart instructions.