Add stack dependency management
Stacks declare host toolchains (uv/pnpm/node/hugo/deno/psql) that can drift
from what's installed and, worse, from what's on a running service's PATH.
Make those dependencies first-class and visible.
Model (core):
- stacks.ToolRequirement + StackHandler.tools declare each stack's toolchains
(command, purpose, phase, install_hint); tools_for() is the single source.
- relations synthesizes them as `kind: tool` requirements so functional?/graph
account for them; checked runtime-env-aware (run-phase tools of a systemd
service are probed against the service's PATH, not the shell) via a shared
generators.systemd.runtime_path helper the unit generator also uses, so the
checker can't drift from the generator. hint_for() makes every unmet
requirement actionable.
- stack_status: the derived per-stack health the CLI/API/UI all render.
- config: add ~/.deno/bin to USER_TOOL_PATH_DIRS so deno (supabase edge fns)
is found by services and the check, same as ~/.local/bin and the pnpm dirs.
Surfaces:
- castle stack list|info (new resource) + GET /stacks/status, /stacks/{name}
(GET /stacks stays a bare name list for the create-form select).
- castle doctor gains a "Stacks & dependencies" section (FAIL for an enabled
deployment's missing tool, WARN otherwise, unused stacks skipped).
- castle apply preflight warns (advisory, like _acme_preflight) when a tool is
missing where a service runs; ConvergePanel renders those warnings.
- Dashboard Stacks page: per-stack tool checklist with versions + copyable
install hints, program links, and verb chips.
Tests: relations (drift + hints), doctor (ok/fail/skip), /stacks endpoints.
This commit is contained in:
29
castle-api/tests/test_stacks_api.py
Normal file
29
castle-api/tests/test_stacks_api.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Tests for the stack-dependency endpoints (/stacks, /stacks/status, /stacks/{name})."""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from castle_core.stacks import available_stacks
|
||||
|
||||
|
||||
class TestStacks:
|
||||
def test_names_endpoint_stays_a_bare_list(self, client: TestClient) -> None:
|
||||
"""`GET /stacks` keeps its back-compat string[] shape (the create-form select
|
||||
depends on it) even though the richer status lives at /stacks/status."""
|
||||
resp = client.get("/stacks")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == available_stacks()
|
||||
|
||||
def test_status_shape(self, client: TestClient) -> None:
|
||||
resp = client.get("/stacks/status")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert {s["name"] for s in body} == set(available_stacks())
|
||||
st = next(s for s in body if s["name"] == "python-fastapi")
|
||||
# python-fastapi declares uv; every tool carries its phase + fix.
|
||||
assert {"in_use", "ok", "tools", "programs", "verbs"} <= st.keys()
|
||||
uv = next(t for t in st["tools"] if t["command"] == "uv")
|
||||
assert uv["phase"] == "both" and uv["install_hint"]
|
||||
|
||||
def test_detail_and_404(self, client: TestClient) -> None:
|
||||
assert client.get("/stacks/python-cli").json()["name"] == "python-cli"
|
||||
assert client.get("/stacks/nope").status_code == 404
|
||||
Reference in New Issue
Block a user