mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Generate daemon plan artifacts
This commit is contained in:
@@ -95,7 +95,9 @@ The current implementation is dependency-free Python:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml plan
|
python3 -m ampg --config examples/wownero.gateway.toml plan
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml plan --write-artifacts
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||||
|
python3 -m ampg --config examples/i2p-only.gateway.toml build
|
||||||
python3 -m ampg docs generate
|
python3 -m ampg docs generate
|
||||||
python3 -m unittest discover -s tests
|
python3 -m unittest discover -s tests
|
||||||
```
|
```
|
||||||
@@ -104,6 +106,9 @@ The Wownero build writes generated files under `dist/wownero/`. Each protocol ou
|
|||||||
root contains `.ampg-output`; AMPG refuses to clean a non-empty output directory unless
|
root contains `.ampg-output`; AMPG refuses to clean a non-empty output directory unless
|
||||||
that marker is present.
|
that marker is present.
|
||||||
|
|
||||||
|
Plan artifacts are written under `dist/ampg-plan/` only when `--write-artifacts` is
|
||||||
|
passed. They are reviewable snippets, not installed daemon config.
|
||||||
|
|
||||||
## Reusable checklist
|
## Reusable checklist
|
||||||
|
|
||||||
- [ ] Pick source kind: `static-html`, `markdown`, or `ssg-output`.
|
- [ ] Pick source kind: `static-html`, `markdown`, or `ssg-output`.
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ def _render_privacy_html_tree(
|
|||||||
files_skipped = 0
|
files_skipped = 0
|
||||||
stats = PrivacyRenderStats()
|
stats = PrivacyRenderStats()
|
||||||
max_asset_bytes = int(protocol.options.get("max_asset_bytes", 1024 * 1024))
|
max_asset_bytes = int(protocol.options.get("max_asset_bytes", 1024 * 1024))
|
||||||
|
script_policy = str(protocol.options.get("script_policy", "strip"))
|
||||||
|
if script_policy != "strip":
|
||||||
|
raise ValueError(
|
||||||
|
f"{site.id}/{protocol.name}: privacy-html only supports script_policy='strip'"
|
||||||
|
)
|
||||||
for source_path in _iter_source_files(site.source.path):
|
for source_path in _iter_source_files(site.source.path):
|
||||||
target_path = output_root / source_path.relative_to(site.source.path)
|
target_path = output_root / source_path.relative_to(site.source.path)
|
||||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
+15
-4
@@ -7,7 +7,7 @@ import sys
|
|||||||
from .build import build_gateway
|
from .build import build_gateway
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
from .docsgen import generate_docs
|
from .docsgen import generate_docs
|
||||||
from .plan import plan_gateway
|
from .plan import plan_gateway, write_plan_artifacts
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
@@ -19,7 +19,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
help="Path to gateway TOML config.",
|
help="Path to gateway TOML config.",
|
||||||
)
|
)
|
||||||
subcommands = parser.add_subparsers(dest="command", required=True)
|
subcommands = parser.add_subparsers(dest="command", required=True)
|
||||||
subcommands.add_parser("plan", help="Print a dry-run plan.")
|
plan_parser = subcommands.add_parser("plan", help="Print a dry-run plan.")
|
||||||
|
plan_parser.add_argument(
|
||||||
|
"--write-artifacts",
|
||||||
|
action="store_true",
|
||||||
|
help="Write generated config snippets to the configured plan root.",
|
||||||
|
)
|
||||||
subcommands.add_parser("build", help="Build enabled protocol outputs.")
|
subcommands.add_parser("build", help="Build enabled protocol outputs.")
|
||||||
docs_parser = subcommands.add_parser("docs", help="Generate or check generated docs.")
|
docs_parser = subcommands.add_parser("docs", help="Generate or check generated docs.")
|
||||||
docs_subcommands = docs_parser.add_subparsers(dest="docs_command", required=True)
|
docs_subcommands = docs_parser.add_subparsers(dest="docs_command", required=True)
|
||||||
@@ -37,7 +42,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
return _cmd_docs(args)
|
return _cmd_docs(args)
|
||||||
config = load_config(args.config)
|
config = load_config(args.config)
|
||||||
if args.command == "plan":
|
if args.command == "plan":
|
||||||
return _cmd_plan(config)
|
return _cmd_plan(config, write_artifacts=args.write_artifacts)
|
||||||
if args.command == "build":
|
if args.command == "build":
|
||||||
return _cmd_build(config)
|
return _cmd_build(config)
|
||||||
except Exception as exc: # noqa: BLE001 - CLI should print concise failures.
|
except Exception as exc: # noqa: BLE001 - CLI should print concise failures.
|
||||||
@@ -46,8 +51,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def _cmd_plan(config) -> int:
|
def _cmd_plan(config, *, write_artifacts: bool) -> int:
|
||||||
for line in plan_gateway(config):
|
for line in plan_gateway(config):
|
||||||
|
artifact_text = ",".join(str(path) for path in line.artifacts) if line.artifacts else "-"
|
||||||
print(
|
print(
|
||||||
"AMPG_PLAN "
|
"AMPG_PLAN "
|
||||||
f"site={line.site_id} "
|
f"site={line.site_id} "
|
||||||
@@ -56,8 +62,13 @@ def _cmd_plan(config) -> int:
|
|||||||
f"output={line.output_root} "
|
f"output={line.output_root} "
|
||||||
f"daemon={line.daemon} "
|
f"daemon={line.daemon} "
|
||||||
f"policy={line.daemon_policy} "
|
f"policy={line.daemon_policy} "
|
||||||
|
f"artifacts={artifact_text} "
|
||||||
f"action=\"{line.action}\""
|
f"action=\"{line.action}\""
|
||||||
)
|
)
|
||||||
|
if write_artifacts:
|
||||||
|
artifacts = write_plan_artifacts(config)
|
||||||
|
for artifact in artifacts:
|
||||||
|
print(f"AMPG_ARTIFACT path={artifact.path}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -33,6 +33,7 @@ class SourceConfig:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class OutputConfig:
|
class OutputConfig:
|
||||||
root: Path
|
root: Path
|
||||||
|
plan_root: Path
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -82,6 +83,7 @@ def _parse_site(raw_site: dict[str, Any], base_dir: Path) -> SiteConfig:
|
|||||||
|
|
||||||
source_path = _resolve_path(base_dir, _required_str(source, "path"))
|
source_path = _resolve_path(base_dir, _required_str(source, "path"))
|
||||||
output_root = _resolve_path(base_dir, _required_str(outputs, "root"))
|
output_root = _resolve_path(base_dir, _required_str(outputs, "root"))
|
||||||
|
plan_root = _resolve_path(base_dir, str(outputs.get("plan_root", "../dist/ampg-plan")))
|
||||||
|
|
||||||
parsed_protocols = {
|
parsed_protocols = {
|
||||||
name: _parse_protocol(name, raw_protocol)
|
name: _parse_protocol(name, raw_protocol)
|
||||||
@@ -97,7 +99,7 @@ def _parse_site(raw_site: dict[str, Any], base_dir: Path) -> SiteConfig:
|
|||||||
path=source_path,
|
path=source_path,
|
||||||
canonical_url=source.get("canonical_url"),
|
canonical_url=source.get("canonical_url"),
|
||||||
),
|
),
|
||||||
outputs=OutputConfig(root=output_root),
|
outputs=OutputConfig(root=output_root, plan_root=plan_root),
|
||||||
protocols=parsed_protocols,
|
protocols=parsed_protocols,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ CONFIG_FIELDS = (
|
|||||||
ConfigField("site.source", "path", "path", True, "", "Source tree path."),
|
ConfigField("site.source", "path", "path", True, "", "Source tree path."),
|
||||||
ConfigField("site.source", "canonical_url", "url", False, "", "Canonical clearnet URL."),
|
ConfigField("site.source", "canonical_url", "url", False, "", "Canonical clearnet URL."),
|
||||||
ConfigField("site.outputs", "root", "path", True, "", "Generated output root."),
|
ConfigField("site.outputs", "root", "path", True, "", "Generated output root."),
|
||||||
|
ConfigField("site.outputs", "plan_root", "path", False, "../dist/ampg-plan", "Generated plan artifact root."),
|
||||||
ConfigField(
|
ConfigField(
|
||||||
"site.interactions",
|
"site.interactions",
|
||||||
"default_tier",
|
"default_tier",
|
||||||
@@ -88,6 +89,22 @@ CONFIG_FIELDS = (
|
|||||||
"protocol default",
|
"protocol default",
|
||||||
"Whether AMPG adopts, manages, or only renders config.",
|
"Whether AMPG adopts, manages, or only renders config.",
|
||||||
),
|
),
|
||||||
|
ConfigField(
|
||||||
|
"site.protocols.<name>",
|
||||||
|
"max_asset_bytes",
|
||||||
|
"integer",
|
||||||
|
False,
|
||||||
|
"1048576",
|
||||||
|
"Maximum asset size copied by privacy-html render targets.",
|
||||||
|
),
|
||||||
|
ConfigField(
|
||||||
|
"site.protocols.<name>",
|
||||||
|
"script_policy",
|
||||||
|
"enum",
|
||||||
|
False,
|
||||||
|
"strip",
|
||||||
|
"Script handling policy for privacy-html render targets.",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+109
-1
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .config import GatewayConfig
|
from .config import GatewayConfig, ProtocolConfig, SiteConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -15,6 +15,13 @@ class PlanLine:
|
|||||||
daemon: str
|
daemon: str
|
||||||
daemon_policy: str
|
daemon_policy: str
|
||||||
action: str
|
action: str
|
||||||
|
artifacts: tuple[Path, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlanArtifact:
|
||||||
|
path: Path
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
def plan_gateway(config: GatewayConfig) -> list[PlanLine]:
|
def plan_gateway(config: GatewayConfig) -> list[PlanLine]:
|
||||||
@@ -23,6 +30,7 @@ def plan_gateway(config: GatewayConfig) -> list[PlanLine]:
|
|||||||
for protocol in site.protocols.values():
|
for protocol in site.protocols.values():
|
||||||
if not protocol.enabled:
|
if not protocol.enabled:
|
||||||
continue
|
continue
|
||||||
|
artifacts = tuple(artifact.path for artifact in plan_artifacts(site, protocol))
|
||||||
lines.append(
|
lines.append(
|
||||||
PlanLine(
|
PlanLine(
|
||||||
site_id=site.id,
|
site_id=site.id,
|
||||||
@@ -32,11 +40,43 @@ def plan_gateway(config: GatewayConfig) -> list[PlanLine]:
|
|||||||
daemon=protocol.daemon,
|
daemon=protocol.daemon,
|
||||||
daemon_policy=protocol.daemon_policy,
|
daemon_policy=protocol.daemon_policy,
|
||||||
action=_action_for_policy(protocol.daemon_policy),
|
action=_action_for_policy(protocol.daemon_policy),
|
||||||
|
artifacts=artifacts,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def write_plan_artifacts(config: GatewayConfig) -> list[PlanArtifact]:
|
||||||
|
artifacts: list[PlanArtifact] = []
|
||||||
|
for site in config.sites:
|
||||||
|
for protocol in site.protocols.values():
|
||||||
|
if not protocol.enabled:
|
||||||
|
continue
|
||||||
|
artifacts.extend(plan_artifacts(site, protocol))
|
||||||
|
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 plan_artifacts(site: SiteConfig, protocol: ProtocolConfig) -> list[PlanArtifact]:
|
||||||
|
if protocol.name == "clearnet" and protocol.daemon == "nginx":
|
||||||
|
return [_nginx_clearnet_artifact(site, protocol)]
|
||||||
|
if protocol.name == "tor":
|
||||||
|
artifacts = []
|
||||||
|
if protocol.daemon == "tor":
|
||||||
|
artifacts.append(_tor_artifact(site, protocol))
|
||||||
|
artifacts.append(_nginx_loopback_artifact(site, protocol))
|
||||||
|
return artifacts
|
||||||
|
if protocol.name == "i2p":
|
||||||
|
artifacts = []
|
||||||
|
if protocol.daemon == "i2pd":
|
||||||
|
artifacts.append(_i2pd_artifact(site, protocol))
|
||||||
|
artifacts.append(_nginx_loopback_artifact(site, protocol))
|
||||||
|
return artifacts
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _action_for_policy(policy: str) -> str:
|
def _action_for_policy(policy: str) -> str:
|
||||||
if policy == "external":
|
if policy == "external":
|
||||||
return "render outputs and config snippets only"
|
return "render outputs and config snippets only"
|
||||||
@@ -47,3 +87,71 @@ def _action_for_policy(policy: str) -> str:
|
|||||||
if policy == "auto":
|
if policy == "auto":
|
||||||
return "adopt healthy daemon, otherwise manage AMPG-owned instance"
|
return "adopt healthy daemon, otherwise manage AMPG-owned instance"
|
||||||
return "unknown policy; no apply support"
|
return "unknown policy; no apply support"
|
||||||
|
|
||||||
|
|
||||||
|
def _protocol_plan_dir(site: SiteConfig, protocol: ProtocolConfig) -> Path:
|
||||||
|
return site.outputs.plan_root / site.id / protocol.name
|
||||||
|
|
||||||
|
|
||||||
|
def _nginx_clearnet_artifact(site: SiteConfig, protocol: ProtocolConfig) -> PlanArtifact:
|
||||||
|
path = _protocol_plan_dir(site, protocol) / "nginx-server.conf"
|
||||||
|
content = f"""# Generated by AMPG. Review before installing.
|
||||||
|
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;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
return PlanArtifact(path=path, content=content)
|
||||||
|
|
||||||
|
|
||||||
|
def _nginx_loopback_artifact(site: SiteConfig, protocol: ProtocolConfig) -> PlanArtifact:
|
||||||
|
port = int(protocol.options.get("local_port", 18080 if protocol.name == "tor" else 18081))
|
||||||
|
path = _protocol_plan_dir(site, protocol) / "nginx-loopback.conf"
|
||||||
|
content = f"""# Generated by AMPG. Review before installing.
|
||||||
|
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;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
return PlanArtifact(path=path, content=content)
|
||||||
|
|
||||||
|
|
||||||
|
def _tor_artifact(site: SiteConfig, protocol: ProtocolConfig) -> PlanArtifact:
|
||||||
|
port = int(protocol.options.get("local_port", 18080))
|
||||||
|
hidden_service_dir = protocol.options.get(
|
||||||
|
"hidden_service_dir", f"/var/lib/ampg/tor/{site.id}"
|
||||||
|
)
|
||||||
|
path = _protocol_plan_dir(site, protocol) / "torrc-snippet.conf"
|
||||||
|
content = f"""# Generated by AMPG. Review before installing.
|
||||||
|
HiddenServiceDir {hidden_service_dir}/
|
||||||
|
HiddenServicePort 80 127.0.0.1:{port}
|
||||||
|
"""
|
||||||
|
return PlanArtifact(path=path, content=content)
|
||||||
|
|
||||||
|
|
||||||
|
def _i2pd_artifact(site: SiteConfig, protocol: ProtocolConfig) -> PlanArtifact:
|
||||||
|
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")
|
||||||
|
path = _protocol_plan_dir(site, protocol) / "i2pd-tunnel.conf"
|
||||||
|
content = f"""# Generated by AMPG. Review before installing.
|
||||||
|
[{tunnel_name}]
|
||||||
|
type = server
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = {port}
|
||||||
|
keys = {key_file}
|
||||||
|
"""
|
||||||
|
return PlanArtifact(path=path, content=content)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ canonical_url = "https://wownero.org"
|
|||||||
|
|
||||||
[site.outputs]
|
[site.outputs]
|
||||||
root = "/var/www/ampg/wownero"
|
root = "/var/www/ampg/wownero"
|
||||||
|
plan_root = "/var/lib/ampg/plans"
|
||||||
|
|
||||||
[site.interactions]
|
[site.interactions]
|
||||||
default_tier = "static"
|
default_tier = "static"
|
||||||
@@ -43,6 +44,8 @@ daemon = "tor"
|
|||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
local_port = 18080
|
local_port = 18080
|
||||||
onion_location = "auto"
|
onion_location = "auto"
|
||||||
|
max_asset_bytes = 1048576
|
||||||
|
script_policy = "strip"
|
||||||
|
|
||||||
[site.protocols.i2p]
|
[site.protocols.i2p]
|
||||||
enabled = false
|
enabled = false
|
||||||
@@ -50,6 +53,10 @@ renderer = "privacy-html"
|
|||||||
daemon = "i2pd"
|
daemon = "i2pd"
|
||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
local_port = 18081
|
local_port = 18081
|
||||||
|
keys_file = "wownero-web.dat"
|
||||||
|
tunnel_name = "wownero-web"
|
||||||
|
max_asset_bytes = 1048576
|
||||||
|
script_policy = "strip"
|
||||||
|
|
||||||
[site.protocols.gemini]
|
[site.protocols.gemini]
|
||||||
enabled = false
|
enabled = false
|
||||||
@@ -88,6 +95,8 @@ workflow.
|
|||||||
- remote fonts, analytics, and third-party embeds.
|
- remote fonts, analytics, and third-party embeds.
|
||||||
- localStorage/sessionStorage assumptions.
|
- localStorage/sessionStorage assumptions.
|
||||||
- non-local asset references unless explicitly allowed.
|
- non-local asset references unless explicitly allowed.
|
||||||
|
- JavaScript files when `script_policy = "strip"`.
|
||||||
|
- assets larger than `max_asset_bytes`.
|
||||||
|
|
||||||
`gemtext` defaults:
|
`gemtext` defaults:
|
||||||
|
|
||||||
@@ -163,6 +172,54 @@ max_tier = "forms"
|
|||||||
`max_tier` is a hard cap. If a route needs a higher tier than the protocol allows, AMPG
|
`max_tier` is a hard cap. If a route needs a higher tier than the protocol allows, AMPG
|
||||||
omits it or renders a safe alternate page.
|
omits it or renders a safe alternate page.
|
||||||
|
|
||||||
|
## Protocol-only deployments
|
||||||
|
|
||||||
|
Enabled protocols are independent. A config can omit clearnet entirely and publish only
|
||||||
|
to I2P, Tor, Gemini, Reticulum, or any combination of supported targets.
|
||||||
|
|
||||||
|
For an existing HTML site that should publish only to I2P:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[site]]
|
||||||
|
id = "example_i2p_only"
|
||||||
|
domain = "example.org"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "../example.org"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "/var/www/ampg/example"
|
||||||
|
plan_root = "/var/lib/ampg/plans"
|
||||||
|
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
local_port = 18081
|
||||||
|
keys_file = "example-web.dat"
|
||||||
|
tunnel_name = "example-web"
|
||||||
|
max_asset_bytes = 524288
|
||||||
|
script_policy = "strip"
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds only the I2P output root and only the I2P/nginx plan snippets.
|
||||||
|
|
||||||
|
## Plan artifacts
|
||||||
|
|
||||||
|
`ampg plan` prints expected artifact paths. `ampg plan --write-artifacts` writes
|
||||||
|
reviewable snippets under `site.outputs.plan_root`.
|
||||||
|
|
||||||
|
Generated snippets are not installed by `plan`:
|
||||||
|
|
||||||
|
- nginx server blocks for static HTTP roots.
|
||||||
|
- Tor hidden-service snippets for loopback HTTP targets.
|
||||||
|
- i2pd server tunnel snippets for I2P web publishing.
|
||||||
|
|
||||||
|
I2P web tunnels should use a web-specific key file such as `wownero-web.dat`; AMPG does
|
||||||
|
not reuse daemon RPC/P2P tunnel keys for web publishing.
|
||||||
|
|
||||||
## Reusable checklist
|
## Reusable checklist
|
||||||
|
|
||||||
- [ ] Keep paths explicit in production configs.
|
- [ ] Keep paths explicit in production configs.
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ from transport-interface readiness.
|
|||||||
## Safety requirements
|
## Safety requirements
|
||||||
|
|
||||||
- Show every config write before applying it.
|
- Show every config write before applying it.
|
||||||
|
- Generate snippets into the plan root before any install or reload step.
|
||||||
- Back up any adopted-daemon snippet AMPG touches.
|
- Back up any adopted-daemon snippet AMPG touches.
|
||||||
- Never delete hidden-service keys, I2P keys, or Reticulum identities without an explicit
|
- Never delete hidden-service keys, I2P keys, or Reticulum identities without an explicit
|
||||||
destructive command.
|
destructive command.
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ This file is generated from code. Do not edit it by hand.
|
|||||||
| site.source | `path` | path | yes | - | Source tree path. |
|
| site.source | `path` | path | yes | - | Source tree path. |
|
||||||
| site.source | `canonical_url` | url | no | - | Canonical clearnet URL. |
|
| site.source | `canonical_url` | url | no | - | Canonical clearnet URL. |
|
||||||
| site.outputs | `root` | path | yes | - | Generated output root. |
|
| site.outputs | `root` | path | yes | - | Generated output root. |
|
||||||
|
| site.outputs | `plan_root` | path | no | ../dist/ampg-plan | Generated plan artifact root. |
|
||||||
| site.interactions | `default_tier` | enum | no | static | Default interaction tier for routes. |
|
| site.interactions | `default_tier` | enum | no | static | Default interaction tier for routes. |
|
||||||
| site.interactions | `deny_routes` | array<string> | no | [] | Route patterns that must not be published. |
|
| site.interactions | `deny_routes` | array<string> | no | [] | Route patterns that must not be published. |
|
||||||
| site.protocols.<name> | `enabled` | boolean | no | false | Whether this protocol target is built and planned. |
|
| site.protocols.<name> | `enabled` | boolean | no | false | Whether this protocol target is built and planned. |
|
||||||
| site.protocols.<name> | `renderer` | enum | no | <protocol> | Renderer profile to use for this protocol. |
|
| site.protocols.<name> | `renderer` | enum | no | <protocol> | Renderer profile to use for this protocol. |
|
||||||
| site.protocols.<name> | `daemon` | enum | no | protocol default | Ingress daemon adapter. |
|
| site.protocols.<name> | `daemon` | enum | no | protocol default | Ingress daemon adapter. |
|
||||||
| site.protocols.<name> | `daemon_policy` | enum | no | protocol default | Whether AMPG adopts, manages, or only renders config. |
|
| site.protocols.<name> | `daemon_policy` | enum | no | protocol default | Whether AMPG adopts, manages, or only renders config. |
|
||||||
|
| site.protocols.<name> | `max_asset_bytes` | integer | no | 1048576 | Maximum asset size copied by privacy-html render targets. |
|
||||||
|
| site.protocols.<name> | `script_policy` | enum | no | strip | Script handling policy for privacy-html render targets. |
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ enabled = true
|
|||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
|
|
||||||
[site.protocols.i2p]
|
[site.protocols.i2p]
|
||||||
enabled = false
|
enabled = true
|
||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
|
|
||||||
[site.protocols.gemini]
|
[site.protocols.gemini]
|
||||||
@@ -133,6 +133,8 @@ daemon_policy = "auto"
|
|||||||
```sh
|
```sh
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml plan
|
python3 -m ampg --config examples/wownero.gateway.toml plan
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||||
|
python3 -m ampg --config examples/i2p-only.gateway.toml plan
|
||||||
|
python3 -m ampg --config examples/i2p-only.gateway.toml build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reusable checklist
|
## Reusable checklist
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
[gateway]
|
||||||
|
state_dir = "./.ampg/state"
|
||||||
|
cache_dir = "./.ampg/cache"
|
||||||
|
run_dir = "./.ampg/run"
|
||||||
|
user = "ampg"
|
||||||
|
|
||||||
|
[[site]]
|
||||||
|
id = "wownero_i2p_only"
|
||||||
|
domain = "wownero.org"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "../../wownero.org-website"
|
||||||
|
canonical_url = "https://wownero.org"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "../dist/wownero-i2p-only"
|
||||||
|
plan_root = "../dist/ampg-plan"
|
||||||
|
|
||||||
|
[site.interactions]
|
||||||
|
default_tier = "static"
|
||||||
|
deny_routes = ["/admin/*", "/internal/*", "/webhooks/*"]
|
||||||
|
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
local_port = 18081
|
||||||
|
keys_file = "wownero-web.dat"
|
||||||
|
tunnel_name = "wownero-web"
|
||||||
|
max_asset_bytes = 524288
|
||||||
|
script_policy = "strip"
|
||||||
@@ -15,6 +15,7 @@ canonical_url = "https://wownero.org"
|
|||||||
|
|
||||||
[site.outputs]
|
[site.outputs]
|
||||||
root = "../dist/wownero"
|
root = "../dist/wownero"
|
||||||
|
plan_root = "../dist/ampg-plan"
|
||||||
|
|
||||||
[site.interactions]
|
[site.interactions]
|
||||||
default_tier = "static"
|
default_tier = "static"
|
||||||
@@ -32,15 +33,21 @@ renderer = "privacy-html"
|
|||||||
daemon = "tor"
|
daemon = "tor"
|
||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
local_port = 18080
|
local_port = 18080
|
||||||
|
hidden_service_dir = "/var/lib/ampg/tor/wownero"
|
||||||
onion_location = "auto"
|
onion_location = "auto"
|
||||||
max_asset_bytes = 1048576
|
max_asset_bytes = 1048576
|
||||||
|
script_policy = "strip"
|
||||||
|
|
||||||
[site.protocols.i2p]
|
[site.protocols.i2p]
|
||||||
enabled = false
|
enabled = true
|
||||||
renderer = "privacy-html"
|
renderer = "privacy-html"
|
||||||
daemon = "i2pd"
|
daemon = "i2pd"
|
||||||
daemon_policy = "auto"
|
daemon_policy = "auto"
|
||||||
local_port = 18081
|
local_port = 18081
|
||||||
|
keys_file = "wownero-web.dat"
|
||||||
|
tunnel_name = "wownero-web"
|
||||||
|
max_asset_bytes = 1048576
|
||||||
|
script_policy = "strip"
|
||||||
|
|
||||||
[site.protocols.gemini]
|
[site.protocols.gemini]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ampg.build import build_gateway
|
||||||
|
from ampg.config import load_config
|
||||||
|
from ampg.plan import plan_gateway
|
||||||
|
|
||||||
|
|
||||||
|
class I2POnlyTest(unittest.TestCase):
|
||||||
|
def test_existing_html_source_can_build_only_i2p(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
source = root / "source"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "index.html").write_text(
|
||||||
|
'<html><body onload="boot()"><script src="app.js"></script><h1>Hi</h1></body></html>',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(source / "app.js").write_text("boot()", encoding="utf-8")
|
||||||
|
config_path = root / "gateway.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[[site]]
|
||||||
|
id = "i2p_only"
|
||||||
|
domain = "example.test"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "./source"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "./out"
|
||||||
|
plan_root = "./plan"
|
||||||
|
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
local_port = 19081
|
||||||
|
keys_file = "example-web.dat"
|
||||||
|
tunnel_name = "example-web"
|
||||||
|
max_asset_bytes = 1024
|
||||||
|
script_policy = "strip"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
plan = plan_gateway(config)
|
||||||
|
results = build_gateway(config)
|
||||||
|
|
||||||
|
self.assertEqual([line.protocol for line in plan], ["i2p"])
|
||||||
|
self.assertEqual([result.protocol for result in results], ["i2p"])
|
||||||
|
self.assertTrue((root / "out/i2p/index.html").exists())
|
||||||
|
self.assertFalse((root / "out/clearnet").exists())
|
||||||
|
self.assertFalse((root / "out/i2p/app.js").exists())
|
||||||
|
rendered = (root / "out/i2p/index.html").read_text(encoding="utf-8")
|
||||||
|
self.assertNotIn("<script", rendered)
|
||||||
|
self.assertNotIn("onload", rendered)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ampg.config import load_config
|
||||||
|
from ampg.plan import plan_gateway, write_plan_artifacts
|
||||||
|
|
||||||
|
|
||||||
|
class PlanArtifactsTest(unittest.TestCase):
|
||||||
|
def test_writes_tor_and_i2p_artifacts(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
source = root / "site"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "index.html").write_text("<h1>Hi</h1>", encoding="utf-8")
|
||||||
|
config_path = root / "gateway.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
"""
|
||||||
|
[[site]]
|
||||||
|
id = "example"
|
||||||
|
domain = "example.test"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "./site"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "./out"
|
||||||
|
plan_root = "./plan"
|
||||||
|
|
||||||
|
[site.protocols.tor]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "tor"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
local_port = 19080
|
||||||
|
hidden_service_dir = "/var/lib/ampg/tor/example"
|
||||||
|
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
local_port = 19081
|
||||||
|
keys_file = "example-web.dat"
|
||||||
|
tunnel_name = "example-web"
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = load_config(config_path)
|
||||||
|
lines = plan_gateway(config)
|
||||||
|
artifacts = write_plan_artifacts(config)
|
||||||
|
|
||||||
|
self.assertEqual(len(lines), 2)
|
||||||
|
self.assertEqual(len(artifacts), 4)
|
||||||
|
|
||||||
|
torrc = root / "plan/example/tor/torrc-snippet.conf"
|
||||||
|
tor_nginx = root / "plan/example/tor/nginx-loopback.conf"
|
||||||
|
i2p_tunnel = root / "plan/example/i2p/i2pd-tunnel.conf"
|
||||||
|
i2p_nginx = root / "plan/example/i2p/nginx-loopback.conf"
|
||||||
|
|
||||||
|
self.assertIn("HiddenServicePort 80 127.0.0.1:19080", torrc.read_text())
|
||||||
|
self.assertIn("listen 127.0.0.1:19080;", tor_nginx.read_text())
|
||||||
|
self.assertIn("[example-web]", i2p_tunnel.read_text())
|
||||||
|
self.assertIn("keys = example-web.dat", i2p_tunnel.read_text())
|
||||||
|
self.assertIn("listen 127.0.0.1:19081;", i2p_nginx.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user