openova/platform/external-dns
e3mrah bcd2e7980a
fix: hide CRD-emitting resources behind Capabilities gates (closes #190) (#200)
* 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>
2026-04-29 20:10:14 +02:00
..
chart fix: hide CRD-emitting resources behind Capabilities gates (closes #190) (#200) 2026-04-29 20:10:14 +02:00
policies feat(external-dns): #109 — Catalyst-curated dynadot-multi-domain policy 2026-04-28 14:45:53 +02:00
README.md refactor(platform): remove k8gb — replaced by PowerDNS lua-records (#171) 2026-04-29 08:51:09 +02:00

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