core+cli: castle apply — one converge verb replaces deploy/start/enable/…
Reconcile the running system to declared config in a single operation, replacing the old deploy && start plus the per-kind enable/disable/ install/uninstall verbs. The mechanism varies by manager (systemd unit / PATH install / gateway route); the verb never does. Core (castle_core.deploy.apply): - render (units, Caddyfile, tunnel) then reconcile: activate every enabled deployment that's down, restart any whose unit bytes changed, deactivate the disabled ones. Returns ApplyResult (the enacted diff). - --plan computes the diff and touches nothing. - restart-on-change is exact: _render_unit_files is the single source of truth for unit bytes, used both to write units and to predict restarts, so the plan can't drift from what deploy writes. Desired state: - DeploymentBase.enabled (default True) — the declarative on/off. apply converges to it; a disabled deployment is defined but not running and gets no gateway route (else it would 502). Registry omits enabled when True, keeping existing registries byte-identical. CLI: - add `castle apply [name] [--plan]`; keep `restart` as the one imperative bounce, plus status/doctor/logs/gateway/program. - retire deploy, start, stop (top-level); service/job deploy|enable| disable|start|stop; tool install|uninstall; program install|uninstall. - next-step strings + doctor hints now say `castle apply`. Verified: core 129 + cli 31 green; live `castle apply` on a converged node reports "nothing to do" with no spurious restarts; --plan shows an empty diff; gateway + api stay up.
This commit is contained in:
60
cli/src/castle_cli/commands/apply.py
Normal file
60
cli/src/castle_cli/commands/apply.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""castle apply — converge the running system to match config.
|
||||
|
||||
The one workhorse verb: renders systemd units + the Caddyfile + tunnel config,
|
||||
then reconciles the runtime (activate what's enabled and down, restart what
|
||||
changed, deactivate what's disabled). Replaces the old `deploy && start` plus the
|
||||
per-kind enable/disable/install/uninstall verbs.
|
||||
|
||||
`--plan` computes and prints the diff without writing or touching the runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
_C = {
|
||||
"activate": "\033[32m", # green
|
||||
"restart": "\033[33m", # yellow
|
||||
"deactivate": "\033[31m", # red
|
||||
"reset": "\033[0m",
|
||||
"dim": "\033[90m",
|
||||
}
|
||||
|
||||
|
||||
def _line(verb_color: str, verb: str, names: list[str]) -> None:
|
||||
if not names:
|
||||
return
|
||||
print(f" {verb_color}{verb}{_C['reset']} {', '.join(sorted(names))}")
|
||||
|
||||
|
||||
def run_apply(args: argparse.Namespace) -> int:
|
||||
from castle_core.deploy import apply
|
||||
|
||||
target = getattr(args, "name", None)
|
||||
plan = getattr(args, "plan", False)
|
||||
|
||||
result = apply(target_name=target, plan=plan)
|
||||
|
||||
# Surface any warnings the render produced (acme prerequisites, tunnel notes).
|
||||
for msg in result.messages:
|
||||
if msg.startswith("Warning"):
|
||||
print(f" {_C['dim']}{msg}{_C['reset']}")
|
||||
|
||||
if plan:
|
||||
print("\n\033[1mPlan\033[0m " + _C["dim"] + "(no changes made)" + _C["reset"])
|
||||
if not result.changed:
|
||||
print(f" {_C['dim']}nothing to do — already converged{_C['reset']}")
|
||||
return 0
|
||||
_line(_C["activate"], "would activate ", result.activated)
|
||||
_line(_C["restart"], "would restart ", result.restarted)
|
||||
_line(_C["deactivate"], "would deactivate", result.deactivated)
|
||||
return 0
|
||||
|
||||
print("\n\033[1mApplied\033[0m")
|
||||
if not result.changed:
|
||||
print(f" {_C['dim']}nothing to do — already converged{_C['reset']}")
|
||||
return 0
|
||||
_line(_C["activate"], "activated ", result.activated)
|
||||
_line(_C["restart"], "restarted ", result.restarted)
|
||||
_line(_C["deactivate"], "deactivated", result.deactivated)
|
||||
return 0
|
||||
Reference in New Issue
Block a user