mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 15:41:41 +02:00
Write managed install artifacts
This commit is contained in:
+26
-2
@@ -14,7 +14,12 @@ from .audit import audit_gateway
|
||||
from .build import build_gateway
|
||||
from .config import load_config
|
||||
from .docsgen import generate_docs
|
||||
from .install_plan import InstallStep, blocked_install_steps, install_plan
|
||||
from .install_plan import (
|
||||
InstallStep,
|
||||
blocked_install_steps,
|
||||
install_plan,
|
||||
write_install_artifacts,
|
||||
)
|
||||
from .manifest import write_fixture_manifests
|
||||
from .plan import plan_gateway, write_plan_artifacts
|
||||
from .platforms import PLATFORM_NAMES, platform_by_name
|
||||
@@ -95,6 +100,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
choices=PLATFORM_NAMES,
|
||||
help="Override platform detection for install planning.",
|
||||
)
|
||||
install_plan_parser.add_argument(
|
||||
"--write-artifacts",
|
||||
action="store_true",
|
||||
help="Write managed daemon config and supervisor artifacts to the plan root.",
|
||||
)
|
||||
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.")
|
||||
@@ -232,7 +242,21 @@ def _cmd_apply(config, args) -> int:
|
||||
|
||||
|
||||
def _cmd_install_plan(config, args) -> int:
|
||||
steps = install_plan(config, platform_provider=_platform_override(config, args))
|
||||
platform_provider = _platform_override(config, args)
|
||||
if _write_artifacts_enabled(config, args):
|
||||
for artifact in write_install_artifacts(
|
||||
config,
|
||||
platform_provider=platform_provider,
|
||||
):
|
||||
print(
|
||||
"AMPG_INSTALL_ARTIFACT "
|
||||
f"site={artifact.site_id} "
|
||||
f"protocol={artifact.protocol} "
|
||||
f"platform={artifact.platform} "
|
||||
f"kind={artifact.kind} "
|
||||
f"path={artifact.path}"
|
||||
)
|
||||
steps = install_plan(config, platform_provider=platform_provider)
|
||||
for step in steps:
|
||||
_print_install_step(step)
|
||||
blocked = blocked_install_steps(steps)
|
||||
|
||||
@@ -22,6 +22,16 @@ class InstallStep:
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InstallArtifact:
|
||||
site_id: str
|
||||
protocol: str
|
||||
platform: str
|
||||
kind: str
|
||||
path: Path
|
||||
content: str
|
||||
|
||||
|
||||
def install_plan(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
@@ -51,6 +61,48 @@ def blocked_install_steps(steps: list[InstallStep]) -> list[InstallStep]:
|
||||
return [step for step in steps if step.status == "blocked"]
|
||||
|
||||
|
||||
def install_artifacts(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
platform_provider: PlatformProvider | None = None,
|
||||
daemon_probe: DaemonProbeFunc | None = None,
|
||||
) -> list[InstallArtifact]:
|
||||
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
|
||||
}
|
||||
artifacts: list[InstallArtifact] = []
|
||||
for site in config.sites:
|
||||
for protocol in site.protocols.values():
|
||||
if not protocol.enabled:
|
||||
continue
|
||||
status = status_by_protocol[(site.id, protocol.name)]
|
||||
artifacts.extend(_protocol_install_artifacts(site, protocol, provider, status))
|
||||
return artifacts
|
||||
|
||||
|
||||
def write_install_artifacts(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
platform_provider: PlatformProvider | None = None,
|
||||
daemon_probe: DaemonProbeFunc | None = None,
|
||||
) -> list[InstallArtifact]:
|
||||
artifacts = install_artifacts(
|
||||
config,
|
||||
platform_provider=platform_provider,
|
||||
daemon_probe=daemon_probe,
|
||||
)
|
||||
for artifact in artifacts:
|
||||
artifact.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
artifact.path.write_text(artifact.content, encoding="utf-8")
|
||||
return artifacts
|
||||
|
||||
|
||||
def _protocol_install_steps(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
@@ -159,6 +211,69 @@ def _protocol_install_steps(
|
||||
]
|
||||
|
||||
|
||||
def _protocol_install_artifacts(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
status: TransportStatus,
|
||||
) -> list[InstallArtifact]:
|
||||
if status.action != "manage-owned" or status.status == "error":
|
||||
return []
|
||||
|
||||
artifacts = [
|
||||
_artifact(
|
||||
site,
|
||||
protocol,
|
||||
provider,
|
||||
kind="daemon-config",
|
||||
filename=_daemon_config_filename(protocol),
|
||||
content=_daemon_config_content(site, protocol, provider),
|
||||
),
|
||||
_artifact(
|
||||
site,
|
||||
protocol,
|
||||
provider,
|
||||
kind="daemon-supervisor",
|
||||
filename=_supervisor_filename(provider, protocol.daemon),
|
||||
content=_supervisor_content(
|
||||
site,
|
||||
protocol,
|
||||
provider,
|
||||
service_suffix=protocol.daemon,
|
||||
command=_daemon_start_command(site, protocol, provider),
|
||||
),
|
||||
),
|
||||
]
|
||||
if protocol.name in {"tor", "i2p"}:
|
||||
artifacts.extend(
|
||||
(
|
||||
_artifact(
|
||||
site,
|
||||
protocol,
|
||||
provider,
|
||||
kind="loopback-config",
|
||||
filename="nginx-loopback.conf",
|
||||
content=_managed_nginx_loopback_content(site, protocol, provider),
|
||||
),
|
||||
_artifact(
|
||||
site,
|
||||
protocol,
|
||||
provider,
|
||||
kind="loopback-supervisor",
|
||||
filename=_supervisor_filename(provider, "nginx-loopback"),
|
||||
content=_supervisor_content(
|
||||
site,
|
||||
protocol,
|
||||
provider,
|
||||
service_suffix="nginx-loopback",
|
||||
command=_loopback_start_command(site, protocol, provider),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return artifacts
|
||||
|
||||
|
||||
def _package_step(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
@@ -239,10 +354,277 @@ def _state_dir(
|
||||
return provider.state_root / site.id / protocol.name
|
||||
|
||||
|
||||
def _artifact_dir(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
) -> Path:
|
||||
return site.outputs.plan_root / site.id / protocol.name / "managed" / provider.name
|
||||
|
||||
|
||||
def _service_name(site: SiteConfig, protocol: ProtocolConfig) -> str:
|
||||
return f"ampg-{site.id}-{protocol.name}"
|
||||
|
||||
|
||||
def _artifact_service_name(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
service_suffix: str,
|
||||
) -> str:
|
||||
return f"{_service_name(site, protocol)}-{service_suffix}"
|
||||
|
||||
|
||||
def _daemon_config_filename(protocol: ProtocolConfig) -> str:
|
||||
if protocol.name == "tor":
|
||||
return "torrc"
|
||||
if protocol.name == "i2p":
|
||||
return "i2pd-tunnels.conf"
|
||||
if protocol.name == "gemini":
|
||||
return "agate-plan.txt"
|
||||
if protocol.name == "clearnet":
|
||||
return "nginx-server.conf"
|
||||
return f"{protocol.daemon}-plan.txt"
|
||||
|
||||
|
||||
def _daemon_config_content(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
) -> str:
|
||||
state_dir = _state_dir(provider, site, protocol)
|
||||
if protocol.name == "tor":
|
||||
port = int(protocol.options.get("local_port", 18080))
|
||||
hidden_service_dir = state_dir / "hidden-service"
|
||||
return f"""# Generated by AMPG. Review before installing.
|
||||
DataDirectory {state_dir / "data"}
|
||||
HiddenServiceDir {hidden_service_dir}/
|
||||
HiddenServicePort 80 127.0.0.1:{port}
|
||||
Log notice file {state_dir / "tor.log"}
|
||||
"""
|
||||
if protocol.name == "i2p":
|
||||
port = int(protocol.options.get("local_port", 18081))
|
||||
key_file = protocol.options.get("keys_file", f"{site.id}-web.dat")
|
||||
tunnel_name = protocol.options.get("tunnel_name", f"{site.id}-web")
|
||||
return f"""# Generated by AMPG. Review before installing.
|
||||
[{tunnel_name}]
|
||||
type = server
|
||||
host = 127.0.0.1
|
||||
port = {port}
|
||||
keys = {state_dir / str(key_file)}
|
||||
"""
|
||||
if protocol.name == "gemini":
|
||||
port = int(protocol.options.get("port", 1965))
|
||||
return f"""# Generated by AMPG. Review before installing.
|
||||
daemon = agate
|
||||
hostname = {site.domain}
|
||||
listen = [::]:{port}
|
||||
content_root = {site.outputs.root / protocol.name}
|
||||
certificate = {state_dir / f"{site.id}.crt"}
|
||||
private_key = {state_dir / f"{site.id}.key"}
|
||||
"""
|
||||
if protocol.name == "clearnet":
|
||||
return _managed_nginx_server_content(site, protocol, provider)
|
||||
return f"""# Generated by AMPG. Review before installing.
|
||||
daemon = {protocol.daemon}
|
||||
protocol = {protocol.name}
|
||||
content_root = {site.outputs.root / protocol.name}
|
||||
state_dir = {state_dir}
|
||||
"""
|
||||
|
||||
|
||||
def _managed_nginx_server_content(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
) -> str:
|
||||
state_dir = _state_dir(provider, site, protocol)
|
||||
return f"""# Generated by AMPG. Review before installing.
|
||||
worker_processes 1;
|
||||
error_log {state_dir / "nginx-error.log"} notice;
|
||||
pid {state_dir / "nginx.pid"};
|
||||
|
||||
events {{
|
||||
worker_connections 128;
|
||||
}}
|
||||
|
||||
http {{
|
||||
access_log {state_dir / "nginx-access.log"};
|
||||
server {{
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name {site.domain} www.{site.domain};
|
||||
root {site.outputs.root / protocol.name};
|
||||
index index.html;
|
||||
|
||||
location / {{
|
||||
try_files $uri $uri/ /index.html;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def _managed_nginx_loopback_content(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
) -> str:
|
||||
state_dir = _state_dir(provider, site, protocol)
|
||||
port = int(protocol.options.get("local_port", 18080 if protocol.name == "tor" else 18081))
|
||||
return f"""# Generated by AMPG. Review before installing.
|
||||
worker_processes 1;
|
||||
error_log {state_dir / "nginx-loopback-error.log"} notice;
|
||||
pid {state_dir / "nginx-loopback.pid"};
|
||||
|
||||
events {{
|
||||
worker_connections 128;
|
||||
}}
|
||||
|
||||
http {{
|
||||
access_log {state_dir / "nginx-loopback-access.log"};
|
||||
server {{
|
||||
listen 127.0.0.1:{port};
|
||||
server_name _;
|
||||
root {site.outputs.root / protocol.name};
|
||||
index index.html;
|
||||
|
||||
location / {{
|
||||
try_files $uri $uri/ /index.html;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def _supervisor_filename(provider: PlatformProvider, service_suffix: str) -> str:
|
||||
if provider.name == "linux-systemd":
|
||||
return f"{service_suffix}.service"
|
||||
if provider.name == "macos-launchd":
|
||||
return f"org.ampg.{service_suffix}.plist"
|
||||
return f"{service_suffix}.run"
|
||||
|
||||
|
||||
def _supervisor_content(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
*,
|
||||
service_suffix: str,
|
||||
command: str,
|
||||
) -> str:
|
||||
service_name = _artifact_service_name(site, protocol, service_suffix)
|
||||
if provider.name == "linux-systemd":
|
||||
return f"""# Generated by AMPG. Review before installing.
|
||||
[Unit]
|
||||
Description=AMPG {protocol.name} {service_suffix} for {site.id}
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory={_state_dir(provider, site, protocol)}
|
||||
ExecStart=/bin/sh -lc '{_shell_single_quote(command)}'
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
if provider.name == "macos-launchd":
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>org.ampg.{service_name}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-lc</string>
|
||||
<string>{_xml_escape(command)}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
shell = "/data/data/com.termux/files/usr/bin/sh" if provider.name == "android-termux" else "/bin/sh"
|
||||
return f"""#!{shell}
|
||||
# Generated by AMPG. Review before installing.
|
||||
set -eu
|
||||
mkdir -p {_state_dir(provider, site, protocol)}
|
||||
{command}
|
||||
"""
|
||||
|
||||
|
||||
def _daemon_start_command(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
) -> str:
|
||||
state_dir = _state_dir(provider, site, protocol)
|
||||
if protocol.name == "tor":
|
||||
return f"exec tor -f {state_dir / 'torrc'}"
|
||||
if protocol.name == "i2p":
|
||||
return f"exec i2pd --datadir={state_dir / 'data'} --tunconf={state_dir / 'i2pd-tunnels.conf'}"
|
||||
if protocol.name == "gemini":
|
||||
port = int(protocol.options.get("port", 1965))
|
||||
return (
|
||||
"exec agate "
|
||||
f"--content {site.outputs.root / protocol.name} "
|
||||
f"--addr [::]:{port} "
|
||||
f"--hostname {site.domain} "
|
||||
f"--certs {state_dir}"
|
||||
)
|
||||
if protocol.name == "clearnet":
|
||||
return f"exec nginx -c {state_dir / 'nginx-server.conf'} -g 'daemon off;'"
|
||||
return f"exec {protocol.daemon}"
|
||||
|
||||
|
||||
def _loopback_start_command(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
) -> str:
|
||||
state_dir = _state_dir(provider, site, protocol)
|
||||
return f"exec nginx -c {state_dir / 'nginx-loopback.conf'} -g 'daemon off;'"
|
||||
|
||||
|
||||
def _artifact(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
provider: PlatformProvider,
|
||||
*,
|
||||
kind: str,
|
||||
filename: str,
|
||||
content: str,
|
||||
) -> InstallArtifact:
|
||||
return InstallArtifact(
|
||||
site_id=site.id,
|
||||
protocol=protocol.name,
|
||||
platform=provider.name,
|
||||
kind=kind,
|
||||
path=_artifact_dir(site, protocol, provider) / filename,
|
||||
content=content,
|
||||
)
|
||||
|
||||
|
||||
def _xml_escape(value: str) -> str:
|
||||
return (
|
||||
value.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace('"', """)
|
||||
)
|
||||
|
||||
|
||||
def _shell_single_quote(value: str) -> str:
|
||||
return value.replace("'", "'\\''")
|
||||
|
||||
|
||||
def _step(
|
||||
site: SiteConfig,
|
||||
protocol: ProtocolConfig,
|
||||
|
||||
Reference in New Issue
Block a user