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.
40 lines
1.5 KiB
Python
40 lines
1.5 KiB
Python
"""Tests for castle doctor."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from argparse import Namespace
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from castle_cli.commands.doctor import run_doctor
|
|
|
|
|
|
class TestDoctor:
|
|
"""The diagnosis path — a bare, unconfigured node should fail loudly."""
|
|
|
|
def test_bare_node_reports_problems(self, castle_root: Path, capsys: object) -> None:
|
|
"""No repo:, no control plane, nothing running → exit 1 with fix hints."""
|
|
from castle_cli.config import load_config
|
|
|
|
# The shared fixture has no repo: and no castle-gateway/api/dashboard, so the
|
|
# Configuration and Runtime sections must FAIL. Patch where doctor imports it.
|
|
with patch("castle_core.config.load_config", return_value=load_config(castle_root)):
|
|
result = run_doctor(Namespace())
|
|
|
|
assert result == 1
|
|
out = capsys.readouterr().out # type: ignore[attr-defined]
|
|
assert "repo: not set" in out
|
|
assert "control plane missing" in out
|
|
# Every failing check offers a concrete next command.
|
|
assert "castle apply" in out
|
|
|
|
def test_load_failure_is_first_fail(self, capsys: object) -> None:
|
|
"""A castle.yaml that won't load is surfaced as a FAIL, not a traceback."""
|
|
with patch("castle_core.config.load_config", side_effect=ValueError("bad yaml")):
|
|
result = run_doctor(Namespace())
|
|
|
|
assert result == 1
|
|
out = capsys.readouterr().out # type: ignore[attr-defined]
|
|
assert "failed to load" in out
|
|
assert "bad yaml" in out
|