Review v2: k8s Job provision admin_app Postgres user + grants
Verdict: READY (with one [SCOPE] carryover)
Re-review of board item #1097 — Forgejo
forgejo_admin/pal-e-platform#302. Issue body fully rewritten since review-1097-2026-04-25: 3-option fork removed, Path A (idempotent k8s Job in pal-e-deployments) selected. Type: Task.Changes since v1
- Title rewritten: "k8s Job: provision admin_app Postgres user + grants on basketball db" (was generic "create admin_app user")
- 3-option fork removed; Path A explicitly chosen with rationale block citing v1 review findings
- SQL spelled out in body (CREATE ROLE DO-block, GRANT, ALTER DEFAULT PRIVILEGES)
- Three concrete deliverables enumerated (password gen, Job manifest + kustomization, DATABASE_URL secret mirror)
- 10 AC including idempotency + blocked-write test + forward-grant test
- Constraints section explicitly forbids cyrilgdn provider, forbids modifying basketball-api Deployment, forbids CREATE/ALTER/DROP grants
- Label drift:
arch:postgres-grants→arch:postgres
Template Completeness
- [x] Type — Task
- [x] Lineage (with scope-revision lineage block)
- [x] Repo
- [x] Scope (3 deliverables, SQL inline)
- [x] Acceptance Criteria — 10 items
- [x] Test Expectations
- [x] Constraints
- [x] Checklist
- [x] Related (links v1 review for provenance)
Traceability
- [x] story:admin-row-crud label — present
- [x] story note verified —
story-westside-admin-admin-row-crudexists inproject-westside-adminuser-stories table - [x] arch:postgres label — present (changed from arch:postgres-grants)
- [ ] arch note MISSING — neither
arch-postgresnorarch-deployment-westside-adminexist as backing notes. [SCOPE] Carryover from v1: createarch-deployment-westside-adminand a stubarch-postgres(or fold grants into the parent). Should not block ticket execution. - [x] Forgejo issue — open, valid (#302)
- [x] type:infra label — present
SQL Idempotency Audit
- CREATE ROLE — wrapped in
DO $$ ... IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='admin_app') ... CREATE ROLE ... END $$. Idempotent for creation. Caveat: if the role already exists, the Job will NOT update the password — there's noALTER ROLEbranch. For initial provisioning + re-apply with same password, this is correct (no-op). For password rotation, the Job won't propagate a new password unless ALTER ROLE is added. Acceptable for v1 since rotation is deferred to the companion SOP. - GRANT USAGE / GRANT SELECT,INSERT,UPDATE,DELETE / GRANT USAGE ON SEQUENCES — Postgres GRANT is idempotent by design (re-grant is a no-op). Correct.
- ALTER DEFAULT PRIVILEGES — idempotent. Correct pattern for "future tables get same grants automatically." This is the right tool for the AC "all future tables" requirement.
- Conclusion: idempotency holds for the scope as written. Re-running the Job is safe.
Grants Minimality
- USAGE on schema public — required for any table access. ✓
- SELECT, INSERT, UPDATE, DELETE on tables — exactly the row-CRUD scope. ✓
- USAGE on sequences — needed for nextval (SERIAL/identity inserts). ✓
- NO CREATE, ALTER, DROP, TRUNCATE, REFERENCES, TRIGGER. ✓
- NO superuser, NO CREATEDB, NO CREATEROLE. ✓ (default for non-superuser CREATE ROLE)
- Minor: if the app ever needs
currval()orlastval(), sequences also needSELECT.USAGEalone coversnextval()which is what Drizzle uses for inserts. Not a blocker — flag for later if reads of sequence state are added. - Blocked-write AC (CREATE TABLE → permission denied) is the correct fail-test signal that minimality holds.
DATABASE_URL Handoff to #1095/#133
- Path:
pal-e-deployments/overlays/westside-admin/prod/westside-admin-secrets.enc.yaml— explicit. - Key:
DATABASE_URL— explicit. - Connection string:
postgresql://admin_app:<password>@postgres.basketball-api.svc.cluster.local:5432/basketball— verified againstpostgres.yaml: Service name ispostgresin namespacebasketball-api, port 5432, DB namebasketball. Correct. - Cross-namespace DNS works in k3s — overlay-side NetworkPolicy must allow westside-admin → basketball-api:5432 (separate concern, likely covered by #133 or a follow-up).
- Companion ticket #1095 (pal-e-deployments#133) is the consumer; contract is unambiguous.
- Verdict: handoff is well-defined.
File Targets / Path Verification
- [x]
pal-e-deployments/overlays/basketball-api/prod/— exists, containskustomization.yaml+postgres.yaml. Newadmin-app-user-job.yamlgoes here. - [x]
pal-e-deployments/overlays/basketball-api/prod/postgres.yaml— verified: plainpostgres:16-alpineDeployment, Service namedpostgres, namespacebasketball-api, DBbasketball, userbasketball, password from Secretbasketball-api-secretskeypostgres-password. - [ ]
pal-e-deployments/overlays/westside-admin/prod/westside-admin-secrets.enc.yaml— directory does NOT exist yet. Created by companion #1095/#133. Acceptable: ordering allows this Job (#1097) to land first; secret mirror coordinates with the consumer overlay. - Minor [BODY] nit (non-blocking): body says "Mounts the basketball-api postgres superuser credentials (existing Secret) for connecting as superuser." The role
basketballis the DB owner (created viaPOSTGRES_USERenv), not formallySUPERUSER. As DB owner it CAN create roles in its database and grant on objects it owns, so the SQL works — but the wording "superuser" is technically inaccurate. Consider "owner credentials" instead.
Repo Placement
OK. Tracking issue lives on pal-e-platform (correct — bootstrap repo). Manifest deliverable lives in pal-e-deployments (correct — overlay home). Secret deliverable also in pal-e-deployments via #133. Three repos involved, two PRs expected (one per repo as needed); body acknowledges this.
Dependencies
Foundational. Blocks:
forgejo_admin/westside-admin#1 (Drizzle integration), forgejo_admin/pal-e-deployments#133 (overlay secret consumer = board item #1095). No upstream blockers. Can start immediately.Acceptance Criteria
10 AC, all testable: role exists check, grants check, negative-grant check, password storage check, secret mirror check, idempotency check, three psql connection tests (read/write/blocked), SOP note creation. Each is verifiable by an agent with explicit psql commands. The blocked-write test (
CREATE TABLE foo → permission denied) is the strongest minimality signal. Test Expectations adds a forward-grant test (create table as superuser, verify admin_app immediately has CRUD) that proves ALTER DEFAULT PRIVILEGES works. AC are excellent.Blast Radius
Low. Plain-Postgres Deployment pattern is also used by westsidekingsandqueens and westside-streamlit — same admin_app pattern would extend cleanly later if needed, but only basketball is in scope here. Job touches only the basketball DB. Do NOT reuse
basketball-api-secrets for the new credential — keep it in a fresh Secret to keep blast radius isolated. Body should call this out explicitly but it's implied by "Mirror the password Secret" pointing to a separate file.Decomposition Assessment
3 deliverables, ~3-4 files (Job manifest + kustomization edit + secret mirror + SOP note), 10 AC but all run in seconds via psql. Estimated agent work: ~10 min. Fits a single agent pass at the upper edge of the 5-minute rule. No decomposition needed, but agent should be briefed tightly to avoid scope drift.
Recommendation
READY to advance backlog → todo. v1 [BODY] recommendations all addressed (concrete approach chosen, secret handoff explicit, cyrilgdn provider option removed). Two non-blocking carryovers:
[SCOPE]Carryover from v1: createarch-deployment-westside-adminand stubarch-postgresbacking notes. Foundational refinement, does not block ticket execution but story:arch traceability is incomplete without it. Recommend Ava queue this as a separate ticket on board-westside-admin.[BODY]Optional polish: replace "superuser credentials" with "owner credentials" (the rolebasketballis DB owner, not formally SUPERUSER). Non-blocking; SQL semantics are correct as written.[BODY]Optional polish: add a one-line note that the Job's Secret holdingadmin_apppassword should be a NEW Secret (notbasketball-api-secrets) for blast-radius isolation. Implied but worth being explicit.
None of the optional polish items block READY. The arch-note carryover is a separate scoping ticket, not a body fix.