{{- /*
CNPG-managed Postgres cluster for PowerDNS Authoritative.

Separate from `pdm-pg` (pool-domain-manager) so the two control-plane
services scale and fail independently. PDM holds O(active_sovereigns)
rows; PDNS holds O(zones × records) where each Sovereign zone is a
small handful of records — workload size is comparable, but lifecycle
isolation matters for blast-radius.

Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) every operationally-
meaningful value flows from values.yaml. The PowerDNS gpgsql schema is
applied via `bootstrap.initdb.postInitApplicationSQL` — pinned to the
upstream auth-5.0.3 schema. CNPG synthesises a `<cluster>-app` Secret
that the upstream chart's deployment template reads via secretRef
indirection (see values.yaml powerdns.db.{host,port,name,username,
password}.secretRef).

`enableSuperuserAccess: true` so that operators can run `pdnsutil`
schema-altering commands inside the database during DNSSEC key
rotations and zone imports.

Capabilities gate (issue #190): bp-cnpg ships the `postgresql.cnpg.io/v1`
CRD that backs Cluster — it is NOT in the bootstrap-kit. On a cold
install of a fresh Sovereign before bp-cnpg is reconciling, the apiserver
rejects this Cluster with `no matches for kind Cluster in version
postgresql.cnpg.io/v1`. The Capabilities check skips this template until
the CRD is registered, at which point the gate becomes a no-op. The
Sovereign's bootstrap order MUST land bp-cnpg before bp-powerdns; the
gate exists so a misordered cluster overlay surfaces as `no Cluster
created yet` rather than a hard install failure of the entire bp-powerdns
release.
*/}}
{{- if .Capabilities.APIVersions.Has "postgresql.cnpg.io/v1" }}
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: {{ .Values.postgres.cluster.name | default "pdns-pg" }}
  namespace: {{ .Values.postgres.cluster.namespace | default .Release.Namespace }}
  labels:
    {{- include "bp-powerdns.labels" . | nindent 4 }}
spec:
  instances: {{ .Values.postgres.cluster.instances | default 1 }}
  imageName: ghcr.io/cloudnative-pg/postgresql:{{ .Values.postgres.cluster.pgVersion | default "16" }}

  # Annotations CNPG propagates onto every Secret it generates
  # (pdns-pg-app, pdns-pg-superuser, etc) so Reflector mirrors
  # `pdns-pg-app` into the chart-emitted placeholder Secret
  # (`pdns-pg-app` lives in the same namespace; same-namespace
  # reflection still requires the source to opt in).
  # auto-enabled prevents the otech28-diagnosed retry race where
  # Reflector tries to update the destination Secret before CNPG
  # finishes provisioning the source — and never logs again.
  inheritedMetadata:
    annotations:
      reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
      reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: {{ .Values.postgres.cluster.namespace | default .Release.Namespace | quote }}
      reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
      reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: {{ .Values.postgres.cluster.namespace | default .Release.Namespace | quote }}

  bootstrap:
    initdb:
      database: {{ .Values.postgres.cluster.database | default "pdns" | quote }}
      owner: {{ .Values.postgres.cluster.owner | default "pdns" | quote }}
      # PowerDNS gpgsql schema (auth-5.0.3). Applied as the database
      # owner so subsequent zone/record writes don't trip permission
      # checks. The schema URL is pinned in values.yaml; rebuilds of
      # the cluster (rare, only on disaster recovery) re-fetch the
      # exact same DDL deterministically.
      postInitApplicationSQL:
        - |
          CREATE TABLE IF NOT EXISTS domains (
            id                    SERIAL PRIMARY KEY,
            name                  VARCHAR(255) NOT NULL,
            master                VARCHAR(128) DEFAULT NULL,
            last_check            INT DEFAULT NULL,
            type                  TEXT NOT NULL,
            notified_serial       BIGINT DEFAULT NULL,
            account               VARCHAR(40) DEFAULT NULL,
            options               TEXT DEFAULT NULL,
            catalog               TEXT DEFAULT NULL,
            CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
          );
          CREATE UNIQUE INDEX IF NOT EXISTS name_index ON domains(name);
          CREATE INDEX IF NOT EXISTS catalog_idx ON domains(catalog);

          CREATE TABLE IF NOT EXISTS records (
            id                    BIGSERIAL PRIMARY KEY,
            domain_id             INT DEFAULT NULL,
            name                  VARCHAR(255) DEFAULT NULL,
            type                  VARCHAR(10) DEFAULT NULL,
            content               VARCHAR(65535) DEFAULT NULL,
            ttl                   INT DEFAULT NULL,
            prio                  INT DEFAULT NULL,
            disabled              BOOL DEFAULT 'f',
            ordername             VARCHAR(255),
            auth                  BOOL DEFAULT 't',
            CONSTRAINT domain_exists FOREIGN KEY (domain_id) REFERENCES domains (id) ON DELETE CASCADE,
            CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
          );
          CREATE INDEX IF NOT EXISTS rec_name_index ON records(name);
          CREATE INDEX IF NOT EXISTS nametype_index ON records(name, type);
          CREATE INDEX IF NOT EXISTS domain_id ON records(domain_id);
          CREATE INDEX IF NOT EXISTS recordorder ON records (domain_id, ordername text_pattern_ops);

          CREATE TABLE IF NOT EXISTS supermasters (
            ip                    INET NOT NULL,
            nameserver            VARCHAR(255) NOT NULL,
            account               VARCHAR(40) NOT NULL,
            PRIMARY KEY (ip, nameserver)
          );

          CREATE TABLE IF NOT EXISTS comments (
            id                    SERIAL PRIMARY KEY,
            domain_id             INT NOT NULL,
            name                  VARCHAR(255) NOT NULL,
            type                  VARCHAR(10) NOT NULL,
            modified_at           INT NOT NULL,
            account               VARCHAR(40) DEFAULT NULL,
            comment               VARCHAR(65535) NOT NULL,
            CONSTRAINT domain_exists FOREIGN KEY (domain_id) REFERENCES domains (id) ON DELETE CASCADE,
            CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
          );
          CREATE INDEX IF NOT EXISTS comments_domain_id_idx ON comments (domain_id);
          CREATE INDEX IF NOT EXISTS comments_name_type_idx ON comments (name, type);
          CREATE INDEX IF NOT EXISTS comments_order_idx ON comments (domain_id, modified_at);

          CREATE TABLE IF NOT EXISTS domainmetadata (
            id                    SERIAL PRIMARY KEY,
            domain_id             INT REFERENCES domains(id) ON DELETE CASCADE,
            kind                  VARCHAR(32),
            content               TEXT
          );
          CREATE INDEX IF NOT EXISTS domainidmetaindex ON domainmetadata(domain_id);

          CREATE TABLE IF NOT EXISTS cryptokeys (
            id                    SERIAL PRIMARY KEY,
            domain_id             INT REFERENCES domains(id) ON DELETE CASCADE,
            flags                 INT NOT NULL,
            active                BOOL,
            published             BOOL DEFAULT TRUE,
            content               TEXT
          );
          CREATE INDEX IF NOT EXISTS domainidindex ON cryptokeys(domain_id);

          CREATE TABLE IF NOT EXISTS tsigkeys (
            id                    SERIAL PRIMARY KEY,
            name                  VARCHAR(255),
            algorithm             VARCHAR(50),
            secret                VARCHAR(255),
            CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
          );
          CREATE UNIQUE INDEX IF NOT EXISTS namealgoindex ON tsigkeys(name, algorithm);

          -- ─── Grant table ownership to the application user ─────────
          -- postInitApplicationSQL runs as the postgres superuser (not the
          -- database owner) so newly-created tables are owned by postgres
          -- by default. PowerDNS connects as `pdns` and needs full DML
          -- access — reassign every PowerDNS gpgsql table to the owner.
          -- Plain ALTER TABLE statements (no DO block) so Helm templating
          -- doesn't have to wrestle with PL/pgSQL `$$` quoting.
          ALTER TABLE domains        OWNER TO {{ .Values.postgres.cluster.owner | default "pdns" }};
          ALTER TABLE records        OWNER TO {{ .Values.postgres.cluster.owner | default "pdns" }};
          ALTER TABLE supermasters   OWNER TO {{ .Values.postgres.cluster.owner | default "pdns" }};
          ALTER TABLE comments       OWNER TO {{ .Values.postgres.cluster.owner | default "pdns" }};
          ALTER TABLE domainmetadata OWNER TO {{ .Values.postgres.cluster.owner | default "pdns" }};
          ALTER TABLE cryptokeys     OWNER TO {{ .Values.postgres.cluster.owner | default "pdns" }};
          ALTER TABLE tsigkeys       OWNER TO {{ .Values.postgres.cluster.owner | default "pdns" }};
          GRANT ALL PRIVILEGES ON SCHEMA public TO {{ .Values.postgres.cluster.owner | default "pdns" }};

  postgresql:
    parameters:
      max_connections: "100"
      shared_buffers: "64MB"
      effective_cache_size: "192MB"
      work_mem: "4MB"
      maintenance_work_mem: "64MB"
      log_statement: "ddl"
      log_min_duration_statement: "1000"

  resources:
    {{- toYaml .Values.postgres.cluster.resources | nindent 4 }}

  storage:
    size: {{ .Values.postgres.cluster.storageSize | default "5Gi" }}
    storageClass: {{ .Values.postgres.cluster.storageClass | default "local-path" | quote }}

  enableSuperuserAccess: true

  monitoring:
    enablePodMonitor: false
{{- end }}
