diff --git a/tests/caddyfile.bats b/tests/caddyfile.bats index 13a7315..28b8f2a 100644 --- a/tests/caddyfile.bats +++ b/tests/caddyfile.bats @@ -240,14 +240,12 @@ CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile" } @test "alloy's UI address comes from the unit, not the config file" { - # alloy 1.x refuses to configure the ui service from a config block - # ("service ui does not support being configured"), so the unit moves the - # UI off the container's loopback with a command-line flag. Without that - # flag the port cannot be published and the service has no reachable - # health endpoint at all — which is why the homepage status card for - # alloy read "down" while it ran under compose. + # alloy 1.x refuses to configure the ui service from a config block, so the + # unit sets the UI address on the command line. As a Network=host root unit + # that address is the host loopback directly (no PublishPort) — reachable + # from the host, from nothing off-host. ! grep -qE '^(ui|server) \{' "$REPO_ROOT/shared/alloy/config.alloy" local unit="$REPO_ROOT/dankclaw/observability/quadlet/alloy.container" - grep -q -- '--server.http.listen-addr=0.0.0.0:12345' "$unit" - grep -q '^PublishPort=127.0.0.1:12345:12345$' "$unit" + grep -q -- '--server.http.listen-addr=127.0.0.1:12345' "$unit" + ! grep -q '^PublishPort=' "$unit" } diff --git a/tests/observability_quadlet.bats b/tests/observability_quadlet.bats index 12119cd..2df7eac 100755 --- a/tests/observability_quadlet.bats +++ b/tests/observability_quadlet.bats @@ -41,13 +41,17 @@ service_section() { } @test "every observability unit is boot-persistent" { - # Generated units cannot be enabled on this host's podman 5.x v1 quadlet - # generator; [Install] + linger is the entire boot-persistence story. + # Generated units cannot be enabled on this host's podman quadlet generator; + # [Install] + linger is the boot-persistence story for the rootless five. + # Alloy is a ROOT system unit, so its [Install] targets multi-user.target. local svc for svc in "${UNITS[@]}"; do + [[ "$svc" == alloy ]] && continue grep -q '^WantedBy=default.target$' "$UNIT_DIR/$svc.container" \ || { echo "$svc is not boot-persistent"; return 1; } done + grep -q '^WantedBy=multi-user.target$' "$UNIT_DIR/alloy.container" \ + || { echo "alloy (root unit) is not boot-persistent"; return 1; } } @test "every observability unit is self-healing" { @@ -140,15 +144,20 @@ service_section() { grep -q 'reverse_proxy prometheus:9090' "$REPO_ROOT/dankclaw/Caddyfile" grep -q 'reverse_proxy loki:3100' "$REPO_ROOT/dankclaw/Caddyfile" local svc - for svc in prometheus grafana loki alloy; do + for svc in prometheus grafana loki; do grep -q "^ContainerName=$svc$" "$UNIT_DIR/$svc.container" \ || { echo "$svc unit renames its container"; return 1; } grep -q '^Network=dankclaw_default$' "$UNIT_DIR/$svc.container" \ || { echo "$svc is not on the network Caddy and its peers use"; return 1; } done + # Alloy is not on that network: it is a root container on Network=host, the + # only way to reach Loki's 127.0.0.1:3100 publish from outside a bridge. + grep -q '^ContainerName=alloy$' "$UNIT_DIR/alloy.container" + grep -q '^Network=host$' "$UNIT_DIR/alloy.container" \ + || { echo "alloy must be Network=host to reach Loki on the host loopback"; return 1; } grep -q '"grafana:3000"' "$REPO_ROOT/shared/prometheus/prometheus.yml" grep -q '"loki:3100"' "$REPO_ROOT/shared/prometheus/prometheus.yml" - grep -q 'url = "http://loki:3100/loki/api/v1/push"' "$REPO_ROOT/shared/alloy/config.alloy" + grep -q 'url = "http://127.0.0.1:3100/loki/api/v1/push"' "$REPO_ROOT/shared/alloy/config.alloy" } @test "every observability unit has a health check" { @@ -239,12 +248,14 @@ service_section() { } @test "startup order follows the data dependencies" { - # Grafana is useless without Prometheus and Alloy drops logs without Loki; - # both pairs are expressed as systemd ordering, not launch luck. + # Grafana is useless without Prometheus (both rootless, same manager). Alloy + # used to order on loki.service; as a ROOT unit it lives on the SYSTEM + # manager and cannot order against a rootless user unit, so it depends only + # on the network and retries its push until Loki answers. grep -q 'After=network-online.target prometheus.service' "$UNIT_DIR/grafana.container" grep -q 'Wants=network-online.target prometheus.service' "$UNIT_DIR/grafana.container" - grep -q 'After=network-online.target loki.service' "$UNIT_DIR/alloy.container" - grep -q 'Wants=network-online.target loki.service' "$UNIT_DIR/alloy.container" + grep -q '^After=network-online.target$' "$UNIT_DIR/alloy.container" + ! grep -q 'loki.service' "$UNIT_DIR/alloy.container" } @test "the deployer installs, restarts and health-checks each unit" { @@ -281,6 +292,24 @@ PAIRS grep -q 'build_compose_image alloy' "$REMOTE_DEPLOY" } +@test "alloy is a root host-network unit that reads every journal" { + # The whole point of the root cutover: a rootless container cannot open the + # 0640 journal files. Pin the properties that make the pipeline carry data. + local u="$UNIT_DIR/alloy.container" + grep -q '^Network=host$' "$u" + grep -q '^Volume=/etc/machine-id:/etc/machine-id:ro$' "$u" \ + || { echo "alloy missing the load-bearing machine-id mount"; return 1; } + grep -q '^Volume=/var/log/journal:/var/log/journal:ro$' "$u" + grep -q '^Volume=/run/log/journal:/run/log/journal:ro$' "$u" + grep -q -- '--server.http.listen-addr=127.0.0.1:12345' "$u" + # Deployer: system install + root image mirror, not the user quadlet loop. + grep -q '^deploy_alloy() {' "$REMOTE_DEPLOY" + grep -q 'install_system_quadlet dankclaw/observability/quadlet alloy' "$REMOTE_DEPLOY" + grep -q '/etc/containers/systemd/' "$REMOTE_DEPLOY" + grep -q 'mirror_image_to_root localhost/franksdank/alloy:latest' "$REMOTE_DEPLOY" + grep -q 'alloy) deploy_alloy ;;' "$REMOTE_DEPLOY" +} + @test "no dankclaw compose file competes with the quadlet units" { # Two owners for one container name is how a compose run resurrects a # stale duplicate next to the quadlet-managed one. This file was retired on