Skip to content

Remove the LocalStack CLI Requirement: Lifecycle via Docker API + REST#53

Open
HarshCasper wants to merge 11 commits into
mainfrom
cli-free-lifecycle
Open

Remove the LocalStack CLI Requirement: Lifecycle via Docker API + REST#53
HarshCasper wants to merge 11 commits into
mainfrom
cli-free-lifecycle

Conversation

@HarshCasper

@HarshCasper HarshCasper commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

  • drop the requirement to install the localstack or lstk CLI: lifecycle, logs, extensions, and ephemeral instances now run through the Docker Engine API (dockerode) and LocalStack's REST APIs
  • start creates the localstack-main container 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 over
  • restart reuses 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 tail
  • read logs via the Docker API (feeds localstack-logs-analysis and 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 on
  • call the LocalStack platform API directly for ephemeral instances and the extensions marketplace, replacing CLI output scraping with real JSON and status codes
  • slim the Docker image to a node base (~1.1 GB): no localstack CLI, docker CLI, or awscli; DooD state moves to the localstack-mcp named volume, retiring the identical-path XDG_CACHE_HOME mount (old configs that still set it keep their state)
  • new config knobs documented in README/server.json: 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

… 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.
@HarshCasper HarshCasper changed the title Cli free lifecycle Remove the LocalStack CLI Requirement: Lifecycle via Docker API + REST Jul 4, 2026
@HarshCasper HarshCasper force-pushed the cli-free-lifecycle branch from 1fcc301 to 317814e Compare July 4, 2026 21:50

@maximkasy maximkasy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 unnecessarilyrestartRuntimeInPlace (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 unvalidatedlocalstack-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 startlocalstack-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 runninglocalstack-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 handlingcontainer-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 ignoredextensions.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 120slocalstack.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.joinpaths.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_HOST env 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: HttpError never captures request headers; analytics redacts sensitive keys.
  • Image-pull vs readiness-poll timeouts: sequential, not shared — pull can't starve the poll.
  • Transient Dead container 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 → healthy localstack-main with 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-container awslocal) → restart (preserved image/name) → logs via Docker API → stop with full removal → start again 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 run with 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. 🚢

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.

2 participants