Five self-contained work briefs in a new docs/plans/junie/ subdirectory, each tagged against a numbered item in docs/TODO.md so Junie can take them independently in any order. 02-k3s-prole-rename.md -> queue #2 (drift R5) 06-patch-garage-script-fixes.md -> queue #6 (drift R9) 07-init-cnpg-gke-sa-wiring.md -> queue #7 (drift R8) 13-podmonitor-manual-management.md-> queue #13 15-remove-dead-dashboard-consumer.md-> queue #15 Each brief follows the same shape: Why -> What changes (concrete file paths + line numbers + before/after) -> Verification -> Out of scope -> Commit shape -> Definition of done. The intent is that Junie reads cold (no shared chat history) and lands the change without escalating questions. Also adds: - docs/plans/junie/README.md describing the convention. - Row in docs/plans/README.md so newcomers find the subdirectory. - Brief reference + "Assigned to Junie" tag on each of the five queue items in docs/TODO.md. Existing in-progress assignment to Junie (Phase 2 pg_oauth) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.8 KiB
Junie brief — Queue #7: wire cnpg-backup-sa into the CNPG cluster spec via etc/init_cnpg_gke.sh
Self-contained brief. Drift R8: live cluster state has the SA wired through, the init script doesn't. Make the script land a fresh deploy in the same end state. Live cluster is healthy — do not run anything against it.
1. Why
etc/init_cnpg_gke.sh runs from a clean state (e.g. for a future second
DB cluster, or DR rebuild) and lays down everything: GCS buckets, GCP
service account, Workload Identity binding, K8s service account, namespace,
CNPG operator, secrets, the CNPG Cluster resource. The 2026-04-29
stabilization session added two pieces that the script doesn't yet
reproduce:
-
cluster.spec.serviceAccountName: cnpg-backup-saon the live cluster. This field was added in CNPG v1.29.0. The init script currently pins CNPG v1.24.0 (line 402:cnpg-1.24.0.yaml), which doesn't support the field — the cluster would either ignore it or fail validation. Bump the operator version AND add the field to the rendered manifest. -
RoleBindings extended with
cnpg-backup-saas a subject. Liveknoe-dbandknoe-db-barman-cloudRoleBindings (auto-created by CNPG at cluster creation) each have two subjects:knoe-db(the default SA CNPG creates) andcnpg-backup-sa. With v1.29+ andspec.serviceAccountNameset from the start, CNPG should auto-create RoleBindings naming the explicit SA — but verify, because the live state was patched after the fact.
The result of this brief: a fresh init_cnpg_gke.sh run produces a
cluster whose RBAC and SA wiring matches kubectl get cluster knoe-db -o yaml
kubectl get rolebinding -n knoe-db-0from the live system, with no post-install patches required.
2. Live state to match
DB=gke_plenary-truck-485623-p7_us-west3_knoe-dev-cnpg-0
kubectl --context=$DB -n knoe-db-0 get cluster knoe-db -o jsonpath='{.spec.serviceAccountName}'
# -> cnpg-backup-sa
kubectl --context=$DB -n knoe-db-0 get rolebinding knoe-db -o jsonpath='{.subjects}' | jq
# -> [{kind:ServiceAccount, name:knoe-db, namespace:knoe-db-0},
# {kind:ServiceAccount, name:cnpg-backup-sa, namespace:knoe-db-0}]
kubectl --context=$DB -n knoe-db-0 get rolebinding knoe-db-barman-cloud -o jsonpath='{.subjects}' | jq
# -> same two subjects
3. What changes
3.1 — etc/init_cnpg_gke.sh
Around line 402 (CNPG operator install):
# Current (stale)
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-1.24.0.yaml
Bump to v1.29.0 (or a newer stable; verify against
CNPG releases
that the version supports Cluster.spec.serviceAccountName). Add a code
comment explaining the version constraint:
# v1.29.0+ is required: spec.serviceAccountName lets the cluster pods run
# as cnpg-backup-sa (annotated with iam.gke.io/gcp-service-account for WI),
# which is how the GCS-backed barman ObjectStore authenticates without a
# static key. Older operators (we used 1.24.0 historically) would silently
# drop the field.
CNPG_OPERATOR_VERSION="1.29.0"
kubectl apply --server-side -f \
"https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-${CNPG_OPERATOR_VERSION}.yaml"
(Promote the version to a top-of-file variable so future bumps are one line.)
3.2 — deploy/gcp/gke/knoe-db.yaml (CNPG Cluster manifest)
Add serviceAccountName: cnpg-backup-sa under spec:. Use the same
indentation as the surrounding fields. Place near the top of spec (right
after description / imageName / instances, before bootstrap —
follow the live cluster's field order if you want to be tidy:
kubectl get cluster knoe-db -o yaml).
Add an inline comment:
# Run cluster pods as cnpg-backup-sa (annotated for Workload Identity to
# the cnpg-backup@... GCP SA). This is how barman-cloud authenticates
# to gs://knoe-0-backups/ without a static key. Requires CNPG v1.29+.
# The SA is provisioned by etc/init_cnpg_gke.sh § "Apply ServiceAccount
# + annotate with WI".
serviceAccountName: cnpg-backup-sa
3.3 — RoleBinding subjects (decide path)
Two paths, depending on what CNPG v1.29 actually does at cluster creation time. Test both before deciding.
Path A — operator does the right thing automatically. With
spec.serviceAccountName: cnpg-backup-sa set from the start, CNPG v1.29+
auto-creates knoe-db and knoe-db-barman-cloud RoleBindings with
cnpg-backup-sa as the (sole) subject. If true: nothing more to do in the
script — the live two-subject state is just a historical artifact of
having patched after-the-fact, and a fresh install would have a single
subject.
Path B — operator still creates with a default SA name. If CNPG
auto-creates the RoleBindings with subject knoe-db (the default SA name
matching the cluster name) regardless of the explicit
serviceAccountName, then the script needs to patch them. Add a section
after cluster apply:
# ── 11. Ensure RoleBindings reference cnpg-backup-sa ─────────────────────────
log "Ensuring CNPG-managed RoleBindings include cnpg-backup-sa ..."
for rb in "${CNPG_CLUSTER_NAME}" "${CNPG_CLUSTER_NAME}-barman-cloud"; do
# Wait briefly for CNPG to create them on first install
for _ in 1 2 3 4 5; do
kubectl -n "$CNPG_NAMESPACE" get rolebinding "$rb" >/dev/null 2>&1 && break
sleep 2
done
if kubectl -n "$CNPG_NAMESPACE" get rolebinding "$rb" >/dev/null 2>&1; then
if ! kubectl -n "$CNPG_NAMESPACE" get rolebinding "$rb" -o jsonpath='{.subjects[*].name}' \
| grep -qw cnpg-backup-sa; then
kubectl -n "$CNPG_NAMESPACE" patch rolebinding "$rb" --type='json' -p="$(cat <<'EOF'
[{"op":"add","path":"/subjects/-","value":{"kind":"ServiceAccount","name":"cnpg-backup-sa","namespace":"NAMESPACE_PLACEHOLDER"}}]
EOF
)" --patch-file=/dev/stdin 2>/dev/null || true
fi
fi
done
(Sketch — finalize the syntax. The NAMESPACE_PLACEHOLDER would be
substituted; --patch-file doesn't accept - in all kubectl versions, so
you may need a temp file.)
How to decide A vs B: spin up a throwaway k3d cluster (or a CNPG-only
test namespace on knoe-dev-cnpg-0 if you can without disturbing
knoe-db-0), apply a minimal Cluster resource with
serviceAccountName: cnpg-backup-sa, observe the RoleBinding subjects
that CNPG auto-creates. If the SA you specified is the only subject:
Path A. Otherwise: Path B.
If you can't easily test, lean toward Path B (the patch is idempotent and costs nothing if the operator already did the right thing).
4. Verification
Without running against the live cluster:
-
Manifest renders cleanly:
ARTIFACT_REGISTRY=us-west3-docker.pkg.dev/plenary-truck-485623-p7/knoe-system \ KNOE_DB_IMAGE_TAG=18-055 \ envsubst '${ARTIFACT_REGISTRY} ${KNOE_DB_IMAGE_TAG}' < deploy/gcp/gke/knoe-db.yaml \ | yq '.spec.serviceAccountName' # Expected: cnpg-backup-sa -
Operator version is 1.29+:
grep -E 'cnpg-[0-9]' etc/init_cnpg_gke.sh # Expected: cnpg-1.29.0.yaml (or newer) -
bash -n etc/init_cnpg_gke.shclean. -
(Optional) Dry-run apply against k3d if you have one running. The cluster won't reach Healthy in k3d (no Workload Identity), but
kubectl get cluster -o yamlshould showspec.serviceAccountName: cnpg-backup-saset.
5. Out of scope
- Don't touch the live cluster. This is purely script + manifest work.
- Don't change the
cnpg-backup-saannotation pattern indeploy/gcp/gke/knoe-db-backup-gcs.yaml— that's how the SA gets the WI annotation, separate concern. - The
patch_garage_cross_cluster.shdefects are queue item #6 with its own brief. If your changes here surface a discrepancy with that brief, flag it but don't try to fix both in one commit. - The PodMonitor manual-management migration (queue #13) doesn't interact with this brief.
6. Commit shape
Likely two commits:
chore(deploy): add serviceAccountName: cnpg-backup-sa to knoe-db cluster specfix(init): bump CNPG operator to v1.29 + ensure RoleBindings include cnpg-backup-sa
Or one combined commit titled
fix(init): wire cnpg-backup-sa as cluster SA + bump CNPG to v1.29 (drift R8).
7. Definition of done
etc/init_cnpg_gke.shreferences CNPG operator v1.29.0+ with a comment explaining why (the SA field requirement).deploy/gcp/gke/knoe-db.yamlcarriesspec.serviceAccountName: cnpg-backup-sawith explanatory comment.- RoleBinding handling: either documented as auto-correct (Path A, with a one-liner test result included in the commit message), OR scripted patch (Path B).
bash -npasses; manifest renders.docs/TODO.mdqueue item #7 + drift R8 row archived to Done.