mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Plan managed supervisor actions
This commit is contained in:
@@ -151,7 +151,8 @@ config first, then captured addresses, then deterministic placeholders.
|
||||
|
||||
`apply --dry-run` includes an address stage. Placeholder addresses remain review items
|
||||
until the generated transport identity is captured or configured. With `--write-artifacts`,
|
||||
apply also prints the reviewed config artifacts that would be copied into managed state.
|
||||
apply also prints reviewed config artifacts that would be copied into managed state and
|
||||
the supervisor services that would be registered or started.
|
||||
|
||||
Use `--protocol` to scope operational commands to one or more enabled protocols. This
|
||||
lets a full site config build or activate only Tor, only I2P, or a selected subset without
|
||||
|
||||
+19
@@ -26,8 +26,10 @@ from .health import HealthCheck, blocked_health_checks, health_plan
|
||||
from .install_plan import (
|
||||
InstallStateCopy,
|
||||
InstallStep,
|
||||
InstallSupervisorAction,
|
||||
blocked_install_steps,
|
||||
install_state_copies,
|
||||
install_supervisor_actions,
|
||||
install_plan,
|
||||
write_install_artifacts,
|
||||
)
|
||||
@@ -305,6 +307,8 @@ def _cmd_apply(config, args) -> int:
|
||||
print(f"AMPG_APPLY_ARTIFACT path={path} status=written")
|
||||
for copy in install_state_copies(config, platform_provider=platform_provider):
|
||||
_print_install_state_copy(copy)
|
||||
for action in install_supervisor_actions(config, platform_provider=platform_provider):
|
||||
_print_supervisor_action(action)
|
||||
|
||||
steps = activation_steps(config, platform_provider=platform_provider)
|
||||
for step in steps:
|
||||
@@ -543,6 +547,21 @@ def _print_install_state_copy(copy: InstallStateCopy) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _print_supervisor_action(action: InstallSupervisorAction) -> None:
|
||||
print(
|
||||
"AMPG_APPLY_SUPERVISOR "
|
||||
f"site={action.site_id} "
|
||||
f"protocol={action.protocol} "
|
||||
f"platform={action.platform} "
|
||||
f"kind={action.kind} "
|
||||
f"service={action.service} "
|
||||
f"source=\"{_quote(str(action.source))}\" "
|
||||
f"status={action.status} "
|
||||
f"command=\"{_quote(action.command)}\" "
|
||||
f"message=\"{_quote(action.message)}\""
|
||||
)
|
||||
|
||||
|
||||
def _print_activation_step(step: ActivationStep) -> None:
|
||||
print(
|
||||
"AMPG_APPLY_STEP "
|
||||
|
||||
@@ -47,6 +47,19 @@ class InstallStateCopy:
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InstallSupervisorAction:
|
||||
site_id: str
|
||||
protocol: str
|
||||
platform: str
|
||||
kind: str
|
||||
service: str
|
||||
source: Path
|
||||
status: str
|
||||
command: str
|
||||
message: str
|
||||
|
||||
|
||||
def install_plan(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
@@ -143,6 +156,31 @@ def install_state_copies(
|
||||
return copies
|
||||
|
||||
|
||||
def install_supervisor_actions(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
platform_provider: PlatformProvider | None = None,
|
||||
daemon_probe: DaemonProbeFunc | None = None,
|
||||
) -> list[InstallSupervisorAction]:
|
||||
provider = platform_provider or detect_platform()
|
||||
statuses = gateway_status(
|
||||
config,
|
||||
platform_provider=provider,
|
||||
daemon_probe=daemon_probe,
|
||||
)
|
||||
status_by_protocol = {
|
||||
(status.site_id, status.protocol): status for status in statuses
|
||||
}
|
||||
actions: list[InstallSupervisorAction] = []
|
||||
for site in config.sites:
|
||||
for protocol in site.protocols.values():
|
||||
if not protocol.enabled:
|
||||
continue
|
||||
status = status_by_protocol[(site.id, protocol.name)]
|
||||
actions.extend(_protocol_supervisor_actions(config, site, protocol, provider, status))
|
||||
return actions
|
||||
|
||||
|
||||
def _protocol_install_steps(
|
||||
config: GatewayConfig,
|
||||
site: SiteConfig,
|
||||
@@ -351,6 +389,51 @@ def _protocol_state_copies(
|
||||
return copies
|
||||
|
||||
|
||||
def _protocol_supervisor_actions(
|
||||
config: GatewayConfig,
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
status: TransportStatus,
|
||||
) -> list[InstallSupervisorAction]:
|
||||
actions: list[InstallSupervisorAction] = []
|
||||
for artifact in _protocol_install_artifacts(config, site, protocol, provider, status):
|
||||
service_suffix = _supervisor_service_suffix(protocol, artifact)
|
||||
if service_suffix is None:
|
||||
continue
|
||||
service = _artifact_service_name(site, protocol, service_suffix)
|
||||
source_exists = artifact.path.exists()
|
||||
actions.append(
|
||||
InstallSupervisorAction(
|
||||
site_id=site.id,
|
||||
protocol=protocol.name,
|
||||
platform=provider.name,
|
||||
kind=artifact.kind,
|
||||
service=service,
|
||||
source=artifact.path,
|
||||
status="planned" if source_exists else "review",
|
||||
command=_supervisor_start_command(provider, service),
|
||||
message=(
|
||||
"register and start reviewed supervisor after state config is copied"
|
||||
if source_exists
|
||||
else "supervisor artifact is missing; rerun with --write-artifacts first"
|
||||
),
|
||||
)
|
||||
)
|
||||
return actions
|
||||
|
||||
|
||||
def _supervisor_service_suffix(
|
||||
protocol: ProtocolConfig,
|
||||
artifact: InstallArtifact,
|
||||
) -> str | None:
|
||||
if artifact.kind == "daemon-supervisor":
|
||||
return protocol.daemon
|
||||
if artifact.kind == "loopback-supervisor":
|
||||
return "nginx-loopback"
|
||||
return None
|
||||
|
||||
|
||||
def _state_copy_target(
|
||||
config: GatewayConfig,
|
||||
site: SiteConfig,
|
||||
@@ -444,6 +527,18 @@ def _supervisor_command(
|
||||
return "-"
|
||||
|
||||
|
||||
def _supervisor_start_command(provider: PlatformProvider, service: str) -> str:
|
||||
if provider.name == "android-termux":
|
||||
return f"sv-enable {service}"
|
||||
if provider.name == "linux-systemd":
|
||||
return f"sudo systemctl enable --now {service}.service"
|
||||
if provider.name == "macos-launchd":
|
||||
return f"launchctl bootstrap gui/$UID ~/Library/LaunchAgents/org.ampg.{service}.plist"
|
||||
if provider.name == "linux-user":
|
||||
return f"run {service} under the user supervisor"
|
||||
return "-"
|
||||
|
||||
|
||||
def _state_dir(
|
||||
config: GatewayConfig,
|
||||
provider: PlatformProvider,
|
||||
|
||||
@@ -113,8 +113,8 @@ render-only environment while making daemon ownership decisions explicit.
|
||||
activation sequence. Each enabled protocol gets output, artifact, daemon, and health
|
||||
steps, plus an address step when AMPG must capture or confirm a generated transport
|
||||
identity. With `--write-artifacts`, it also prints planned copies from reviewed artifacts
|
||||
into managed state. Blocked steps stop the command before any live apply path can touch
|
||||
services.
|
||||
into managed state and planned supervisor start/reload actions. Blocked steps stop the
|
||||
command before any live apply path can touch services.
|
||||
|
||||
The dry-run command is intentionally stricter than `doctor`: missing generated output is
|
||||
blocked during activation, because AMPG must not point a transport at an unbuilt output
|
||||
|
||||
@@ -86,8 +86,8 @@ Missing output blocks the plan, while placeholder transport addresses are review
|
||||
output readiness, config artifacts to review, daemon action, address capture or reuse,
|
||||
and post-apply health checks. It exits nonzero when any step is blocked. `--write-artifacts`
|
||||
writes reviewable config snippets to the configured plan root, then prints the planned
|
||||
copies from those reviewed artifacts into `gateway.state_dir`. It still does not install
|
||||
or reload services.
|
||||
copies from those reviewed artifacts into `gateway.state_dir` and the supervisor services
|
||||
that would be registered or started. It still does not install or reload services.
|
||||
|
||||
`--protocol` scopes operational commands to selected enabled protocols. A clearnet
|
||||
`adopt` failure will block a full activation run, but it will not block
|
||||
|
||||
@@ -39,6 +39,7 @@ Current managed defaults:
|
||||
|
||||
AMPG must never delete sensitive contract paths without an explicit destructive command.
|
||||
`apply --dry-run --write-artifacts` prints `AMPG_APPLY_STATE_COPY` rows that show which
|
||||
reviewed artifacts would be copied into managed state. Live apply should perform those
|
||||
reviewed artifacts would be copied into managed state and `AMPG_APPLY_SUPERVISOR` rows
|
||||
for the services that would be registered or started. Live apply should perform those
|
||||
reviewed copies, start daemons, capture public addresses, and then run published health
|
||||
checks.
|
||||
|
||||
@@ -247,6 +247,9 @@ daemon_policy = "auto"
|
||||
self.assertIn("kind=loopback-config", output_text)
|
||||
self.assertIn(str(root / ".ampg/state/example/i2p/i2pd-tunnels.conf"), output_text)
|
||||
self.assertIn(str(root / ".ampg/state/example/i2p/nginx-loopback.conf"), output_text)
|
||||
self.assertIn("AMPG_APPLY_SUPERVISOR site=example protocol=i2p", output_text)
|
||||
self.assertIn("service=ampg-example-i2p-i2pd", output_text)
|
||||
self.assertIn("service=ampg-example-i2p-nginx-loopback", output_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -11,6 +11,7 @@ from ampg.install_plan import (
|
||||
install_artifacts,
|
||||
install_plan,
|
||||
install_state_copies,
|
||||
install_supervisor_actions,
|
||||
write_install_artifacts,
|
||||
)
|
||||
from ampg.platforms import platform_by_name
|
||||
@@ -304,6 +305,47 @@ daemon_policy = "auto"
|
||||
)
|
||||
self.assertIn("cp", by_kind["daemon-config"].command)
|
||||
|
||||
def test_supervisor_action_plan_uses_reviewed_supervisor_artifacts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config = load_config(
|
||||
_write_config(
|
||||
root,
|
||||
"""
|
||||
[site.protocols.i2p]
|
||||
enabled = true
|
||||
renderer = "privacy-html"
|
||||
daemon = "i2pd"
|
||||
daemon_policy = "auto"
|
||||
""",
|
||||
)
|
||||
)
|
||||
write_install_artifacts(
|
||||
config,
|
||||
platform_provider=platform_by_name("android-termux"),
|
||||
daemon_probe=_probe(installed=False, running=False),
|
||||
)
|
||||
|
||||
actions = install_supervisor_actions(
|
||||
config,
|
||||
platform_provider=platform_by_name("android-termux"),
|
||||
daemon_probe=_probe(installed=False, running=False),
|
||||
)
|
||||
|
||||
by_kind = {action.kind: action for action in actions}
|
||||
self.assertEqual({"daemon-supervisor", "loopback-supervisor"}, set(by_kind))
|
||||
self.assertEqual("planned", by_kind["daemon-supervisor"].status)
|
||||
self.assertEqual("ampg-example-i2p-i2pd", by_kind["daemon-supervisor"].service)
|
||||
self.assertEqual("sv-enable ampg-example-i2p-i2pd", by_kind["daemon-supervisor"].command)
|
||||
self.assertEqual(
|
||||
root / "plan/example/i2p/managed/android-termux/i2pd.run",
|
||||
by_kind["daemon-supervisor"].source,
|
||||
)
|
||||
self.assertEqual(
|
||||
"sv-enable ampg-example-i2p-nginx-loopback",
|
||||
by_kind["loopback-supervisor"].command,
|
||||
)
|
||||
|
||||
def test_install_plan_cli_can_write_managed_artifacts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user