* fix(bp-external-dns): hide CRD-emitting resources behind Capabilities gates (refs #190) Wrap the Catalyst overlay's ServiceMonitor and ExternalSecret templates in `.Capabilities.APIVersions.Has` checks so a cold install on a fresh Sovereign — where bp-kube-prometheus-stack and bp-external-secrets have not yet reconciled — no longer fails with `no matches for kind X in version Y`. The values toggles (`externalDns.serviceMonitor.enabled`, `externalDns.externalSecret.enabled`) remain — Capabilities is defense in depth so an operator flipping the toggle on a Sovereign that hasn't reached Phase 2 doesn't break the bp-external-dns reconcile. Verified locally: `helm template` with toggles off renders 0 of these resources; with toggles ON and `--api-versions monitoring.coreos.com/v1 --api-versions external-secrets.io/v1beta1` both render exactly once. Bump version 1.1.0 → 1.1.2 to align with the Phase-1 architectural-fix wave from issue #190. * fix(bp-powerdns): hide CRD-emitting resources behind Capabilities gates (refs #190) Three Catalyst overlay templates emit resources whose CRDs ship in OTHER charts and were unconditionally rendered, causing a cold install of bp-powerdns to fail with `no matches for kind X` on a Sovereign that hasn't yet reconciled the upstream chart: - cnpg-cluster.yaml → postgresql.cnpg.io/v1 Cluster (CRD ships in bp-cnpg) - api-ingress.yaml → traefik.io/v1alpha1 Middleware (CRD ships with the Traefik controller; k3s ships it by default but a Sovereign overlay MAY disable Traefik in favour of cilium-only ingress) - crossplane-floatingip.yaml → compose.openova.io/v1alpha1 HetznerFloatingIP (CRD ships when the Catalyst Crossplane composition family lands — see GAP DISCLOSURE in that template) Each is wrapped in `.Capabilities.APIVersions.Has "<group>/<version>"`. The Traefik router-middleware annotation on the Ingress is similarly gated so the auth posture cleanly moves to the Sovereign's chosen ingress controller when Traefik is absent. Verified locally: `helm template` with default values renders 0 of these resources; with `--api-versions postgresql.cnpg.io/v1 --api-versions traefik.io/v1alpha1 --api-versions compose.openova.io/v1alpha1` plus `--set crossplane.floatingIP.enabled=true`, all three render exactly once. Existing tests/observability-toggle.sh still passes. Bump version 1.1.1 → 1.1.2. * fix(bp-powerdns): bump blueprint.yaml to match Chart.yaml 1.1.2 after Capabilities gate work --------- Co-authored-by: hatiyildiz <hatice.yildiz@openova.io> |
||
|---|---|---|
| .. | ||
| chart | ||
| policies | ||
| README.md | ||
ExternalDNS
DNS synchronization (registers/deletes records via the PowerDNS REST API and external cloud DNS APIs where applicable). Per-host-cluster infrastructure (see docs/PLATFORM-TECH-STACK.md §3.1) — runs on every host cluster, primarily on the DMZ block. PowerDNS (see docs/PLATFORM-POWERDNS.md) is the authoritative server for every Sovereign zone; ExternalDNS uses the webhook provider (external-dns-pdns) to write A/AAAA/CNAME records into PowerDNS. Health-checked geo-failover lives in PowerDNS lua-records — see docs/MULTI-REGION-DNS.md.
Status: Accepted | Updated: 2026-04-27
Overview
ExternalDNS synchronizes Kubernetes resources (Gateway, Service, Ingress) with external DNS providers, enabling automatic DNS record management.
Architecture
flowchart TB
subgraph K8s["Kubernetes"]
GW[Gateway API]
Svc[Services]
ExtDNS[ExternalDNS]
end
subgraph DNS["DNS Providers"]
PDNS[PowerDNS<br>(authoritative — every Sovereign zone)]
CF[Cloudflare]
R53[Route53]
HDNS[Hetzner DNS]
end
GW --> ExtDNS
Svc --> ExtDNS
ExtDNS --> PDNS
ExtDNS --> CF
ExtDNS --> R53
ExtDNS --> HDNS
Supported DNS Providers
| Provider | Availability |
|---|---|
| Cloudflare | Always |
| Hetzner DNS | If Hetzner chosen |
| AWS Route53 | If AWS chosen |
| GCP Cloud DNS | If GCP chosen |
| Azure DNS | If Azure chosen |
Configuration
ExternalDNS Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: external-dns
spec:
template:
spec:
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.14.0
args:
- --source=gateway-httproute
- --source=gateway-grpcroute
- --source=service
- --provider=cloudflare
- --cloudflare-proxied
- --txt-owner-id=openova
- --txt-prefix=_externaldns.
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-credentials
key: api-token
PowerDNS Integration (geo + health-checked failover)
ExternalDNS writes plain A/AAAA/CNAME records into PowerDNS via the REST API. Geo-aware and health-checked failover responses are owned by PowerDNS lua-records, written by the catalyst-dns controller — ExternalDNS does NOT manage lua-record content.
flowchart LR
subgraph Region1["Region 1"]
App1[Application]
ExtDNS1[ExternalDNS]
end
subgraph Region2["Region 2"]
App2[Application]
ExtDNS2[ExternalDNS]
end
subgraph PDNS["PowerDNS Authoritative"]
ZoneAPI[REST API]
Lua[lua-records (ifurlup, pickclosest)]
end
ExtDNS1 -->|"plain A/AAAA"| ZoneAPI
ExtDNS2 -->|"plain A/AAAA"| ZoneAPI
ZoneAPI --- Lua
See docs/MULTI-REGION-DNS.md for the lua-record patterns.
Record Types
| Source | Record Type | Example |
|---|---|---|
| Gateway | A/CNAME | api.<domain> |
| Service (LoadBalancer) | A | svc.<domain> |
| catalyst-dns (lua-record author) | LUA A | app.<domain> (geo + health-checked) |
TXT Registry
ExternalDNS uses TXT records to track ownership:
_externaldns.api.<domain> TXT "heritage=external-dns,external-dns/owner=openova"
This prevents ExternalDNS from modifying records it doesn't own.
Part of OpenOva