testsAndMisc/linux_configuration/features/lib/kcd2_serve.sh
Krzysztof kuhy Rudnicki 2a76d05f9d Static-site Caddies: shared AgentReady site block, serve redeploy, verify
static_site_caddy.sh is the one site-block body for the personal website
and kcd2 dice Caddies: prerendered paths, real 404s and the ai-catalog Link
header instead of an SPA catch-all. Both setup scripts gain a sudo-free
`serve` that recreates only their container; kcd2's readiness wait needs a
200. redeploy_agent_ready_sites.sh serves both, restarts the popes site and
runs the AgentReady checker against all three public URLs. Tested by
test_static_site_serve.sh, test_redeploy_agent_ready_sites.sh and the
caddy:2.8 e2e_agent_ready_caddy.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
2026-10-22 21:23:59 +02:00

105 lines
4.1 KiB
Bash
Executable File

#!/bin/bash
# Compose stack, Caddy snippet and readiness polling for the dice solver.
#
# Sourced by setup_kcd2_dice_solver.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
# shellcheck source=static_site_caddy.sh
source "${BASH_SOURCE[0]%/*}/static_site_caddy.sh"
# --- Phase 3: the static-serve stack ----------------------------------------
write_serve_stack() {
ensure_dir "$DICE_DATA_DIR"
cat >"$DICE_COMPOSE" <<EOF
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
services:
${DICE_CONTAINER}:
image: caddy:2.8
container_name: ${DICE_CONTAINER}
restart: unless-stopped
# host networking is REQUIRED, not a style choice: the nftables forward
# chain is policy drop with no accept rules, so a bridge-networked
# container has no outbound egress at all.
network_mode: host
volumes:
- ${DICE_SRC}/dist:/srv:ro
- ./Caddyfile:/etc/caddy/Caddyfile:ro
EOF
cat >"$DICE_INNER_CADDY" <<EOF
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
# Static file server on loopback; fronted by ${CADDY_CONTAINER} on the public
# domain.
#
# admin off is REQUIRED: this container shares the host network namespace with
# ${CADDY_CONTAINER}, so without it both would listen on the admin API (:2019)
# and a 'caddy reload' aimed at the edge could be applied here instead — which
# would make this process grab 80/443 and run its own ACME.
#
# auto_https off and the explicit http:// scheme are EQUALLY required, and this
# was found the hard way. A site address of "127.0.0.1:${DICE_PORT}" reads as a
# HOSTNAME, so Caddy turns on automatic HTTPS for it: it issues a local
# certificate, serves TLS on ${DICE_PORT} (so the plain-HTTP health check fails)
# and — the real damage — starts an HTTP->HTTPS redirect listener on :80. Under
# host networking that binds *:80 next to the edge, and SO_REUSEPORT then splits
# inbound HTTP between the two, which would intermittently break ACME
# challenges for every domain on this host.
#
# The site address is port-only and the loopback restriction is done with
# "bind". These are two different things and using the wrong one breaks in a way
# that looks like it works:
#
# A site address of "http://127.0.0.1:${DICE_PORT}" sets a host MATCHER of
# 127.0.0.1. ${CADDY_CONTAINER} forwards the original "Host:
# ${DICE_DOMAIN}" header, which then matches no route, and Caddy answers 200
# with an empty body. A local health check passes (curl sends Host:
# 127.0.0.1) while every real request serves a blank page.
#
# ":${DICE_PORT}" matches any Host, and "bind 127.0.0.1" restricts the
# listener itself — which the address never did. Without bind, host
# networking would expose this unfronted server to the whole LAN.
{
admin off
auto_https off
}
:${DICE_PORT} {
bind 127.0.0.1
$(static_site_caddy_body)
}
EOF
log_ok "Wrote ${DICE_COMPOSE} and ${DICE_INNER_CADDY}."
}
# --- Phase 4: front it on the public domain ---------------------------------
write_snippet() {
cat >"$DICE_SNIPPET" <<EOF
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
${DICE_DOMAIN} {
reverse_proxy 127.0.0.1:${DICE_PORT}
}
EOF
log_ok "Wrote ${DICE_SNIPPET}."
}
# --- Phase 5: start ---------------------------------------------------------
# Sends the Host header the edge will actually forward, and requires a
# non-empty body. A bare "curl http://127.0.0.1:PORT/" sends Host: 127.0.0.1
# and returns an empty 200 against a host-matcher misconfiguration — it passes
# while every real request serves a blank page. Ask the question the edge asks.
# The status matters as much as the body: the handle_errors 404 page has a body
# too, so a non-empty 404 (an empty or stale /srv) must not count as ready.
wait_for_site() {
local _ code size
for _ in $(seq 1 30); do
read -r code size < <(curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
-H "Host: ${DICE_DOMAIN}" "http://127.0.0.1:${DICE_PORT}/" 2>/dev/null || echo "000 0")
if [[ $code == "200" && ${size:-0} -gt 0 ]]; then
return 0
fi
sleep 1
done
return 1
}