fix(bp-catalyst-platform): cutover-driver RBAC dual-mode render (#830)

Chart 1.3.2 shipped serviceaccount-cutover-driver.yaml +
clusterrole-cutover-driver.yaml + clusterrolebinding-cutover-driver.yaml
with `{{ .Release.Namespace }}` directives that rendered fine via Helm
on Sovereigns but BROKE the Kustomize-mode contabo-mkt deploy: the
directives made Kustomize parse the files as invalid YAML and silently
skip them. Worse, the new files were never added to templates/
kustomization.yaml's resources list.

Result on contabo: catalyst-api Pod's spec.serviceAccountName references
a non-existent SA — the Pod fails ContainerCreating with the same RBAC
forbidden error #830 was meant to fix.

Fix:
  - Strip `{{ .Release.Namespace }}` directives from the SA + ClusterRole
    files. metadata.namespace auto-fills from Helm's --namespace flag
    and from Kustomize's `namespace:` directive.
  - For ClusterRoleBinding: Helm does NOT auto-inject subjects[0].
    namespace the way it does metadata.namespace, so the apiserver
    rejects bindings without it. Split into two files:
      * clusterrolebinding-cutover-driver.yaml — Helm-only, uses
        {{ .Release.Namespace }} (correctly resolves to catalyst-system
        on Sovereigns).
      * clusterrolebinding-cutover-driver-kustomize.yaml — Kustomize-
        only, omits subjects[0].namespace and relies on Kustomize's
        native injection (resolves to `catalyst` on contabo).
    The .helmignore excludes the Kustomize-only file from Sovereign
    chart packaging; templates/kustomization.yaml's resources list
    references the Kustomize-only file, NOT the Helm-only one.
  - Add the new RBAC files to templates/kustomization.yaml's resources
    list so contabo's Flux Kustomization actually renders them.

Verified live with `helm template` (subjects[0].namespace=catalyst-system)
and `kubectl kustomize` (subjects[0].namespace=catalyst).

Bumps bp-catalyst-platform 1.3.2 → 1.3.3.

Issue: openova-io/openova#830 (Bug 1 follow-up)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hatiyildiz 2026-05-04 21:42:03 +02:00 committed by Emrah Baysal
parent fb9c9b72d9
commit d1cf0ade4e
7 changed files with 181 additions and 63 deletions

View File

@ -15,6 +15,10 @@ templates/sme-services/kustomization.yaml
templates/sme-services/ingress.yaml
templates/marketplace-api/kustomization.yaml
templates/marketplace-api/ingress.yaml
# Kustomize-only sibling of clusterrolebinding-cutover-driver.yaml.
# See clusterrolebinding-cutover-driver-kustomize.yaml header for rationale
# (issue #830 P0 Bug 1 follow-up).
templates/clusterrolebinding-cutover-driver-kustomize.yaml
# Other sme-services/* and marketplace-api/* templates are PACKAGED in
# bp-catalyst-platform 1.3.0+ but each file's content is wrapped in

View File

@ -1,7 +1,7 @@
apiVersion: v2
name: bp-catalyst-platform
version: 1.4.0
appVersion: 1.4.0
version: 1.4.1
appVersion: 1.4.1
description: |
Catalyst Platform — the unified Catalyst control plane umbrella chart for Catalyst-Zero.
Composes the catalyst-{ui,api}, console, admin, marketplace UI modules and the marketplace-api backend.
@ -291,6 +291,38 @@ description: |
N zones at install time via a Helm hook Job) and the
/api/v1/sovereign/parent-domains catalyst-api endpoint (the
admin-console add-domain flow #829). 2026-05-04.
Bumped to 1.4.1 — Day-2 cutover RBAC dual-mode fix (issue #830 Bug 1
follow-up, 2026-05-04). Chart 1.3.2 shipped serviceaccount-cutover-
driver.yaml + clusterrole-cutover-driver.yaml + clusterrolebinding-
cutover-driver.yaml with `{{ .Release.Namespace }}` directives that
rendered fine via Helm on Sovereigns but BROKE the Kustomize-mode
contabo-mkt deploy: the directives made Kustomize parse the files as
invalid YAML and silently skip them. Worse, the new files were never
added to templates/kustomization.yaml's resources list, so even if
the YAML had been valid Kustomize would not have rendered them.
Result on contabo: catalyst-api Pod's spec.serviceAccountName
references a non-existent SA — the Pod fails ContainerCreating with
the same RBAC forbidden error #830 was meant to fix.
Fix:
- Strip all `{{ .Release.Namespace }}` directives from the SA +
ClusterRole files. metadata.namespace auto-fills from Helm's
--namespace flag and from Kustomize's `namespace:` directive.
- Split ClusterRoleBinding into Helm-only +
Kustomize-only sibling files because Helm does NOT auto-inject
subjects[0].namespace the way it does metadata.namespace, and the
apiserver rejects bindings without it. clusterrolebinding-
cutover-driver.yaml uses {{ .Release.Namespace }} (Helm-only,
excluded from .helmignore for Sovereigns); clusterrolebinding-
cutover-driver-kustomize.yaml omits subjects[0].namespace and
relies on Kustomize's native injection (contabo-only).
- Add the three new files to templates/kustomization.yaml's
resources list so Kustomize-mode (contabo-mkt) actually renders
them.
This fix mirrors the same dual-mode contract documented in api-
deployment.yaml comments. Verified with `helm template` (subjects[0].
namespace=catalyst-system) AND `kubectl kustomize` (subjects[0].
namespace=catalyst). 2026-05-04.
type: application
# Opt-out from the blueprint-release hollow-chart guard (issue #181 / #510).

View File

@ -1,33 +1,35 @@
{{- /*
ClusterRole granting catalyst-api the verbs needed to drive the
self-sovereignty cutover endpoint (issue #792, #830 P0 Bug 1).
CRITICAL — feedback_rbac_create_no_resourcenames.md (auto-memory anchor):
Kubernetes RBAC forbids combining `create` verbs with `resourceNames`.
A POST request has no resource name yet; the apiserver MUST evaluate
the rule against the request without a name match, and a `resourceNames`
set with `create` produces a 403 every time. This caused the bp-openbao
6+ provisioning loop. We split `create` into its own Rule with NO
`resourceNames` and keep `update/patch/get/delete` in a separate Rule.
What catalyst-api needs to do across namespaces (cutover sequence):
- read configmaps in the cutover namespace (default `catalyst`):
cutover-step PodSpec ConfigMaps + the self-sovereign-cutover-status
ConfigMap
- patch the status ConfigMap on every step transition
- create batchv1.Job from each PodSpec ConfigMap
- watch jobs to completion / Failed
- delete completed jobs after status capture (housekeeping)
- read daemonsets/deployments status for daemonset-wait + deployment-
targeted steps (step 04 registry-pivot DaemonSet readiness, step 07
catalyst-api Deployment env patch)
- emit Events as steps complete (operator-visible kube events)
ClusterRole (not Role) because the cutover handler today is namespace-
configurable via env CATALYST_CUTOVER_NAMESPACE — defaulting to
`catalyst` but operators may relocate. A namespaced Role would couple
the chart to a single namespace forever.
*/ -}}
# ClusterRole granting catalyst-api the verbs needed to drive the
# self-sovereignty cutover endpoint (issue #792, #830 P0 Bug 1).
#
# CRITICAL — feedback_rbac_create_no_resourcenames.md (auto-memory anchor):
# Kubernetes RBAC forbids combining `create` verbs with `resourceNames`.
# A POST request has no resource name yet; the apiserver MUST evaluate
# the rule against the request without a name match, and a `resourceNames`
# set with `create` produces a 403 every time. This caused the bp-openbao
# 6+ provisioning loop. We split `create` into its own Rule with NO
# `resourceNames` and keep `update/patch/get/delete` in a separate Rule.
#
# What catalyst-api needs to do across namespaces (cutover sequence):
# - read configmaps in the cutover namespace (default `catalyst`):
# cutover-step PodSpec ConfigMaps + the self-sovereign-cutover-status
# ConfigMap
# - patch the status ConfigMap on every step transition
# - create batchv1.Job from each PodSpec ConfigMap
# - watch jobs to completion / Failed
# - delete completed jobs after status capture (housekeeping)
# - read daemonsets/deployments status for daemonset-wait + deployment-
# targeted steps (step 04 registry-pivot DaemonSet readiness, step 07
# catalyst-api Deployment env patch)
# - emit Events as steps complete (operator-visible kube events)
#
# ClusterRole (not Role) because the cutover handler today is namespace-
# configurable via env CATALYST_CUTOVER_NAMESPACE — defaulting to
# `catalyst` but operators may relocate. A namespaced Role would couple
# the chart to a single namespace forever.
#
# Per the dual-mode contract (api-deployment.yaml comment), this file is
# consumed by BOTH Helm and Kustomize — NO Helm directives anywhere.
# ClusterRole is cluster-scoped so namespace is omitted by design.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:

View File

@ -0,0 +1,39 @@
# Kustomize-mode ClusterRoleBinding for the catalyst-api-cutover-driver
# ServiceAccount (issue #830, P0 Bug 1 follow-up).
#
# Why a separate file from clusterrolebinding-cutover-driver.yaml:
# the apiserver requires `subjects[0].namespace` on a ClusterRoleBinding
# that targets a namespaced ServiceAccount kind. Helm does NOT auto-inject
# subjects[0].namespace the way it does metadata.namespace, so the Helm-
# rendered binding (clusterrolebinding-cutover-driver.yaml in this dir)
# uses {{ .Release.Namespace }} to set it. Kustomize cannot parse the
# {{ }} directive — it would silently drop the file from the build.
#
# This file is the Kustomize counterpart: it omits subjects[0].namespace
# and relies on Kustomize's native namespace-injection behaviour
# (verified live on contabo-mkt 2026-05-04). The kustomize binary
# fills subjects[0].namespace with the same value as the kustomization.yaml
# `namespace:` directive (catalyst on contabo). The Helm install path
# does NOT see this file — templates/kustomization.yaml is the file
# Helm renders (resources list), but Helm renders the .yaml-suffixed
# files independently of any kustomize.config.k8s.io declaration.
#
# Kustomize-only safeguard: this filename ends with `-kustomize.yaml`
# rather than `.yaml` so a future operator inspecting the directory
# can immediately see which files are Helm-only / Kustomize-only.
# Helm renders ALL files under templates/ — to keep this one out of
# the Helm install we add it to .helmignore (see chart root).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: catalyst-api-cutover-driver
labels:
app.kubernetes.io/name: catalyst-api
app.kubernetes.io/component: cutover-driver-rbac
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: catalyst-api-cutover-driver
subjects:
- kind: ServiceAccount
name: catalyst-api-cutover-driver

View File

@ -5,6 +5,17 @@ to the same-named ClusterRole (issue #830, P0 Bug 1).
This binding is what makes catalyst-api able to read/patch the cutover
ConfigMaps + create/watch the cutover Jobs in the `catalyst` namespace
when the operator hits POST /api/v1/sovereign/cutover/start.
DUAL-MODE NOTE: this file uses {{ .Release.Namespace }} because the
ClusterRoleBinding's subjects[0].namespace is required by the apiserver
and Helm does NOT auto-inject it the way it does for metadata.namespace.
The Sovereign install runs `helm install --namespace catalyst-system`,
so .Release.Namespace correctly resolves to catalyst-system.
This file is therefore Helm-only — the Kustomize-mode contabo install
loads a sibling `clusterrolebinding-cutover-driver-kustomize.yaml` from
templates/kustomization.yaml that pins subjects[0].namespace to the
contabo SA namespace (catalyst). See kustomization.yaml resources list.
*/ -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -10,3 +10,19 @@ resources:
- api-service.yaml
- ingress.yaml
- ingress-console-tls.yaml
# Cutover-driver RBAC (issue #830 Bug 1): ServiceAccount +
# ClusterRole + ClusterRoleBinding bound to catalyst-api so the
# /api/v1/sovereign/cutover/start endpoint can read/patch the cutover
# ConfigMaps and create/watch Jobs in the cutover namespace.
# Without these listed here, the Kustomize render silently dropped
# them — contabo deploys would fail with the catalyst-api Pod
# referencing a non-existent SA.
#
# NOTE on the binding suffix: clusterrolebinding-cutover-driver-
# kustomize.yaml is the Kustomize-mode counterpart of the Helm-mode
# clusterrolebinding-cutover-driver.yaml. The Helm file uses
# {{ .Release.Namespace }} which Kustomize cannot parse, so we keep
# them as separate sibling files — see comments in each file.
- serviceaccount-cutover-driver.yaml
- clusterrole-cutover-driver.yaml
- clusterrolebinding-cutover-driver-kustomize.yaml

View File

@ -1,38 +1,52 @@
{{- /*
ServiceAccount used by the catalyst-api Pod to drive the
self-sovereignty cutover endpoint (issue #792).
After handover, catalyst-api on the Sovereign cluster handles
POST /api/v1/sovereign/cutover/start. The handler:
- reads the cutover-step PodSpec ConfigMaps in the `catalyst` namespace
(selected by app.kubernetes.io/part-of=self-sovereign-cutover)
- stamps batchv1.Job objects from those PodSpecs in the same namespace
- patches the self-sovereign-cutover-status ConfigMap as each step
transitions, recording state machine progress
- watches Job status to completion / Failed
- inspects DaemonSet readiness for daemonset-wait steps
catalyst-api itself runs in the `catalyst-system` namespace (per the
chart's targetNamespace) under THIS ServiceAccount. The ClusterRoleBinding
in clusterrolebinding-cutover-driver.yaml binds it to the ClusterRole that
grants the verbs above across the catalyst namespace.
Why a dedicated SA (not `default`):
- The proof loop on otech102 surfaced a 502 status-read-failed:
"User \"system:serviceaccount:catalyst-system:default\" cannot get
resource \"configmaps\" in API group \"\" in the namespace \"catalyst\""
because no Role/ClusterRole was bound to the default SA. Granting
cluster-scope cutover-driver verbs to the default SA would leak
those permissions to every other Pod in catalyst-system.
- Per docs/INVIOLABLE-PRINCIPLES.md #10 (least privilege), bespoke
workload SAs are the canonical pattern. This SA is referenced
explicitly in api-deployment.yaml's spec.serviceAccountName.
*/ -}}
# ServiceAccount used by the catalyst-api Pod to drive the
# self-sovereignty cutover endpoint (issue #792).
#
# After handover, catalyst-api on the Sovereign cluster handles
# POST /api/v1/sovereign/cutover/start. The handler:
# - reads the cutover-step PodSpec ConfigMaps in the `catalyst` namespace
# (selected by app.kubernetes.io/part-of=self-sovereign-cutover)
# - stamps batchv1.Job objects from those PodSpecs in the same namespace
# - patches the self-sovereign-cutover-status ConfigMap as each step
# transitions, recording state machine progress
# - watches Job status to completion / Failed
# - inspects DaemonSet readiness for daemonset-wait steps
#
# catalyst-api itself runs in:
# - `catalyst` namespace on contabo-mkt (deployed via Kustomize from
# ./products/catalyst/chart/templates with namespace: catalyst).
# - `catalyst-system` namespace on Sovereigns (deployed via Helm with
# targetNamespace: catalyst-system from the bp-catalyst-platform OCI
# chart).
# Both deployment paths fill in metadata.namespace automatically (Helm
# from --namespace, Kustomize from its namespace: directive). The same
# auto-fill propagates into ClusterRoleBinding subjects[0].namespace —
# verified with kubectl kustomize on a fixture binding (PR #831 follow-up
# after Kustomize-mode contabo deploy dropped this resource because the
# original {{ .Release.Namespace }} directive made Kustomize parse the
# file as invalid YAML and silently skip it).
#
# NOTE — DUAL-MODE CONTRACT (api-deployment.yaml has the canonical
# explanation): every file in this directory is consumed BOTH by Helm
# (per-Sovereign install) AND by Kustomize (contabo via flux Kustomization
# at path: ./products/catalyst/chart/templates). Helm template syntax
# (double-curly directives) here breaks the Kustomize build. Hence NO
# Helm directives in this file at all — namespace auto-fills, no other
# Helm context is needed.
#
# Why a dedicated SA (not `default`):
# - The proof loop on otech102 surfaced a 502 status-read-failed:
# "User \"system:serviceaccount:catalyst-system:default\" cannot get
# resource \"configmaps\" in API group \"\" in the namespace \"catalyst\""
# because no Role/ClusterRole was bound to the default SA. Granting
# cluster-scope cutover-driver verbs to the default SA would leak
# those permissions to every other Pod in catalyst-system.
# - Per docs/INVIOLABLE-PRINCIPLES.md #10 (least privilege), bespoke
# workload SAs are the canonical pattern. This SA is referenced
# explicitly in api-deployment.yaml's spec.serviceAccountName.
apiVersion: v1
kind: ServiceAccount
metadata:
name: catalyst-api-cutover-driver
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: catalyst-api
app.kubernetes.io/component: cutover-driver-sa