Make the image registry global, not per-cluster — Plan

Goal

Remove the per-Cluster registry_url / registry_auth columns and make the runner image registry a single global deployment setting, sourced from backend/config.py (settings.registry_url / settings.registry_auth, already overridden by the k8s ConfigMap in prod). Per-cluster registry configuration is a bug: the runner images only exist wherever docker/build.sh pushed them, so an arbitrary cluster cannot pull from "its own" registry — every cluster must reach the one global registry the build published to. A per-cluster field only invites an admin to point a new cluster at a registry the images were never pushed to, which fails the pull at job time.

Scope is: drop the two columns (+ migration), route job_orchestrator through the existing global settings.*, and strip registry from the admin routes and the Clusters admin form. It does not add any global-settings admin UI (decision below: config/ConfigMap-only), and does not touch the cluster_type / provider_config connection mechanism, namespace, or StorageBackend.

Supersedes / modifies

Modifies surface landed by cluster-admin-ui.md (still in docs/plans/, not yet moved to done/) and the registry env-var injection landed by multi-cluster-selection.md / multi-cluster-execution.md. No behavior change for the current single-cluster deployment: today's default cluster has registry_url=NULL, so job_orchestrator.py:45-48 already injects no REGISTRY_URL/REGISTRY_AUTH env vars; after this change it injects them from settings.*, whose default (registry:5000 / None) matches what the runner already assumes.

Background — current state

(Confirmed by reading the code.)

Design decisions (settled in discussion)


Phase 1 — job_orchestrator reads global settings

backend/services/job_orchestrator.py:

python if cluster.registry_url: env_vars.append(client.V1EnvVar(name="REGISTRY_URL", value=cluster.registry_url)) if cluster.registry_auth: env_vars.append(client.V1EnvVar(name="REGISTRY_AUTH", value=cluster.registry_auth))

with (using the already-imported settings, job_orchestrator.py:24):

python if settings.registry_url: env_vars.append(client.V1EnvVar(name="REGISTRY_URL", value=settings.registry_url)) if settings.registry_auth: env_vars.append(client.V1EnvVar(name="REGISTRY_AUTH", value=settings.registry_auth))

This is the only production consumer of the columns, so after this change nothing reads them.

Phase 2 — Model + migration: drop the columns

2.1 backend/models/cluster.py

2.2 Migration (off head 182d880e84c7)

Generate the revision id with real entropy per CLAUDE.md rule 9 (python3 -c "import uuid; print(uuid.uuid4().hex[:12])"), verify uniqueness with grep -rn "revision = '<id>'" --include=*.py . across all migration dirs before committing.

batch_alter_table('clusters'): drop_column('registry_url'), drop_column('registry_auth'). downgrade() re-adds both as sa.Column(sa.String(255), nullable=True).

No data pass — nothing is migrated into global config (see Design decisions).

Phase 3 — Admin routes: strip registry

backend/routers/admin.py:

Phase 4 — Frontend: remove registry from the Clusters form

frontend/src/ClustersAdminPanel.jsx:

No change to datamodel/api.js or useAuthQueries.js — the routes' paths and shapes are unchanged, only two body keys disappear.


Implementation Order

  1. Phase 1job_orchestratorsettings.*. Verify a job still launches on the default cluster and (for a build_frontend_plugin run) REGISTRY_URL is present in the pod env, sourced from settings.registry_url.
  2. Phase 2 — model edit + drop-column migration; run backend/bin/nagelfluh-migrate; confirm the default cluster still resolves and jobs run.
  3. Phase 3 — admin route edits; verify GET/POST/PATCH /admin/clusters via /docs no longer accept or emit registry fields and don't 500.
  4. Phase 4 — frontend form/table edits; verify the Clusters admin tab renders, add/edit still works, and no registry fields remain.

Resolved questions