Add fixture-based health plans

This commit is contained in:
jwinterm
2026-07-06 06:02:58 -04:00
parent 1641f2009d
commit c52e700b6f
7 changed files with 409 additions and 0 deletions
+67
View File
@@ -14,6 +14,7 @@ from .audit import audit_gateway
from .build import build_gateway
from .config import load_config
from .docsgen import generate_docs
from .health import HealthCheck, blocked_health_checks, health_plan
from .install_plan import (
InstallStep,
blocked_install_steps,
@@ -105,6 +106,28 @@ def main(argv: list[str] | None = None) -> int:
action="store_true",
help="Write managed daemon config and supervisor artifacts to the plan root.",
)
health_parser = subcommands.add_parser(
"health-plan",
help="Print fixture-based transport health checks.",
)
_add_target_selection(health_parser)
health_parser.add_argument(
"--mode",
choices=("published", "preview"),
default="published",
help="Plan checks against published transport URLs or local preview URLs.",
)
health_parser.add_argument(
"--base-port",
type=int,
default=19080,
help="First loopback port assigned to preview health checks.",
)
health_parser.add_argument(
"--host",
default="127.0.0.1",
help="Loopback host for preview health checks.",
)
build_parser = subcommands.add_parser("build", help="Build enabled protocol outputs.")
_add_target_selection(build_parser)
fixture_parser = subcommands.add_parser("manifest", help="Write AMPB fixture manifests.")
@@ -195,6 +218,8 @@ def main(argv: list[str] | None = None) -> int:
return _cmd_apply(config, args)
if args.command == "install-plan":
return _cmd_install_plan(config, args)
if args.command == "health-plan":
return _cmd_health_plan(config, args)
if args.command == "build":
return _cmd_build(config)
if args.command == "manifest":
@@ -272,6 +297,28 @@ def _cmd_install_plan(config, args) -> int:
return 1 if blocked else 0
def _cmd_health_plan(config, args) -> int:
checks = health_plan(
config,
mode=args.mode,
base_port=args.base_port,
host=args.host,
)
for check in checks:
_print_health_check(check)
blocked = blocked_health_checks(checks)
print(
"AMPG_HEALTH_SUMMARY "
f"mode={args.mode} "
f"sites={len(config.sites)} "
f"checks={len(checks)} "
f"planned={sum(1 for check in checks if check.status == 'planned')} "
f"review={sum(1 for check in checks if check.status == 'review')} "
f"blocked={len(blocked)}"
)
return 1 if blocked else 0
def _add_target_selection(parser) -> None:
parser.add_argument(
"--profile",
@@ -388,6 +435,26 @@ def _cmd_build(config) -> int:
return 0
def _print_health_check(check: HealthCheck) -> None:
print(
"AMPG_HEALTH_CHECK "
f"site={check.site_id} "
f"protocol={check.protocol} "
f"mode={check.mode} "
f"route={check.route} "
f"url=\"{_quote(check.url)}\" "
f"output_root=\"{_quote(str(check.output_root))}\" "
f"output_path={check.output_path} "
f"transport={check.transport} "
f"profile={check.profile} "
f"address_status={check.address_status} "
f"status={check.status} "
f"action={check.action} "
f"command=\"{_quote(check.command)}\" "
f"message=\"{_quote(check.message)}\""
)
def _print_install_step(step: InstallStep) -> None:
print(
"AMPG_INSTALL_STEP "
+122
View File
@@ -0,0 +1,122 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from .config import GatewayConfig, SiteConfig
from .manifest import fixture_manifest
from .preview import preview_endpoints, preview_fixture_manifest
@dataclass(frozen=True)
class HealthCheck:
site_id: str
protocol: str
mode: str
url: str
output_root: Path
output_path: str
transport: str
profile: str
address_status: str
route: str
status: str
action: str
command: str
message: str
def health_plan(
config: GatewayConfig,
*,
mode: str = "published",
base_port: int = 19080,
host: str = "127.0.0.1",
) -> list[HealthCheck]:
if mode not in {"published", "preview"}:
raise ValueError(f"unsupported health mode {mode!r}")
endpoints = preview_endpoints(config, base_port=base_port, host=host) if mode == "preview" else []
checks: list[HealthCheck] = []
for site in config.sites:
manifest = _manifest_for_mode(site, mode=mode, endpoints=endpoints)
for fixture in manifest["fixtures"]:
checks.append(_health_check(site, fixture, mode=mode))
return checks
def blocked_health_checks(checks: list[HealthCheck]) -> list[HealthCheck]:
return [check for check in checks if check.status == "blocked"]
def _manifest_for_mode(
site: SiteConfig,
*,
mode: str,
endpoints,
) -> dict[str, Any]:
if mode == "preview":
return preview_fixture_manifest(site, endpoints)
return fixture_manifest(site)
def _health_check(site: SiteConfig, fixture: dict[str, Any], *, mode: str) -> HealthCheck:
protocol = str(fixture["protocol"])
output_root = site.outputs.root / protocol
route = str(fixture.get("route", {}).get("fixture_path", "/"))
transport = str(fixture["checks"]["transport"])
profile = str(fixture["checks"]["profile"])
address_status = str(fixture["address_status"])
output_ready = (output_root / ".ampg-output").exists()
preview_status = fixture.get("preview", {}).get("status")
if not output_ready or preview_status == "missing-output":
status = "blocked"
action = "build-output"
message = f"generated output is missing: {output_root}"
elif address_status == "placeholder":
status = "review"
action = "configure-address"
message = "fixture uses a placeholder address; update protocol URL after daemon identity exists"
else:
status = "planned"
action = "check-fixture"
message = (
"verify fixture through the local preview server"
if mode == "preview"
else "verify fixture through the selected transport/profile after services start"
)
return HealthCheck(
site_id=site.id,
protocol=protocol,
mode=mode,
url=str(fixture["url"]),
output_root=output_root,
output_path=str(fixture["output_path"]),
transport=transport,
profile=profile,
address_status=address_status,
route=route,
status=status,
action=action,
command=_check_command(str(fixture["url"]), transport=transport, profile=profile),
message=message,
)
def _check_command(url: str, *, transport: str, profile: str) -> str:
if transport == "clearnet":
return f"curl -fsSL {url}"
if transport == "tor":
return f"curl --proxy socks5h://127.0.0.1:9050 -fsSL {url}"
if transport == "i2p":
return f"curl --proxy http://127.0.0.1:4444 -fsSL {url}"
if transport == "gemini":
return f"gemini fetch {url}"
if transport == "ipfs":
return f"ipfs cat {url}"
if transport == "reticulum":
return f"rns-fetch {url}"
return f"fetch {url} with {profile}"