"""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 ( DefaultsSpec, ExposeSpec, HttpExposeSpec, HttpInternal, ManageSpec, RunCommand, RunPython, SystemdDeployment, SystemdSpec, ) def _defaults(env_args: list[str] | None) -> DefaultsSpec | None: """Parse repeated --env KEY=VALUE into a DefaultsSpec, or None.""" if not env_args: return None env: dict[str, str] = {} for item in env_args: key, _, value = item.partition("=") env[key.strip()] = value return DefaultsSpec(env=env) 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, 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 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, "service"): print(err) return 1 run = _run_spec(args.launcher, args.run or args.program or name, name) expose = None proxy = False if args.port is not None: expose = ExposeSpec( http=HttpExposeSpec( internal=HttpInternal(port=args.port), health_path=args.health, ) ) # Expose at . (the subdomain is the service name). proxy = not args.no_proxy config.deployments[name] = SystemdDeployment( id=name, manager="systemd", program=args.program, description=args.description or None, run=run, expose=expose, proxy=proxy, manage=ManageSpec(systemd=SystemdSpec()), defaults=_defaults(args.env), ) save_config(config) print(f"Created service '{name}'.") print(f" runs: {args.launcher} ({args.run or args.program or name})") if expose: print(f" port: {args.port}") if proxy: print(f" subdomain: {name}.") 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, "job"): print(err) return 1 run = _run_spec(args.launcher, args.run or args.program or name, name) # 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, schedule=args.schedule, manage=ManageSpec(systemd=SystemdSpec()), defaults=_defaults(args.env), ) save_config(config) print(f"Created job '{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