diff --git a/dankclaw/observability/quadlet/grafana.container b/dankclaw/observability/quadlet/grafana.container index c9e31c3..a790d89 100644 --- a/dankclaw/observability/quadlet/grafana.container +++ b/dankclaw/observability/quadlet/grafana.container @@ -62,9 +62,16 @@ HealthRetries=3 HealthStartPeriod=60s [Service] -# systemd-side only: supplies ${GRAFANA_OAUTH_CLIENT_SECRET} for expansion -# above. The container does not receive this file. -EnvironmentFile=/home/dank/serverAppConfigs/.env +# systemd-side only: supplies ${GRAFANA_OAUTH_CLIENT_SECRET} for the expansion +# above. The container never receives this file. +# +# It is the per-service env file the deployer seeds from dankclaw/.env — the +# fleet convention (caddy, pihole, copyparty, romm and seerr all read one). +# The repo-root .env that the compose service happened to pick up is a gluetun +# env file: it carries HOME=/root and the WireGuard keys, and importing it into +# this service makes podman look for its configuration under /root, which it +# cannot read, so the unit fails to start. Seed the value into dankclaw/.env. +EnvironmentFile=/home/dank/Containers/grafana/env Restart=always RestartSec=10 diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh index 887325e..7a67348 100755 --- a/scripts/deploy-remote.sh +++ b/scripts/deploy-remote.sh @@ -378,6 +378,24 @@ deploy_compose_service() { esac } +# grafana is the one observability service that holds a credential: the +# Authentik OAuth2 client secret. It is seeded into a per-service env file (the +# fleet convention: caddy, pihole, copyparty, romm, seerr) instead of being +# imported from the repo .env wholesale. The repo-root .env that the compose +# service used to pick up is a gluetun env file — it also carries HOME=/root +# and the WireGuard keys, and an EnvironmentFile of that file sets HOME for the +# service, which makes podman look for its config under /root and fail. +seed_grafana_env() { + local dest=/home/dank/Containers/grafana/env + mkdir -p /home/dank/Containers/grafana + if [[ ! -f "$dest" ]]; then + awk -F= '/^GRAFANA_OAUTH_CLIENT_SECRET=/ { print }' dankclaw/.env > "$dest" + chmod 600 "$dest" + fi + grep -q '^GRAFANA_OAUTH_CLIENT_SECRET=.' "$dest" \ + || { echo "grafana env missing GRAFANA_OAUTH_CLIENT_SECRET; set it in dankclaw/.env" >&2; return 1; } +} + # The five observability containers left compose on 2026-09-11 (they were # started by hand with podman-compose and never came back after a reboot). # They are ordinary rootless Quadlet units now: same images, same mounts, same @@ -399,7 +417,10 @@ deploy_observability() { copy_alloy_config build_compose_image alloy ;; - grafana|node-exporter) + grafana) + seed_grafana_env || return 1 + ;; + node-exporter) ;; *) echo "unknown observability service: $service" >&2 diff --git a/tests/observability_quadlet.bats b/tests/observability_quadlet.bats index 32052e4..e20b592 100755 --- a/tests/observability_quadlet.bats +++ b/tests/observability_quadlet.bats @@ -146,17 +146,32 @@ service_section() { done } -@test "grafana receives the OAuth client secret without receiving the whole .env" { - # systemd expands ${GRAFANA_OAUTH_CLIENT_SECRET} in [Container] from the - # [Service] EnvironmentFile. Putting that file in [Container] would hand - # the container every fleet secret in it (Porkbun keys, Authentik keys, - # the sudo password file's neighbours) for the sake of one value. +@test "grafana gets its OAuth client secret without getting a whole env file" { + # systemd expands ${GRAFANA_OAUTH_CLIENT_SECRET} for the [Container] + # Environment= line from the [Service] EnvironmentFile, which keeps the + # file out of the container entirely. That file is the per-service one the + # deployer seeds (fleet convention: caddy, pihole, copyparty, romm, seerr) + # — NOT a repo .env. The repo-root .env the compose service happened to + # read is a gluetun env file carrying HOME=/root and the WireGuard keys; + # importing it sets HOME for this service and podman then looks for its + # configuration under /root and refuses to start. local unit="$UNIT_DIR/grafana.container" - service_section "$unit" | grep -q '^EnvironmentFile=/home/dank/serverAppConfigs/.env$' + service_section "$unit" | grep -q '^EnvironmentFile=/home/dank/Containers/grafana/env$' + ! service_section "$unit" | grep -q 'serverAppConfigs' ! container_section "$unit" | grep -q '^EnvironmentFile=' container_section "$unit" | grep -q '^Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=\${GRAFANA_OAUTH_CLIENT_SECRET}$' } +@test "the deployer seeds exactly one value into the grafana env file" { + # The single-variable filter is the point: a whole-file copy is how the + # HOME=/root failure got in. + grep -q '^seed_grafana_env() {' "$REMOTE_DEPLOY" + grep -q '^ local dest=/home/dank/Containers/grafana/env$' "$REMOTE_DEPLOY" + grep -q "awk -F= '/^GRAFANA_OAUTH_CLIENT_SECRET=/ { print }' dankclaw/.env > \"\$dest\"" "$REMOTE_DEPLOY" + grep -q "grep -q '\^GRAFANA_OAUTH_CLIENT_SECRET=\.' \"\$dest\"" "$REMOTE_DEPLOY" + grep -q 'seed_grafana_env || return 1' "$REMOTE_DEPLOY" +} + @test "observability units hold no literal secret material" { # A secret-named setting is allowed only when its value is a ${VAR} # reference resolved from an EnvironmentFile systemd reads; a literal value