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:
@@ -72,6 +72,9 @@ class Deployment:
|
||||
base_url: str | None = None
|
||||
schedule: str | None = None
|
||||
managed: bool = False
|
||||
# Declared desired state (from the deployment's `enabled:`). `castle apply`
|
||||
# activates enabled deployments and deactivates disabled ones. Default True.
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -155,6 +158,7 @@ def load_registry(path: Path | None = None) -> NodeRegistry:
|
||||
base_url=comp_data.get("base_url"),
|
||||
schedule=comp_data.get("schedule"),
|
||||
managed=comp_data.get("managed", False),
|
||||
enabled=comp_data.get("enabled", True),
|
||||
)
|
||||
|
||||
return NodeRegistry(node=node, deployed=deployed)
|
||||
@@ -225,6 +229,10 @@ def save_registry(registry: NodeRegistry, path: Path | None = None) -> None:
|
||||
entry["schedule"] = comp.schedule
|
||||
if comp.managed:
|
||||
entry["managed"] = comp.managed
|
||||
# Only emit when disabled — default-True omission keeps existing
|
||||
# registries byte-identical and matches the load-side default.
|
||||
if not comp.enabled:
|
||||
entry["enabled"] = comp.enabled
|
||||
data["deployed"][name] = entry
|
||||
|
||||
with open(path, "w") as f:
|
||||
|
||||
Reference in New Issue
Block a user