Skip to content

Run the launchers in a long-lived pod on Kubernetes/OpenShift (Apptainer runtime) - #80

Merged
guimou merged 8 commits into
mainfrom
feat/kubernetes-apptainer
Sep 4, 2026
Merged

guimou merged 8 commits into
mainfrom
feat/kubernetes-apptainer

Conversation

@guimou

@guimou guimou commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Same launchers, same per-project isolation, but with a long-lived pod playing the role of the workstation: oc rsh in, cd ~/repos/<repo>, run ccbox / ocbox / qcbox / cxbox. The harness runs through Apptainer from a SIF converted from the regular quay.io/guimou/<box> image. Design and decisions in docs/plans/kubernetes-apptainer.md; user guide in docs/kubernetes.md.

Commits, in order:

  1. Plan (docs/plans/kubernetes-apptainer.md) with the decisions: extend this repo rather than a new project, storage class as a parameter (any RWX class), UBI9 pod image, credentials on the PVC as on a host, repos under ~/repos/<name>.
  2. Golden test (tests/render-test.sh, tests/golden/): every launcher against stub podman/gh/npm in a throwaway home; the rendered command line is compared with recorded expectations. Recorded on the unmodified engine.
  3. Phase 1, engine refactor, no behavior change. Wrappers declare mounts and env with add_mount / add_optional_mount / add_env into an ordered session spec; a runtime backend renders it. The Podman backend keeps today's flags exactly (nolabel marks the system paths that were never relabeled). All golden files unchanged. An old wrapper with the new engine is refused instead of launching without mounts.
  4. Phase 2, Apptainer backend. --runtime / CODEBOX_RUNTIME selection, SIF conversion with atomic move (--pull to refresh, --local to refuse), apptainer exec --userns --no-home --no-mount tmp,cwd --pid --ipc --cleanenv --writable-tmpfs --pwd /workspace + explicit binds, env via APPTAINERENV_* exports (--env splits on commas), optional per-session /tmp on CODEBOX_SCRATCH_DIR, session marker files for --list-sessions. Clipboard/audio/npm-global skipped, --with-firewall and --build refused with pointers. Ten new scenarios, three of them refusals.
  5. Phase 3, k8s/. UBI9 pod image (quay.io/guimou/codebox-pod: Apptainer, tmux, gh, launchers; 4 GB session dir), entrypoint, SCC + ClusterRole (k8s/cluster, cluster-admin once), kustomize base + example overlay (token-less SA, RWX PVC over /home/coder, Deployment with /dev/fuse annotation, UID/fsGroup 1000, node-local /scratch), gen-egress-firewall.sh (OVN EgressFirewall from the firewall domain lists), build-pod.yml, docs.

Verification

  • shellcheck on launchers, engine, test, and the two k8s/ scripts.
  • tests/render-test.sh: 25 scenarios pass (15 Podman, recorded before the refactor; 10 Apptainer).
  • kubectl kustomize k8s/overlays/example and k8s/cluster render; the egress generator emits one rule per domain.
  • New tests.yml workflow runs all of the above on PRs.

Not verified

Nothing has run on a cluster or a real Apptainer yet. The gate list in the plan is the next step; the points most likely to need a follow-up are the image's /home/coder under --no-home + writable overlay, credential file binds on top of directory binds, sessiondir max size, and fsGroup on the target RWX class (EFS needs an access point with uid/gid 1000).

Note: merging triggers release.yml (it rebuilds all harnesses on lib/ changes) and the new build-pod.yml.

https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

guimou and others added 8 commits September 4, 2026 08:13
Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
…redentials on PVC, repos layout

Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Runs every launcher against stub podman/gh/npm in a fixed environment and
compares the rendered 'podman run' argv with tests/golden. Recorded on the
unmodified engine so the runtime-backend refactor can prove it changes
nothing.

Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Wrappers and the engine no longer append raw `podman run` flags. Mounts and
environment variables are declared with add_mount / add_optional_mount /
add_env into an ordered BOX_SPEC, and a runtime backend renders that spec,
the image tag and the launch command into the final command line. The
Podman backend is the only one and keeps today's behavior exactly
(rootless flags, :z labels, firewall capabilities, macOS handling); a
`nolabel` mount option marks the system paths that were never relabeled
(/etc/localtime, X11 and Wayland sockets).

No behavior change: the golden test (recorded on the previous engine,
including the new scenarios) passes unchanged for all four launchers. An
old wrapper paired with this engine is detected and refused instead of
silently launching without its mounts.

Also: tests.yml CI workflow (shellcheck + golden test), docs updated
(development, architecture, contributing, AGENTS.md), plan updated to the
single-file layout.

Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Second backend for the pod-as-host case. Selected with --runtime, else
CODEBOX_RUNTIME, else podman when installed, else apptainer. The registry
image is converted once to $CODEBOX_SIF_DIR/<box>-<tag>.sif (temp name +
atomic move; --pull re-converts, --local refuses to convert) and run with
`apptainer exec --userns --no-home --no-mount tmp,cwd --pid --ipc
--cleanenv --writable-tmpfs --pwd /workspace` plus the session spec as
--bind flags. Environment goes through APPTAINERENV_* exports (survive
--cleanenv, not split on commas like --env). Optional CODEBOX_SCRATCH_DIR
binds a per-session directory at /tmp. Sessions are tracked with marker
files (host, PID, start time) since Apptainer rewrites its command line.

Runtime capabilities gate host features: clipboard, audio and npm-global
are skipped, --with-firewall and --build are refused with a pointer to the
pod-level alternative. The Podman path is unchanged (golden files
untouched); ten apptainer scenarios added, three of them refusals.

Docs: usage.md Runtimes section (flags, env vars), architecture.md,
AGENTS.md, plan updated.

Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Run the launchers inside a long-lived pod on Kubernetes/OpenShift, the
pod playing the role of the workstation:

- k8s/Containerfile: slim UBI9 image (quay.io/guimou/codebox-pod) with
  rootless Apptainer + FUSE helpers, tmux, git/gh, and the launchers in the
  flat layout. apptainer.conf gets a 4 GB session dir and no default
  home/tmp mounts. Runs as coder/1000 with CODEBOX_RUNTIME=apptainer and
  the scratch/cache dirs on node-local /scratch.
- k8s/entrypoint.sh: home skeleton on the RWX PVC (~/repos, SIF store,
  session markers, .bashrc), then idles; sessions run from tmux.
- k8s/cluster: SCC codebox-apptainer (restricted-v2 + seccomp unconfined,
  MustRunAsNonRoot, fsGroup RunAsAny) and a ClusterRole to use it.
- k8s/base + overlays/example: service account without token, RoleBinding,
  RWX PVC mounted at /home/coder (storage class set by the overlay),
  Deployment with the /dev/fuse annotation, UID/fsGroup 1000, Recreate
  strategy, optional codebox-env Secret, node-local emptyDir at /scratch.
- k8s/gen-egress-firewall.sh: OVN EgressFirewall from the firewall domain
  lists, the pod-level replacement for --with-firewall.
- build-pod.yml workflow; tests.yml also renders the kustomizations and
  runs the generator; docs/kubernetes.md (deploy, daily use, PVC layout,
  security posture, troubleshooting); README, AGENTS.md, development.md,
  CONTRIBUTING.md and the plan updated. Real env files are git-ignored.

Nothing here has run on a cluster yet; the gate list in the plan is next.

Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bsm8NJasSPeyJ9xeamuuch

---
Signed-off-by: Guillaume Moutier <guimou@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
@guimou
guimou merged commit e368b54 into main Sep 4, 2026
1 check passed
@guimou
guimou deleted the feat/kubernetes-apptainer branch September 4, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant