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:
@@ -15,11 +15,10 @@ from castle_cli.manifest import (
|
||||
ExposeSpec,
|
||||
HttpExposeSpec,
|
||||
HttpInternal,
|
||||
JobSpec,
|
||||
ManageSpec,
|
||||
RunCommand,
|
||||
RunPython,
|
||||
ServiceSpec,
|
||||
SystemdDeployment,
|
||||
SystemdSpec,
|
||||
)
|
||||
|
||||
@@ -35,17 +34,16 @@ def _defaults(env_args: list[str] | None) -> DefaultsSpec | None:
|
||||
return DefaultsSpec(env=env)
|
||||
|
||||
|
||||
def _run_spec(runner: str, target: str, name: str) -> RunPython | RunCommand:
|
||||
if runner == "command":
|
||||
return RunCommand(runner="command", argv=target.split() or [name])
|
||||
return RunPython(runner="python", program=target or name)
|
||||
def _run_spec(launcher: str, target: str, name: str) -> RunPython | RunCommand:
|
||||
if launcher == "command":
|
||||
return RunCommand(launcher="command", argv=target.split() or [name])
|
||||
return RunPython(launcher="python", program=target or name)
|
||||
|
||||
|
||||
def _check_new(config: object, name: str, section: str) -> str | None:
|
||||
"""Return an error message if the name can't be created, else None."""
|
||||
existing = getattr(config, section)
|
||||
if name in existing:
|
||||
return f"Error: {section[:-1]} '{name}' already exists."
|
||||
def _check_new(config: object, name: str, label: str) -> str | None:
|
||||
"""Return an error message if the deployment name is taken, else None."""
|
||||
if name in config.deployments: # type: ignore[attr-defined]
|
||||
return f"Error: {label} '{name}' already exists."
|
||||
return None
|
||||
|
||||
|
||||
@@ -53,11 +51,11 @@ def run_service_create(args: argparse.Namespace) -> int:
|
||||
"""Create a service entry in castle.yaml."""
|
||||
config = load_config()
|
||||
name = args.name
|
||||
if err := _check_new(config, name, "services"):
|
||||
if err := _check_new(config, name, "service"):
|
||||
print(err)
|
||||
return 1
|
||||
|
||||
run = _run_spec(args.runner, args.run or args.program or name, name)
|
||||
run = _run_spec(args.launcher, args.run or args.program or name, name)
|
||||
|
||||
expose = None
|
||||
proxy = False
|
||||
@@ -71,8 +69,9 @@ def run_service_create(args: argparse.Namespace) -> int:
|
||||
# Expose at <name>.<gateway.domain> (the subdomain is the service name).
|
||||
proxy = not args.no_proxy
|
||||
|
||||
config.services[name] = ServiceSpec(
|
||||
config.deployments[name] = SystemdDeployment(
|
||||
id=name,
|
||||
manager="systemd",
|
||||
program=args.program,
|
||||
description=args.description or None,
|
||||
run=run,
|
||||
@@ -84,7 +83,7 @@ def run_service_create(args: argparse.Namespace) -> int:
|
||||
save_config(config)
|
||||
|
||||
print(f"Created service '{name}'.")
|
||||
print(f" runs: {args.runner} ({args.run or args.program or name})")
|
||||
print(f" runs: {args.launcher} ({args.run or args.program or name})")
|
||||
if expose:
|
||||
print(f" port: {args.port}")
|
||||
if proxy:
|
||||
@@ -97,14 +96,16 @@ def run_job_create(args: argparse.Namespace) -> int:
|
||||
"""Create a job entry in castle.yaml."""
|
||||
config = load_config()
|
||||
name = args.name
|
||||
if err := _check_new(config, name, "jobs"):
|
||||
if err := _check_new(config, name, "job"):
|
||||
print(err)
|
||||
return 1
|
||||
|
||||
run = _run_spec(args.runner, args.run or args.program or name, name)
|
||||
run = _run_spec(args.launcher, args.run or args.program or name, name)
|
||||
|
||||
config.jobs[name] = JobSpec(
|
||||
# A job is a systemd deployment with a schedule (→ a .timer).
|
||||
config.deployments[name] = SystemdDeployment(
|
||||
id=name,
|
||||
manager="systemd",
|
||||
program=args.program,
|
||||
description=args.description or None,
|
||||
run=run,
|
||||
@@ -115,7 +116,7 @@ def run_job_create(args: argparse.Namespace) -> int:
|
||||
save_config(config)
|
||||
|
||||
print(f"Created job '{name}'.")
|
||||
print(f" runs: {args.runner} ({args.run or args.program or name})")
|
||||
print(f" runs: {args.launcher} ({args.run or args.program or name})")
|
||||
print(f" schedule: {args.schedule}")
|
||||
print(f"\nNext: castle job deploy {name} && castle job enable {name}")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user