mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add package deploy apply stage
This commit is contained in:
@@ -56,6 +56,7 @@ python3 -m ampg --config gateway.toml dns plan --free-domain-hints
|
|||||||
python3 -m ampg --config gateway.toml build
|
python3 -m ampg --config gateway.toml build
|
||||||
python3 -m ampg --config gateway.toml doctor
|
python3 -m ampg --config gateway.toml doctor
|
||||||
python3 -m ampg --config gateway.toml install-plan --profile vps-full --write-artifacts
|
python3 -m ampg --config gateway.toml install-plan --profile vps-full --write-artifacts
|
||||||
|
python3 -m ampg --config gateway.toml deploy apply --stage packages --dry-run --profile vps-full
|
||||||
python3 -m ampg --config gateway.toml approvals list --profile vps-full
|
python3 -m ampg --config gateway.toml approvals list --profile vps-full
|
||||||
python3 -m ampg --config gateway.toml approvals approve --profile vps-full --all
|
python3 -m ampg --config gateway.toml approvals approve --profile vps-full --all
|
||||||
python3 -m ampg --config gateway.toml apply --dry-run --profile vps-full
|
python3 -m ampg --config gateway.toml apply --dry-run --profile vps-full
|
||||||
@@ -70,11 +71,12 @@ python3 -m ampg --config gateway.toml deploy apply --stage health --dry-run --pr
|
|||||||
transports left as one-line toggles. `deploy plan` condenses the lower-level checks into
|
transports left as one-line toggles. `deploy plan` condenses the lower-level checks into
|
||||||
clear next steps. `dns plan` covers static DNS, Dynamic DNS, and behind-router
|
clear next steps. `dns plan` covers static DNS, Dynamic DNS, and behind-router
|
||||||
reachability hints. `apply --dry-run` prints the activation sequence and a preflight
|
reachability hints. `apply --dry-run` prints the activation sequence and a preflight
|
||||||
verdict without changing services. `deploy apply --stage state` copies approved
|
verdict without changing services. `deploy apply --stage packages` installs selected
|
||||||
managed-daemon config into AMPG-owned state. `deploy apply --stage supervisor` installs
|
managed-daemon packages using structured platform commands. `deploy apply --stage state`
|
||||||
approved supervisor files after state exists. `deploy apply --stage start` starts
|
copies approved managed-daemon config into AMPG-owned state. `deploy apply --stage
|
||||||
AMPG-owned services after state and supervisor files are present. `deploy apply --stage
|
supervisor` installs approved supervisor files after state exists. `deploy apply --stage
|
||||||
addresses` captures daemon-written public addresses into the address registry.
|
start` starts AMPG-owned services after state and supervisor files are present. `deploy
|
||||||
|
apply --stage addresses` captures daemon-written public addresses into the address registry.
|
||||||
`deploy apply --stage health` verifies published fixture URLs through the selected
|
`deploy apply --stage health` verifies published fixture URLs through the selected
|
||||||
transport.
|
transport.
|
||||||
|
|
||||||
@@ -140,6 +142,8 @@ python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --protoco
|
|||||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --write-artifacts
|
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --write-artifacts
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml approvals list --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml approvals list --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml approvals approve --profile mobile-i2p --all
|
python3 -m ampg --config examples/wownero.gateway.toml approvals approve --profile mobile-i2p --all
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage packages --dry-run --profile mobile-i2p
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage packages --profile mobile-i2p --yes
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --profile mobile-i2p --yes
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --profile mobile-i2p --yes
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --dry-run --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --dry-run --profile mobile-i2p
|
||||||
@@ -198,6 +202,11 @@ would be copied into managed state and the supervisor services that would be reg
|
|||||||
or started. The preflight gate reports `blocked`, `review`, or `ready` across activation
|
or started. The preflight gate reports `blocked`, `review`, or `ready` across activation
|
||||||
steps, managed-state copies, and supervisor actions.
|
steps, managed-state copies, and supervisor actions.
|
||||||
|
|
||||||
|
`deploy apply --stage packages --dry-run` shows managed-daemon packages that can be
|
||||||
|
installed automatically for the selected platform. The live form requires `--yes`, runs
|
||||||
|
only allowlisted package-manager commands, and leaves config, services, keys, and
|
||||||
|
addresses unchanged. Adopted and external daemons are skipped.
|
||||||
|
|
||||||
`deploy apply --stage state --dry-run` shows approved managed-daemon artifacts that are
|
`deploy apply --stage state --dry-run` shows approved managed-daemon artifacts that are
|
||||||
ready to copy into `gateway.state_dir`. The live form requires `--yes`; it creates only
|
ready to copy into `gateway.state_dir`. The live form requires `--yes`; it creates only
|
||||||
AMPG-owned state directories and copies approved artifact contents. It does not install
|
AMPG-owned state directories and copies approved artifact contents. It does not install
|
||||||
@@ -247,6 +256,7 @@ Explicit `--protocol` and `--platform` flags override the profile when present.
|
|||||||
- [ ] Run `ampg health-plan`; review post-start transport checks.
|
- [ ] Run `ampg health-plan`; review post-start transport checks.
|
||||||
- [ ] Run `ampg approvals approve --all` after reviewing generated artifacts.
|
- [ ] Run `ampg approvals approve --all` after reviewing generated artifacts.
|
||||||
- [ ] Run `ampg apply --dry-run`; review the activation sequence and preflight gate.
|
- [ ] Run `ampg apply --dry-run`; review the activation sequence and preflight gate.
|
||||||
|
- [ ] Run `ampg deploy apply --stage packages --dry-run`; confirm package-manager commands.
|
||||||
- [ ] Run `ampg deploy apply --stage state --dry-run`; confirm approved state copies.
|
- [ ] Run `ampg deploy apply --stage state --dry-run`; confirm approved state copies.
|
||||||
- [ ] Run `ampg deploy apply --stage supervisor --dry-run`; confirm approved service files.
|
- [ ] Run `ampg deploy apply --stage supervisor --dry-run`; confirm approved service files.
|
||||||
- [ ] Run `ampg deploy apply --stage start --dry-run`; confirm service-manager commands.
|
- [ ] Run `ampg deploy apply --stage start --dry-run`; confirm service-manager commands.
|
||||||
|
|||||||
+169
-5
@@ -16,8 +16,10 @@ from .addresses import (
|
|||||||
from .config import GatewayConfig
|
from .config import GatewayConfig
|
||||||
from .health import HealthCheck, health_plan
|
from .health import HealthCheck, health_plan
|
||||||
from .install_plan import (
|
from .install_plan import (
|
||||||
|
InstallPackageAction,
|
||||||
InstallStateCopy,
|
InstallStateCopy,
|
||||||
InstallSupervisorAction,
|
InstallSupervisorAction,
|
||||||
|
install_package_actions,
|
||||||
install_state_copies,
|
install_state_copies,
|
||||||
install_supervisor_actions,
|
install_supervisor_actions,
|
||||||
)
|
)
|
||||||
@@ -61,6 +63,19 @@ class CommandRunResult:
|
|||||||
stderr: str = ""
|
stderr: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PackageApplyResult:
|
||||||
|
site_id: str
|
||||||
|
protocol: str
|
||||||
|
platform: str
|
||||||
|
package: str
|
||||||
|
status: str
|
||||||
|
mode: str
|
||||||
|
command: tuple[str, ...]
|
||||||
|
return_code: int | None
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StartApplyResult:
|
class StartApplyResult:
|
||||||
site_id: str
|
site_id: str
|
||||||
@@ -106,6 +121,62 @@ class HealthApplyResult:
|
|||||||
CommandRunner = Callable[[tuple[str, ...]], CommandRunResult]
|
CommandRunner = Callable[[tuple[str, ...]], CommandRunResult]
|
||||||
|
|
||||||
|
|
||||||
|
def apply_packages(
|
||||||
|
config: GatewayConfig,
|
||||||
|
*,
|
||||||
|
dry_run: bool,
|
||||||
|
platform_provider: PlatformProvider | None = None,
|
||||||
|
daemon_probe: DaemonProbeFunc | None = None,
|
||||||
|
command_runner: CommandRunner | None = None,
|
||||||
|
) -> list[PackageApplyResult]:
|
||||||
|
provider = platform_provider or detect_platform()
|
||||||
|
actions = install_package_actions(
|
||||||
|
config,
|
||||||
|
platform_provider=provider,
|
||||||
|
daemon_probe=daemon_probe,
|
||||||
|
)
|
||||||
|
runner = command_runner or _run_command
|
||||||
|
results: list[PackageApplyResult] = []
|
||||||
|
for action in actions:
|
||||||
|
validation = _validate_package_action(action, dry_run=dry_run)
|
||||||
|
if validation.status in {"blocked", "skipped"}:
|
||||||
|
results.append(validation)
|
||||||
|
continue
|
||||||
|
if dry_run:
|
||||||
|
results.append(
|
||||||
|
_package_result(
|
||||||
|
action,
|
||||||
|
status="planned",
|
||||||
|
mode="dry-run",
|
||||||
|
return_code=None,
|
||||||
|
message="would install managed daemon package",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
run = runner(action.command)
|
||||||
|
if run.return_code != 0:
|
||||||
|
results.append(
|
||||||
|
_package_result(
|
||||||
|
action,
|
||||||
|
status="blocked",
|
||||||
|
mode="live",
|
||||||
|
return_code=run.return_code,
|
||||||
|
message=_command_failure_message(run, label="package install command"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
results.append(
|
||||||
|
_package_result(
|
||||||
|
action,
|
||||||
|
status="installed",
|
||||||
|
mode="live",
|
||||||
|
return_code=run.return_code,
|
||||||
|
message="installed managed daemon package",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def apply_state(
|
def apply_state(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
*,
|
*,
|
||||||
@@ -268,7 +339,7 @@ def apply_start(
|
|||||||
status="blocked",
|
status="blocked",
|
||||||
mode="live",
|
mode="live",
|
||||||
return_code=run.return_code,
|
return_code=run.return_code,
|
||||||
message=_command_failure_message(run),
|
message=_command_failure_message(run, label="start command"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -333,7 +404,7 @@ def apply_health(
|
|||||||
status="blocked",
|
status="blocked",
|
||||||
mode="live",
|
mode="live",
|
||||||
return_code=run.return_code,
|
return_code=run.return_code,
|
||||||
message=_command_failure_message(run),
|
message=_command_failure_message(run, label="health check command"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -350,6 +421,61 @@ def apply_health(
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_package_action(
|
||||||
|
action: InstallPackageAction,
|
||||||
|
*,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> PackageApplyResult:
|
||||||
|
mode = "dry-run" if dry_run else "live"
|
||||||
|
if action.status == "ready":
|
||||||
|
return _package_result(
|
||||||
|
action,
|
||||||
|
status="skipped",
|
||||||
|
mode=mode,
|
||||||
|
return_code=None,
|
||||||
|
message=action.message,
|
||||||
|
)
|
||||||
|
if action.status != "planned":
|
||||||
|
return _package_result(
|
||||||
|
action,
|
||||||
|
status="blocked",
|
||||||
|
mode=mode,
|
||||||
|
return_code=None,
|
||||||
|
message=action.message,
|
||||||
|
)
|
||||||
|
if not _valid_package_name(action.package):
|
||||||
|
return _package_result(
|
||||||
|
action,
|
||||||
|
status="blocked",
|
||||||
|
mode=mode,
|
||||||
|
return_code=None,
|
||||||
|
message=f"invalid package name: {action.package}",
|
||||||
|
)
|
||||||
|
if not action.command:
|
||||||
|
return _package_result(
|
||||||
|
action,
|
||||||
|
status="blocked",
|
||||||
|
mode=mode,
|
||||||
|
return_code=None,
|
||||||
|
message="selected platform does not have an automatic package install command",
|
||||||
|
)
|
||||||
|
if not _allowed_package_command(action):
|
||||||
|
return _package_result(
|
||||||
|
action,
|
||||||
|
status="blocked",
|
||||||
|
mode=mode,
|
||||||
|
return_code=None,
|
||||||
|
message="package install command is not allowlisted",
|
||||||
|
)
|
||||||
|
return _package_result(
|
||||||
|
action,
|
||||||
|
status="ready",
|
||||||
|
mode=mode,
|
||||||
|
return_code=None,
|
||||||
|
message="package install command passed safety checks",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_state_copy(
|
def _validate_state_copy(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
copy: InstallStateCopy,
|
copy: InstallStateCopy,
|
||||||
@@ -614,6 +740,27 @@ def _address_result(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _package_result(
|
||||||
|
action: InstallPackageAction,
|
||||||
|
*,
|
||||||
|
status: str,
|
||||||
|
mode: str,
|
||||||
|
return_code: int | None,
|
||||||
|
message: str,
|
||||||
|
) -> PackageApplyResult:
|
||||||
|
return PackageApplyResult(
|
||||||
|
site_id=action.site_id,
|
||||||
|
protocol=action.protocol,
|
||||||
|
platform=action.platform,
|
||||||
|
package=action.package,
|
||||||
|
status=status,
|
||||||
|
mode=mode,
|
||||||
|
command=action.command,
|
||||||
|
return_code=return_code,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _health_result(
|
def _health_result(
|
||||||
check: HealthCheck,
|
check: HealthCheck,
|
||||||
*,
|
*,
|
||||||
@@ -758,6 +905,23 @@ def _valid_service_name(service: str) -> bool:
|
|||||||
return all(char.isalnum() or char in {"-", "_", "."} for char in service)
|
return all(char.isalnum() or char in {"-", "_", "."} for char in service)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_package_name(package: str) -> bool:
|
||||||
|
if not package or package == "-":
|
||||||
|
return False
|
||||||
|
return all(char.isalnum() or char in {"-", "_", ".", "+"} for char in package)
|
||||||
|
|
||||||
|
|
||||||
|
def _allowed_package_command(action: InstallPackageAction) -> bool:
|
||||||
|
package = action.package
|
||||||
|
if action.platform == "android-termux":
|
||||||
|
return action.command == ("pkg", "install", package)
|
||||||
|
if action.platform == "linux-systemd":
|
||||||
|
return action.command == ("sudo", "apt", "install", package)
|
||||||
|
if action.platform == "macos-launchd":
|
||||||
|
return action.command == ("brew", "install", package)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _chmod_supervisor_target(provider: PlatformProvider, target: Path) -> None:
|
def _chmod_supervisor_target(provider: PlatformProvider, target: Path) -> None:
|
||||||
if provider.name not in {"android-termux", "linux-user"}:
|
if provider.name not in {"android-termux", "linux-user"}:
|
||||||
return
|
return
|
||||||
@@ -790,11 +954,11 @@ def _run_command(command: tuple[str, ...]) -> CommandRunResult:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _command_failure_message(run: CommandRunResult) -> str:
|
def _command_failure_message(run: CommandRunResult, *, label: str = "command") -> str:
|
||||||
detail = run.stderr or run.stdout
|
detail = run.stderr or run.stdout
|
||||||
if detail:
|
if detail:
|
||||||
return f"start command failed: {detail}"
|
return f"{label} failed: {detail}"
|
||||||
return "start command failed"
|
return f"{label} failed"
|
||||||
|
|
||||||
|
|
||||||
def format_command(command: tuple[str, ...]) -> str:
|
def format_command(command: tuple[str, ...]) -> str:
|
||||||
|
|||||||
+42
-1
@@ -24,11 +24,13 @@ from .addresses import (
|
|||||||
from .apply import (
|
from .apply import (
|
||||||
AddressApplyResult,
|
AddressApplyResult,
|
||||||
HealthApplyResult,
|
HealthApplyResult,
|
||||||
|
PackageApplyResult,
|
||||||
StateApplyResult,
|
StateApplyResult,
|
||||||
StartApplyResult,
|
StartApplyResult,
|
||||||
SupervisorApplyResult,
|
SupervisorApplyResult,
|
||||||
apply_addresses,
|
apply_addresses,
|
||||||
apply_health,
|
apply_health,
|
||||||
|
apply_packages,
|
||||||
apply_state,
|
apply_state,
|
||||||
apply_start,
|
apply_start,
|
||||||
apply_supervisor,
|
apply_supervisor,
|
||||||
@@ -164,7 +166,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
_add_target_selection(deploy_apply_parser)
|
_add_target_selection(deploy_apply_parser)
|
||||||
deploy_apply_parser.add_argument(
|
deploy_apply_parser.add_argument(
|
||||||
"--stage",
|
"--stage",
|
||||||
choices=("state", "supervisor", "start", "addresses", "health"),
|
choices=("packages", "state", "supervisor", "start", "addresses", "health"),
|
||||||
required=True,
|
required=True,
|
||||||
help="Deployment stage to apply.",
|
help="Deployment stage to apply.",
|
||||||
)
|
)
|
||||||
@@ -583,6 +585,29 @@ def _cmd_deploy(config, args) -> int:
|
|||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
platform_provider = _platform_override(config, args)
|
platform_provider = _platform_override(config, args)
|
||||||
|
if args.stage == "packages":
|
||||||
|
results = apply_packages(
|
||||||
|
config,
|
||||||
|
dry_run=args.dry_run,
|
||||||
|
platform_provider=platform_provider,
|
||||||
|
)
|
||||||
|
for result in results:
|
||||||
|
_print_package_apply_result(result)
|
||||||
|
blocked = [result for result in results if result.status == "blocked"]
|
||||||
|
installed = [result for result in results if result.status == "installed"]
|
||||||
|
planned = [result for result in results if result.status == "planned"]
|
||||||
|
skipped = [result for result in results if result.status == "skipped"]
|
||||||
|
print(
|
||||||
|
"AMPG_DEPLOY_APPLY_SUMMARY "
|
||||||
|
f"stage=packages "
|
||||||
|
f"mode={'dry-run' if args.dry_run else 'live'} "
|
||||||
|
f"results={len(results)} "
|
||||||
|
f"planned={len(planned)} "
|
||||||
|
f"installed={len(installed)} "
|
||||||
|
f"skipped={len(skipped)} "
|
||||||
|
f"blocked={len(blocked)}"
|
||||||
|
)
|
||||||
|
return 1 if blocked else 0
|
||||||
if args.stage == "state":
|
if args.stage == "state":
|
||||||
results = apply_state(
|
results = apply_state(
|
||||||
config,
|
config,
|
||||||
@@ -1171,6 +1196,22 @@ def _print_state_apply_result(result: StateApplyResult) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_package_apply_result(result: PackageApplyResult) -> None:
|
||||||
|
return_code = "-" if result.return_code is None else str(result.return_code)
|
||||||
|
print(
|
||||||
|
"AMPG_DEPLOY_PACKAGE "
|
||||||
|
f"site={result.site_id} "
|
||||||
|
f"protocol={result.protocol} "
|
||||||
|
f"platform={result.platform} "
|
||||||
|
f"package=\"{_quote(result.package)}\" "
|
||||||
|
f"mode={result.mode} "
|
||||||
|
f"status={result.status} "
|
||||||
|
f"return_code={return_code} "
|
||||||
|
f"command=\"{_quote(format_command(result.command))}\" "
|
||||||
|
f"message=\"{_quote(result.message)}\""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _print_supervisor_apply_result(result: SupervisorApplyResult) -> None:
|
def _print_supervisor_apply_result(result: SupervisorApplyResult) -> None:
|
||||||
print(
|
print(
|
||||||
"AMPG_DEPLOY_SUPERVISOR "
|
"AMPG_DEPLOY_SUPERVISOR "
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .dns import dns_plan
|
|||||||
from .health import health_plan
|
from .health import health_plan
|
||||||
from .install_plan import (
|
from .install_plan import (
|
||||||
install_artifacts,
|
install_artifacts,
|
||||||
|
install_package_actions,
|
||||||
install_plan,
|
install_plan,
|
||||||
install_state_copies,
|
install_state_copies,
|
||||||
install_supervisor_actions,
|
install_supervisor_actions,
|
||||||
@@ -82,6 +83,12 @@ def deploy_plan(
|
|||||||
platform_provider=platform_provider,
|
platform_provider=platform_provider,
|
||||||
daemon_probe=daemon_probe,
|
daemon_probe=daemon_probe,
|
||||||
),
|
),
|
||||||
|
_package_step(
|
||||||
|
config,
|
||||||
|
command_context,
|
||||||
|
platform_provider=platform_provider,
|
||||||
|
daemon_probe=daemon_probe,
|
||||||
|
),
|
||||||
_artifact_step(config, command_context, platform_provider=platform_provider),
|
_artifact_step(config, command_context, platform_provider=platform_provider),
|
||||||
_apply_preflight_step(
|
_apply_preflight_step(
|
||||||
config,
|
config,
|
||||||
@@ -230,6 +237,52 @@ def _daemon_step(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _package_step(
|
||||||
|
config: GatewayConfig,
|
||||||
|
commands: _CommandContext,
|
||||||
|
*,
|
||||||
|
platform_provider: PlatformProvider | None,
|
||||||
|
daemon_probe: DaemonProbeFunc | None,
|
||||||
|
) -> DeployStep:
|
||||||
|
actions = install_package_actions(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_provider,
|
||||||
|
daemon_probe=daemon_probe,
|
||||||
|
)
|
||||||
|
if not actions:
|
||||||
|
return DeployStep("packages", "ready", "-", "no managed package installs are needed")
|
||||||
|
blocked = [action for action in actions if action.status == "blocked"]
|
||||||
|
planned = [action for action in actions if action.status == "planned"]
|
||||||
|
manual = [action for action in planned if not action.command]
|
||||||
|
if blocked:
|
||||||
|
return DeployStep(
|
||||||
|
"packages",
|
||||||
|
"blocked",
|
||||||
|
commands.command("install-plan", platform=True),
|
||||||
|
f"fix {len(blocked)} package planning blocker(s)",
|
||||||
|
)
|
||||||
|
if manual:
|
||||||
|
return DeployStep(
|
||||||
|
"packages",
|
||||||
|
"review",
|
||||||
|
commands.command("install-plan", platform=True),
|
||||||
|
f"install {len(manual)} package(s) with the local package manager",
|
||||||
|
)
|
||||||
|
if planned:
|
||||||
|
return DeployStep(
|
||||||
|
"packages",
|
||||||
|
"todo",
|
||||||
|
commands.command("deploy apply --stage packages --dry-run", platform=True),
|
||||||
|
f"install {len(planned)} managed daemon package(s)",
|
||||||
|
)
|
||||||
|
return DeployStep(
|
||||||
|
"packages",
|
||||||
|
"ready",
|
||||||
|
commands.command("deploy apply --stage packages --dry-run", platform=True),
|
||||||
|
"managed daemon packages are installed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _artifact_step(
|
def _artifact_step(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
commands: _CommandContext,
|
commands: _CommandContext,
|
||||||
|
|||||||
+63
-4
@@ -66,6 +66,17 @@ class InstallSupervisorAction:
|
|||||||
message: str
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InstallPackageAction:
|
||||||
|
site_id: str
|
||||||
|
protocol: str
|
||||||
|
platform: str
|
||||||
|
package: str
|
||||||
|
status: str
|
||||||
|
command: tuple[str, ...]
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
def install_plan(
|
def install_plan(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
*,
|
*,
|
||||||
@@ -120,6 +131,47 @@ def install_artifacts(
|
|||||||
return artifacts
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
|
def install_package_actions(
|
||||||
|
config: GatewayConfig,
|
||||||
|
*,
|
||||||
|
platform_provider: PlatformProvider | None = None,
|
||||||
|
daemon_probe: DaemonProbeFunc | None = None,
|
||||||
|
) -> list[InstallPackageAction]:
|
||||||
|
provider = platform_provider or detect_platform()
|
||||||
|
steps = install_plan(
|
||||||
|
config,
|
||||||
|
platform_provider=provider,
|
||||||
|
daemon_probe=daemon_probe,
|
||||||
|
)
|
||||||
|
actions: list[InstallPackageAction] = []
|
||||||
|
for step in steps:
|
||||||
|
if step.stage == "package":
|
||||||
|
actions.append(
|
||||||
|
InstallPackageAction(
|
||||||
|
site_id=step.site_id,
|
||||||
|
protocol=step.protocol,
|
||||||
|
platform=step.platform,
|
||||||
|
package=step.target,
|
||||||
|
status=step.status,
|
||||||
|
command=package_install_command(provider, step.target),
|
||||||
|
message=step.message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif step.status == "blocked":
|
||||||
|
actions.append(
|
||||||
|
InstallPackageAction(
|
||||||
|
site_id=step.site_id,
|
||||||
|
protocol=step.protocol,
|
||||||
|
platform=step.platform,
|
||||||
|
package=step.target,
|
||||||
|
status="blocked",
|
||||||
|
command=(),
|
||||||
|
message=step.message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
def write_install_artifacts(
|
def write_install_artifacts(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
*,
|
*,
|
||||||
@@ -513,13 +565,20 @@ def _package_name(provider: PlatformProvider, daemon: str) -> str:
|
|||||||
return daemon
|
return daemon
|
||||||
|
|
||||||
|
|
||||||
def _package_command(provider: PlatformProvider, package: str) -> str:
|
def package_install_command(provider: PlatformProvider, package: str) -> tuple[str, ...]:
|
||||||
if provider.name == "android-termux":
|
if provider.name == "android-termux":
|
||||||
return f"pkg install {package}"
|
return ("pkg", "install", package)
|
||||||
if provider.name == "linux-systemd":
|
if provider.name == "linux-systemd":
|
||||||
return f"sudo apt install {package}"
|
return ("sudo", "apt", "install", package)
|
||||||
if provider.name == "macos-launchd":
|
if provider.name == "macos-launchd":
|
||||||
return f"brew install {package}"
|
return ("brew", "install", package)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
def _package_command(provider: PlatformProvider, package: str) -> str:
|
||||||
|
command = package_install_command(provider, package)
|
||||||
|
if command:
|
||||||
|
return shlex.join(command)
|
||||||
if provider.name == "linux-user":
|
if provider.name == "linux-user":
|
||||||
return f"install {package} with the user package manager"
|
return f"install {package} with the user package manager"
|
||||||
return "-"
|
return "-"
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ managed state and planned supervisor start/reload actions. The command emits an
|
|||||||
Blocked or unapproved preflight items stop mutation before any live apply path can touch
|
Blocked or unapproved preflight items stop mutation before any live apply path can touch
|
||||||
services.
|
services.
|
||||||
|
|
||||||
|
`ampg deploy apply --stage packages` is the narrow live stage for package installation.
|
||||||
|
It runs only structured package-manager commands for selected managed daemons. Adopted
|
||||||
|
or external daemons do not require package apply.
|
||||||
|
|
||||||
`ampg deploy apply --stage state` is the narrow live stage for managed state. It reads
|
`ampg deploy apply --stage state` is the narrow live stage for managed state. It reads
|
||||||
the same approved artifact plan, refuses missing, stale, or unapproved inputs, and copies
|
the same approved artifact plan, refuses missing, stale, or unapproved inputs, and copies
|
||||||
only approved generated daemon config into AMPG-owned state.
|
only approved generated daemon config into AMPG-owned state.
|
||||||
@@ -159,7 +163,7 @@ root.
|
|||||||
`ampg install-plan` translates managed daemon decisions into package, state, config,
|
`ampg install-plan` translates managed daemon decisions into package, state, config,
|
||||||
supervisor, and health-check steps. It does not install packages, create directories, or
|
supervisor, and health-check steps. It does not install packages, create directories, or
|
||||||
start services. The output is a reviewable bridge between daemon ownership decisions and
|
start services. The output is a reviewable bridge between daemon ownership decisions and
|
||||||
future live apply support.
|
the staged live apply commands.
|
||||||
|
|
||||||
With `--write-artifacts`, install-plan writes managed config and supervisor files under
|
With `--write-artifacts`, install-plan writes managed config and supervisor files under
|
||||||
the configured `plan_root`; those files are review artifacts, not installed service
|
the configured `plan_root`; those files are review artifacts, not installed service
|
||||||
|
|||||||
+24
-10
@@ -56,6 +56,8 @@ python3 -m ampg --config gateway.toml apply --dry-run --protocol i2p
|
|||||||
python3 -m ampg --config gateway.toml apply --dry-run --write-artifacts
|
python3 -m ampg --config gateway.toml apply --dry-run --write-artifacts
|
||||||
python3 -m ampg --config gateway.toml approvals list --profile mobile-i2p
|
python3 -m ampg --config gateway.toml approvals list --profile mobile-i2p
|
||||||
python3 -m ampg --config gateway.toml approvals approve --profile mobile-i2p --all
|
python3 -m ampg --config gateway.toml approvals approve --profile mobile-i2p --all
|
||||||
|
python3 -m ampg --config gateway.toml deploy apply --stage packages --dry-run --profile mobile-i2p
|
||||||
|
python3 -m ampg --config gateway.toml deploy apply --stage packages --profile mobile-i2p --yes
|
||||||
python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
||||||
python3 -m ampg --config gateway.toml deploy apply --stage state --profile mobile-i2p --yes
|
python3 -m ampg --config gateway.toml deploy apply --stage state --profile mobile-i2p --yes
|
||||||
python3 -m ampg --config gateway.toml deploy apply --stage supervisor --dry-run --profile mobile-i2p
|
python3 -m ampg --config gateway.toml deploy apply --stage supervisor --dry-run --profile mobile-i2p
|
||||||
@@ -70,8 +72,8 @@ python3 -m ampg --config gateway.toml doctor --platform android-termux
|
|||||||
```
|
```
|
||||||
|
|
||||||
`deploy plan` summarizes the current deployment in plain stages: source, build, DNS,
|
`deploy plan` summarizes the current deployment in plain stages: source, build, DNS,
|
||||||
doctor, daemons, artifacts, addresses, and apply preflight. It prints the next commands
|
doctor, daemons, packages, artifacts, addresses, and apply preflight. It prints the next
|
||||||
to run and does not change files or services.
|
commands to run and does not change files or services.
|
||||||
|
|
||||||
`dns plan` prints clearnet DNS records and reachability hints. Static mode plans A/AAAA
|
`dns plan` prints clearnet DNS records and reachability hints. Static mode plans A/AAAA
|
||||||
and `www` records. Dynamic mode supports a Dynamic DNS hostname and warns when apex
|
and `www` records. Dynamic mode supports a Dynamic DNS hostname and warns when apex
|
||||||
@@ -99,6 +101,10 @@ Tor/I2P HTTP publishing, AMPG writes both transport-daemon artifacts and loopbac
|
|||||||
artifacts. The generated daemon configs store runtime state, identity keys, logs, and
|
artifacts. The generated daemon configs store runtime state, identity keys, logs, and
|
||||||
daemon-written addresses under `gateway.state_dir`.
|
daemon-written addresses under `gateway.state_dir`.
|
||||||
|
|
||||||
|
`deploy apply --stage packages` installs selected managed-daemon packages with
|
||||||
|
structured, allowlisted platform commands. Adopted and external daemons are skipped.
|
||||||
|
Platforms without an automatic package backend remain manual review items.
|
||||||
|
|
||||||
`state-contract` prints the resolved managed-state paths for enabled protocols, including
|
`state-contract` prints the resolved managed-state paths for enabled protocols, including
|
||||||
which paths are AMPG-owned, daemon-written, required, or sensitive.
|
which paths are AMPG-owned, daemon-written, required, or sensitive.
|
||||||
|
|
||||||
@@ -127,30 +133,36 @@ reviewable config snippets to the configured plan root, then prints the copies f
|
|||||||
approved artifacts into `gateway.state_dir` and the supervisor services that would be
|
approved artifacts into `gateway.state_dir` and the supervisor services that would be
|
||||||
registered or started. It still does not install or reload services.
|
registered or started. It still does not install or reload services.
|
||||||
|
|
||||||
`deploy apply --stage state` is the first live apply stage. Dry-run mode prints
|
`deploy apply --stage packages` is the first live apply stage. Dry-run mode prints
|
||||||
|
`AMPG_DEPLOY_PACKAGE` rows. Live mode requires `--yes`, runs only structured
|
||||||
|
package-manager commands for selected managed daemons, and reports installed, skipped, or
|
||||||
|
blocked results. It does not write config, register services, start daemons, capture
|
||||||
|
addresses, or run health checks.
|
||||||
|
|
||||||
|
`deploy apply --stage state` is the second live apply stage. Dry-run mode prints
|
||||||
`AMPG_DEPLOY_STATE` rows. Live mode requires `--yes`, refuses unapproved or stale
|
`AMPG_DEPLOY_STATE` rows. Live mode requires `--yes`, refuses unapproved or stale
|
||||||
artifacts, creates AMPG-owned state directories, and copies approved managed-daemon
|
artifacts, creates AMPG-owned state directories, and copies approved managed-daemon
|
||||||
config into `gateway.state_dir`. It does not install packages, edit adopted daemon
|
config into `gateway.state_dir`. It does not install packages, edit adopted daemon
|
||||||
configs, start services, or delete identity material.
|
configs, start services, or delete identity material.
|
||||||
|
|
||||||
`deploy apply --stage supervisor` is the second live apply stage. Dry-run mode prints
|
`deploy apply --stage supervisor` is the third live apply stage. Dry-run mode prints
|
||||||
`AMPG_DEPLOY_SUPERVISOR` rows. Live mode requires `--yes`, refuses unapproved or stale
|
`AMPG_DEPLOY_SUPERVISOR` rows. Live mode requires `--yes`, refuses unapproved or stale
|
||||||
artifacts, requires the state stage to have copied managed config first, and writes
|
artifacts, requires the state stage to have copied managed config first, and writes
|
||||||
AMPG-named service files for the selected platform. It does not install packages, start
|
AMPG-named service files for the selected platform. It does not install packages, start
|
||||||
services, reload service managers, or delete service files.
|
services, reload service managers, or delete service files.
|
||||||
|
|
||||||
`deploy apply --stage start` is the third live apply stage. Dry-run mode prints
|
`deploy apply --stage start` is the fourth live apply stage. Dry-run mode prints
|
||||||
`AMPG_DEPLOY_START` rows with exact service-manager commands. Live mode requires `--yes`,
|
`AMPG_DEPLOY_START` rows with exact service-manager commands. Live mode requires `--yes`,
|
||||||
requires both state and supervisor files to exist, and runs only structured platform
|
requires both state and supervisor files to exist, and runs only structured platform
|
||||||
commands for AMPG-named services. It does not install packages, rewrite config, remove
|
commands for AMPG-named services. It does not install packages, rewrite config, remove
|
||||||
files, capture addresses, or run health checks.
|
files, capture addresses, or run health checks.
|
||||||
|
|
||||||
`deploy apply --stage addresses` is the fourth live apply stage. Dry-run mode prints
|
`deploy apply --stage addresses` is the fifth live apply stage. Dry-run mode prints
|
||||||
`AMPG_DEPLOY_ADDRESS` rows for configured, skipped, missing, and capturable addresses.
|
`AMPG_DEPLOY_ADDRESS` rows for configured, skipped, missing, and capturable addresses.
|
||||||
Live mode requires `--yes` and writes captured transport addresses under
|
Live mode requires `--yes` and writes captured transport addresses under
|
||||||
`gateway.state_dir`. It does not rewrite daemon config or run health checks.
|
`gateway.state_dir`. It does not rewrite daemon config or run health checks.
|
||||||
|
|
||||||
`deploy apply --stage health` is the fifth live apply stage. Dry-run mode prints
|
`deploy apply --stage health` is the sixth live apply stage. Dry-run mode prints
|
||||||
`AMPG_DEPLOY_HEALTH` rows with published fixture URLs and transport commands. Live mode
|
`AMPG_DEPLOY_HEALTH` rows with published fixture URLs and transport commands. Live mode
|
||||||
requires `--yes`, runs those commands, and reports pass/fail. It does not change daemon
|
requires `--yes`, runs those commands, and reports pass/fail. It does not change daemon
|
||||||
config, service state, or address registries.
|
config, service state, or address registries.
|
||||||
@@ -171,9 +183,11 @@ Platform providers describe how AMPG may supervise managed daemons:
|
|||||||
- `android-termux`: Termux-style user-space daemons for mobile/server experiments.
|
- `android-termux`: Termux-style user-space daemons for mobile/server experiments.
|
||||||
- `unknown`: render and plan only; daemon management is disabled.
|
- `unknown`: render and plan only; daemon management is disabled.
|
||||||
|
|
||||||
Package commands in `install-plan` are review hints, not executed actions. Systemd hosts
|
Package commands in `install-plan` are review hints. `deploy apply --stage packages` is
|
||||||
currently emit `apt`-style package hints, Termux emits `pkg`, macOS emits `brew`, and
|
the executable package stage for supported automatic backends: Systemd hosts use
|
||||||
user-space Linux asks the operator to use the local user package manager.
|
`sudo apt install`, Termux uses `pkg install`, and macOS uses `brew install`. User-space
|
||||||
|
Linux asks the operator to use the local package manager until AMPG has a concrete
|
||||||
|
backend for that environment.
|
||||||
|
|
||||||
## Adapter notes
|
## Adapter notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# State Contract
|
# State Contract
|
||||||
|
|
||||||
> Status: draft | Updated 2026-07-06 | Applies to: managed daemon state
|
> Status: draft | Updated 2026-07-07 | Applies to: managed daemon state
|
||||||
|
|
||||||
AMPG keeps managed daemon state under `gateway.state_dir`. The state contract describes
|
AMPG keeps managed daemon state under `gateway.state_dir`. The state contract describes
|
||||||
which files AMPG owns, which files daemons or adapters write, which paths may contain
|
which files AMPG owns, which files daemons or adapters write, which paths may contain
|
||||||
@@ -48,6 +48,10 @@ for the services that would be registered or started. The accompanying
|
|||||||
`AMPG_APPLY_PREFLIGHT` verdict includes those rows so live apply can refuse mutation when
|
`AMPG_APPLY_PREFLIGHT` verdict includes those rows so live apply can refuse mutation when
|
||||||
state or supervisor inputs still need review.
|
state or supervisor inputs still need review.
|
||||||
|
|
||||||
|
`deploy apply --stage packages` performs package installation only. It uses structured
|
||||||
|
platform package-manager commands for selected managed daemons and does not write AMPG
|
||||||
|
state, service files, generated config, or address registries.
|
||||||
|
|
||||||
`deploy apply --stage state` performs the approved copy portion only. It requires
|
`deploy apply --stage state` performs the approved copy portion only. It requires
|
||||||
`--yes` for live writes, refuses unapproved or stale artifacts, writes only under
|
`--yes` for live writes, refuses unapproved or stale artifacts, writes only under
|
||||||
`gateway.state_dir`, and leaves supervisor file installation, daemon start, address
|
`gateway.state_dir`, and leaves supervisor file installation, daemon start, address
|
||||||
|
|||||||
@@ -141,9 +141,9 @@ daemon_policy = "auto"
|
|||||||
- `ampg build` writes a fixture manifest that AMPB can check.
|
- `ampg build` writes a fixture manifest that AMPB can check.
|
||||||
- `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live
|
- `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live
|
||||||
transport daemons are installed or adopted.
|
transport daemons are installed or adopted.
|
||||||
- `ampg deploy apply --stage state`, `--stage supervisor`, `--stage start`,
|
- `ampg deploy apply --stage packages`, `--stage state`, `--stage supervisor`,
|
||||||
`--stage addresses`, and `--stage health` stage Wownero transport publication without
|
`--stage start`, `--stage addresses`, and `--stage health` stage Wownero transport
|
||||||
mixing concerns.
|
publication without mixing concerns.
|
||||||
- Tor output contains no `<script>` tags or inline event handlers.
|
- Tor output contains no `<script>` tags or inline event handlers.
|
||||||
- Generated Gemini output has readable headings, paragraphs, and links.
|
- Generated Gemini output has readable headings, paragraphs, and links.
|
||||||
- Generated Micron output fits terminal-first browsing.
|
- Generated Micron output fits terminal-first browsing.
|
||||||
@@ -165,6 +165,7 @@ python3 -m ampg --config examples/wownero.gateway.toml preview manifest
|
|||||||
python3 -m ampg --config examples/wownero.gateway.toml install-plan --profile mobile-i2p --write-artifacts
|
python3 -m ampg --config examples/wownero.gateway.toml install-plan --profile mobile-i2p --write-artifacts
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml approvals list --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml approvals list --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml approvals approve --profile mobile-i2p --all
|
python3 -m ampg --config examples/wownero.gateway.toml approvals approve --profile mobile-i2p --all
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage packages --dry-run --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --dry-run --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --dry-run --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage start --dry-run --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage start --dry-run --profile mobile-i2p
|
||||||
@@ -182,6 +183,7 @@ python3 -m ampg --config examples/i2p-only.gateway.toml build
|
|||||||
- [ ] Add Gemini conversion fixtures.
|
- [ ] Add Gemini conversion fixtures.
|
||||||
- [ ] Add Reticulum/Micron conversion fixtures.
|
- [ ] Add Reticulum/Micron conversion fixtures.
|
||||||
- [ ] Enable I2P and Reticulum daemon management only after static outputs are stable.
|
- [ ] Enable I2P and Reticulum daemon management only after static outputs are stable.
|
||||||
|
- [ ] Review package install commands for selected managed Wownero transports.
|
||||||
- [ ] Start only AMPG-owned Wownero transport services after reviewed files are applied.
|
- [ ] Start only AMPG-owned Wownero transport services after reviewed files are applied.
|
||||||
- [ ] Capture Wownero transport addresses before published health checks.
|
- [ ] Capture Wownero transport addresses before published health checks.
|
||||||
- [ ] Run published health checks through each selected transport.
|
- [ ] Run published health checks through each selected transport.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ampg.addresses import address_registry_path
|
|||||||
from ampg.apply import (
|
from ampg.apply import (
|
||||||
CommandRunResult,
|
CommandRunResult,
|
||||||
apply_health,
|
apply_health,
|
||||||
|
apply_packages,
|
||||||
apply_start,
|
apply_start,
|
||||||
apply_state,
|
apply_state,
|
||||||
apply_supervisor,
|
apply_supervisor,
|
||||||
@@ -163,6 +164,94 @@ def _write_i2p_address_file(root: Path, value: str = "captureexample.b32.i2p") -
|
|||||||
|
|
||||||
|
|
||||||
class DeployApplyTest(unittest.TestCase):
|
class DeployApplyTest(unittest.TestCase):
|
||||||
|
def test_deploy_apply_packages_dry_run_plans_termux_installs(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp).resolve()
|
||||||
|
config_path = _init_i2p_config(root)
|
||||||
|
config = load_config(config_path)
|
||||||
|
|
||||||
|
results = apply_packages(
|
||||||
|
config,
|
||||||
|
dry_run=True,
|
||||||
|
platform_provider=_termux_provider(root),
|
||||||
|
daemon_probe=_missing_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(all(result.status == "planned" for result in results))
|
||||||
|
self.assertIn(("pkg", "install", "i2pd"), [result.command for result in results])
|
||||||
|
self.assertIn(("pkg", "install", "nginx"), [result.command for result in results])
|
||||||
|
|
||||||
|
def test_deploy_apply_packages_live_invokes_runner(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp).resolve()
|
||||||
|
config_path = _init_i2p_config(root)
|
||||||
|
config = load_config(config_path)
|
||||||
|
commands: list[tuple[str, ...]] = []
|
||||||
|
|
||||||
|
def runner(command: tuple[str, ...]) -> CommandRunResult:
|
||||||
|
commands.append(command)
|
||||||
|
return CommandRunResult(return_code=0)
|
||||||
|
|
||||||
|
results = apply_packages(
|
||||||
|
config,
|
||||||
|
dry_run=False,
|
||||||
|
platform_provider=_termux_provider(root),
|
||||||
|
daemon_probe=_missing_probe,
|
||||||
|
command_runner=runner,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(all(result.status == "installed" for result in results))
|
||||||
|
self.assertIn(("pkg", "install", "i2pd"), commands)
|
||||||
|
self.assertIn(("pkg", "install", "nginx"), commands)
|
||||||
|
|
||||||
|
def test_deploy_apply_packages_blocks_manual_linux_user_install(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp).resolve()
|
||||||
|
config_path = _init_i2p_config(root)
|
||||||
|
config = load_config(config_path)
|
||||||
|
|
||||||
|
results = apply_packages(
|
||||||
|
config,
|
||||||
|
dry_run=True,
|
||||||
|
platform_provider=_linux_user_provider(root),
|
||||||
|
daemon_probe=_missing_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(any(result.status == "blocked" for result in results))
|
||||||
|
self.assertTrue(
|
||||||
|
any("automatic package install command" in result.message for result in results)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_deploy_apply_packages_cli_prints_summary(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp).resolve()
|
||||||
|
config_path = _init_i2p_config(root)
|
||||||
|
config_path.write_text(
|
||||||
|
config_path.read_text(encoding="utf-8").replace(
|
||||||
|
'daemon_policy = "auto"',
|
||||||
|
'daemon_policy = "manage"',
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
status, output, error = _run_cli(
|
||||||
|
[
|
||||||
|
"--config",
|
||||||
|
str(config_path),
|
||||||
|
"deploy",
|
||||||
|
"apply",
|
||||||
|
"--stage",
|
||||||
|
"packages",
|
||||||
|
"--dry-run",
|
||||||
|
"--profile",
|
||||||
|
"mobile-i2p",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, status, error)
|
||||||
|
self.assertIn("AMPG_DEPLOY_PACKAGE", output)
|
||||||
|
self.assertIn("AMPG_DEPLOY_APPLY_SUMMARY stage=packages mode=dry-run", output)
|
||||||
|
|
||||||
def test_deploy_apply_state_blocks_unapproved_artifacts(self):
|
def test_deploy_apply_state_blocks_unapproved_artifacts(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp).resolve()
|
root = Path(tmp).resolve()
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ampg.cli import main
|
from ampg.cli import main
|
||||||
|
from ampg.config import load_config
|
||||||
|
from ampg.deploy import deploy_plan
|
||||||
|
from ampg.platforms import platform_by_name
|
||||||
|
from ampg.status import DaemonProbe
|
||||||
|
|
||||||
|
|
||||||
def _source(root: Path) -> Path:
|
def _source(root: Path) -> Path:
|
||||||
@@ -22,6 +26,10 @@ def _run_cli(args: list[str]) -> tuple[int, str, str]:
|
|||||||
return status, stdout.getvalue(), stderr.getvalue()
|
return status, stdout.getvalue(), stderr.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_probe(adapter):
|
||||||
|
return DaemonProbe(installed=False, running=False, executable_path=None)
|
||||||
|
|
||||||
|
|
||||||
class DeployPlanTest(unittest.TestCase):
|
class DeployPlanTest(unittest.TestCase):
|
||||||
def test_deploy_plan_guides_fresh_i2p_only_site(self):
|
def test_deploy_plan_guides_fresh_i2p_only_site(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
@@ -66,6 +74,39 @@ class DeployPlanTest(unittest.TestCase):
|
|||||||
self.assertIn("build --profile mobile-i2p", output)
|
self.assertIn("build --profile mobile-i2p", output)
|
||||||
self.assertIn("deploy apply --stage addresses --dry-run --profile mobile-i2p", output)
|
self.assertIn("deploy apply --stage addresses --dry-run --profile mobile-i2p", output)
|
||||||
|
|
||||||
|
def test_deploy_plan_guides_package_apply_for_missing_managed_daemons(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp).resolve()
|
||||||
|
config_path = root / "gateway.toml"
|
||||||
|
init_status, _, init_error = _run_cli(
|
||||||
|
[
|
||||||
|
"--config",
|
||||||
|
str(config_path),
|
||||||
|
"init",
|
||||||
|
"site",
|
||||||
|
"example",
|
||||||
|
"--domain",
|
||||||
|
"example.test",
|
||||||
|
"--source",
|
||||||
|
str(_source(root)),
|
||||||
|
"--protocol",
|
||||||
|
"i2p",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(0, init_status, init_error)
|
||||||
|
|
||||||
|
plan = deploy_plan(
|
||||||
|
load_config(config_path),
|
||||||
|
profile="mobile-i2p",
|
||||||
|
platform_provider=platform_by_name("android-termux"),
|
||||||
|
daemon_probe=_missing_probe,
|
||||||
|
)
|
||||||
|
|
||||||
|
packages = next(step for step in plan.steps if step.stage == "packages")
|
||||||
|
self.assertEqual("todo", packages.status)
|
||||||
|
self.assertIn("deploy apply --stage packages --dry-run", packages.command)
|
||||||
|
self.assertIn("managed daemon package", packages.message)
|
||||||
|
|
||||||
def test_deploy_plan_shows_dns_review_for_clearnet_site(self):
|
def test_deploy_plan_shows_dns_review_for_clearnet_site(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp).resolve()
|
root = Path(tmp).resolve()
|
||||||
|
|||||||
Reference in New Issue
Block a user