Implement supported ingress and TLS profiles
This commit is contained in:
@@ -22,6 +22,8 @@ ENV_FILENAME = "secrets.env"
|
||||
COMPOSE_FILENAME = "compose.json"
|
||||
GARAGE_CONFIG_FILENAME = "garage.toml"
|
||||
LOAD_BALANCER_CONFIG_FILENAME = "load-balancer.cfg"
|
||||
CADDY_CONFIG_FILENAME = "Caddyfile"
|
||||
EXISTING_PROXY_FILENAME = "existing-proxy.json"
|
||||
PLAN_FILENAME = "plan.json"
|
||||
RECEIPT_FILENAME = "receipt.json"
|
||||
MANIFEST_FILENAME = "distribution-manifest.json"
|
||||
@@ -87,6 +89,8 @@ class BundlePaths:
|
||||
compose: Path
|
||||
garage_config: Path
|
||||
load_balancer_config: Path
|
||||
caddy_config: Path
|
||||
existing_proxy: Path
|
||||
plan: Path
|
||||
receipt: Path
|
||||
manifest: Path
|
||||
@@ -106,6 +110,8 @@ def bundle_paths(root: Path) -> BundlePaths:
|
||||
compose=resolved / COMPOSE_FILENAME,
|
||||
garage_config=resolved / GARAGE_CONFIG_FILENAME,
|
||||
load_balancer_config=resolved / LOAD_BALANCER_CONFIG_FILENAME,
|
||||
caddy_config=resolved / CADDY_CONFIG_FILENAME,
|
||||
existing_proxy=resolved / EXISTING_PROXY_FILENAME,
|
||||
plan=resolved / PLAN_FILENAME,
|
||||
receipt=resolved / RECEIPT_FILENAME,
|
||||
manifest=resolved / MANIFEST_FILENAME,
|
||||
@@ -455,6 +461,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
"restart": "unless-stopped",
|
||||
"volumes": data_mounts,
|
||||
"networks": ["internal"],
|
||||
"read_only": True,
|
||||
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"cap_drop": ["ALL"],
|
||||
}
|
||||
if dependency_conditions:
|
||||
common_runtime["depends_on"] = dependency_conditions
|
||||
@@ -470,6 +480,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
"restart": "no",
|
||||
"volumes": data_mounts,
|
||||
"networks": ["internal"],
|
||||
"read_only": True,
|
||||
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"cap_drop": ["ALL"],
|
||||
**({"depends_on": dependency_conditions} if dependency_conditions else {}),
|
||||
}
|
||||
services["api"] = {
|
||||
@@ -515,9 +529,13 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
"restart": "unless-stopped",
|
||||
"scale": spec.replicas.web,
|
||||
"environment": {"GOVOPLAN_API_UPSTREAM": "http://load-balancer:8000"},
|
||||
"read_only": True,
|
||||
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"cap_drop": ["ALL"],
|
||||
"networks": ["internal"],
|
||||
}
|
||||
services["load-balancer"] = {
|
||||
load_balancer: dict[str, object] = {
|
||||
"image": spec.components.load_balancer.image,
|
||||
"restart": "unless-stopped",
|
||||
"healthcheck": {
|
||||
@@ -537,7 +555,6 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
render_load_balancer_config(spec).encode("utf-8")
|
||||
).hexdigest()
|
||||
},
|
||||
"ports": [_published_port(spec.listen.address, spec.listen.port, 8080)],
|
||||
"read_only": True,
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"volumes": [
|
||||
@@ -545,6 +562,53 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
],
|
||||
"networks": ["internal"],
|
||||
}
|
||||
if spec.ingress.mode != "managed":
|
||||
load_balancer["ports"] = [
|
||||
_published_port(spec.listen.address, spec.listen.port, 8080)
|
||||
]
|
||||
services["load-balancer"] = load_balancer
|
||||
if spec.ingress.mode == "managed":
|
||||
services["ingress"] = {
|
||||
"image": spec.ingress.image,
|
||||
"restart": "unless-stopped",
|
||||
"command": [
|
||||
"caddy",
|
||||
"run",
|
||||
"--config",
|
||||
"/etc/caddy/Caddyfile",
|
||||
"--adapter",
|
||||
"caddyfile",
|
||||
],
|
||||
"healthcheck": {
|
||||
"test": [
|
||||
"CMD",
|
||||
"caddy",
|
||||
"validate",
|
||||
"--config",
|
||||
"/etc/caddy/Caddyfile",
|
||||
"--adapter",
|
||||
"caddyfile",
|
||||
],
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 3,
|
||||
},
|
||||
"ports": [
|
||||
_published_port("0.0.0.0", spec.ingress.http_port, 8080),
|
||||
_published_port("0.0.0.0", spec.ingress.https_port, 8443),
|
||||
],
|
||||
"read_only": True,
|
||||
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"cap_drop": ["ALL"],
|
||||
"volumes": [
|
||||
f"./{CADDY_CONFIG_FILENAME}:/etc/caddy/Caddyfile:ro",
|
||||
"caddy-data:/data",
|
||||
"caddy-config:/config",
|
||||
],
|
||||
"networks": ["internal"],
|
||||
"depends_on": {"load-balancer": {"condition": "service_healthy"}},
|
||||
}
|
||||
if spec.components.redis.mode != "disabled":
|
||||
services["worker"] = {
|
||||
**common_runtime,
|
||||
@@ -606,6 +670,9 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
|
||||
if spec.components.storage.mode == "garage":
|
||||
volumes["garage-meta"] = {}
|
||||
volumes["garage-data"] = {}
|
||||
if spec.ingress.mode == "managed":
|
||||
volumes["caddy-data"] = {}
|
||||
volumes["caddy-config"] = {}
|
||||
|
||||
return {
|
||||
"name": spec.installation_id,
|
||||
@@ -642,6 +709,31 @@ api_bind_addr = "[::]:3903"
|
||||
|
||||
def render_load_balancer_config(spec: InstallationSpec) -> str:
|
||||
health_host = urlsplit(spec.public_url).hostname or "localhost"
|
||||
trusted_proxy_cidrs = (
|
||||
(spec.network_subnet,)
|
||||
if spec.ingress.mode == "managed"
|
||||
else spec.ingress.trusted_proxy_cidrs
|
||||
if spec.ingress.mode == "existing-proxy"
|
||||
else ()
|
||||
)
|
||||
trusted_acl = (
|
||||
" acl trusted_forward_proxy src " + " ".join(trusted_proxy_cidrs) + "\n"
|
||||
if trusted_proxy_cidrs
|
||||
else ""
|
||||
)
|
||||
forwarded_rules = (
|
||||
" http-request set-var(txn.forwarded_proto) req.hdr(X-Forwarded-Proto) if trusted_forward_proxy\n"
|
||||
" http-request del-header X-Forwarded-Proto\n"
|
||||
" http-request set-header X-Forwarded-Proto https if trusted_forward_proxy { var(txn.forwarded_proto) -m str https }\n"
|
||||
" http-request set-header X-Forwarded-Proto http unless { var(txn.forwarded_proto) -m str https }\n"
|
||||
" http-request del-header X-Forwarded-For unless trusted_forward_proxy\n"
|
||||
if trusted_proxy_cidrs
|
||||
else (
|
||||
" http-request del-header X-Forwarded-Proto\n"
|
||||
" http-request set-header X-Forwarded-Proto http\n"
|
||||
" http-request del-header X-Forwarded-For\n"
|
||||
)
|
||||
)
|
||||
return f"""global
|
||||
log stdout format raw local0
|
||||
maxconn 4096
|
||||
@@ -669,6 +761,9 @@ resolvers docker
|
||||
|
||||
frontend public_web
|
||||
bind :8080
|
||||
{trusted_acl}{forwarded_rules} option forwardfor
|
||||
http-request del-header X-Forwarded-Host
|
||||
http-request set-header X-Forwarded-Host %[req.hdr(host)]
|
||||
default_backend web_replicas
|
||||
|
||||
backend web_replicas
|
||||
@@ -690,6 +785,52 @@ backend api_replicas
|
||||
"""
|
||||
|
||||
|
||||
def render_caddy_config(spec: InstallationSpec) -> str:
|
||||
if spec.ingress.mode != "managed":
|
||||
return "# Managed ingress is not selected.\n"
|
||||
hostname = urlsplit(spec.public_url).hostname or ""
|
||||
return f"""{{
|
||||
admin off
|
||||
email {spec.ingress.acme_email}
|
||||
http_port 8080
|
||||
https_port 8443
|
||||
}}
|
||||
|
||||
{hostname} {{
|
||||
encode zstd gzip
|
||||
header {{
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
}}
|
||||
reverse_proxy load-balancer:8080 {{
|
||||
header_up X-Forwarded-Proto https
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def render_existing_proxy_contract(spec: InstallationSpec) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"mode": spec.ingress.mode,
|
||||
"public_url": spec.public_url,
|
||||
"upstream": (
|
||||
f"http://{spec.listen.address}:{spec.listen.port}"
|
||||
if spec.ingress.mode == "existing-proxy"
|
||||
else None
|
||||
),
|
||||
"trusted_proxy_cidrs": list(spec.ingress.trusted_proxy_cidrs),
|
||||
"required_headers": {
|
||||
"Host": urlsplit(spec.public_url).hostname or "",
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-For": "client, proxy chain",
|
||||
},
|
||||
"health_paths": {
|
||||
"load_balancer": "/health",
|
||||
"api_readiness": "/health/ready",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def service_names(spec: InstallationSpec) -> tuple[str, ...]:
|
||||
return tuple(render_compose(spec)["services"].keys())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user