Remove the LocalStack CLI Requirement: Lifecycle via Docker API + REST#53
Remove the LocalStack CLI Requirement: Lifecycle via Docker API + REST#53HarshCasper wants to merge 11 commits into
Conversation
… options) Replicates the 2026.x localstack CLI start spec: pro image default, localstack-main name, 4566/443/4510-4560 port enumeration with in-docker 0.0.0.0 bind, GATEWAY_LISTEN host-stripping, structured Mounts, layered env forwarding, AI_AGENT tagging, and per-OS CLI-identical volume dir resolution (named volume fallback for DooD).
ping (friendly daemon-unreachable errors), imageExists/pullImage (manual progress stream consumption — followProgress misses in-stream errors and can throw uncaught), createAndStartContainer (409 -> typed conflict error), attachLogBuffer (follow-stream rolling tail that survives AutoRemove and doubles as exit signal), ordered getContainerLogs (frame-walker over the multiplexed buffer), any-state name lookup, removeContainer, waitForRemoval, ensureNetwork, and 304 tolerance in stopContainer.
- launchRuntime: pull-if-missing, any-state name-conflict handling (running -> actionable error, stopped -> auto-remove), attached log buffer for crash diagnostics + exit detection, readiness poll preserved (5s/120s) - Snowflake health probe via node:http with explicit Host header (replaces curl shell-out; fetch/undici drops the Host header) - getLocalStackStatus enriched from /_localstack/info instead of 'localstack status' - restartRuntimeInPlace: POST /_localstack/health restart + session-transition polling - management tool: restart recreates from the inspected container's image/name/volume (no provenance regression for externally-started runtimes); stop cleans up stale stopped containers; docker-daemon preflight on lifecycle actions - delete checkLocalStackCli/detectLifecycleCli/tryCliStatus/startRuntime and the requireLocalStackCli preflight (vestigial on cloud-pods/iam/logs; extensions and ephemeral migrate off the CLI in follow-up commits)
log-retriever now finds the container (provenance-agnostic) and reads an ordered log tail through dockerode instead of spawning 'localstack logs'. Feeds both localstack-logs-analysis and the IAM analyzer's analyze-policies. Error strings no longer reference the CLI.
list/install/uninstall now run the container's own extension manager module (python -m localstack.pro.core.bootstrap.extensions) through docker exec with DEBUG=0 and a 120s timeout, parsing its JSON-lines event protocol instead of scraping CLI stdout. Activation uses POST /_localstack/health restart with session-transition polling. Self-heals the extensions venv when its interpreter symlinks were created by a different image (a case the official CLI fails on). Marketplace fetch moves to the new PlatformApiClient. 'available' no longer requires a running container. Validated live: install/uninstall round-trip of localstack-extension-httpbin incl. restart; existing keycloak extension intact.
All four actions now call https://api.localstack.cloud/v1/compute/instances directly (the same REST surface the CLI wrapped) with Basic token auth — real JSON and HTTP status codes replace lenient stdout scraping, 401/404 map to actionable messages. No CLI, container, or Docker daemon involved. Validated live (read-only list).
The npx method needs only Node + Docker now. The Docker method's generated spec loses the identical-path cache mount and XDG_CACHE_HOME (the server creates the LocalStack container itself; state defaults to a named volume). --cache-dir is accepted but ignored with a deprecation warning so existing scripts keep working.
Runtime base moves back to node:22-bookworm-slim (drops the python:3.12 pin the localstack CLI forced and the NodeSource install). Removes pip localstack, docker-ce-cli + its apt repo, and the unused awscli/awscli-local. IaC CLIs (tflocal, samlocal, snow) live in a /opt/venv on PATH (PEP 668). The aws-cdk S3 patch resolves its path via npm root -g instead of the NodeSource-era literal. Harness/docs/workflow drop the identical-path cache mount + XDG_CACHE_HOME — the server creates the sibling container itself; state defaults to the localstack-mcp named volume with a legacy XDG_CACHE_HOME continuity path.
…; CI runs tsc+jest README drops the lifecycle-CLI prerequisite, documents LOCALSTACK_IMAGE_NAME / LOCALSTACK_VOLUME_DIR / GATEWAY_LISTEN / DOCKER_HOST, and adds migration notes (profiles/DOCKER_FLAGS/unprefixed-env forwarding no longer apply; 443 published by default). server.json lists the new env vars. CLAUDE.md catalog/gotchas/§11 updated for the dockerode lifecycle. ci.yml gains type-check + unit-test steps so the rewritten jest suites are actually enforced.
When an earlier iteration left the mode at SOFT_MODE, Gemini rationally skipped set-mode after seeing get-status — a state-leak flake between iterations. The scenario now asks for an explicit confirming set-mode call.
1fcc301 to
317814e
Compare
maximkasy
left a comment
There was a problem hiding this comment.
Reviewed the full diff, and also built the branch and ran it live on Windows (Docker Desktop) — details at the bottom. Overall: strong PR, right architecture. Going CLI-free removes an entire class of environment bugs — including one we just hit where the official PyInstaller-frozen Windows CLI ignores PYTHONIOENCODING and charmap-crashes on piped emoji output; with this PR nothing Python is ever spawned, so that whole category is gone. I'd merge this after the items below.
Would fix before merge
1. Restart confirmation can report success prematurely / wait unnecessarily — restartRuntimeInPlace (localstack.utils.ts ~596–608): when the initial getSessionInfo() returns null, the !before fallback treats missing session info as proof the restart completed. Conversely, a fast restart where the gateway never blips and session_id/uptime don't visibly change waits the full 120s despite having succeeded. Suggest requiring an explicit signal (gateway downtime OR session change) and treating before == null as "unknown", plus validating the restart POST response, rather than defaulting to success.
2. pip install specifiers are unvalidated — localstack-extensions.ts ~176: tool-provided name/source go straight to pip install inside the container. The exec-array kills shell injection, but pip itself accepts git+https://… URLs, local paths, and option syntax — and this container has the docker socket mounted, so container compromise ≈ host compromise. Tool args come from an LLM, so treating them as untrusted matters here. A conservative allowlist regex for package names and restricting source to known git hosts would cover it:
const EXTENSION_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*(\[[A-Za-z0-9,._-]+\])?(==[A-Za-z0-9.*+!-]+)?$/;
if (name && !EXTENSION_NAME_RE.test(name)) {
return ResponseBuilder.error("Invalid extension name", `"${name}" is not a valid extension package name.`);
}3. Snowflake Pro gate skipped on start — localstack-management.ts ~73–76: the license check is deferred to onReady, so an unlicensed user pulls the full image and starts the container before hearing "no". Moving requireProFeature(ProFeature.SNOWFLAKE) into the preflight for start too fails fast.
4. status service=snowflake probes the Snowflake endpoint even when the AWS stack is running — localstack-management.ts ~287–319: reports "Snowflake health check did not pass", which reads as a Snowflake failure rather than "a different stack is running". recreateOverrides() already detects the stack from the container image — reusing that here would give an accurate message.
Should fix (small, non-blocking)
5. DOCKER_SOCK handling — container-spec.logic.ts ~381: the value isn't validated or normalizeBindPath()-ed, and when a bad path makes container creation fail, describeDockerConnectivityError matches docker.sock in the message and reports "Docker daemon is not reachable" — misleading, since the daemon is fine and the socket mount source is what's wrong. Validating existence when DOCKER_SOCK is explicitly set + a specific error message would fix the last of the "opaque errors" class. (For the record: the default works fine on Docker Desktop — verified live on Windows — the gap is rootless/custom-socket Linux setups.)
6. Venv-repair result ignored — extensions.logic.ts ~28 + localstack-extensions.ts ~106: the repair script hardcodes /opt/code/localstack/.venv/bin/python and its exec result (exit code/stderr) is discarded. On a custom image with a different layout it fails silently and the user gets an unrelated extension-manager error. Checking the exit code and surfacing it in the failure detail is enough.
7. Pre-start stale-container cleanup can block up to 120s — localstack.utils.ts ~418–424: removeContainer + waitForRemoval each default to 60s; if the daemon hangs, start blocks 2 minutes before erroring. Short fail-fast timeouts (~10s) for this pre-start path would be kinder.
Tests (both fail on real Windows machines today; CI hides it because jest runs on Linux only)
8. launchRuntime test doesn't isolate MAIN_CONTAINER_NAME — any machine with that env var set user-wide (i.e. anyone who used the old workaround this project is fixing!) fails localstack.utils.test.ts ("expected localstack-main, received localstack-aws"). Reproduced here. Fix is the same env cleanup docker.client.test.ts already does in beforeEach:
delete process.env.MAIN_CONTAINER_NAME;
delete process.env.LOCALSTACK_MAIN_CONTAINER_NAME;9. antigravityConfigPath uses bare path.join — paths.logic.ts:27: every other resolver in this file uses the pathFor(ctx) platform-flavor helper so the injected-platform tests pass on any host OS; this one (from #51) uses the host's path and fails on Windows. One-liner since this PR already touches the file:
export function antigravityConfigPath(ctx: ClientContext): string {
return pathFor(ctx).join(ctx.homeDir, ".gemini", "config", "mcp_config.json");
}Checked and fine (so nobody re-raises these)
DOCKER_HOSTenv handling: docker-modem reads it (incl. npipe) — the dockerode client doesn't need explicit options.- Auth token forwarded into the container: by design; the runtime needs it and the CLI does the same.
- Token leakage via HTTP errors:
HttpErrornever captures request headers; analytics redacts sensitive keys. - Image-pull vs readiness-poll timeouts: sequential, not shared — pull can't starve the poll.
- Transient
Deadcontainer record after stop: races the AutoRemove, self-clears in seconds — cosmetic.
Live verification (Windows 11, Docker Desktop, real pro token)
- Full lifecycle, zero CLI:
start→ healthylocalstack-mainwith correct spec (pro image; 4566/443/4510–4560 all bound to 127.0.0.1; docker.sock + volume mounts; env parity incl.GATEWAY_LISTEN,EXTERNAL_SERVICE_PORTS_*) →aws-client s3 mb(in-containerawslocal) →restart(preserved image/name) →logsvia Docker API →stopwith full removal →startagain cleanly. - Volume-path parity confirmed empirically: the spec's Windows default (
%LOCALAPPDATA%\cache\localstack\volume) is byte-identical to the directory the real pip CLI created on this machine — existing state genuinely carries over. - Externally-managed container accepted: raw
docker runwith a custom name (my-custom-ls) — status/aws-client/logs/stop all worked against it via the image+port fallback, and stop removed it cleanly. - tsc clean, build clean (14 tools), 191/193 tests (the 2 failures are items 8–9 above).
Nice work — the restart-preserving-overrides design and the container-spec parity are genuinely careful. 🚢
Summary
localstackorlstkCLI: lifecycle, logs, extensions, and ephemeral instances now run through the Docker Engine API (dockerode) and LocalStack's REST APIsstartcreates thelocalstack-maincontainer directly with the same spec the CLI produced (pro image, ports 4566/443/4510–4560, docker socket + volume mounts, env forwarding); the default volume dir matches the CLI's per-OS path, so existing state carries overrestartreuses the running container's image, name, and volume instead of resetting to defaults, so externally started runtimes (lstk, docker-compose, custom names) keep their state; startup crashes are reported with the container's log taillocalstack-logs-analysisand the IAM analyzer), and manage extensions by running the container's own extension manager over docker exec plus an in-place runtime restart — this also repairs extension venvs created by older images, a case the CLI fails onlocalstack-mcpnamed volume, retiring the identical-pathXDG_CACHE_HOMEmount (old configs that still set it keep their state)LOCALSTACK_IMAGE_NAME,LOCALSTACK_VOLUME_DIR,GATEWAY_LISTEN,DOCKER_HOST; wizard drops the lifecycle-CLI prerequisite and deprecates--cache-dir; CI now runs the type-check and unit tests