Files
adaptive-multi-protocol-gat…/ampg/apply.py
T
2026-07-06 18:49:34 -04:00

138 lines
3.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import shutil
from .config import GatewayConfig
from .install_plan import InstallStateCopy, install_state_copies
from .platforms import PlatformProvider
from .state_contract import gateway_state_root
from .status import DaemonProbeFunc
@dataclass(frozen=True)
class StateApplyResult:
site_id: str
protocol: str
platform: str
kind: str
source: Path
target: Path
status: str
mode: str
message: str
def apply_state(
config: GatewayConfig,
*,
dry_run: bool,
platform_provider: PlatformProvider | None = None,
daemon_probe: DaemonProbeFunc | None = None,
) -> list[StateApplyResult]:
copies = install_state_copies(
config,
platform_provider=platform_provider,
daemon_probe=daemon_probe,
)
results: list[StateApplyResult] = []
for copy in copies:
validation = _validate_state_copy(config, copy, dry_run=dry_run)
if validation.status == "blocked":
results.append(validation)
continue
if not dry_run:
copy.target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(copy.source, copy.target)
results.append(
StateApplyResult(
site_id=copy.site_id,
protocol=copy.protocol,
platform=copy.platform,
kind=copy.kind,
source=copy.source,
target=copy.target,
status="planned" if dry_run else "written",
mode="dry-run" if dry_run else "live",
message=(
"would copy approved artifact into AMPG-owned state"
if dry_run
else "copied approved artifact into AMPG-owned state"
),
)
)
return results
def _validate_state_copy(
config: GatewayConfig,
copy: InstallStateCopy,
*,
dry_run: bool,
) -> StateApplyResult:
if copy.status != "planned":
return _result(
copy,
status="blocked",
mode="dry-run" if dry_run else "live",
message=copy.message,
)
state_root = gateway_state_root(config).resolve()
target = copy.target.resolve()
if not _is_relative_to(target, state_root):
return _result(
copy,
status="blocked",
mode="dry-run" if dry_run else "live",
message=f"target is outside AMPG state root: {target}",
)
if not copy.source.exists():
return _result(
copy,
status="blocked",
mode="dry-run" if dry_run else "live",
message="approved source artifact is missing",
)
if copy.source.is_dir():
return _result(
copy,
status="blocked",
mode="dry-run" if dry_run else "live",
message="approved source artifact is a directory",
)
return _result(
copy,
status="ready",
mode="dry-run" if dry_run else "live",
message="state copy passed safety checks",
)
def _result(
copy: InstallStateCopy,
*,
status: str,
mode: str,
message: str,
) -> StateApplyResult:
return StateApplyResult(
site_id=copy.site_id,
protocol=copy.protocol,
platform=copy.platform,
kind=copy.kind,
source=copy.source,
target=copy.target,
status=status,
mode=mode,
message=message,
)
def _is_relative_to(path: Path, parent: Path) -> bool:
try:
path.relative_to(parent)
except ValueError:
return False
return True