Implement supported ingress and TLS profiles
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user