Skip to content

Container exec mode: sprites as containers on Docker or Kubernetes - #24

Merged
lex00 merged 2 commits into
mainfrom
feat/22-real-exec
Sep 25, 2026
Merged

lex00 merged 2 commits into
mainfrom
feat/22-real-exec

Conversation

@lex00

@lex00 lex00 commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Opt-in SPRITZER_EXEC=container. Each sprite is a container: a pod sprite-<name> in spritzer's namespace on Kubernetes, or a container spritzer-<name> through the Docker socket. The interpreter stays the default; internal/sprite, ws.go, config.go and every existing test are untouched.

  • Exec runs the real command over the existing framed WebSocket: stdout and stderr streamed as produced, stdin passed through (StreamStdinEOF closes it), env and dir, the real exit code. Repeated cmd params are an argv; a single cmd holding a command line runs under /bin/sh -c. A client that disconnects before exit has its process group killed.
  • Services use wisp's guest API shapes. From outside, /v1/sprites/{id}/services/... is forwarded verbatim to the agent (PUT creates and starts, streaming NDJSON for ?duration; start, stop, restart, logs, signal, DELETE). From inside, sprite-env services ... talks to the same agent on /.sprite/api.sock, and PUT /v1/tasks is accepted for the box's keep-awake loop. Services outlive the exec, restart on crash, start again at container restart.
  • The sprite URL, /s/<name>/... and <name>.<SPRITZER_URL_DOMAIN>, proxies HTTP and WebSocket to the http_port service, else 8080.
  • The filesystem API reads and writes the container's files. GET /v1/sprites lists sprites; a restarted spritzer adopts existing ones.
  • Checkpoints answer 501 in this mode: Checkpoints and restore in container exec mode #23.

The spritzer binary is also the in-sprite agent (spritzer agent, sprite-env, x-exec, x-relay, x-fs, x-kill), copied into each sprite from SPRITZER_AGENT_IMAGE (an init container on Kubernetes, a named volume on Docker). spritzer reaches it only by exec, so nothing inside a sprite has to be routable, which is what makes Docker Desktop and a cluster the same code path. Kubernetes needs only pods and pods/exec in the namespace; deploy/k8s/container-mode.yaml has the RBAC and Deployment.

Env: SPRITZER_EXEC, SPRITZER_RUNTIME (docker | kubernetes, auto), SPRITZER_SPRITE_IMAGE (default node:22-bookworm), SPRITZER_AGENT_IMAGE (default ghcr.io/intentius/spritzer:<version>), SPRITZER_NAMESPACE, SPRITZER_URL_DOMAIN, SPRITZER_CREATE_TIMEOUT.

Acceptance

e2e/real_test.go, run by just e2e-real (throwaway k3d cluster, spritzer deployed in-cluster with its RBAC, port-forwarded) and just e2e-docker, and by the new CI job on both. It creates a sprite, runs uname -a and a multi-line bash script (stderr, exit 3, env, dir), pipes stdin, checks a disconnected exec is killed, starts python3 -m http.server 8080 as a service with sprite-env from inside, fetches it through /s/<name> and the host-routed URL, again 3s after the exec ended, then destroys the sprite and checks with kubectl that the pod is gone. Local k3d run (k3s v1.33.6, arm64):

real_test.go:140: created sprite e2e-1c5bd5 in 2.036s, url http://e2e-1c5bd5.localhost:4392
real_test.go:153: uname -a -> Linux e2e-1c5bd5 7.0.12-linuxkit #1 SMP PREEMPT Thu Aug 27 14:02:21 UTC 2026 aarch64 GNU/Linux
real_test.go:172: multi-line script -> exit 3, stdout "answer 42\ngreeting hello from env\n/tmp\n1,2,3,\n", stderr "to stderr\n"
real_test.go:205: disconnected exec -> its process was killed
real_test.go:219: sprite-env services create web -> exit 0 (the exec that started it has ended)
real_test.go:239: sprite URL, 3s after the exec ended: GET /s/e2e-1c5bd5/hello.txt -> 200 "hello from the sprite\n"
real_test.go:246: host-routed URL http://e2e-1c5bd5.localhost:4392/hello.txt -> 200 "hello from the sprite\n"
real_test.go:264: destroyed: Error from server (NotFound): pods "sprite-e2e-1c5bd5" not found
--- PASS: TestContainerModeAcceptance (7.42s)

Related issues

Closes #22. Follow-up: #23 (checkpoints in container mode). Consumer: arugula-salad/studio#29.

Checklist

  • gofmt clean
  • golangci-lint passes (darwin and GOOS=linux; windows builds)
  • go test -race ./... passes
  • mkdocs build --strict passes
  • Tests added (agent, container-mode routes, e2e)
  • CHANGELOG.md updated under ## [Unreleased]

SPRITZER_EXEC=container makes each sprite a container: a pod per sprite
in spritzer's namespace on Kubernetes, or a container through the Docker
socket. Exec runs the real command over the existing framed WebSocket
(stdin, env, dir, exit code) and kills it if the client disconnects.
Services follow wisp's guest API, outside at /v1/sprites/{id}/services
and inside through sprite-env on /.sprite/api.sock, and outlive the exec
that started them. The sprite URL (/s/<name> and <name>.<domain>)
proxies HTTP and WebSocket to the http_port service or 8080. The
filesystem API reads and writes the container's files. Checkpoints
answer 501 in this mode (#23).

The spritzer binary doubles as the in-sprite agent, copied in from
SPRITZER_AGENT_IMAGE, and spritzer reaches it only by exec, so no port
in a sprite has to be routable from spritzer.

The interpreter stays the default; its code and tests are untouched.
e2e/ holds the acceptance, run by scripts/e2e-docker.sh and
scripts/e2e-k3d.sh (just e2e-docker, just e2e-real) and by CI.
@lex00
lex00 merged commit 62fb9b1 into main Sep 25, 2026
3 checks passed
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.

A real exec mode: sprites as containers on k3d, with long-lived services and a working per-sprite URL

1 participant