From 45c771ae5e9cb36ddda4e751e5e01286474037fb Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 4 Oct 2026 15:08:32 +0000 Subject: [PATCH] fix: fail the app when CONFIG_SVC config load fails Co-Authored-By: Claude Sonnet 5.5 --- scripts/docker-entrypoint.sh | 46 +++++++++-- tests/test-entrypoint-config-load-failure.sh | 80 ++++++++++++++++++++ 2 files changed, 119 insertions(+), 7 deletions(-) create mode 100755 tests/test-entrypoint-config-load-failure.sh diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index c321b94..99a1fa5 100644 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -219,19 +219,51 @@ if [ -n "${OSC_ACCESS_TOKEN:-}" ] && [ -n "${CONFIG_SVC:-}" ]; then # failing command substitution used as an assignment's RHS is a "simple # command" and is NOT exempt from -e, so a bare non-zero exit here would # kill the whole script immediately, before config_exit is ever read below - # — silently skipping the tolerant log-and-continue path this config_exit - # check implements, for a timeout exit (124) same as any other non-zero + # — silently skipping the explicit config_exit handling below (so no + # [CONFIG] message), for a timeout exit (124) same as any other non-zero # exit from this call. - config_env_output=$(timeout 60s npx -y @osaas/cli@latest web config-to-env ${OSC_ENV:+--env "$OSC_ENV"} "$CONFIG_SVC" 2>&1) && config_exit=0 || config_exit=$? - if [ $config_exit -eq 0 ]; then - valid_exports=$(echo "$config_env_output" | grep "^export [A-Za-z_][A-Za-z0-9_]*=") + # The same set -e rule applies to the grep below: `x=$(... | grep ...)` + # exits the script silently when grep matches nothing, so it is guarded + # with an explicit `|| valid_exports=""`. + # A failure to load the config is fatal: continuing would start the app + # without its environment variables. This runner has no loading server, so + # (like the setup.sh timeout below) failure is a non-zero exit. + config_err_file=$(mktemp) + config_env_output=$(timeout 60s npx -y @osaas/cli@latest web config-to-env ${OSC_ENV:+--env "$OSC_ENV"} "$CONFIG_SVC" 2>"$config_err_file") && config_exit=0 || config_exit=$? + config_err_output=$(cat "$config_err_file" 2>/dev/null || true) + rm -f "$config_err_file" + config_failed=0 + if [ $config_exit -ne 0 ]; then + config_failed=1 + if [ $config_exit -eq 124 ]; then + echo "[CONFIG] ERROR: Timed out loading config from config service '$CONFIG_SVC'" >&2 + else + echo "[CONFIG] ERROR: Failed to load config (exit $config_exit): $config_env_output $config_err_output" >&2 + fi + if echo "$config_env_output $config_err_output" | grep -qiE '401|403|unauthori[sz]ed|forbidden|expired'; then + echo "[CONFIG] Hint: the access token may have expired. Run refresh-app-config to renew it." >&2 + fi + elif [ -z "$(echo "$config_env_output" | tr -d '[:space:]')" ]; then + # Empty store is not a failure + echo "[CONFIG] Config service '$CONFIG_SVC' has no parameters" + else + valid_exports=$(echo "$config_env_output" | grep "^export [A-Za-z_][A-Za-z0-9_]*=") || valid_exports="" if [ -n "$valid_exports" ]; then eval "$valid_exports" var_count=$(echo "$valid_exports" | wc -l | tr -d ' ') echo "[CONFIG] Loaded $var_count environment variable(s)" + else + config_failed=1 + echo "[CONFIG] ERROR: Config service '$CONFIG_SVC' returned output with no valid export lines: $config_env_output" >&2 fi - else - echo "[CONFIG] ERROR: Failed to load config (exit $config_exit): $config_env_output" >&2 + fi + if [ $config_failed -ne 0 ]; then + if [ $config_exit -eq 124 ]; then + exit 124 + elif [ $config_exit -ne 0 ]; then + exit "$config_exit" + fi + exit 1 fi fi diff --git a/tests/test-entrypoint-config-load-failure.sh b/tests/test-entrypoint-config-load-failure.sh new file mode 100755 index 0000000..f11979d --- /dev/null +++ b/tests/test-entrypoint-config-load-failure.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Behavioural tests for the CONFIG_SVC load block in scripts/docker-entrypoint.sh. +# The real block is extracted from the entrypoint and run under `set -e` with +# stub `npx`, `curl` and `timeout` on PATH. + +ENTRYPOINT="scripts/docker-entrypoint.sh" +PASS=0 +FAIL=0 +pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } + +sandbox=$(mktemp -d) +trap 'rm -rf "$sandbox"' EXIT +mkdir "$sandbox/bin" + +awk '/^# Load environment variables from config service if configured/{f=1} /^# Install Python dependencies/{f=0} f' "$ENTRYPOINT" > "$sandbox/block.sh" +if [ ! -s "$sandbox/block.sh" ]; then + fail "could not extract config block from entrypoint" + exit 1 +fi + +cat > "$sandbox/bin/curl" <<'STUB' +#!/bin/bash +exit 22 +STUB +# timeout stub: STUB_MODE=timeout simulates exit 124, otherwise run the command +cat > "$sandbox/bin/timeout" <<'STUB' +#!/bin/bash +if [ "$STUB_MODE" = "timeout" ]; then exit 124; fi +shift +exec "$@" +STUB +cat > "$sandbox/bin/npx" <<'STUB' +#!/bin/bash +case "$STUB_MODE" in + fail) echo "boom 401 unauthorized" >&2; exit 2 ;; + empty) exit 0 ;; + garbled) echo "npm notice something"; echo "export my-key=1"; exit 0 ;; + stderr_only) echo "npm warn noise" >&2; exit 0 ;; + ok) echo "export FOO=bar"; echo "export BAZ=qux"; exit 0 ;; +esac +STUB +chmod +x "$sandbox/bin/"* + +run_block() { + # $1 = mode; remaining args are env assignments + local mode=$1; shift + env -i PATH="$sandbox/bin:/usr/bin:/bin" STUB_MODE="$mode" "$@" \ + bash -c 'set -e; '"$(cat "$sandbox/block.sh")"'; echo "REACHED_PIP FOO=${FOO:-}"' 2>"$sandbox/stderr" >"$sandbox/stdout" + echo $? +} + +ENVV=(OSC_ACCESS_TOKEN=tok CONFIG_SVC=store) + +# Failure paths: non-zero exit and never reach the next phase +rc=$(run_block timeout "${ENVV[@]}") +if [ "$rc" = "124" ] && ! grep -q REACHED_PIP "$sandbox/stdout" && grep -q '\[CONFIG\] ERROR' "$sandbox/stderr"; then pass "timeout exits 124 before pip install"; else fail "timeout (rc=$rc)"; fi + +rc=$(run_block fail "${ENVV[@]}") +if [ "$rc" = "2" ] && ! grep -q REACHED_PIP "$sandbox/stdout" && grep -q '\[CONFIG\] ERROR' "$sandbox/stderr" && grep -q refresh-app-config "$sandbox/stderr"; then pass "non-zero exit propagates and prints refresh hint"; else fail "non-zero exit (rc=$rc)"; fi + +rc=$(run_block garbled "${ENVV[@]}") +if [ "$rc" != "0" ] && ! grep -q REACHED_PIP "$sandbox/stdout" && grep -q '\[CONFIG\] ERROR' "$sandbox/stderr"; then pass "non-empty output with no valid exports exits non-zero"; else fail "garbled output (rc=$rc)"; fi + +# Passing paths +rc=$(run_block empty "${ENVV[@]}") +if [ "$rc" = "0" ] && grep -q "REACHED_PIP" "$sandbox/stdout" && grep -q "has no parameters" "$sandbox/stdout"; then pass "empty store logs and continues"; else fail "empty store (rc=$rc)"; fi + +rc=$(run_block stderr_only "${ENVV[@]}") +if [ "$rc" = "0" ] && grep -q "REACHED_PIP" "$sandbox/stdout"; then pass "stderr noise with empty stdout still continues"; else fail "stderr only (rc=$rc)"; fi + +rc=$(run_block ok "${ENVV[@]}") +if [ "$rc" = "0" ] && grep -q "REACHED_PIP FOO=bar" "$sandbox/stdout" && grep -q "Loaded 2 environment variable" "$sandbox/stdout"; then pass "valid exports are evaluated"; else fail "success path (rc=$rc)"; fi + +rc=$(run_block fail OSC_ACCESS_TOKEN=tok) +if [ "$rc" = "0" ] && grep -q "REACHED_PIP" "$sandbox/stdout"; then pass "no CONFIG_SVC: block skipped"; else fail "no CONFIG_SVC (rc=$rc)"; fi + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ $FAIL -eq 0 ]