From 79db4d1e29677af7637e38b6df1d2630ef09c6b1 Mon Sep 17 00:00:00 2001 From: William Gill Date: Wed, 16 Sep 2026 14:21:35 -0500 Subject: [PATCH] fix(beszel): apply BESZEL_DOMAIN passed to a re-run Same defect as webfinger, and the typo case makes it concrete. A first deploy with monitroing.example.com never gets an ACME certificate. The operator spots it and re-runs with the correct hostname -- .env still holds the typo, so Caddy still has only that site block, still fails ACME, and the endpoint still does not work, while the script exits 0 printing "URL: https://monitoring.example.com". The two sources disagree and nothing compares them: validation greps .env for non-empty (a typo passes), while the confirmation prompt and the DEPLOYED banner expand this shell's variable. Interactively the prompted value is not exported, so compose produces a byte-identical config and does not even recreate the container -- nothing happens at all. Fixed with the same idiom as webfinger and d654299. Verified: a corrected BESZEL_DOMAIN lands in .env and is logged; a re-run passing nothing leaves .env untouched. Co-Authored-By: Claude Opus 5 --- deployments/beszel/deploy.sh | 67 ++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/deployments/beszel/deploy.sh b/deployments/beszel/deploy.sh index ce631e2..e55c665 100644 --- a/deployments/beszel/deploy.sh +++ b/deployments/beszel/deploy.sh @@ -7,7 +7,9 @@ # What this does: # 1. Installs docker + docker-cli-compose if missing. # 2. Lays down docker-compose.yml, Caddyfile, .env.example in $STACK_DIR. -# 3. Generates .env on first run; existing .env is never overwritten. +# 3. Generates .env on first run. On a re-run an existing .env is kept, +# except for values passed explicitly that run, which are written +# through -- .env is what compose interpolates into the container. # 4. Prompts for required values not preset (BESZEL_DOMAIN, ACME_EMAIL). # 5. Opens TCP 80/443 in UFW if active. # 6. Pulls images, brings the stack up, waits for healthchecks. @@ -32,6 +34,18 @@ set -euo pipefail : "${FORCE:=0}" : "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts [[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1 +ENV_FILE="$STACK_DIR/.env" + +# Both values reach Caddy only by compose interpolating .env, so .env has to absorb +# what this run passed or the run deploys the old values while reporting the +# new ones. Which keys arrived in the ENVIRONMENT must be recorded BEFORE the +# ":=" defaults below, which make an unset variable look like an empty one. +RUNTIME_KEYS=(BESZEL_DOMAIN ACME_EMAIL) +KEYS_FROM_ENV=() +for _k in "${RUNTIME_KEYS[@]}"; do + if [[ -n "${!_k+x}" ]]; then KEYS_FROM_ENV+=("$_k"); fi +done + : "${BESZEL_DOMAIN:=}" : "${ACME_EMAIL:=}" @@ -142,6 +156,24 @@ prompt() { fi } +# On a re-run the deployed values live in .env. Load them into any key NOT +# passed this run, so the prompts below do not force the operator to retype +# everything (and so SKIP_PROMPTS=1 does not die demanding values .env has). +if [[ -f "$ENV_FILE" ]]; then + _preloaded=() + for _k in "${RUNTIME_KEYS[@]}"; do + if [[ " ${KEYS_FROM_ENV[*]-} " == *" ${_k} "* ]]; then continue; fi + _v=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1) + if [[ -z "$_v" ]]; then continue; fi + printf -v "$_k" '%s' "$_v" + _preloaded+=("$_k") + done + if (( ${#_preloaded[@]} > 0 )); then + log "Reusing ${ENV_FILE} values for: ${_preloaded[*]}" + log " (pass VAR=... to change one; it is then written back to .env)" + fi +fi + prompt BESZEL_DOMAIN "Public hostname for beszel (e.g. monitoring.example.com)" prompt ACME_EMAIL "Let's Encrypt email" @@ -163,6 +195,28 @@ install -d -m 0750 "$STACK_DIR" install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml" install -m 0640 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile" +set_env() { # : update KEY in .env, or append if absent + # The value goes through the ENVIRONMENT, never interpolated into a sed + # script. Interpolating it corrupts any value containing & (sed expands it to + # the whole match) and aborts the run on one containing the s||| delimiter -- + # which is reachable for an OIDC issuer URL or a redirect query string. + local key="$1" val="$2" tmp + if [[ ! -f "$ENV_FILE" ]]; then + printf '%s=%s +' "$key" "$val" >> "$ENV_FILE" + return 0 + fi + tmp="$(mktemp)" + _SE_KEY="$key" _SE_VAL="$val" awk ' + BEGIN { k = ENVIRON["_SE_KEY"]; v = ENVIRON["_SE_VAL"]; seen = 0 } + !seen && index($0, k "=") == 1 { print k "=" v; seen = 1; next } + { print } + END { if (!seen) print k "=" v } + ' "$ENV_FILE" > "$tmp" + cat "$tmp" > "$ENV_FILE" # rewrite in place: keeps the original mode/owner + rm -f "$tmp" +} + ENV_FILE="$STACK_DIR/.env" if [[ ! -f "$ENV_FILE" ]]; then log "Seeding $ENV_FILE..." @@ -172,7 +226,16 @@ if [[ ! -f "$ENV_FILE" ]]; then -e "s|^ACME_EMAIL=.*|ACME_EMAIL=${ACME_EMAIL}|" \ "$ENV_FILE" else - log ".env exists; leaving it alone." + # Keep the file, but absorb what was passed this run: compose reads .env, + # so letting it drift means deploying the old values while the banner + # prints the new ones. Keys not passed are left untouched. + log ".env exists; keeping it (only values passed this run are updated)." + for _k in ${KEYS_FROM_ENV[@]+"${KEYS_FROM_ENV[@]}"}; do + _cur=$(sed -n "s/^${_k}=//p" "$ENV_FILE" | tail -n1) + if [[ "$_cur" == "${!_k}" ]]; then continue; fi + set_env "$_k" "${!_k}" + log " ${_k}: ${_cur:-} -> ${!_k:-}" + done fi # Validate