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:
@@ -12,11 +12,12 @@ def _make_registry() -> NodeRegistry:
|
||||
node=NodeConfig(hostname="tower", castle_root="/data/repos/castle", gateway_port=9000),
|
||||
deployed={
|
||||
"my-svc": Deployment(
|
||||
runner="python",
|
||||
manager="systemd",
|
||||
launcher="python",
|
||||
run_cmd=["uv", "run", "my-svc"],
|
||||
env={"PORT": "9001", "SECRET_KEY": "super-secret"},
|
||||
description="My service",
|
||||
behavior="daemon",
|
||||
kind="service",
|
||||
stack="python-fastapi",
|
||||
port=9001,
|
||||
health_path="/health",
|
||||
@@ -24,9 +25,10 @@ def _make_registry() -> NodeRegistry:
|
||||
managed=True,
|
||||
),
|
||||
"my-job": Deployment(
|
||||
runner="command",
|
||||
manager="systemd",
|
||||
launcher="command",
|
||||
run_cmd=["my-job"],
|
||||
behavior="tool",
|
||||
kind="job",
|
||||
stack="python-cli",
|
||||
schedule="0 2 * * *",
|
||||
),
|
||||
@@ -51,12 +53,13 @@ class TestRegistrySerialization:
|
||||
|
||||
assert "my-svc" in restored.deployed
|
||||
svc = restored.deployed["my-svc"]
|
||||
assert svc.runner == "python"
|
||||
assert svc.manager == "systemd"
|
||||
assert svc.launcher == "python"
|
||||
assert svc.port == 9001
|
||||
assert svc.health_path == "/health"
|
||||
assert svc.subdomain == "my-svc"
|
||||
assert svc.managed is True
|
||||
assert svc.behavior == "daemon"
|
||||
assert svc.kind == "service"
|
||||
assert svc.stack == "python-fastapi"
|
||||
|
||||
def test_job_fields_preserved(self) -> None:
|
||||
@@ -65,9 +68,9 @@ class TestRegistrySerialization:
|
||||
|
||||
assert "my-job" in restored.deployed
|
||||
job = restored.deployed["my-job"]
|
||||
assert job.runner == "command"
|
||||
assert job.launcher == "command"
|
||||
assert job.schedule == "0 2 * * *"
|
||||
assert job.behavior == "tool"
|
||||
assert job.kind == "job"
|
||||
assert job.stack == "python-cli"
|
||||
|
||||
def test_optional_fields_omitted(self) -> None:
|
||||
@@ -75,7 +78,7 @@ class TestRegistrySerialization:
|
||||
reg = NodeRegistry(
|
||||
node=NodeConfig(hostname="minimal"),
|
||||
deployed={
|
||||
"bare": Deployment(runner="command", run_cmd=["bare"]),
|
||||
"bare": Deployment(manager="systemd", launcher="command", run_cmd=["bare"]),
|
||||
},
|
||||
)
|
||||
restored = _json_to_registry(_registry_to_json(reg))
|
||||
|
||||
Reference in New Issue
Block a user