From c5a7ff06765771b7ec740530341ef33ac832c285 Mon Sep 17 00:00:00 2001 From: hatiyildiz Date: Wed, 29 Apr 2026 20:03:03 +0200 Subject: [PATCH] fix(bp-powerdns): hide CRD-emitting resources behind Capabilities gates (refs #190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 "/"`. 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. --- platform/powerdns/chart/Chart.yaml | 2 +- .../powerdns/chart/templates/api-ingress.yaml | 18 ++++++++++++++++++ .../powerdns/chart/templates/cnpg-cluster.yaml | 13 +++++++++++++ .../chart/templates/crossplane-floatingip.yaml | 13 ++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/platform/powerdns/chart/Chart.yaml b/platform/powerdns/chart/Chart.yaml index 34d09d62..aa8b525b 100644 --- a/platform/powerdns/chart/Chart.yaml +++ b/platform/powerdns/chart/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: bp-powerdns -version: 1.1.1 +version: 1.1.2 description: | Catalyst-curated Blueprint wrapper for PowerDNS Authoritative. Carries Catalyst-specific values.yaml + templates (CNPG cluster, dnsdist diff --git a/platform/powerdns/chart/templates/api-ingress.yaml b/platform/powerdns/chart/templates/api-ingress.yaml index 05c25f9d..b1de9ec3 100644 --- a/platform/powerdns/chart/templates/api-ingress.yaml +++ b/platform/powerdns/chart/templates/api-ingress.yaml @@ -12,6 +12,18 @@ or commits) the basicAuth Secret is created out-of-band by the private-repo cluster manifest at clusters/contabo-mkt/apps/powerdns/. */}} {{- if .Values.api.enabled }} +{{- /* +Capabilities gate (issue #190): the `traefik.io/v1alpha1` Middleware CRD +ships with the Traefik ingress controller. k3s ships Traefik by default +on most Sovereigns, but a Sovereign overlay MAY disable Traefik (e.g. in +favour of cilium-only ingress) — in that case the CRD is absent and the +apiserver rejects this Middleware with `no matches for kind Middleware +in version traefik.io/v1alpha1`. The Capabilities check skips the +Middleware while still rendering the Ingress below, so the auth posture +must then be supplied by the cluster overlay's chosen ingress controller +(per docs/INVIOLABLE-PRINCIPLES.md #4 — never hardcode ingress class). +*/}} +{{- if .Capabilities.APIVersions.Has "traefik.io/v1alpha1" }} apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: @@ -22,6 +34,7 @@ metadata: spec: basicAuth: secret: {{ .Values.api.basicAuth.secretName | default "powerdns-api-basicauth" | quote }} +{{- end }} --- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -32,7 +45,12 @@ metadata: {{- include "bp-powerdns.labels" . | nindent 4 }} annotations: cert-manager.io/cluster-issuer: {{ .Values.api.tls.issuer | default "letsencrypt-prod" | quote }} + {{- /* Traefik router middleware annotation only renders when the Traefik + Middleware CRD is present (gated above). When Traefik is absent the + auth posture moves to the Sovereign's chosen ingress controller. */}} + {{- if .Capabilities.APIVersions.Has "traefik.io/v1alpha1" }} traefik.ingress.kubernetes.io/router.middlewares: {{ printf "%s-%s@kubernetescrd" .Release.Namespace (.Values.api.middlewareName | default "powerdns-api-auth") | quote }} + {{- end }} spec: ingressClassName: {{ .Values.api.ingressClassName | default "traefik" | quote }} {{- if .Values.api.tls.enabled }} diff --git a/platform/powerdns/chart/templates/cnpg-cluster.yaml b/platform/powerdns/chart/templates/cnpg-cluster.yaml index 744ba67f..376f38cd 100644 --- a/platform/powerdns/chart/templates/cnpg-cluster.yaml +++ b/platform/powerdns/chart/templates/cnpg-cluster.yaml @@ -18,7 +18,19 @@ 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: @@ -161,3 +173,4 @@ spec: monitoring: enablePodMonitor: false +{{- end }} diff --git a/platform/powerdns/chart/templates/crossplane-floatingip.yaml b/platform/powerdns/chart/templates/crossplane-floatingip.yaml index c07a897c..cdd837ae 100644 --- a/platform/powerdns/chart/templates/crossplane-floatingip.yaml +++ b/platform/powerdns/chart/templates/crossplane-floatingip.yaml @@ -37,7 +37,18 @@ This file is not deleted in the cutover — it stays as the canonical target-state shape so a fresh chart install on a new Sovereign produces a Floating IP automatically once the composition exists. */}} -{{- if .Values.crossplane.floatingIP.enabled }} +{{- /* +Capabilities gate (issue #190): the `compose.openova.io/v1alpha1` +HetznerFloatingIP CRD is registered by the Catalyst Crossplane +composition family (platform/crossplane/compositions/xrd-floating-ip.yaml, +not yet authored — see GAP DISCLOSURE above). On a Sovereign without +that composition reconciled, the apiserver rejects the XR with `no +matches for kind HetznerFloatingIP in version compose.openova.io/v1alpha1`. +The Capabilities check skips this template until the CRD is registered; +combined with the values toggle (`crossplane.floatingIP.enabled`, default +false) the placeholder ships safely on every Sovereign. +*/}} +{{- if and .Values.crossplane.floatingIP.enabled (.Capabilities.APIVersions.Has "compose.openova.io/v1alpha1") }} apiVersion: compose.openova.io/v1alpha1 kind: HetznerFloatingIP metadata: