core: drop pre-migration read-compat now both nodes are current-format
Both machines (civil, primer) are on the current on-disk layout, so the read-compat for older formats is dead weight. Remove it: - config loader: only deployments/<kind>/<name>.yaml (drop flat deployments/*.yaml and the services/+jobs/ split); drop the run.runner→manager normalizer. - registry loader: require composite <kind>/<name> keys + manager/kind (registry.yaml is regenerated every apply, so it's always current). - manifest: drop the proxy/public→reach legacy input and the component→program alias (the derived proxy/public read accessors stay). reach field defaults already match. - config_editor: parse incoming specs directly (frontend sends the current shape). - save_config: drop the flat-file migration cleanup. Bootstrap seeds move to bootstrap/deployments/<kind>/ and install.sh seeds that per-kind tree directly. The legacy→current translation for terse test fixtures moves into each test conftest (out of production). All suites green (362).
This commit is contained in:
@@ -69,20 +69,68 @@ def nats_url() -> Generator[str, None, None]:
|
||||
subprocess.run(["docker", "rm", "-f", name], capture_output=True)
|
||||
|
||||
|
||||
def _modernize_deployment(spec: dict) -> dict:
|
||||
"""Translate a test's terse legacy deployment dict to the current
|
||||
manager-discriminated shape (production dropped this read-compat post-migration).
|
||||
``proxy``/``public`` → ``reach``; ``run.runner`` → ``manager`` (+ ``run.launcher``)."""
|
||||
d = dict(spec)
|
||||
proxy = bool(d.pop("proxy", False))
|
||||
public = bool(d.pop("public", False))
|
||||
if "reach" not in d:
|
||||
if public:
|
||||
d["reach"] = "public"
|
||||
elif proxy:
|
||||
d["reach"] = "internal"
|
||||
if "manager" not in d:
|
||||
run = dict(d.pop("run", None) or {})
|
||||
runner = run.get("runner")
|
||||
if runner == "static":
|
||||
d["manager"] = "caddy"
|
||||
if run.get("root"):
|
||||
d["root"] = run["root"]
|
||||
elif runner == "path":
|
||||
d["manager"] = "path"
|
||||
elif runner == "remote":
|
||||
d["manager"] = "none"
|
||||
for k in ("base_url", "health_url"):
|
||||
if run.get(k):
|
||||
d[k] = run[k]
|
||||
else:
|
||||
launch = {k: v for k, v in run.items() if k != "runner"}
|
||||
launch["launcher"] = runner
|
||||
d["manager"] = "systemd"
|
||||
d["run"] = launch
|
||||
return d
|
||||
|
||||
|
||||
def _store_for(spec: dict) -> str:
|
||||
if spec.get("schedule"):
|
||||
return "jobs"
|
||||
return {"systemd": "services", "caddy": "statics", "path": "tools", "none": "references"}[
|
||||
spec["manager"]
|
||||
]
|
||||
|
||||
|
||||
def _write_castle_config(root: Path, config: dict) -> None:
|
||||
"""Scatter a nested castle config dict into the directory-per-resource layout."""
|
||||
"""Scatter a nested castle config dict into the on-disk layout: castle.yaml globals,
|
||||
programs/<name>.yaml, and deployments/<kind>/<name>.yaml (fields modernized)."""
|
||||
globals_data = {k: v for k, v in config.items() if k in ("gateway", "repo")}
|
||||
(root / "castle.yaml").write_text(yaml.dump(globals_data, default_flow_style=False))
|
||||
for section in ("programs", "services", "jobs"):
|
||||
entries = config.get(section) or {}
|
||||
if not entries:
|
||||
continue
|
||||
section_dir = root / section
|
||||
section_dir.mkdir(parents=True, exist_ok=True)
|
||||
for name, spec in entries.items():
|
||||
(section_dir / f"{name}.yaml").write_text(
|
||||
programs = config.get("programs") or {}
|
||||
if programs:
|
||||
(root / "programs").mkdir(parents=True, exist_ok=True)
|
||||
for name, spec in programs.items():
|
||||
(root / "programs" / f"{name}.yaml").write_text(
|
||||
yaml.dump(spec, default_flow_style=False)
|
||||
)
|
||||
for section in ("services", "jobs", "deployments"):
|
||||
for name, spec in (config.get(section) or {}).items():
|
||||
modern = _modernize_deployment(spec)
|
||||
store_dir = root / "deployments" / _store_for(modern)
|
||||
store_dir.mkdir(parents=True, exist_ok=True)
|
||||
(store_dir / f"{name}.yaml").write_text(
|
||||
yaml.dump(modern, default_flow_style=False)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -161,7 +209,7 @@ def registry_path(tmp_path: Path, castle_root: Path) -> Generator[Path, None, No
|
||||
gateway_port=9000,
|
||||
),
|
||||
deployed={
|
||||
"test-svc": Deployment(
|
||||
NodeRegistry.key("service", "test-svc"): Deployment(
|
||||
manager="systemd",
|
||||
launcher="python",
|
||||
run_cmd=["uv", "run", "test-svc"],
|
||||
@@ -170,6 +218,7 @@ def registry_path(tmp_path: Path, castle_root: Path) -> Generator[Path, None, No
|
||||
"TEST_SVC_DATA_DIR": "/home/user/.castle/data/test-svc",
|
||||
},
|
||||
description="Test service",
|
||||
name="test-svc",
|
||||
kind="service",
|
||||
port=19000,
|
||||
health_path="/health",
|
||||
@@ -177,10 +226,11 @@ def registry_path(tmp_path: Path, castle_root: Path) -> Generator[Path, None, No
|
||||
managed=True,
|
||||
),
|
||||
# A deployed tool (path) — must NOT leak into the /services list.
|
||||
"test-tool": Deployment(
|
||||
NodeRegistry.key("tool", "test-tool"): Deployment(
|
||||
manager="path",
|
||||
run_cmd=[],
|
||||
description="Test tool",
|
||||
name="test-tool",
|
||||
kind="tool",
|
||||
),
|
||||
},
|
||||
|
||||
@@ -74,9 +74,12 @@ def public_client(
|
||||
"reach": "internal",
|
||||
},
|
||||
}
|
||||
(root / "deployments").mkdir()
|
||||
_store = {"caddy": "statics", "path": "tools", "none": "references"}
|
||||
for name, spec in deps.items():
|
||||
(root / "deployments" / f"{name}.yaml").write_text(yaml.dump(spec))
|
||||
store = "jobs" if spec.get("schedule") else _store.get(spec["manager"], "services")
|
||||
store_dir = root / "deployments" / store
|
||||
store_dir.mkdir(parents=True, exist_ok=True)
|
||||
(store_dir / f"{name}.yaml").write_text(yaml.dump(spec))
|
||||
|
||||
reg = NodeRegistry(
|
||||
node=NodeConfig(
|
||||
@@ -88,38 +91,42 @@ def public_client(
|
||||
public_domain="pub.test",
|
||||
),
|
||||
deployed={
|
||||
"calc": Deployment(
|
||||
NodeRegistry.key("static", "calc"): Deployment(
|
||||
manager="caddy",
|
||||
run_cmd=[],
|
||||
name="calc",
|
||||
kind="static",
|
||||
subdomain="calc",
|
||||
public=True,
|
||||
static_root=str(root / "calc" / "public"),
|
||||
),
|
||||
"web": Deployment(
|
||||
NodeRegistry.key("service", "web"): Deployment(
|
||||
manager="systemd",
|
||||
launcher="python",
|
||||
run_cmd=["x"],
|
||||
name="web",
|
||||
kind="service",
|
||||
port=9001,
|
||||
subdomain="web",
|
||||
public=True,
|
||||
managed=True,
|
||||
),
|
||||
"intern": Deployment(
|
||||
NodeRegistry.key("service", "intern"): Deployment(
|
||||
manager="systemd",
|
||||
launcher="python",
|
||||
run_cmd=["x"],
|
||||
name="intern",
|
||||
kind="service",
|
||||
port=9002,
|
||||
subdomain="intern",
|
||||
public=False,
|
||||
managed=True,
|
||||
),
|
||||
"pg": Deployment(
|
||||
NodeRegistry.key("service", "pg"): Deployment(
|
||||
manager="systemd",
|
||||
launcher="container",
|
||||
run_cmd=["x"],
|
||||
name="pg",
|
||||
kind="service",
|
||||
port=None,
|
||||
subdomain=None,
|
||||
|
||||
@@ -21,15 +21,17 @@ from castle_core.config import load_config
|
||||
# so the trio passes subdomain-uniqueness validation.
|
||||
_SVC = {
|
||||
"program": "backup",
|
||||
"run": {"runner": "python", "program": "backup"},
|
||||
"manager": "systemd",
|
||||
"run": {"launcher": "python", "program": "backup"},
|
||||
"manage": {"systemd": {}},
|
||||
}
|
||||
_JOB = {
|
||||
"program": "backup",
|
||||
"run": {"runner": "command", "argv": ["backup"]},
|
||||
"manager": "systemd",
|
||||
"run": {"launcher": "command", "argv": ["backup"]},
|
||||
"schedule": "0 3 * * *",
|
||||
}
|
||||
_TOOL = {"program": "backup", "run": {"runner": "path"}}
|
||||
_TOOL = {"program": "backup", "manager": "path"}
|
||||
|
||||
|
||||
def _put(client: TestClient, section: str, name: str, cfg: dict) -> None:
|
||||
|
||||
Reference in New Issue
Block a user