Implement supported ingress and TLS profiles
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m2s

This commit is contained in:
2026-08-03 01:15:06 +02:00
parent b40f1428fd
commit 2c515f73c2
12 changed files with 1174 additions and 37 deletions
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Exercise generated managed ingress with a real Caddy container."""
from __future__ import annotations
import argparse
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import time
from uuid import uuid4
META_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(META_ROOT / "tools" / "deployment"))
from govoplan_deploy.bundle import ( # noqa: E402
render_caddy_config,
render_load_balancer_config,
)
from govoplan_deploy.model import default_spec # noqa: E402
DIGEST_IMAGE = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$")
def _run(argv: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
argv,
check=check,
capture_output=True,
text=True,
timeout=60,
)
def _published_port(container: str, target: int) -> int:
output = _run(["docker", "port", container, f"{target}/tcp"]).stdout.strip()
try:
return int(output.rsplit(":", 1)[1])
except (IndexError, ValueError) as exc:
raise RuntimeError(f"cannot determine published port from {output!r}") from exc
def _curl(url: str, *, headers: bool = False) -> str:
argv = ["curl", "--silent", "--show-error", "--insecure"]
if headers:
argv.extend(["--head"])
argv.append(url)
return _run(argv).stdout
def _wait_for_https(port: int) -> str:
deadline = time.monotonic() + 30
last_error = ""
while time.monotonic() < deadline:
try:
return _curl(f"https://localhost:{port}/health")
except subprocess.CalledProcessError as exc:
last_error = exc.stderr.strip()
time.sleep(0.5)
raise RuntimeError(f"managed ingress did not become ready: {last_error}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--caddy-image", required=True)
parser.add_argument("--load-balancer-image", required=True)
args = parser.parse_args()
for label, image in (
("--caddy-image", args.caddy_image),
("--load-balancer-image", args.load_balancer_image),
):
if DIGEST_IMAGE.fullmatch(image) is None:
parser.error(f"{label} must be pinned by sha256 digest")
if shutil.which("docker") is None or shutil.which("curl") is None:
parser.error("docker and curl are required")
suffix = uuid4().hex[:10]
network = f"govoplan-ingress-drill-{suffix}"
ingress = f"govoplan-ingress-{suffix}"
backend = f"govoplan-ingress-backend-{suffix}"
data_volume = f"govoplan-ingress-data-{suffix}"
config_volume = f"govoplan-ingress-config-{suffix}"
cleanup = [
["docker", "rm", "--force", ingress, backend],
["docker", "network", "rm", network],
["docker", "volume", "rm", data_volume, config_volume],
]
try:
_run(["docker", "network", "create", network])
_run(["docker", "volume", "create", data_volume])
_run(["docker", "volume", "create", config_volume])
with tempfile.TemporaryDirectory(prefix="govoplan-ingress-") as directory:
root = Path(directory)
backend_config = root / "backend.Caddyfile"
backend_config.write_text(
"""{
admin off
auto_https off
}
:8080 {
respond /health "proto={http.request.header.X-Forwarded-Proto}"
}
""",
encoding="utf-8",
)
backend_config.chmod(0o644)
spec = default_spec(
profile="self-hosted",
public_url="https://localhost:8443",
ingress_mode="managed",
ingress_image=args.caddy_image,
ingress_https_port=8443,
acme_email="operator@example.test",
)
ingress_config = root / "Caddyfile"
ingress_config.write_text(render_caddy_config(spec), encoding="utf-8")
ingress_config.chmod(0o644)
load_balancer_config = root / "haproxy.cfg"
load_balancer_config.write_text(
render_load_balancer_config(spec),
encoding="utf-8",
)
load_balancer_config.chmod(0o644)
_run(
[
"docker",
"run",
"--rm",
"--read-only",
"--cap-drop",
"ALL",
"--mount",
f"type=bind,src={load_balancer_config},dst=/usr/local/etc/haproxy/haproxy.cfg,readonly",
args.load_balancer_image,
"haproxy",
"-c",
"-f",
"/usr/local/etc/haproxy/haproxy.cfg",
]
)
_run(
[
"docker",
"run",
"--detach",
"--name",
backend,
"--network",
network,
"--read-only",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=16m",
"--mount",
f"type=bind,src={backend_config},dst=/etc/caddy/Caddyfile,readonly",
args.caddy_image,
"caddy",
"run",
"--config",
"/etc/caddy/Caddyfile",
]
)
ingress_command = [
"docker",
"run",
"--detach",
"--name",
ingress,
"--network",
network,
"--read-only",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=16m",
"--security-opt",
"no-new-privileges",
"--cap-drop",
"ALL",
"--publish",
"127.0.0.1::8080",
"--publish",
"127.0.0.1::8443",
"--mount",
f"type=bind,src={ingress_config},dst=/etc/caddy/Caddyfile,readonly",
"--mount",
f"type=volume,src={data_volume},dst=/data",
"--mount",
f"type=volume,src={config_volume},dst=/config",
args.caddy_image,
"caddy",
"run",
"--config",
"/etc/caddy/Caddyfile",
]
_run(ingress_command)
http_port = _published_port(ingress, 8080)
https_port = _published_port(ingress, 8443)
body = _wait_for_https(https_port)
if body.strip() != "proto=https":
raise RuntimeError(f"forwarded protocol was not normalized: {body!r}")
redirect = _curl(f"http://localhost:{http_port}/health", headers=True)
if not redirect.startswith("HTTP/1.1 308"):
raise RuntimeError(f"HTTP was not redirected to HTTPS: {redirect!r}")
_run(["docker", "rm", "--force", ingress])
_run(ingress_command)
https_port = _published_port(ingress, 8443)
if _wait_for_https(https_port).strip() != "proto=https":
raise RuntimeError(
"managed ingress did not recover with persistent state"
)
_run(
[
"docker",
"run",
"--rm",
"--mount",
f"type=volume,src={data_volume},dst=/data,readonly",
"--entrypoint",
"sh",
args.caddy_image,
"-c",
'test -n "$(find /data -type f -print -quit)"',
]
)
finally:
for command in cleanup:
_run(command, check=False)
print("Managed ingress container drill passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())