refactor(cli): resource-first command surface
Names collide across resource types (a program and a service can share a name),
so the CLI no longer resolves bare names. Operations live under the resource
they act on; platform lifecycle and the cross-resource overview stay top-level.
castle program list|info|create|add|clone|delete|run|install|uninstall|
build|test|lint|format|type-check|check
castle service list|info|create|delete|deploy|enable|disable|start|stop|restart|logs
castle job <same verbs> (create takes --schedule)
castle gateway start|stop|reload|status
castle list | status | deploy [name] # cross-cutting
castle start | stop | restart # whole platform (systemd verbs)
Key changes:
- info/delete/list/run are resource-scoped (no more cross-section name match).
- delete only removes the named resource's entry; program delete still blocks
when a deployment references it.
- per-resource start/stop/restart (systemctl on the unit/timer); top-level
start/stop/restart act on the whole platform.
- expose → 'service create' (more general: --program ref, --runner); gained
'job create' (the CLI never had one).
- dropped up/down — bringing things online is the honest 'deploy && start'.
Docs (CLAUDE.md, registry.md, stack guides) and user-facing messages updated.
core 94 / cli 24 / api 52 green; ruff clean. Verified live: program/service/job
list+info scope correctly, service create+scoped-delete round-trips.
This commit is contained in:
119
cli/src/castle_cli/commands/deploy_create.py
Normal file
119
cli/src/castle_cli/commands/deploy_create.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""castle service create / castle job create — declare a deployment.
|
||||
|
||||
A service or job can run anything (a castle program or not). `--program`
|
||||
records a convenience reference for description fallthrough; the run target is
|
||||
the console script (python) or argv (command) to execute.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
from castle_cli.config import load_config, save_config
|
||||
from castle_cli.manifest import (
|
||||
CaddySpec,
|
||||
ExposeSpec,
|
||||
HttpExposeSpec,
|
||||
HttpInternal,
|
||||
JobSpec,
|
||||
ManageSpec,
|
||||
ProxySpec,
|
||||
RunCommand,
|
||||
RunPython,
|
||||
ServiceSpec,
|
||||
SystemdSpec,
|
||||
)
|
||||
|
||||
|
||||
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 _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."
|
||||
return None
|
||||
|
||||
|
||||
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"):
|
||||
print(err)
|
||||
return 1
|
||||
|
||||
run = _run_spec(args.runner, args.run or args.program or name, name)
|
||||
|
||||
expose = None
|
||||
proxy = None
|
||||
if args.port is not None:
|
||||
expose = ExposeSpec(
|
||||
http=HttpExposeSpec(
|
||||
internal=HttpInternal(port=args.port, port_env=args.port_env),
|
||||
health_path=args.health,
|
||||
)
|
||||
)
|
||||
if not args.no_proxy:
|
||||
path = args.path
|
||||
if args.path:
|
||||
path = args.path if args.path.startswith("/") else f"/{args.path}"
|
||||
elif args.host:
|
||||
path = None
|
||||
else:
|
||||
path = f"/{name}"
|
||||
proxy = ProxySpec(caddy=CaddySpec(path_prefix=path, host=args.host))
|
||||
|
||||
config.services[name] = ServiceSpec(
|
||||
id=name,
|
||||
program=args.program,
|
||||
description=args.description or None,
|
||||
run=run,
|
||||
expose=expose,
|
||||
proxy=proxy,
|
||||
manage=ManageSpec(systemd=SystemdSpec()),
|
||||
)
|
||||
save_config(config)
|
||||
|
||||
print(f"Created service '{name}'.")
|
||||
print(f" runs: {args.runner} ({args.run or args.program or name})")
|
||||
if expose:
|
||||
print(f" port: {args.port}" + (f" (env: {args.port_env})" if args.port_env else ""))
|
||||
if proxy and proxy.caddy:
|
||||
if proxy.caddy.path_prefix:
|
||||
print(f" proxy: {proxy.caddy.path_prefix}")
|
||||
if proxy.caddy.host:
|
||||
print(f" host: {proxy.caddy.host}")
|
||||
print(f"\nNext: castle service deploy {name} && castle service start {name}")
|
||||
return 0
|
||||
|
||||
|
||||
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"):
|
||||
print(err)
|
||||
return 1
|
||||
|
||||
run = _run_spec(args.runner, args.run or args.program or name, name)
|
||||
|
||||
config.jobs[name] = JobSpec(
|
||||
id=name,
|
||||
program=args.program,
|
||||
description=args.description or None,
|
||||
run=run,
|
||||
schedule=args.schedule,
|
||||
manage=ManageSpec(systemd=SystemdSpec()),
|
||||
)
|
||||
save_config(config)
|
||||
|
||||
print(f"Created job '{name}'.")
|
||||
print(f" runs: {args.runner} ({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