Close the un-gated-UI security finding for forward-auth apps (R-IDP-02) by rendering the Option-B per-app oauth2-proxy reverse-proxy sidecar that crafty proves working. catalog/render.py now emits, for any forward-auth app with an exposed UI: the oauth2-proxy sidecar (upstream http://127.0.0.1:<uiport>, :4180, --allowed-group per auth.group), an authproxy :4180 Service port, an HTTPRoute backendRef -> :4180, and a gateway-ingress CiliumNetworkPolicy that allows the Gateway to reach ONLY :4180 (fail-closed: the bare UI is never served un-authenticated). oidc/native/none apps render byte-identical. This is the CANARY: only pihole's golden carries the sidecar and is verified fail-closed in-cluster first. jackett/qbittorrent/radarr/sonarr are deferred to the rollout commit (temporarily listed in ci/render-golden.sh) so their UIs keep serving exactly as today until their clients+secrets land. - keycloak: confidential pihole client (realm-import + realm-clients mirror), provisioned live via kcadm (groups membership mapper, bare names). - secret: SOPS/age pihole-oauth2-proxy-credentials (client-id/secret/cookie), wired into apps/wave-0 via the renderer's new sidecar-creds wave rule. - L1 render goldens regenerated for the sidecar (forward-auth fixtures). Co-Authored-By: Claude Opus 4.8 <[email protected]>
128 lines
6.1 KiB
Bash
Executable File
128 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ci/render-golden.sh — Stage: render==golden reproducibility gate.
|
|
#
|
|
# WHY: the committed apps/ tree is a set of GOLDENS produced by ONE canonical
|
|
# invocation of catalog/render.py. If a golden is hand-edited, or a catalog
|
|
# ServiceDefinition is changed without re-rendering, the goldens drift from
|
|
# their source of truth and Flux deploys something no one reviewed. The three
|
|
# L1 render spot-checks (conformance/tests/l1-rendering/*) only cover a couple
|
|
# of assertions on a couple of services — they let a real source/golden
|
|
# mismatch through (image-automation objects present in gitea/nextcloud but
|
|
# absent everywhere else). This stage closes that hole: it renders EVERY
|
|
# onboarded catalog service with the canonical command and FAILS on ANY diff
|
|
# vs the committed golden.
|
|
#
|
|
# CANONICAL RENDER (source of truth for apps/):
|
|
# python3 catalog/render.py --all catalog -o apps --wave-layout --no-image-automation
|
|
#
|
|
# The `--no-image-automation` flag is canonical because image automation is
|
|
# authored-but-DISABLED (DECISIONS-LOG: "Phase 4 image-automation merged but
|
|
# DISABLED pending a Gitea write-cred"; pihole/all apps onboarded via
|
|
# `--no-image-automation`). The Flux image-reflector/-automation controllers
|
|
# are NOT installed in-cluster, so emitting ImageRepository/ImagePolicy CRs
|
|
# would leave apps referencing controllers that don't exist. Every onboarded
|
|
# app must render identically under this ONE command.
|
|
#
|
|
# Runs in CI WITHOUT a cluster (pure render + diff). Exit codes per ci/lib.sh:
|
|
# 0 = pass, 1 = fail (blocks merge), 3 = skip (renderer/tooling missing).
|
|
set -euo pipefail
|
|
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck disable=SC1091
|
|
source "${CI_DIR}/lib.sh"
|
|
|
|
APPS_DIR="${APPS_DIR:-${REPO_ROOT}/apps}"
|
|
RENDER_PY="${RENDER_PY:-${REPO_ROOT}/catalog/render.py}"
|
|
PYTHON="${PYTHON:-python3}"
|
|
|
|
# Catalog services that are intentionally NOT onboarded to the apps tree yet
|
|
# (computed into a wave by the renderer but with NO committed manifest — see
|
|
# DESIGN-waves.md §"Catalog services not yet onboarded"). They render as a
|
|
# by-product of `--all catalog` but must NOT have a golden; the gate ignores
|
|
# their rendered output. Onboarding one = add its golden and drop it here.
|
|
#
|
|
# CANARY DEFERRAL (forward-auth oauth2-proxy sidecar rollout, 2026-08-31): the renderer now
|
|
# emits a per-app oauth2-proxy sidecar for every forward-auth app (pihole/jackett/qbittorrent/
|
|
# radarr/sonarr). pihole is the CANARY — its golden carries the sidecar and is verified fail-closed
|
|
# in-cluster FIRST. The other four are deferred to the ROLLOUT commit so partial progress is safe
|
|
# (their committed goldens stay sidecar-less and keep serving exactly as today). They are listed
|
|
# here TEMPORARILY so the canonical render's fresh (sidecar) output does not flag drift against the
|
|
# deferred sidecar-less goldens. The ROLLOUT commit re-renders their goldens and REMOVES them here.
|
|
: "${RENDER_GOLDEN_NOT_ONBOARDED:=keycloak vm-demo jackett qbittorrent radarr sonarr}"
|
|
|
|
log "Stage: render==golden reproducibility (canonical render vs committed apps/)"
|
|
|
|
if ! entrypoint_ready "$PYTHON"; then
|
|
skip "python3 not available — cannot run the renderer. UNWIRED, not green."
|
|
summary "- render-golden: **SKIPPED** (python3 missing)"
|
|
exit 3
|
|
fi
|
|
if [ ! -f "$RENDER_PY" ]; then
|
|
skip "renderer not present at '${RENDER_PY}' (catalog/render.py) — UNWIRED, not green."
|
|
summary "- render-golden: **SKIPPED** (renderer missing)"
|
|
exit 3
|
|
fi
|
|
if [ ! -d "$CATALOG_DIR" ] || ! compgen -G "${CATALOG_DIR}/*.yaml" >/dev/null 2>&1; then
|
|
skip "no catalog ServiceDefinitions under '${CATALOG_DIR}' — nothing to render."
|
|
summary "- render-golden: **SKIPPED** (empty catalog)"
|
|
exit 3
|
|
fi
|
|
|
|
# --- Render every service with the ONE canonical command into a temp dir -----
|
|
TMP_RENDER="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP_RENDER"' EXIT
|
|
|
|
if ! "$PYTHON" "$RENDER_PY" --all "$CATALOG_DIR" -o "$TMP_RENDER" --wave-layout --no-image-automation; then
|
|
err "canonical render failed to run"
|
|
summary "- render-golden: **FAILED** (renderer error)"
|
|
exit 1
|
|
fi
|
|
|
|
# Drop the not-onboarded services' rendered output — they have no golden by design.
|
|
for svc in $RENDER_GOLDEN_NOT_ONBOARDED; do
|
|
find "$TMP_RENDER" -type f -name "${svc}.yaml" -delete
|
|
done
|
|
|
|
# --- Compare EVERY rendered service against its committed golden --------------
|
|
# For each rendered file (relative path wave-N/<name>.yaml):
|
|
# - the committed golden apps/wave-N/<name>.yaml MUST exist, and
|
|
# - it MUST be byte-identical to the freshly rendered output.
|
|
# Any missing golden or any diff is a hard FAIL (blocks merge).
|
|
fail=0
|
|
checked=0
|
|
while IFS= read -r -d '' rendered; do
|
|
rel="${rendered#"$TMP_RENDER"/}"
|
|
golden="${APPS_DIR}/${rel}"
|
|
checked=$((checked + 1))
|
|
if [ ! -f "$golden" ]; then
|
|
err "MISSING GOLDEN: catalog renders '${rel}' but apps/${rel} does not exist"
|
|
err " -> re-render (see canonical command in this script's header) and commit, or add it to RENDER_GOLDEN_NOT_ONBOARDED"
|
|
fail=1
|
|
continue
|
|
fi
|
|
if ! diff -u "$golden" "$rendered" >/dev/null 2>&1; then
|
|
err "GOLDEN DRIFT: apps/${rel} does not match the canonical render:"
|
|
diff -u "$golden" "$rendered" >&2 || true
|
|
fail=1
|
|
fi
|
|
done < <(find "$TMP_RENDER" -type f -print0)
|
|
|
|
if [ "$checked" -eq 0 ]; then
|
|
err "renderer produced no output — cannot verify goldens"
|
|
summary "- render-golden: **FAILED** (renderer produced nothing)"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$fail" -ne 0 ]; then
|
|
err "render==golden check FAILED — the committed apps/ goldens are NOT reproducible"
|
|
err " from the canonical render. A golden was hand-edited, or a catalog source"
|
|
err " changed without re-rendering. Fix:"
|
|
err " ${PYTHON} catalog/render.py --all catalog -o apps --wave-layout --no-image-automation"
|
|
err " then remove the not-onboarded outputs (${RENDER_GOLDEN_NOT_ONBOARDED// /, }) and commit."
|
|
summary "- render-golden: **FAILED** (goldens drifted from canonical render)"
|
|
exit 1
|
|
fi
|
|
|
|
ok "render==golden: all ${checked} onboarded service golden(s) reproduce from the canonical render"
|
|
summary "- render-golden: **passed** (${checked} services; canonical \`--no-image-automation\` render)"
|
|
exit 0
|