From bf814854fd6b6ba3ef3a6357f685e297bed5a0d5 Mon Sep 17 00:00:00 2001 From: Mukul Sharma Date: Thu, 10 Sep 2026 07:38:20 +0530 Subject: [PATCH] Grant deployer Job rights, for one-off commands against an app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toolshed can now run a one-off command against an app — a migration, a seed, a backfill — in a throwaway Job built from the app's own image with its own configuration (internal/deploy.Client.RunCommand). It exists because a user with a provisioned database otherwise has no way to reach it: the operator can exec into the Postgres pod, a user cannot, and the premise is that nobody needs kubectl. Without this the deployer gets "forbidden" the moment anyone runs one — the two-repositories drift internal/deploy/kubernetes.go's own package doc warns about, and the third feature in a row to need both copies changed together. Delete is included because the Job is removed once its output has been captured into the run record; without it they would accumulate one per command forever. Notably absent is pods/exec, which an exec-into-the- running-pod design would have required — a far more dangerous grant on the one credential here whose compromise means the cluster, and one reason the Job approach was chosen over it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Wajog7nELA3i8JWTjxYGHF --- extra-manifests/toolshed-deployer-rbac.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extra-manifests/toolshed-deployer-rbac.yaml b/extra-manifests/toolshed-deployer-rbac.yaml index 644934e..a38b920 100644 --- a/extra-manifests/toolshed-deployer-rbac.yaml +++ b/extra-manifests/toolshed-deployer-rbac.yaml @@ -57,6 +57,13 @@ rules: - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # A one-off command run against an app executes in a throwaway Job built + # from the app's own image — see internal/deploy.Client.RunCommand. Delete + # is needed because the Job is removed once its output has been captured; + # without it they accumulate one per command forever. + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "delete"] # An app with persistent storage is a StatefulSet instead of a Deployment # (internal/deploy.Client.ensureStatefulSet). Delete is needed on both: # switching an app between stateless and stateful must remove whichever