Manager-first deployment model: split runner, merge service/job, frontend→static
Replace the conflated `runner` axis with two orthogonal ones: `manager`
(systemd|caddy|path|none) — who supervises/realizes a deployment — and, for
systemd only, a nested `launcher` (python|command|container|compose|node) — how
the process starts. ServiceSpec and JobSpec collapse into one manager-
discriminated DeploymentSpec union (Systemd/Caddy/Path/Remote); the services/
and jobs/ config dirs collapse into one deployments/ dir. The human "kind"
(service|job|tool|static|reference) is fully derived (kind_for), never stored —
the frontend kind is renamed static. behavior is gone.
- core: DeploymentSpec union + LaunchSpec + kind_for; legacy-aware loader
normalizes old runner shapes; CastleConfig.deployments with derived
services/jobs/tools views; registry.Deployment carries manager/launcher/kind.
- cli: service/job/tool as filtered views + a deployment group; --behavior→--kind,
create --runner→--launcher; lifecycle dispatches over config.deployments.
- castle-api: /deployments primary with /services,/jobs as views; summaries
derive kind; PUT/DELETE /config/deployments/{name} (services/jobs aliased).
- app: KindBadge, frontend→static everywhere, pick-a-kind creation wizard,
per-kind config editors.
- docs: single deployments/ layout, manager/launcher, static kind throughout.
Live migration verified byte-identical: regenerated Caddyfile and every unit
ExecStart line unchanged, so nothing restarted. Suites: core 124, cli 25,
castle-api 55; dashboard build + type-check clean.
This commit is contained in:
@@ -34,37 +34,38 @@ class TestIsActive:
|
||||
config = load_config(castle_root)
|
||||
assert lifecycle.is_active("does-not-exist", config) is False
|
||||
|
||||
def test_path_service_checks_path(self, castle_root: Path) -> None:
|
||||
# A `runner: path` service (a tool) is active when on PATH.
|
||||
from castle_core.manifest import ProgramSpec, RunPath, ServiceSpec, manager_for
|
||||
def test_path_deployment_checks_path(self, castle_root: Path) -> None:
|
||||
# A `manager: path` deployment (a tool) is active when on PATH.
|
||||
from castle_core.manifest import PathDeployment, ProgramSpec
|
||||
|
||||
assert manager_for("path") == "path"
|
||||
config = load_config(castle_root)
|
||||
config.programs["mytool"] = ProgramSpec(id="mytool", source="/tmp/mytool")
|
||||
config.services["mytool"] = ServiceSpec(program="mytool", run=RunPath(runner="path"))
|
||||
config.deployments["mytool"] = PathDeployment(manager="path", program="mytool")
|
||||
with patch.object(lifecycle, "_on_path", return_value=True) as mock:
|
||||
assert lifecycle.is_active("mytool", config) is True
|
||||
mock.assert_called_once_with("mytool")
|
||||
|
||||
def test_remote_service_is_active(self, castle_root: Path) -> None:
|
||||
# A remote service has no local process; the manager is `none` → available.
|
||||
from castle_core.manifest import RunRemote, ServiceSpec
|
||||
def test_remote_deployment_is_active(self, castle_root: Path) -> None:
|
||||
# A remote deployment has no local process; the manager is `none` → available.
|
||||
from castle_core.manifest import RemoteDeployment
|
||||
|
||||
config = load_config(castle_root)
|
||||
config.services["ext"] = ServiceSpec(
|
||||
program="ext", run=RunRemote(runner="remote", base_url="http://x")
|
||||
config.deployments["ext"] = RemoteDeployment(
|
||||
manager="none", program="ext", base_url="http://x"
|
||||
)
|
||||
assert lifecycle.is_active("ext", config) is True
|
||||
|
||||
def test_static_service_active_when_dist_built(self, castle_root: Path, tmp_path: Path) -> None:
|
||||
# A frontend is a `runner: static` service; active once its served dir exists.
|
||||
from castle_core.manifest import ProgramSpec, RunStatic, ServiceSpec
|
||||
def test_static_deployment_active_when_dist_built(
|
||||
self, castle_root: Path, tmp_path: Path
|
||||
) -> None:
|
||||
# A static (caddy) deployment is active once its served dir exists.
|
||||
from castle_core.manifest import CaddyDeployment, ProgramSpec
|
||||
|
||||
config = load_config(castle_root)
|
||||
repo = tmp_path / "fe"
|
||||
config.programs["fe"] = ProgramSpec(id="fe", source=str(repo))
|
||||
config.services["fe"] = ServiceSpec(
|
||||
program="fe", run=RunStatic(runner="static", root="dist")
|
||||
config.deployments["fe"] = CaddyDeployment(
|
||||
manager="caddy", program="fe", root="dist"
|
||||
)
|
||||
# No dist yet → inactive (caddy manager checks the served dir)
|
||||
assert lifecycle.is_active("fe", config) is False
|
||||
|
||||
Reference in New Issue
Block a user