Public registry addressing + a self-service "minikube" cluster type — Plan

Goal & scope

Registering a second, remote cluster (e.g. a minikube instance on another host, such as mimer) currently fails in two ways once you actually try to run a process on it:

  1. Image pull is unreachable. Environment.docker_image is a fully-qualified image reference baked in at build time by docker/build.sh, using the local minikube VM's private IP ($(minikube ip):30500). That address is meaningless from any other host's cluster.
  2. Nothing sets up a newly-registered cluster's Kubernetes-side prerequisites (namespace, Kueue operator + queues, RBAC, registry pull trust). Today only dev/setup-minikube.sh does this, by hand, once, against the single default/bootstrap cluster.

Both gaps were explicitly flagged and deferred to this work by earlier plans:

This is that work. Scope:

Out of scope: cloud-managed clusters (GKE/EKS/etc). Those would use the cloud provider's own registry (GCR/Artifact Registry/ECR) with IAM-based pull auth, which sidesteps the self-signed-TLS-trust problem entirely — a different registry backend, not addressed here. GKE specifically has its own viable path (a gcloud container node-pools ... --metadata-from-file startup-script=... that installs the registry's CA into every node, Standard mode only — Autopilot forbids node customization entirely), but that's a separate future plan, not built here.

Background — current state

(Confirmed by reading the code.)

Design decisions (settled in discussion)

  1. Registry addressing: one public address, used everywhere, always. Both docker/build.sh's push and every cluster's pull (local minikube included) use the same publicly-exposed host:30500 address. No per-cluster branching. Configured via a new config.env var (working name REGISTRY_PUBLIC_HOST, alongside the existing MINIKUBE_APISERVER_IPS LAN-IP pattern). Flag at implementation: confirm minikube's own node can route back to the host's public IP:port for its own push/pull (hairpin NAT) — if not, document as a caveat, not a redesign.
  2. New, additive minikube cluster type. same_as_backend and kubeconfig are unchanged. This is specifically for "I stood up (or will stand up) a minikube instance on a host I have shell access to" — the realistic majority case for this on-prem-ish tool. The generic kubeconfig type remains for anyone who already has a kubeconfig from anywhere and doesn't need any of this automation.
  3. Registration flow for minikube, one command, one paste:
  4. Admin clicks "Add Cluster" → type minikube. Backend creates the Cluster row up front in a pending state, plus a single-use, short-lived (~30–60 min) registration token, stored hashed.
  5. UI shows one command (reusing the existing copy-button UI pattern from KubeconfigClusterForm.jsx): curl -fsSL http://<nagelfluh-host>/static/assets/setup-minikube-remote.sh \ | REGISTER_TOKEN=<token> bash
  6. Admin pastes it into an SSH session on the target host. The script:
    • Installs minikube if the binary is missing (assumes Docker + sudo already present on the host — not itself installed by this script).
    • Drops the registry's CA cert into ~/.minikube/files/etc/docker/certs.d/<host:port>/ca.crt before minikube start, so the very first pull is already trusted, and it survives any future minikube delete && minikube start recreate (files-based, not a live edit of a running node).
    • Runs (or reconciles) minikube start.
    • Runs the shared provisioning logic (Phase 2) — namespace, Kueue operator, RBAC, queues, imagePullSecret.
    • Extracts the kubeconfig using the existing sed-rewrite (host IP + docker-published apiserver port), same logic as today's KUBECONFIG_COMMAND.
    • POSTs it back: wget --post-data=... --header="Authorization: Bearer $REGISTER_TOKEN" http://<nagelfluh-host>/admin/clusters/register-callback.
  7. Backend validates the token against the pending row (single-use — invalidated immediately on redemption; rejected if expired), stores the kubeconfig into that Cluster's provider_config, marks it active.
  8. Frontend polls the pending cluster and flips from "waiting..." to "connected" once redeemed.
  9. The whole script must be idempotent — safe to re-paste if the callback fails partway (network blip, expired token before the POST lands), rather than leaving a half-provisioned host.
  10. Registry TLS trust: handled directly by the host script, no DaemonSet. Since the script has real root access to the actual host, this is just a file write — the earlier DaemonSet-based approach (considered when we thought this had to run purely over the Kubernetes API from the backend) is unnecessary for this cluster type.
  11. curl | bash over plain HTTP is not a special risk to design around. It's only a risk if the backend itself isn't proxied behind TLS — which is a choice already visible and owned by the operator via config.env/SERVER_URL, not something new introduced here.
  12. Shared provisioning logic is shell, not Python, since the remote host has no Nagelfluh Python backend installed on it. The namespace/Kueue-operator/RBAC/queue steps in dev/setup-minikube.sh get factored into a reusable script/section, sourced by both the local dev/prod bootstrap flow and the new remote setup script — not a Python module the shell calls into.
  13. Kueue operator install is included (CRDs + controller + webhook + readiness waits, same as today's setup-minikube.sh) — since it now runs synchronously inside a script the admin is watching in their own SSH session (not inside an HTTP request to the backend), there's no need for the async background-task/status-polling machinery that would have been required if this ran through the K8s API from the backend instead.

Phase 1 — Registry: always address it publicly

Phase 2 — Factor out shared provisioning shell logic (+ close the RBAC gap)

Phase 3 — Remote setup-and-callback script

Phase 4 — Backend: pending registration + single-use token + callback endpoint

Phase 5 — New MinikubeClusterProvider

Phase 6 — Frontend: minikube cluster form + pending-state UI

Phase 7 — imagePullSecrets in job_orchestrator.py


Manual verification

Open questions / follow-ups