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:
@@ -352,42 +352,9 @@ def _parse_program(name: str, data: dict) -> ProgramSpec:
|
||||
return ProgramSpec.model_validate(data_copy)
|
||||
|
||||
|
||||
def _normalize_deployment_dict(data: dict) -> dict:
|
||||
"""Map a legacy service/job entry to the manager-discriminated shape.
|
||||
|
||||
Legacy entries carry `run.runner` (including static/path/remote); new entries
|
||||
carry `manager` and (for systemd) `run.launcher`. New-shape entries pass through.
|
||||
"""
|
||||
if "manager" in data:
|
||||
return data
|
||||
d = dict(data)
|
||||
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"
|
||||
if run.get("base_url"):
|
||||
d["base_url"] = run["base_url"]
|
||||
if run.get("health_url"):
|
||||
d["health_url"] = run["health_url"]
|
||||
else:
|
||||
# A process launcher (python/command/container/compose/node) → systemd.
|
||||
d["manager"] = "systemd"
|
||||
launch = {k: v for k, v in run.items() if k != "runner"}
|
||||
launch["launcher"] = runner
|
||||
d["run"] = launch
|
||||
return d
|
||||
|
||||
|
||||
def _parse_deployment(name: str, data: dict) -> DeploymentSpec:
|
||||
"""Parse a deployment entry (new or legacy shape) into a DeploymentSpec."""
|
||||
data_copy = _normalize_deployment_dict(data)
|
||||
data_copy = dict(data_copy)
|
||||
"""Parse a deployment entry (manager-discriminated shape) into a DeploymentSpec."""
|
||||
data_copy = dict(data)
|
||||
data_copy["id"] = name
|
||||
return _DEPLOYMENT_ADAPTER.validate_python(data_copy)
|
||||
|
||||
@@ -515,31 +482,16 @@ def _validate_subdomains(stores: dict[str, dict[str, DeploymentSpec]]) -> None:
|
||||
def _load_deployments(root: Path) -> dict[str, dict[str, DeploymentSpec]]:
|
||||
"""Load the per-kind deployment stores for a config root.
|
||||
|
||||
New layout: ``deployments/<store>/<name>.yaml`` (store = services|jobs|tools|
|
||||
statics|references). Read-compat for the pre-migration layouts: flat
|
||||
``deployments/*.yaml`` files, and the older ``services/``+``jobs/`` split. Every
|
||||
file is routed to its store by ``kind_for(spec)`` — the dir is a namespace, the
|
||||
spec's manager/schedule is the source of truth (they must agree post-migration).
|
||||
Layout: ``deployments/<store>/<name>.yaml`` (store = services|jobs|tools|statics|
|
||||
references). Every file is routed to its store by ``kind_for(spec)`` — the dir is
|
||||
a namespace, the spec's manager/schedule is the source of truth.
|
||||
"""
|
||||
stores: dict[str, dict[str, DeploymentSpec]] = {s: {} for s in _KIND_STORE.values()}
|
||||
|
||||
def route(name: str, data: dict) -> None:
|
||||
spec = _parse_deployment(name, data)
|
||||
stores[_KIND_STORE[kind_for(spec)]][name] = spec
|
||||
|
||||
dep_dir = root / "deployments"
|
||||
# New per-kind subdirs.
|
||||
for store in _KIND_STORE.values():
|
||||
for name, data in _load_resource_dir(dep_dir / store).items():
|
||||
route(name, data)
|
||||
# Legacy flat deployments/*.yaml (top-level only — subdirs handled above).
|
||||
for name, data in _load_resource_dir(dep_dir).items():
|
||||
route(name, data)
|
||||
# Oldest layout: services/ + jobs/ dirs, only if there's no deployments/ dir.
|
||||
if not dep_dir.is_dir():
|
||||
for legacy in ("services", "jobs"):
|
||||
for name, data in _load_resource_dir(root / legacy).items():
|
||||
route(name, data)
|
||||
spec = _parse_deployment(name, data)
|
||||
stores[_KIND_STORE[kind_for(spec)]][name] = spec
|
||||
return stores
|
||||
|
||||
|
||||
@@ -750,10 +702,6 @@ def save_config(config: CastleConfig) -> None:
|
||||
dep_dir / store,
|
||||
{n: _spec_to_yaml_dict(d) for n, d in config.store_for(kind).items()},
|
||||
)
|
||||
# Migration cleanup: drop any pre-migration flat deployments/*.yaml files.
|
||||
if dep_dir.is_dir():
|
||||
for path in dep_dir.glob("*.yaml"):
|
||||
path.unlink()
|
||||
|
||||
|
||||
def ensure_dirs(config: CastleConfig) -> None:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal, Union
|
||||
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
EnvMap = dict[str, str]
|
||||
|
||||
@@ -25,8 +25,7 @@ class Reach(str, Enum):
|
||||
``public`` → *also* projected to the internet (HTTP via the tunnel origin;
|
||||
TCP via ``cloudflared access tcp``). Implies ``internal``.
|
||||
|
||||
Replaces the old ``proxy``/``public`` booleans; ``proxy``/``public`` survive as
|
||||
derived read-only accessors and as accepted *legacy input* (normalized below).
|
||||
``proxy``/``public`` survive as derived read-only accessors on the deployment.
|
||||
"""
|
||||
|
||||
OFF = "off"
|
||||
@@ -34,29 +33,6 @@ class Reach(str, Enum):
|
||||
PUBLIC = "public"
|
||||
|
||||
|
||||
def _reach_from_legacy(data: object, default: Reach) -> object:
|
||||
"""Map legacy ``proxy``/``public`` booleans on a raw deployment dict to ``reach``.
|
||||
|
||||
Runs as a ``mode="before"`` validator. When ``reach`` is given explicitly it
|
||||
wins (legacy keys are dropped); otherwise ``reach`` is derived from the old
|
||||
booleans: ``public`` → PUBLIC, ``proxy`` → INTERNAL, else ``default``. Non-dict
|
||||
input (e.g. model re-validation) passes through untouched.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
d = dict(data)
|
||||
proxy = bool(d.pop("proxy", False))
|
||||
public = bool(d.pop("public", False))
|
||||
if "reach" not in d:
|
||||
if public:
|
||||
d["reach"] = Reach.PUBLIC
|
||||
elif proxy:
|
||||
d["reach"] = Reach.INTERNAL
|
||||
else:
|
||||
d["reach"] = default
|
||||
return d
|
||||
|
||||
|
||||
# ---------------------
|
||||
# Launch specs — how systemd starts a process (discriminated union on `launcher`)
|
||||
# ---------------------
|
||||
@@ -416,10 +392,8 @@ class DeploymentBase(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
id: str = ""
|
||||
# The program this deployment materializes. (`component` = legacy alias.)
|
||||
program: str | None = Field(
|
||||
default=None, validation_alias=AliasChoices("program", "component")
|
||||
)
|
||||
# The program this deployment materializes.
|
||||
program: str | None = None
|
||||
description: str | None = None
|
||||
defaults: DefaultsSpec | None = None
|
||||
# Deployment-to-deployment preconditions: other deployments this one needs
|
||||
@@ -447,11 +421,6 @@ class SystemdDeployment(DeploymentBase):
|
||||
reach: Reach = Reach.OFF
|
||||
manage: ManageSpec | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_reach(cls, data: object) -> object:
|
||||
return _reach_from_legacy(data, default=Reach.OFF)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_reach(self) -> SystemdDeployment:
|
||||
# An exposed reach needs a port to expose. Without an `expose` block the
|
||||
@@ -519,11 +488,6 @@ class CaddyDeployment(DeploymentBase):
|
||||
# `internal` or `public` (never `off`). `public` = also project via the tunnel.
|
||||
reach: Reach = Reach.INTERNAL
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_reach(cls, data: object) -> object:
|
||||
return _reach_from_legacy(data, default=Reach.INTERNAL)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_reach(self) -> CaddyDeployment:
|
||||
if self.reach == Reach.OFF:
|
||||
|
||||
@@ -167,34 +167,12 @@ def load_registry(path: Path | None = None) -> NodeRegistry:
|
||||
|
||||
deployed: dict[str, Deployment] = {}
|
||||
for key, comp_data in data.get("deployed", {}).items():
|
||||
# Key is the composite "<kind>/<name>" (new) or a bare name (legacy).
|
||||
key_kind, name = key.split("/", 1) if "/" in key else (None, key)
|
||||
# New shape carries manager/launcher/kind; legacy carries runner/behavior.
|
||||
manager = comp_data.get("manager")
|
||||
launcher = comp_data.get("launcher")
|
||||
if manager is None:
|
||||
runner = comp_data.get("runner", "command")
|
||||
manager = {"static": "caddy", "path": "path", "remote": "none"}.get(
|
||||
runner, "systemd"
|
||||
)
|
||||
if manager == "systemd":
|
||||
launcher = runner
|
||||
kind = comp_data.get("kind") or key_kind
|
||||
if kind is None:
|
||||
behavior = comp_data.get("behavior")
|
||||
if comp_data.get("schedule"):
|
||||
kind = "job"
|
||||
elif manager == "caddy" or behavior == "frontend":
|
||||
kind = "static"
|
||||
elif manager == "path" or behavior == "tool":
|
||||
kind = "tool"
|
||||
elif manager == "none":
|
||||
kind = "reference"
|
||||
else:
|
||||
kind = "service"
|
||||
# Key is the composite "<kind>/<name>"; manager/kind are always present
|
||||
# (save_registry writes them). registry.yaml is regenerated every apply.
|
||||
kind, name = key.split("/", 1)
|
||||
deployed[NodeRegistry.key(kind, name)] = Deployment(
|
||||
manager=manager,
|
||||
launcher=launcher,
|
||||
manager=comp_data["manager"],
|
||||
launcher=comp_data.get("launcher"),
|
||||
run_cmd=comp_data.get("run_cmd", []),
|
||||
stop_cmd=comp_data.get("stop_cmd", []),
|
||||
env=comp_data.get("env", {}),
|
||||
|
||||
Reference in New Issue
Block a user