Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Publish Docker Image

on:
# Trigger on push
push:
# Trigger on all pull requests
pull_request:
# Trigger when a release is published
release:
types: ['published']

jobs:
publish-container-image:
permissions:
id-token: write
contents: read
packages: write
attestations: write
uses: datum-cloud/actions/.github/workflows/publish-docker.yaml@v1.5.1
with:
image-name: dns-operator
secrets: inherit

publish-kustomize-bundles:
permissions:
id-token: write
contents: read
packages: write
uses: datum-cloud/actions/.github/workflows/publish-kustomize-bundle.yaml@v1.5.1
with:
bundle-name: ghcr.io/datum-cloud/dns-operator-kustomize
bundle-path: config
secrets: inherit
32 changes: 0 additions & 32 deletions .github/workflows/test-e2e.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

bin/
vendor/
dist/
dev/
.go-version
config/**/charts

# Test binary, built with `go test -c`
*.test
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ linters:
rules:
- name: comment-spacings
- name: import-shadowing
gocyclo:
min-complexity: 70
exclusions:
generated: lax
rules:
Expand Down
661 changes: 0 additions & 661 deletions LICENSE

This file was deleted.

256 changes: 246 additions & 10 deletions Makefile

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ resources:
kind: DNSRecordSet
path: go.miloapis.com/dns-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: networking.miloapis.com
group: dns
kind: DNSZoneDiscovery
path: go.miloapis.com/dns-operator/api/v1alpha1
version: v1alpha1
version: "3"
136 changes: 135 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,135 @@
# dns-operator
## 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
```

### Quickstart: Replicator (upstream → downstream)
1. Create Secret on the replicator namespace containing the downstream kubeconfig (`data.kubeconfig`):
```bash
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

### 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/`
Loading