From 69a280399cac0b506cd7a3ca1225efef628d26ff Mon Sep 17 00:00:00 2001 From: jklein Date: Wed, 26 Aug 2026 14:30:42 +0200 Subject: [PATCH 1/2] ci: run the freesailarr install-test for real instead of skipping it freesailarr was excluded from the install-test because gluetun never becomes healthy without a real Wireguard key and no app container behind its kill switch can become Ready. The green "Install-test freesailarr pass" was a 25-second SKIP on the most complex chart in the repo - a signal that proved nothing. With a CI-only Mullvad key in 1Password the reason is gone: - ci/run-install-test.sh learns a general rule: ci//fixtures.env.yaml is rendered with envsubst before it is applied, restricted to the variable names a chart declares in REQUIRED_ENV. A missing or empty declared variable fails the run loudly and names the variable - it never skips the chart. - The install-test job loads the key from 1Password Connect for freesailarr only, following the pattern publish-helm-chart.yml documents: values via steps..outputs.*, mapped into the step's own env, quoted, with ":?" guards that name the missing op:// field before a cluster is created. - Fixtures for freesailarr: three media claims, the gluetun secret template with variable names only, firewallOutboundSubnets so the kill switch does not block cluster DNS, and INSTALL_TIMEOUT=900 for nine containers. freesailarr/Chart.yaml goes 3.0.0+up1.0.0 -> 3.0.1+up1.0.0. That bump is deliberately empty of content: the install-test matrix follows changed chart directories, and without it this PR could not run the test it adds. --- .github/workflows/validate-charts.yml | 50 ++++++++++++++++++++++++++- README.md | 9 +++-- ci/excluded-charts.txt | 1 - ci/freesailarr/fixtures.env.yaml | 22 ++++++++++++ ci/freesailarr/fixtures.yaml | 43 +++++++++++++++++++++++ ci/freesailarr/settings.env | 10 ++++++ ci/freesailarr/test-values.yaml | 39 +++++++++++++++++++++ ci/run-install-test.sh | 46 +++++++++++++++++++++++- freesailarr/Chart.yaml | 2 +- 9 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 ci/freesailarr/fixtures.env.yaml create mode 100644 ci/freesailarr/fixtures.yaml create mode 100644 ci/freesailarr/settings.env create mode 100644 ci/freesailarr/test-values.yaml diff --git a/.github/workflows/validate-charts.yml b/.github/workflows/validate-charts.yml index 4448682..4a358f1 100644 --- a/.github/workflows/validate-charts.yml +++ b/.github/workflows/validate-charts.yml @@ -149,13 +149,61 @@ jobs: uses: actions/checkout@v7 - name: "Install Helm" uses: azure/setup-helm@v5 + # freesailarr is the one chart whose install-test needs credentials: + # without a real Wireguard key gluetun never completes a handshake, and + # behind its kill switch not a single app container becomes Ready. The key + # comes from 1Password Connect like the registry credentials in + # publish-helm-chart.yml, and it is a CI-ONLY Mullvad key: Mullvad ties an + # address to a key and sends the tunnel to the endpoint it saw last, so + # reusing the production key would let every PR run pull the tunnel out + # from under the running stack - with the symptom in the cluster and the + # cause in a pull request. Loaded only for this chart; no other chart run + # touches the VPN key. + - name: "Configure OP-Connect" + if: matrix.chart == 'freesailarr' + uses: 1password/load-secrets-action/configure@v5 + with: + connect-host: https://op-connect.jk-dev.de + - name: "Load freesailarr VPN credentials" + id: load-freesailarr-vpn + if: matrix.chart == 'freesailarr' + uses: 1password/load-secrets-action@v5 + env: + OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} + FREESAILARR_WG_KEY: op://GitHub-Actions/freesailarr-mullvad-test-config/credential + FREESAILARR_WG_ADDRESSES: op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address + # The same trap publish-helm-chart.yml documents at length: v5 of + # load-secrets-action does NOT export the loaded values as environment + # variables for later steps, only its step outputs carry them, and the + # loading step logs "Populating variable" and stays green either way. So + # the values are mapped into each step's own env block via + # steps..outputs.* and read as quoted shell variables. The ":?" guards + # run before the cluster is even created and name the op:// field that is + # missing - an empty key would otherwise reach gluetun and look like a + # broken chart instead of a missing secret. They print the variable name, + # never its value. + - name: "Verify the VPN credentials arrived" + if: matrix.chart == 'freesailarr' + env: + FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }} + FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }} + run: |- + : "${FREESAILARR_WG_KEY:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/credential}" + : "${FREESAILARR_WG_ADDRESSES:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address}" + echo "Both VPN values arrived non-empty." - name: "Install k3d" run: curl -sSfL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG="$K3D_VERSION" bash - name: "Create k3d cluster" run: k3d cluster create install-test --image "$K3S_IMAGE" --wait --timeout 180s # Excluded charts (see ci/excluded-charts.txt) are skipped inside the - # script with their documented reason. + # script with their documented reason. The variables below are empty for + # every other chart and are read only by the chart that declares them in + # ci//settings.env (REQUIRED_ENV); the script fails loudly if a + # declared one is missing, it never skips over it. - name: "Install chart and wait for readiness" + env: + FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }} + FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }} run: ci/run-install-test.sh "${{ matrix.chart }}" - name: "Delete k3d cluster" if: always() diff --git a/README.md b/README.md index cdb5ec9..eeb5cf9 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo - `publish-.yml` — one thin workflow per chart; triggers on pushes to `main` touching `/**` (or manually) and calls the reusable workflow below. **A push to a chart directory on `main` publishes that chart**, so the chart version must be bumped in the same change. - `publish-helm-chart.yml` — reusable workflow: `helm package`, then `helm push` to the private JK-Registry. Registry credentials come from 1Password Connect (`OP_CONNECT_TOKEN` secret, items under `op://GitHub-Actions/`). The loaded values are read via **`steps..outputs.*`**, mapped into each step's own `env:` block and used as quoted shell variables — never interpolated into the command line. `load-secrets-action` **v5 no longer exports them as environment variables** for later steps the way v4 did, so after the v5 bump every `${{ env.JK_CR_* }}` reference resolved to an empty string while the loading step still logged `Populating variable` and stayed green. Each value is additionally guarded with `:?`, because an empty one used to collapse into a missing argument rather than an error: `-u` consumed the following `--password-stdin`, helm fell back to an interactive prompt and the job died with `inappropriate ioctl for device` — a terminal error for what was really an unset variable. - `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list. -- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (same changed-charts matrix) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. +- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (same changed-charts matrix) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` exercises nothing by itself; the Trivy scan has a fixed-sample fallback for that case, the install-test has none — bump the affected chart's patch version to pull it into the matrix. ### Install-test (PR pipeline stage 2) @@ -51,9 +51,12 @@ Fixtures live at the **repo root** under `ci/` (deliberately *not* inside the ch - `ci/common/` — fixtures applied for every chart (schema-less stub CRD for `OnePasswordItem`) - `ci//test-values.yaml` — values for the CI install (required for every installable chart) - `ci//fixtures.yaml` — dummy secrets with in-cluster connection data, throwaway infrastructure (postgres/redis/dex), static hostPath PVs/StorageClasses where the chart pins a storage class or needs RWX, and seed jobs (e.g. `synapse generate`) -- `ci//settings.env` — optional overrides (e.g. `INSTALL_TIMEOUT` for slow starters) +- `ci//fixtures.env.yaml` — fixtures that need a value which must not be committed. The file holds `${VARIABLE}` placeholders and nothing else; `ci/run-install-test.sh` renders it with `envsubst` and pipes the result straight into `kubectl` (never printed). Substitution is restricted to the names from `REQUIRED_ENV`, so an undeclared variable is never filled in and no unrelated `$…` in the manifest is touched +- `ci//settings.env` — optional overrides: `INSTALL_TIMEOUT` for slow starters, and `REQUIRED_ENV="VAR_A VAR_B"` to declare the variables `fixtures.env.yaml` needs. **A missing or empty declared variable fails the install-test loudly, naming the variable — it never skips the chart.** Skipping would restore exactly the green-but-meaningless signal this mechanism exists to remove; the one legitimate case of absent secrets, a PR from a fork, belongs in an `if` on the workflow job rather than in the script - `ci/excluded-charts.txt` — charts that cannot run in CI, each with a justification. Currently: `satisfactory` (multi-GB image + 10GB+ Steam download), `template-chart` (scaffold with placeholder image) +`freesailarr` is the only chart using `fixtures.env.yaml` today: gluetun needs a real Wireguard key, or it never completes a handshake and no app container behind its kill switch becomes Ready. The install-test job loads the key from 1Password Connect for that chart only, using the same pattern as `publish-helm-chart.yml` — `steps..outputs.*` into the step's own `env:`, quoted, with `:?` guards that name the missing `op://` field before the cluster is even created. The key in that item is a **CI-only Mullvad key**, deliberately not the production one: Mullvad ties an address to a key and routes the tunnel to the endpoint it saw last, so a shared key would make every PR run pull the tunnel out from under the running stack — with the symptom in the cluster and the cause in a pull request. + Fixture images avoid Docker Hub where possible (`public.ecr.aws`/`ghcr.io`) to dodge runner rate limits. The job **blocks the PR** on failure — the stabilization phase of #22 ended when Renovate started automerging patch/minor updates, since a chart that no longer starts must not reach `main` and publish unattended. When adding a new chart, add its `ci//` fixtures (or an entry in `ci/excluded-charts.txt`) — the install-test fails if both are missing. The script also runs locally against any existing cluster: `k3d cluster create t && ci/run-install-test.sh && k3d cluster delete t`. ## Dependency updates (Renovate) @@ -251,5 +254,5 @@ Reference implementation: `template-chart/values.schema.json` and `template-char - `icon:` is mandatory in every `Chart.yaml`. **Documented exception — freesailarr, the only chart without one**: the rule everywhere else is "the icon is this chart's one upstream's logo", and a chart bundling nine independently versioned projects has no such upstream — picking one of the nine would imply a "main app" the stack deliberately does not have. That is the whole criterion, and it applies to no other chart in the repo: a single-upstream chart without an icon is a missing icon, not an exception. The omission costs an INFO line in `helm lint --strict` (`icon is recommended`), which still exits 0. - `helm lint --strict` must pass without findings. - `NOTES.txt` for every chart is planned (#43); seven charts ship one so far (`apache-tika`, `cyberchef`, `freesailarr`, `gotenberg`, `homeassistant`, `patchmon`, `rss-bridge`). -- CI: every installable chart has fixtures under `ci//` (`test-values.yaml`, optionally `fixtures.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing. +- CI: every installable chart has fixtures under `ci//` (`test-values.yaml`, optionally `fixtures.yaml`/`fixtures.env.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing. - Charts have no dependencies (`charts/` directories are empty). diff --git a/ci/excluded-charts.txt b/ci/excluded-charts.txt index ca96541..d53b450 100644 --- a/ci/excluded-charts.txt +++ b/ci/excluded-charts.txt @@ -1,5 +1,4 @@ # Charts excluded from the PR install-test, one per line: . # ci/run-install-test.sh skips these with the given reason. -freesailarr gluetun never becomes healthy without a real Wireguard key and VPN connectivity, and every app container sits behind its firewall - the pod cannot become Ready on a CI runner satisfactory the game-server image is multiple GB and Steam downloads 10GB+ game files on first start - impractical on a CI runner template-chart scaffold with placeholder image "ghcr.io/ORG/IMAGE:xxx", never published diff --git a/ci/freesailarr/fixtures.env.yaml b/ci/freesailarr/fixtures.env.yaml new file mode 100644 index 0000000..b170ba1 --- /dev/null +++ b/ci/freesailarr/fixtures.env.yaml @@ -0,0 +1,22 @@ +# The gluetun credentials Secret the 1Password operator materializes on the +# real cluster - here as a template with variable NAMES only. This file must +# never contain a value; ci/run-install-test.sh renders it with envsubst, +# restricted to the names declared in ci/freesailarr/settings.env +# (REQUIRED_ENV), and pipes the result straight into kubectl. +# +# The values come from a 1Password item holding a CI-only Mullvad key, loaded in +# the install-test job of .github/workflows/validate-charts.yml. A CI-only key +# on purpose: Mullvad ties an address to a key and sends the tunnel to the +# endpoint it saw last, so a shared key would let every pull request pull the +# tunnel out from under the running stack. +# +# Name and keys follow the chart: --gluetun-secret with the +# fields the StatefulSet reads via secretKeyRef. Release name is ci. +apiVersion: v1 +kind: Secret +metadata: + name: ci-freesailarr-gluetun-secret +type: Opaque +stringData: + wireguard-private-key: "${FREESAILARR_WG_KEY}" + wireguard-addresses: "${FREESAILARR_WG_ADDRESSES}" diff --git a/ci/freesailarr/fixtures.yaml b/ci/freesailarr/fixtures.yaml new file mode 100644 index 0000000..6179089 --- /dev/null +++ b/ci/freesailarr/fixtures.yaml @@ -0,0 +1,43 @@ +# Fixtures for the freesailarr install-test: the three media claims the chart +# mounts but never creates (media.*.existingClaim). The config volumes come from +# the StatefulSet's own volumeClaimTemplates and need nothing here. +# +# Default StorageClass (k3d's local-path), so the claims stay Pending until the +# pod is scheduled - WaitForFirstConsumer, which is exactly what the chart sees +# on a real cluster. local-path creates its directories mode 0777, so the claim +# roots are writable for PUID/PGID 1000 without a chown job. +# +# The gluetun credentials Secret is NOT here: it carries values that must not be +# committed and lives in fixtures.env.yaml. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ci-freesailarr-downloads +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ci-freesailarr-movies +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ci-freesailarr-tv +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/ci/freesailarr/settings.env b/ci/freesailarr/settings.env new file mode 100644 index 0000000..fdf3a39 --- /dev/null +++ b/ci/freesailarr/settings.env @@ -0,0 +1,10 @@ +# Nine containers in one pod, several GB of images to pull, and the app +# containers only start once gluetun has completed its Wireguard handshake - +# each of them then runs its own startup probe (150s budget, 300s for seerr's +# first-boot migrations). The default of 300s is not enough for that. +INSTALL_TIMEOUT=900 + +# The gluetun credentials for ci/freesailarr/fixtures.env.yaml. Missing or empty +# values fail the install-test loudly instead of skipping it - see +# ci/run-install-test.sh. +REQUIRED_ENV="FREESAILARR_WG_KEY FREESAILARR_WG_ADDRESSES" diff --git a/ci/freesailarr/test-values.yaml b/ci/freesailarr/test-values.yaml new file mode 100644 index 0000000..d86754c --- /dev/null +++ b/ci/freesailarr/test-values.yaml @@ -0,0 +1,39 @@ +# freesailarr: the whole stack in one pod - gluetun plus eight app containers +# that share its network namespace and sit behind its kill switch. Nothing here +# is a shortcut around that: the install-test brings up a real Wireguard tunnel +# with a CI-only Mullvad key (see ci/freesailarr/fixtures.env.yaml), because a +# gluetun without a handshake keeps every app container from becoming Ready. +# +# Release name is ci, so the chart looks for the gluetun credentials in the +# Secret ci-freesailarr-gluetun-secret, which the fixtures create. The chart's +# own OnePasswordItem CR applies against the stub CRD and stays unreconciled - +# there is no 1Password operator in the CI cluster. +secrets: + # Required by the chart. Only the CR's itemPath, never resolved in CI. + gluetunSecretRef: op://ci/freesailarr-gluetun + +media: + # The three media claims are pre-created in fixtures.yaml (one claim per + # category, no subPath - the shared-claim layout is a values question, not a + # startup question). local-path hands out its directories mode 0777, so the + # claim roots are writable for PUID/PGID as the chart requires. + downloads: + existingClaim: ci-freesailarr-downloads + movies: + existingClaim: ci-freesailarr-movies + tv: + existingClaim: ci-freesailarr-tv + +gluetun: + vpn: + # Without this the kill switch drops cluster-internal egress as well, and + # the very first DNS lookup against cluster DNS fails. k3s defaults: + # 10.42.0.0/16 pods, 10.43.0.0/16 services. + firewallOutboundSubnets: "10.42.0.0/16,10.43.0.0/16" + +ingress: + # Shared-domain mode with a placeholder host: the Ingress objects only have to + # be accepted by the API server. There is no ingress-nginx and no cert-manager + # in the CI cluster, and the install-test checks readiness, not routing. + host: freesailarr.example.com + clusterIssuer: ci diff --git a/ci/run-install-test.sh b/ci/run-install-test.sh index c00737c..0f988a7 100755 --- a/ci/run-install-test.sh +++ b/ci/run-install-test.sh @@ -9,7 +9,8 @@ # ci/common/ fixtures applied for every chart (OnePasswordItem stub CRD) # ci//test-values.yaml values for the CI install (required) # ci//fixtures.yaml dummy secrets / throwaway infra / static PVs (optional) -# ci//settings.env optional overrides, e.g. INSTALL_TIMEOUT=600 +# ci//fixtures.env.yaml fixtures with ${VARS} filled in from the environment (optional) +# ci//settings.env optional overrides, e.g. INSTALL_TIMEOUT=600, REQUIRED_ENV=... # ci/excluded-charts.txt charts that cannot run in CI, with reasons set -euo pipefail @@ -20,8 +21,11 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CI_DIR="$ROOT/ci" # Per-chart override via ci//settings.env (INSTALL_TIMEOUT in seconds). +# REQUIRED_ENV is a space-separated list of environment variables the chart's +# fixtures need; see the check below. INSTALL_TIMEOUT=300 FIXTURE_JOB_TIMEOUT=600 +REQUIRED_ENV="" if [ -f "$CI_DIR/$CHART/settings.env" ]; then # shellcheck disable=SC1090 source "$CI_DIR/$CHART/settings.env" @@ -47,6 +51,32 @@ if [ ! -f "$CI_DIR/$CHART/test-values.yaml" ]; then exit 1 fi +# --- Required environment for ci//fixtures.env.yaml ------------------ +# A chart whose fixtures need a value that must never be committed (freesailarr +# needs a real Wireguard key, or gluetun's kill switch keeps every app container +# from becoming Ready) declares the variable NAMES in settings.env: +# +# REQUIRED_ENV="FREESAILARR_WG_KEY FREESAILARR_WG_ADDRESSES" +# +# The values come from the environment - in CI from 1Password Connect, see the +# install-test job in .github/workflows/validate-charts.yml. +# +# A missing or empty value is a hard error on purpose. Skipping the chart +# instead would restore exactly the green-but-meaningless signal this mechanism +# exists to remove, and an empty key produces a gluetun without a handshake, +# which reads as a broken chart rather than as a missing secret. The one +# legitimate case of absent secrets - a pull request from a fork - belongs in an +# "if" on the workflow job, not in this script. +# Only the variable name is ever printed, never the value. +for VAR in $REQUIRED_ENV; do + if [ -z "${!VAR:-}" ]; then + echo "ERROR: required environment variable '$VAR' is unset or empty." >&2 + echo "ci/$CHART/settings.env declares REQUIRED_ENV=\"$REQUIRED_ENV\"; ci/$CHART/fixtures.env.yaml" >&2 + echo "cannot be rendered without it, and the install-test does not skip charts over missing values." >&2 + exit 1 + fi +done + # --- Failure diagnostics ---------------------------------------------------- diagnose() { echo "=================== INSTALL-TEST FAILED: DIAGNOSTICS ===================" @@ -72,6 +102,20 @@ kubectl apply -f "$CI_DIR/common/" if [ -f "$CI_DIR/$CHART/fixtures.yaml" ]; then kubectl apply -f "$CI_DIR/$CHART/fixtures.yaml" fi +# Fixtures with values that must not be committed are rendered first: the +# checked-in fixtures.env.yaml holds ${VARIABLE} placeholders and nothing else. +# envsubst is restricted to the names from REQUIRED_ENV (its SHELL-FORMAT +# argument), so a variable that is not declared there is never substituted, no +# unrelated "$..." in the manifest is touched, and no other variable of the +# runner environment can end up in the cluster. The rendered document is piped +# straight into kubectl and never printed - kubectl reports object names only. +if [ -f "$CI_DIR/$CHART/fixtures.env.yaml" ]; then + SHELL_FORMAT="" + for VAR in $REQUIRED_ENV; do + SHELL_FORMAT="$SHELL_FORMAT\${$VAR}" + done + envsubst "$SHELL_FORMAT" < "$CI_DIR/$CHART/fixtures.env.yaml" | kubectl apply -f - +fi echo "::endgroup::" # --- 2. Wait for fixture jobs (node setup, data seeding) and infra ---------- diff --git a/freesailarr/Chart.yaml b/freesailarr/Chart.yaml index 8da9f0c..7b261f3 100644 --- a/freesailarr/Chart.yaml +++ b/freesailarr/Chart.yaml @@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack type: application -version: 3.0.0+up1.0.0 +version: 3.0.1+up1.0.0 # The chart's own version marker, freely chosen and tied to no upstream. The # stack bundles nine independently versioned projects, so there is no single # "app version" to track here. From e5f98435ed6d1a6dba33b9023563f8c08ad21cbe Mon Sep 17 00:00:00 2001 From: jklein Date: Wed, 26 Aug 2026 14:37:46 +0200 Subject: [PATCH 2/2] ci: redact declared secret values from install-test diagnostics The failure path prints container logs, and an app may print a value it was given - gluetun logs its Wireguard address on startup. diagnose() now runs through a filter that replaces the values of the declared REQUIRED_ENV variables with the variable name. GitHub masks them as well, but that is the platform's promise rather than this harness's, and the script runs locally too, where nothing masks anything. --- ci/run-install-test.sh | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ci/run-install-test.sh b/ci/run-install-test.sh index 0f988a7..efd9f3b 100755 --- a/ci/run-install-test.sh +++ b/ci/run-install-test.sh @@ -78,6 +78,25 @@ for VAR in $REQUIRED_ENV; do done # --- Failure diagnostics ---------------------------------------------------- +# The diagnostics echo application output, and an app may well print a value it +# was given: gluetun logs its Wireguard address on startup. So everything the +# failure path prints runs through a filter that replaces the values of the +# declared REQUIRED_ENV variables with the variable name. GitHub masks them as +# well, but that is the platform's promise rather than this harness's - and the +# script runs locally too, where nothing masks anything. +redact_secrets() { + if [ -z "$REQUIRED_ENV" ]; then + cat + return + fi + local SED_ARGS=() + local VAR + for VAR in $REQUIRED_ENV; do + SED_ARGS+=(-e "s|${!VAR}||g") + done + sed "${SED_ARGS[@]}" +} + diagnose() { echo "=================== INSTALL-TEST FAILED: DIAGNOSTICS ===================" echo "--- pods ---" @@ -94,7 +113,7 @@ diagnose() { done echo "========================================================================" } -trap 'RC=$?; if [ $RC -ne 0 ]; then diagnose; fi; exit $RC' EXIT +trap 'RC=$?; if [ $RC -ne 0 ]; then diagnose | redact_secrets; fi; exit $RC' EXIT # --- 1. Apply fixtures ------------------------------------------------------ echo "::group::Apply fixtures" @@ -102,8 +121,8 @@ kubectl apply -f "$CI_DIR/common/" if [ -f "$CI_DIR/$CHART/fixtures.yaml" ]; then kubectl apply -f "$CI_DIR/$CHART/fixtures.yaml" fi -# Fixtures with values that must not be committed are rendered first: the -# checked-in fixtures.env.yaml holds ${VARIABLE} placeholders and nothing else. +# Fixtures with values that must not be committed are rendered before they are +# applied: the checked-in fixtures.env.yaml holds ${VARIABLE} placeholders only. # envsubst is restricted to the names from REQUIRED_ENV (its SHELL-FORMAT # argument), so a variable that is not declared there is never substituted, no # unrelated "$..." in the manifest is touched, and no other variable of the