# bp-powerdns — Catalyst Blueprint #11 of 13. Per-Sovereign authoritative
# DNS. Every Sovereign owns its own PowerDNS Authoritative server with the
# Sovereign's zones loaded into a CNPG-backed gpgsql backend; per-zone
# DNSSEC (ECDSAP256SHA256), lua-records for geo + health-checked failover,
# and a dnsdist companion for query rate-limiting + DDoS posture.
#
# Architectural anchor — per-Sovereign PowerDNS, NOT a shared instance:
#   This file was added so each Sovereign has its own bp-powerdns release
#   alongside bp-external-dns (#12). The original Phase-0 dial-tone
#   PowerDNS used to live as a singleton in the openova-system namespace
#   on contabo-mkt; #168 ("per-Sovereign PowerDNS zones") flipped that
#   model so a Sovereign's zones never leave the Sovereign's own cluster.
#   bp-external-dns's `dependsOn: [bp-powerdns]` therefore must be
#   satisfied by an in-cluster sibling release — this file IS that
#   sibling.
#
# Wrapper chart: platform/powerdns/chart/
# Catalyst-curated values: platform/powerdns/chart/values.yaml
#   - 3 PowerDNS Authoritative replicas behind dnsdist
#   - CNPG-managed `pdns-pg` Postgres cluster for zone storage
#   - DNSSEC ON by default (ECDSAP256SHA256)
#   - Lua-records ON (geo + health-checked failover patterns)
#   - REST API at pdns.<sovereign>/api behind Traefik basicAuth
#
# Reconciled by: Flux on the new Sovereign's k3s control plane.
#
# dependsOn:
#   - bp-cert-manager — the REST API ingress requests a TLS cert from
#     the cluster's letsencrypt-prod ClusterIssuer; without bp-cert-manager
#     reconciled first the Certificate resource sits Pending.
#
# Hard-but-implicit dependencies (CRDs, NOT sibling Blueprints):
#   - postgresql.cnpg.io/v1.Cluster — provided by the CNPG operator. The
#     chart's templates/cnpg-cluster.yaml renders this CR; if CNPG isn't
#     yet installed on the Sovereign, the HelmRelease will wait. CNPG is
#     a fixture of Catalyst-Zero (FABRIC group, componentGroups.ts `cnpg`)
#     and is installed alongside this kit by the bootstrap installer.
#   - traefik.io/v1alpha1.Middleware — Traefik is the Catalyst-Zero
#     ingress controller and is a fixture of every Sovereign.
#
# Per-Sovereign overrides intentionally NOT set here:
#   - Zones, API basic-auth credentials, anycast Floating-IP target — all
#     of those are provisioned by Crossplane / pool-domain-manager (PDM)
#     after the cluster comes up, NOT baked into the bootstrap kit. This
#     file installs the chart with defaults so the Sovereign has a working
#     authoritative DNS surface that PDM can then load zones into.

---
apiVersion: v1
kind: Namespace
metadata:
  name: powerdns
  labels:
    catalyst.openova.io/sovereign: ${SOVEREIGN_FQDN}
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: bp-powerdns
  namespace: flux-system
spec:
  type: oci
  interval: 15m
  url: oci://ghcr.io/openova-io
  secretRef:
    name: ghcr-pull
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: bp-powerdns
  namespace: flux-system
  labels:
    catalyst.openova.io/slot: "11"
spec:
  interval: 15m
  timeout: 15m
  releaseName: powerdns
  targetNamespace: powerdns
  dependsOn:
    - name: bp-cert-manager
    # bp-gateway-api (issue #503): chart ships an api-httproute.yaml
    # template; gateway.networking.k8s.io/v1 CRDs must be registered first.
    - name: bp-gateway-api
    # bp-cnpg — chart's templates/cnpg-cluster.yaml renders a
    # postgresql.cnpg.io/v1.Cluster gated on Capabilities.APIVersions.
    # Without this dependency Helm renders before the CRD is registered,
    # the gate evaluates false, the Cluster CR is silently skipped,
    # CNPG never creates pdns-pg-app, and powerdns Pods fail at boot
    # with "secret pdns-pg-app not found" (caught live during otech28).
    - name: bp-cnpg
  chart:
    spec:
      chart: bp-powerdns
      # 1.2.0 (issue #827): adds multi-zone bootstrap Job. Sovereign
      # parent zones (`omani.works`, `omani.trade`, ...) are POSTed to
      # /api/v1/servers/localhost/zones at Helm post-install/post-upgrade
      # time, idempotent on HTTP 409. The list below is populated from
      # ${PARENT_DOMAINS_YAML} via Flux postBuild.substitute (see
      # infra/hetzner/cloudinit-control-plane.tftpl); a single-zone
      # fallback derived from ${SOVEREIGN_FQDN} keeps legacy
      # provisioning paths operative.
      # 1.2.1: zone-bootstrap Job needs /tmp emptyDir (readOnlyRootFS+
      # curl -o /tmp/zone-resp). Caught live on otech103 2026-05-04.
      # 1.2.2 (issue #144): zone-bootstrap Job activeDeadlineSeconds
      # raised 300s → 840s. Cold Sovereign on prov #22 had bp-cnpg
      # still synthesising the `pdns-pg-app` Secret when this Job
      # ran; powerdns Pod was not Ready, curl against
      # http://powerdns:8081 looped, Job hit 5m DeadlineExceeded,
      # Helm post-install hook failed, HR FAILED 4× → terminal.
      # New deadline (14m) sits below the HR install.timeout cap of
      # 15m so Flux's remediation can still reclaim a true failure.
      # 1.2.3 (Fix #144-followup, prov #37+#38 recurrence 2026-05-12):
      # bumping activeDeadlineSeconds alone was insufficient — the Job
      # hit BackoffLimitExceeded (NOT DeadlineExceeded) at ~10min
      # because each container invocation curl'd a Service with empty
      # Ready endpoints (powerdns Pods Pending behind a worker-capacity
      # wedge that kept bp-cnpg's pdns-pg-1-initdb itself Pending).
      # Container restartPolicy=OnFailure + backoffLimit=6 killed the
      # Job long before activeDeadlineSeconds had any effect. Fix moves
      # the wait-for-API loop INSIDE the container (restartPolicy=Never,
      # bounded by new apiReadyTimeoutSeconds=600s) so one Pod owns
      # the full 14m budget. Trace: in chroot prov #38, HR status
      # message read "Helm install failed for release powerdns/powerdns
      # with chart bp-powerdns@1.2.2: failed post-install: 1 error
      # occurred: * job powerdns-zone-bootstrap failed: BackoffLimitExceeded".
      version: 1.2.4
      sourceRef:
        kind: HelmRepository
        name: bp-powerdns
        namespace: flux-system
  # disableWait: a Sovereign without bp-cnpg yet reconciled has no
  # `pdns-pg-app` Secret (the chart's CNPG Cluster template is gated
  # behind the `postgresql.cnpg.io/v1` CRD via Capabilities.APIVersions
  # check — see chart/templates/cnpg-cluster.yaml). Without disableWait,
  # Helm's `--wait` would hold until the powerdns Deployment is Ready,
  # which can't happen until CNPG comes up and synthesises the Secret.
  # The HelmRelease itself reports Ready as soon as the manifests apply
  # cleanly; runtime convergence (powerdns pods becoming Ready once
  # CNPG lands) is observed via kubectl, not gated on Helm.
  install:
    timeout: 15m
    disableWait: true
    remediation:
      retries: 3
  upgrade:
    timeout: 15m
    disableWait: true
    remediation:
      retries: 3
  # Per-Sovereign overrides — issue #387:
  # Flip the REST API exposure from legacy Traefik Ingress to Cilium
  # Gateway HTTPRoute. The Ingress template still renders (gated by
  # api.enabled=true) but is harmless on a Sovereign without Traefik
  # — the apiserver accepts the Ingress object; nothing routes it.
  # The HTTPRoute attaches to cilium-gateway/kube-system and is the
  # active path on Sovereigns.
  values:
    api:
      host: pdns.${SOVEREIGN_FQDN}
      gateway:
        enabled: true
    # DNS-01 wildcard cert: expose PowerDNS on NodePort 30053 so the
    # Sovereign LB can forward :53 → PowerDNS. This is the NS-delegated
    # endpoint that Let's Encrypt resolvers query when validating ACME
    # DNS-01 challenges for *.${SOVEREIGN_FQDN}. Per ADR-0001 §9.4 the
    # Sovereign must be self-sufficient post-handover — no Dynadot
    # dependency for cert renewals.
    anycast:
      enabled: true
      serviceName: powerdns-anycast
      # NodePort on Sovereign Hetzner clusters: lb11 LB forwards TCP:53 to
      # NodePort 30053; k3s iptables DNAT handles UDP:53 NodePort natively.
      serviceType: NodePort
      ports:
        - name: dns-udp
          port: 53
          targetPort: 5353
          nodePort: 30053
          protocol: UDP
        - name: dns-tcp
          port: 53
          targetPort: 5353
          nodePort: 30053
          protocol: TCP
    # ─── Multi-zone bootstrap (issue #827, parent epic #825) ───────────
    # The Sovereign creates one PowerDNS zone per parent domain at Helm
    # post-install/post-upgrade time via the chart's zone-bootstrap Job
    # (templates/zone-bootstrap-job.yaml). Idempotent on HTTP 409 so
    # re-applies after upgrades or chart bumps never fail.
    #
    # Source of truth: ${PARENT_DOMAINS_YAML} is a Flux
    # postBuild.substitute variable containing the operator-supplied
    # parent-domain list rendered as a YAML inline-array literal, e.g.
    #   PARENT_DOMAINS_YAML='[{name: "omani.works", role: "primary"}, {name: "omani.trade", role: "sme-pool"}]'
    # When the operator brings only one parent domain (default
    # zero-touch flow), cloud-init pre-renders this variable to a
    # single-entry array derived from ${sovereign_fqdn} so the
    # Sovereign still owns its own apex zone. See
    # infra/hetzner/cloudinit-control-plane.tftpl for the substitute
    # block that materialises the default.
    zones: ${PARENT_DOMAINS_YAML}
