api: POST /apply converge endpoint; enabled in summaries
- Replace POST /deploy with POST /apply (name + plan), returning the
enacted diff (activated/restarted/deactivated/unchanged). /config/apply
now delegates to core apply() too, so there's one convergence path.
- Retire /services/{name}/start and /stop (keep /restart as the
imperative bounce). Drop install/uninstall from program actions — tool/
static activation is convergence now.
- Surface `enabled` on ServiceSummary/JobSummary/DeploymentSummary so the
UI can show and toggle desired state.
- conftest: drop the obsolete config_editor.get_registry patch (apply_config
no longer reads the registry directly).
59 API tests pass; route table shows /apply (no /deploy), /restart only.
This commit is contained in:
@@ -1,49 +1,67 @@
|
||||
"""Deploy API endpoint."""
|
||||
"""Apply (converge) API endpoint.
|
||||
|
||||
`POST /apply` reconciles the running system to match config — render units/
|
||||
Caddyfile/tunnel, then activate/restart/deactivate to match. It replaces the old
|
||||
`/deploy` (+ separate start/enable calls). `?plan=true` returns the diff without
|
||||
touching anything.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from castle_core.deploy import deploy
|
||||
from castle_core.deploy import apply
|
||||
|
||||
router = APIRouter(tags=["deploy"])
|
||||
router = APIRouter(tags=["apply"])
|
||||
|
||||
|
||||
class DeployRequest(BaseModel):
|
||||
"""Optional request body for deploy."""
|
||||
class ApplyRequest(BaseModel):
|
||||
"""Optional request body for apply."""
|
||||
|
||||
name: str | None = None
|
||||
plan: bool = False
|
||||
|
||||
|
||||
class DeployResponse(BaseModel):
|
||||
"""Response from a deploy operation."""
|
||||
class ApplyResponse(BaseModel):
|
||||
"""The diff a converge enacted (or would enact, for a plan)."""
|
||||
|
||||
status: str
|
||||
deployed_count: int
|
||||
planned: bool
|
||||
changed: bool
|
||||
activated: list[str]
|
||||
restarted: list[str]
|
||||
deactivated: list[str]
|
||||
unchanged: list[str]
|
||||
messages: list[str]
|
||||
|
||||
|
||||
@router.post("/deploy", response_model=DeployResponse)
|
||||
def run_deploy(request: DeployRequest | None = None) -> DeployResponse:
|
||||
"""Deploy services and jobs from castle.yaml to runtime.
|
||||
@router.post("/apply", response_model=ApplyResponse)
|
||||
def run_apply(request: ApplyRequest | None = None) -> ApplyResponse:
|
||||
"""Converge the running system to match castle.yaml.
|
||||
|
||||
Resolves env vars and secrets, generates systemd units and Caddyfile,
|
||||
copies frontend build outputs, and reloads systemd.
|
||||
|
||||
Optionally pass a name to deploy a single service or job.
|
||||
Renders systemd units + the Caddyfile + tunnel config, then reconciles the
|
||||
runtime: activate enabled deployments that are down, restart any whose unit
|
||||
changed, deactivate disabled ones. Pass a name to converge one deployment,
|
||||
or `plan: true` to compute the diff without changing anything.
|
||||
"""
|
||||
target_name = request.name if request else None
|
||||
name = request.name if request else None
|
||||
plan = request.plan if request else False
|
||||
|
||||
try:
|
||||
result = deploy(target_name=target_name)
|
||||
result = apply(target_name=name, plan=plan)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
return DeployResponse(
|
||||
return ApplyResponse(
|
||||
status="ok",
|
||||
deployed_count=result.deployed_count,
|
||||
planned=result.planned,
|
||||
changed=result.changed,
|
||||
activated=result.activated,
|
||||
restarted=result.restarted,
|
||||
deactivated=result.deactivated,
|
||||
unchanged=result.unchanged,
|
||||
messages=result.messages,
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user