# Catalyst Blueprint wrapper metadata — read by the bootstrap-kit installer.
# The installer combines the upstream chart reference below with the values
# overlay also in this file when running 'helm install'.
#
# PowerDNS does NOT publish an official Helm chart. After surveying the
# Artifact Hub catalogue (curl https://artifacthub.io/api/v1/packages/search?
# ts_query_web=powerdns) the only verified-publisher chart that tracks the
# canonical PowerDNS Authoritative image (docker.io/powerdns/pdns-auth-50)
# at appVersion 5.0.3 is `pschichtel/powerdns` 0.10.0 from
# https://schich.tel/helm-charts. We pin that as the upstream reference;
# the Catalyst overlay adds:
#   - CNPG-backed gpgsql backend (separate `pdns-pg` cluster in this chart)
#   - DNSSEC enabled by default (api-rectify + ECDSAP256SHA256 keys)
#   - lua-records support
#   - dnsdist companion for query rate-limiting
#   - REST API ingress at pdns.openova.io behind Traefik basicAuth
#   - placeholder anycast endpoint Service (XHetznerFloatingIP composition
#     not yet authored — see Crossplane gap note in
#     templates/crossplane-floatingip.yaml)
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) every operationally-
# meaningful value flows from this file or from a referenced K8s Secret —
# image tags come from the upstream chart's `image.repository:appVersion`
# default, never duplicated here.

global:
  # When set, ALL image pulls in this chart route through this registry.
  # Used post-handover when the Sovereign's own Harbor takes over the
  # proxy_cache role from contabo's central Harbor. Empty = no rewrite
  # (image references use upstream defaults). When non-empty, the
  # dnsdist.yaml template prepends this value to dnsdist.image.repository.
  # The upstream powerdns subchart exposes `powerdns.image.repository`
  # for the PowerDNS Authoritative image. Tracked under #560.
  imageRegistry: ""

catalystBlueprint:
  upstream:
    chart: powerdns
    version: "0.10.0"
    repo: "https://schich.tel/helm-charts"
    # Catalyst rationale for this upstream:
    # - Verified publisher on Artifact Hub
    # - Tracks docker.io/powerdns/pdns-auth-50 (canonical PowerDNS image)
    # - appVersion 5.0.3 — current stable Authoritative line
    # - Templates support the gpgsql backend driver out of the box
    # - Apache-2.0 / MIT-style permissive licence on the chart itself

# ─── Upstream chart values (subchart key: powerdns) ───────────────────────
# `helm dependency build` resolves the upstream as a subchart; values here
# under the `powerdns:` key flow into that subchart unchanged.
powerdns:
  # ─── Catalyst-only credential injection knobs ───────────────────────
  # Read by templates/api-credentials-secret.yaml (wrapper-level). The
  # upstream subchart ignores these keys.
  #
  # Default behaviour: chart self-generates a 32-char api-key and 32-char
  # webserver-password on first install (Helm `lookup` re-uses on every
  # subsequent reconcile so the values are stable across upgrades).
  #
  # Operator overrides:
  #   apiKey + webserverPassword   — pin specific values (e.g. when a
  #                                   sealed-secret already encodes them
  #                                   for cross-cluster GitOps).
  #   useExistingApiSecret: true   — skip Secret creation entirely; the
  #                                   chart assumes a Secret named
  #                                   `powerdns-api-credentials` is
  #                                   provided out-of-band (ExternalSecret
  #                                   / SealedSecret / kubectl create).
  apiKey: ""
  webserverPassword: ""
  useExistingApiSecret: false

  # 3 replicas across regions — anycast-fronted public NS endpoints
  # (per #167 acceptance criteria). Each replica connects to the same CNPG
  # database; PowerDNS Authoritative is stateless beyond the database.
  replicaCount: 3

  image:
    # tag: null → upstream chart falls back to .Chart.AppVersion (5.0.3).
    # Per INVIOLABLE-PRINCIPLES #4 we don't pin a literal tag here — the
    # chart's appVersion is the single source of truth and bumping the
    # upstream version in `catalystBlueprint.upstream.version` above
    # propagates the new image tag automatically.
    pullPolicy: IfNotPresent

  podSecurityContext:
    fsGroup: 953              # PowerDNS image's `pdns` group
    runAsNonRoot: true
    runAsUser: 953            # PowerDNS image's `pdns` user
    seccompProfile:
      type: RuntimeDefault

  securityContext:
    allowPrivilegeEscalation: false
    # readOnlyRootFilesystem MUST be false: the upstream chart's
    # /usr/local/sbin/pdns_server-startup script writes rendered
    # /etc/powerdns/pdns.d/*.conf at container start, and the upstream
    # template doesn't expose an emptyDir mount we could redirect that
    # path to. Locking the rootfs would crash-loop the pod (verified on
    # Contabo k3s — Errno 30 Read-only file system: '/etc/powerdns/pdns.d/0-api.conf.conf').
    readOnlyRootFilesystem: false
    runAsNonRoot: true
    runAsUser: 953
    runAsGroup: 953
    capabilities:
      drop:
        - ALL
      # PowerDNS binds to UDP/53 + TCP/53 in production; the upstream
      # chart's containerPort 5353 keeps the workload non-privileged.
      # When/if we run on :53 directly we re-add NET_BIND_SERVICE here.

  service:
    # ClusterIP is fine for the chart's own DNS service — the public NS
    # endpoints are realised by the placeholder LoadBalancer Service in
    # templates/anycast-endpoint.yaml (replaced by XHetznerFloatingIP
    # once that Crossplane composition is authored — see
    # templates/crossplane-floatingip.yaml).
    type: ClusterIP

  ingress:
    # The chart's own ingress (chart-example.local) is disabled — the
    # operator API ingress is rendered by templates/api-ingress.yaml so
    # we can layer Traefik basicAuth middleware on top.
    enabled: false

  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      cpu: 500m
      memory: 512Mi

  # ─── Observability — DEFAULT OFF ─────────────────────────────────────
  # Per docs/INVIOLABLE-PRINCIPLES.md #4 and
  # docs/BLUEPRINT-AUTHORING.md §11.2 (Observability toggles must
  # default false). The upstream pschichtel/powerdns 0.10.0 chart does
  # not currently render any monitoring.coreos.com/v1 resources — but
  # we declare the rule explicitly here as a forward-compatibility
  # guard: a future upstream bump that adds a ServiceMonitor template
  # cannot regress a fresh-Sovereign install before
  # bp-kube-prometheus-stack reconciles. Operator opts in via
  # per-cluster overlay (issue #182).
  serviceMonitor:
    enabled: false
  metrics:
    enabled: false

  powerdns:
    # ─── REST API + webserver ───────────────────────────────────────────
    # API key + webserver password flow from a Catalyst-managed K8s Secret
    # (`powerdns-api-credentials` — see templates/api-credentials-secret.yaml).
    # Per INVIOLABLE-PRINCIPLES #4 + #10 the values are NEVER inlined here;
    # the wrapper's api-credentials-secret.yaml self-generates 32-char
    # randoms on first install and reuses them on every subsequent
    # reconcile via Helm `lookup`. The upstream chart's secretRef helper
    # takes flat name+key fields (see powerdns.secretRef in upstream
    # _helpers.tpl).
    api:
      key:
        name: powerdns-api-credentials
        key: api-key
    webserver:
      bindPort: 8081
      # Webserver/API listener is exposed in-cluster only. The
      # api-ingress.yaml Ingress is the sanctioned external entry point;
      # operator basicAuth + TLS terminate at Traefik before reaching pdns.
      allowFrom: 0.0.0.0/0
      password:
        name: powerdns-api-credentials
        key: webserver-password

    # ─── Bootstrap zone (DISABLED — Catalyst loads zones via PDM) ──────
    # The upstream chart renders a `create-zone-if-not-exists-sh` Job
    # that POSTs `zoneName` to /api/v1/servers/localhost/zones at install
    # time. Catalyst does NOT use this — the Sovereign's zones are
    # provisioned later by pool-domain-manager (PDM) via the same REST
    # API after the cluster comes up. Setting `zoneName: ""` short-
    # circuits the upstream Job's `{{- if and .Values.powerdns.zoneName
    # .Values.powerdns.api.key }}` gate so the Job is never rendered,
    # which means the install completes the moment the powerdns
    # Deployment is Ready instead of waiting for a Job whose only effect
    # is creating an `example.de.` placeholder zone we don't want.
    zoneName: ""

    # ─── DNS UPDATE (RFC 2136) ──────────────────────────────────────────
    # Off — Catalyst writes records via the REST API only (cert-manager
    # webhook + external-dns webhook + crossplane DNS XR all use the
    # API).
    dnsupdate:
      enabled: false

    # ─── Database backend (gpgsql via CNPG) ─────────────────────────────
    # The CNPG-managed `pdns-pg` cluster is rendered by
    # templates/cnpg-cluster.yaml (separate from `pdm-pg`). CNPG generates
    # a `pdns-pg-app` Secret with keys host/port/dbname/user/password
    # which the upstream chart consumes via secretRef indirection.
    db:
      driver: gpgsql
      host:
        name: pdns-pg-app
        key: host
      port:
        name: pdns-pg-app
        key: port
      name:
        name: pdns-pg-app
        key: dbname
      username:
        name: pdns-pg-app
        key: username
      password:
        name: pdns-pg-app
        key: password

    # ─── Catalyst additionalConfig ──────────────────────────────────────
    # PowerDNS-Authoritative directives layered on top of the upstream
    # chart's defaults. Source of truth for DNSSEC + lua-records + zone-
    # cache + multi-master posture — all the bits that distinguish the
    # OpenOva production NS from a vanilla pdns deployment.
    #
    # Reference: https://doc.powerdns.com/authoritative/settings.html
    additionalConfig: |
      # ── DNSSEC ────────────────────────────────────────────────────────
      # `gpgsql-dnssec=yes` is REQUIRED for the gpgsql backend to expose
      # the DNSSEC API — without it `PUT /zones/<zone>` with dnssec:true
      # returns 422 "no DNSSEC-capable backends are loaded". Pair with
      # `direct-dnskey=yes` so DNSKEY/CDS/CDNSKEY are served, and
      # `default-soa-edit-signed=INCEPTION-EPOCH` to keep SOA serials in
      # sync across replicas after every signed-zone change. DNSSEC keys
      # are generated per-zone by pool-domain-manager (#168) using
      # POST /zones/<zone>/cryptokeys with algorithm=ecdsa256 — every
      # Sovereign zone is DNSSEC-signed (ECDSAP256SHA256, algo 13) per
      # docs/PLATFORM-POWERDNS.md.
      gpgsql-dnssec=yes
      default-soa-edit-signed=INCEPTION-EPOCH
      direct-dnskey=yes

      # ── Records that mutate behaviour ────────────────────────────────
      # `enable-lua-records=yes` enables PowerDNS Lua records used by
      # Catalyst for geo-aware + health-checked failover. See
      # docs/MULTI-REGION-DNS.md for the lua-record patterns. Reference:
      # https://doc.powerdns.com/authoritative/lua-records/index.html
      enable-lua-records=yes

      # ── Performance tuning ───────────────────────────────────────────
      # `query-cache-ttl=20` smooths bursts (normal default) without
      # sacrificing rapid record-update propagation. `cache-ttl=20` caps
      # the packet cache. Both align with the dnsdist companion's
      # rate-limiting posture — bursts that survive dnsdist hit a warm
      # cache.
      query-cache-ttl=20
      cache-ttl=20

      # ── REST API CORS ────────────────────────────────────────────────
      # The Catalyst console posts to /api/v1 from console.openova.io;
      # the api-ingress.yaml Ingress sets the Access-Control-Allow-Origin
      # header at the Traefik layer, but PowerDNS itself must permit the
      # method+header set its own webserver returns.
      webserver-allow-from=0.0.0.0/0,::/0

      # ── Logging ──────────────────────────────────────────────────────
      log-dns-queries=no              # off in prod — dnsdist captures
      log-dns-details=no
      loglevel=4                      # warning + above

# ─── Catalyst additions (rendered by templates/) ──────────────────────────

# CNPG-managed Postgres for PowerDNS zones+records. Separate from `pdm-pg`
# (pool-domain-manager) so the two components scale independently and a
# DB outage in one doesn't cascade. Per INVIOLABLE-PRINCIPLES #4 every
# value is configurable.
postgres:
  # ─── Cluster ────────────────────────────────────────────────────────
  cluster:
    name: pdns-pg
    # On Sovereign clusters the chart's targetNamespace is `powerdns` and
    # `openova-system` does not exist — Helm upgrade fails with
    # `namespaces "openova-system" not found` and the CNPG Cluster CR is
    # never applied, so the powerdns Deployment crashloops on the missing
    # `pdns-pg-app` Secret. Default to the chart's own targetNamespace so
    # the Secret materialises in the same place the consumer reads it
    # from. Contabo legacy can override via per-Sovereign values overlay
    # if it still uses openova-system. Issue #553.
    namespace: powerdns
    instances: 1                 # bump to 3 alongside replicaCount=3 once we move beyond Phase-0 single-region
    storageSize: 5Gi
    storageClass: local-path     # Contabo k3s default; Sovereign Hetzner clusters override to hcloud-volumes
    pgVersion: "16"
    database: pdns
    owner: pdns
    resources:
      requests: { cpu: 50m, memory: 128Mi }
      limits:   { cpu: 500m, memory: 512Mi }
  # ─── Schema bootstrap ───────────────────────────────────────────────
  # PowerDNS gpgsql driver expects a specific schema; we apply it via a
  # CNPG `bootstrap.initdb.postInitApplicationSQL` block referenced by
  # cnpg-cluster.yaml. Pinned to the gpgsql 5.0.3 schema published at
  # https://github.com/PowerDNS/pdns/tree/auth-5.0.3/modules/gpgsqlbackend
  schemaURL: "https://raw.githubusercontent.com/PowerDNS/pdns/auth-5.0.3/modules/gpgsqlbackend/schema.pgsql.sql"

# dnsdist companion — query rate-limiting + DDoS posture in front of every
# PowerDNS replica. Defaults: 100 qps per source IP, configurable.
dnsdist:
  enabled: true
  image:
    # Official dnsdist image. Repo `powerdns/dnsdist-19` carries the 1.9.x
    # line (matches Authoritative 5.0.x release cadence). Pin a concrete
    # tag here — the dnsdist Deployment template defaults to the value
    # below when image.tag is unset.
    repository: docker.io/powerdns/dnsdist-19
    tag: "1.9.14"
    pullPolicy: IfNotPresent
  replicaCount: 1                   # scale alongside Sovereign expansion; single instance fronts one region
  service:
    type: ClusterIP                 # public exposure goes through the anycast endpoint, not directly
  ratelimit:
    qpsPerSource: 100               # queries/sec per source IP
    blockSeconds: 60                # how long a violator stays blocked
  resources:
    requests: { cpu: 50m, memory: 64Mi }
    limits:   { cpu: 250m, memory: 256Mi }
  config: |
    -- Catalyst dnsdist policy. Forwards to the in-cluster powerdns
    -- Service; rate-limits per source IP. Lua-records-ready downstream.
    --
    -- Backend address resolution: dnsdist 1.9 newServer{address=...}
    -- requires IP[:port], not a hostname. Kubernetes auto-injects the
    -- POWERDNS_SERVICE_HOST env var into every pod in the same namespace
    -- as the powerdns Service — that gives us the ClusterIP without
    -- needing an init container to do DNS resolution. (DNS-name parsing
    -- on dnsdist newServer would require a setResolver call before, and
    -- pinning a hostname in cluster is no different than pinning an IP.)
    setLocal('0.0.0.0:53')
    addLocal('[::]:53', { reusePort=true })

    local backend = os.getenv("POWERDNS_SERVICE_HOST") or "127.0.0.1"
    newServer({ address = backend .. ":5353", name = 'powerdns-auth' })

    -- ─── Rate limiting ───────────────────────────────────────────────
    -- Hits per IP within a 1s window; once exceeded, drop further queries
    -- for blockSeconds. Catalyst defaults: 100 qps per source IP.
    addAction(
      MaxQPSIPRule({{ .Values.dnsdist.ratelimit.qpsPerSource | default 100 }}),
      DropAction()
    )

    -- ─── Health checks + verbose logging off ────────────────────────
    setVerboseHealthChecks(false)

# REST API exposure — operator-only access at <host>/<path>.
#
# Two emission modes (mutually exclusive on a given cluster):
#
#   1. Legacy Ingress  (`api.enabled: true`, `api.gateway.enabled: false`)
#      Renders networking.k8s.io/v1.Ingress + Traefik basicAuth Middleware.
#      Used on contabo (the legacy demo cluster) per ADR-0001 §9.4.
#
#   2. Cilium Gateway HTTPRoute  (`api.gateway.enabled: true`)
#      Renders gateway.networking.k8s.io/v1.HTTPRoute attached to the
#      per-Sovereign cilium-gateway. Used on Sovereigns (issue #387).
#      basicAuth is enforced via the upstream PowerDNS webserver itself
#      (lookup-based; the basicAuth Secret is still consumed by the
#      pod, not by Traefik).
api:
  enabled: true
  host: pdns.openova.io
  path: /api
  pathType: Prefix
  ingressClassName: traefik
  tls:
    enabled: true
    secretName: powerdns-api-tls
    issuer: letsencrypt-prod
  basicAuth:
    # PR #681 followup (2026-05-03): basicAuth disabled by default.
    # Reason: PowerDNS already enforces X-API-Key on every API call
    # (powerdns.openova-system.svc:8081 + the public pdns.openova.io
    # ingress both require X-API-Key). Layering Traefik basicAuth on
    # top blocked the Sovereign-side bp-cert-manager-powerdns-webhook
    # from issuing DNS-01 challenges, because the webhook only knows
    # how to send X-API-Key (not Authorization: Basic). Caught live
    # on otech47 — DNS-01 challenges hung with `Unauthorized` despite
    # the API key being correct.
    #
    # Operators who want a UI-only basicAuth gate can flip this back
    # to true in their cluster overlay; the X-API-Key contract for
    # API consumers is unchanged either way.
    enabled: false
    secretName: powerdns-api-basicauth      # bcrypt(operator:<password>) — only used when enabled=true
  middlewareName: powerdns-api-auth
  # Cilium Gateway API mode (issue #387). Set `enabled: true` ON SOVEREIGNS
  # ONLY. parentRef defaults to cilium-gateway/kube-system, the per-Sovereign
  # Gateway shipped by clusters/_template/bootstrap-kit/01-cilium.yaml.
  # When this is true, the per-Sovereign overlay should ALSO set the
  # legacy Ingress off by setting `api.ingressClassName` to "" and
  # accepting that the Ingress template still renders if `api.enabled`
  # remains true — the Ingress is harmless on a cluster that has no
  # ingress controller configured for that class. (To suppress the Ingress
  # entirely, set `api.enabled: false` and rely solely on the HTTPRoute.)
  gateway:
    enabled: false
    name: powerdns-api
    parentRef:
      name: cilium-gateway
      namespace: kube-system
      # sectionName intentionally empty — multi-zone Sovereigns rename HTTPS
      # listeners to https-<sanitised-zone> (e.g. https-omani-works), so
      # pinning sectionName: https breaks NoMatchingListener. Cilium Gateway
      # matches by hostname filter. See PR #1888 / TBD-A40 / issue #1902.
      sectionName: ""
    # Backend defaults to the existing powerdns subchart Service
    backendService: ""    # default: powerdns
    backendPort: 8081     # matches powerdns.powerdns.webserver.bindPort default

# Anycast public NS endpoint — placeholder until XHetznerFloatingIP
# Crossplane composition is authored. The Service of type=LoadBalancer
# below is a Phase-0 stand-in; per #167 the target state is anycast
# Floating IPs across regions.
anycast:
  enabled: true
  serviceName: powerdns-anycast
  serviceType: LoadBalancer       # placeholder — Hetzner LB allocates a public IPv4
  annotations:
    # Cluster overlays MAY add hcloud annotations here once a Hetzner-
    # managed Sovereign hosts the workload (e.g.
    #   load-balancer.hetzner.cloud/location: fsn1
    #   load-balancer.hetzner.cloud/use-private-ip: "true"
    # — left empty here so Contabo k3s skips the LB allocation and
    # falls back to NodePort/external-IP routing during Phase-0).
    catalyst.openova.io/comment: |
      Placeholder Service. Replaced by XHetznerFloatingIP once the
      Crossplane composition is authored (see
      templates/crossplane-floatingip.yaml).
  ports:
    - { name: dns-udp, port: 53, targetPort: dns-udp, protocol: UDP }
    - { name: dns-tcp, port: 53, targetPort: dns-tcp, protocol: TCP }

# Crossplane XHetznerFloatingIP target-state config. Disabled by default;
# operator flips this on after the composition lands (tracking issue
# linked from templates/crossplane-floatingip.yaml).
crossplane:
  floatingIP:
    enabled: false                # gap: XHetznerFloatingIP composition not yet authored
    name: pdns-public-ns          # name used when the XR is created
    region: fsn1                  # initial region; multi-region added as the composition matures

# ─── Multi-zone bootstrap (issue #827, parent epic #825) ──────────────────
# A Sovereign supports N parent zones, NOT one. The operator brings 1+
# parent domains at signup (`omani.works` for own use, `omani.trade` for
# the SME pool, etc.) and may add more post-handover via the admin
# console (#829). Each zone listed below is created in PowerDNS at
# install time by a Helm post-install/post-upgrade hook Job
# (templates/zone-bootstrap-job.yaml) which POSTs each zone to
# /api/v1/servers/localhost/zones via the REST API. The Job is
# idempotent: an HTTP 409 (zone already exists) is treated as success
# so re-runs after upgrades or chart upgrades do not fail.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) the zones list
# is fully data-driven. Default empty: a fresh-Sovereign install with
# no zones configured renders the bootstrap Job as a no-op (Job
# exits 0 on the first iteration of an empty list — see template).
# Per-Sovereign overlays in clusters/<sovereign>/bootstrap-kit/
# 11-powerdns.yaml populate this from ${PARENT_DOMAINS_YAML} via
# Flux postBuild.substitute.
#
# Each zone entry:
#   - name (required): apex domain, no trailing dot. The Job appends
#     the trailing dot at API call time per the PowerDNS contract.
#   - role (optional): operator-meaningful tag — "primary" for the
#     operator's own console/api hostnames, "sme-pool" for zones
#     offered to SME tenants. Not consumed by PowerDNS itself; carried
#     in the Job's audit ConfigMap so the admin console can show role
#     per zone without an extra source of truth.
#   - kind (optional, default "Native"): PowerDNS zone kind. "Native"
#     is the only mode supported by the gpgsql backend without
#     additional configuration; "Master" / "Slave" require axfr peer
#     setup we don't run.
#   - nameservers (optional): list of NS records. Defaults to
#     [ns1.<name>., ns2.<name>., ns3.<name>.] if empty so a fresh
#     install delegates back to the Sovereign's own PowerDNS.
#   - dnssec (optional, default true): enable DNSSEC at zone create
#     time. The Job calls POST /zones/<zone>/cryptokeys with
#     algorithm=ecdsa256 immediately after the zone is created.
#
# Example (per-Sovereign overlay):
#   zones:
#     - name: omani.works
#       role: primary
#     - name: omani.trade
#       role: sme-pool
zones: []

# ─── Zone bootstrap Job ───────────────────────────────────────────────────
# Helm hook Job that creates every entry in `zones` above against the
# in-cluster PowerDNS REST API. Renders as a Helm post-install,post-upgrade
# hook so the powerdns Deployment is Ready before the Job calls the API.
zoneBootstrap:
  # Toggle the Job render. Default true so a Sovereign with `zones`
  # populated creates them automatically. Operators that load zones
  # via an external pool-domain-manager service can flip this off
  # to suppress the Job entirely.
  enabled: true
  # Image used by the Job — needs curl + sh. Alpine is the smallest
  # well-known option. Pinned by digest in production overlays per
  # INVIOLABLE-PRINCIPLES #4a; tag here is the upstream-mutable default.
  image:
    repository: docker.io/curlimages/curl
    tag: "8.10.1"
    pullPolicy: IfNotPresent
  # Resource budget — small. Each zone is a single HTTPS POST against
  # the in-cluster PowerDNS Service.
  resources:
    requests:
      cpu: 10m
      memory: 32Mi
    limits:
      cpu: 100m
      memory: 64Mi
  # Hook delete policy — keep the most recent Job around for operator
  # forensics (`kubectl logs job/powerdns-zone-bootstrap`) but delete
  # the previous one before each new install/upgrade so we don't pile
  # up immutable Jobs across upgrades.
  hookDeletePolicy: "before-hook-creation"
  # backoffLimit — retries on transient API errors (powerdns Pod still
  # warming up, etc.) before the Job marks itself Failed.
  backoffLimit: 6
  # activeDeadlineSeconds — upper bound on Job runtime.
  #
  # Issue #144: raised from 300s → 840s (14m, below the HR install.timeout
  # of 15m in clusters/_template/bootstrap-kit/11-powerdns.yaml). Cold
  # Sovereign provisioning observed on prov #22 (e2fc1004362ce765): bp-cnpg
  # had not yet synthesised the `pdns-pg-app` Secret when this post-install
  # hook Job started, so the powerdns Deployment was not Ready and curl
  # against http://powerdns:8081 kept failing. With backoffLimit=6 +
  # restartPolicy=OnFailure the script will keep retrying within the Job,
  # but the 5-minute Job deadline triggered DeadlineExceeded → Helm hook
  # failure → HR `post-install: timed out waiting for the condition` →
  # 4 retries (one initial + 3 remediation) → terminal HR FAILED.
  #
  # 840s gives the upstream CNPG cluster + powerdns subchart Deployment
  # ample time to converge on a cold k3s control plane while staying
  # under the HR 15m wrapper cap (a Job that exceeded the HR cap would
  # leave Flux waiting forever; a Job that fits inside the HR cap lets
  # Flux's own remediation cycle reclaim the failure path).
  activeDeadlineSeconds: 840
  # apiReadyTimeoutSeconds — inner poll budget the container spends
  # waiting for http://powerdns:8081/api/v1/servers to return HTTP 200
  # before giving up.
  #
  # Recurrence of #144 on prov #37+#38 (2026-05-12) traced to
  # BackoffLimitExceeded, NOT DeadlineExceeded: each container invocation
  # curl'd the in-cluster PowerDNS Service whose endpoints stayed empty
  # (powerdns Pods Pending behind a worker-capacity wedge + slow CNPG
  # initdb). curl exited 7 (connection refused), the container restarted,
  # 6 backoffs (~10min wall-time) tripped the Job's backoffLimit well
  # before activeDeadlineSeconds=840s.
  #
  # New posture: one container run, one long inner loop. This timeout
  # bounds the inner poll; restartPolicy is `Never` (the container owns
  # the wait budget). 600s default sits below activeDeadlineSeconds=840s
  # so the zone-creation phase retains >=240s of headroom AFTER the API
  # comes Ready. Operators on slower clusters can raise this without
  # forking the chart per docs/INVIOLABLE-PRINCIPLES.md #4.
  apiReadyTimeoutSeconds: 600
