diff --git a/deploy/stack-deploy.sh b/deploy/stack-deploy.sh index e8e0bd0..0326df3 100755 --- a/deploy/stack-deploy.sh +++ b/deploy/stack-deploy.sh @@ -1,26 +1,117 @@ #!/usr/bin/env bash -# stack-deploy.sh - Safe Docker Swarm stack deployer -# Handles special chars in .env (||, backticks, %, *) via Python +# stack-deploy.sh — Safe Docker Swarm stack deployer +# Supports: +# - Flat: .yaml (legacy, all existing stacks) +# - Folder: / (new, multi-compose stacks like immich) +# +# Folder layout: +# compose-files/ +# └── immich/ +# ├── immich.yml ← main compose (must match stack name) +# ├── hwaccel.transcoding.yml ← extension files, merged in glob order +# ├── hwaccel.ml.yml +# └── immich.env ← optional, same as flat pattern +# +# Merge strategy: docker compose ... config | docker stack deploy -c - +# All files in the folder matching *.yml or *.yaml are included. +# Main file (.yml or .yaml) is always passed FIRST. +# Remaining files are sorted and appended. + set -euo pipefail + STACK="${1:?Usage: stack-deploy.sh }" DIR="/volume1/docker/compose-files" -YAML="$DIR/${STACK}.yaml" -ENVFILE="$DIR/${STACK}.env" -PY="/volume1/docker/compose-files/deploy/envparse.py" -[ -f "$YAML" ] || { echo "ERROR: $YAML not found"; exit 1; } -echo "==> Deploying stack: $STACK" -if [ -f "$ENVFILE" ]; then - echo " Loading: $ENVFILE" - eval "$(python3 $PY export "$ENVFILE")" - VARS="$(python3 $PY vars "$ENVFILE")" - echo " Vars: $VARS" - envsubst "$VARS" < "$YAML" \ - | python3 $PY strip \ - | docker stack deploy -c - "$STACK"\ - || { echo -e "\\n Hint: 'secret not found' means Woodpecker hasn't provisioned secrets yet for this stack."; echo -e " Trigger the pipeline first: https://woodpecker.bryanmail.net\\n"; exit 1; } +PY="$DIR/deploy/envparse.py" + +# ── Locate compose file(s) ────────────────────────────────────────────────── + +FOLDER="$DIR/$STACK" +FLAT_YAML="$DIR/${STACK}.yaml" +FLAT_YML="$DIR/${STACK}.yml" + +if [ -d "$FOLDER" ]; then + MODE="folder" + + # Main file must be named .yml or .yaml + if [ -f "$FOLDER/${STACK}.yml" ]; then MAIN="$FOLDER/${STACK}.yml" + elif [ -f "$FOLDER/${STACK}.yaml" ]; then MAIN="$FOLDER/${STACK}.yaml" + else echo "ERROR: No main compose file ($STACK.yml or $STACK.yaml) in $FOLDER"; exit 1 + fi + + # Collect extension files: everything else in the folder, sorted + EXTRAS=() + while IFS= read -r -d '' f; do + [ "$f" = "$MAIN" ] && continue + EXTRAS+=("$f") + done < <(find "$FOLDER" -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -z) + + ENVFILE="$FOLDER/${STACK}.env" + +elif [ -f "$FLAT_YAML" ]; then + MODE="flat" + MAIN="$FLAT_YAML" + EXTRAS=() + ENVFILE="$DIR/${STACK}.env" + +elif [ -f "$FLAT_YML" ]; then + MODE="flat" + MAIN="$FLAT_YML" + EXTRAS=() + ENVFILE="$DIR/${STACK}.env" + else - echo " No env file" - docker stack deploy -c "$YAML" "$STACK"\ - || { echo -e "\\n Hint: 'secret not found' means Woodpecker hasn't provisioned secrets yet for this stack."; echo -e " Trigger the pipeline first: https://woodpecker.bryanmail.net\\n"; exit 1; } + echo "ERROR: No compose file or folder found for stack '$STACK' in $DIR" + exit 1 fi + +echo "==> Deploying stack: $STACK [$MODE]" +echo " Main: $MAIN" +for f in "${EXTRAS[@]:-}"; do [ -n "$f" ] && echo " Extra: $f"; done + +# ── Build -f flag list ─────────────────────────────────────────────────────── + +F_FLAGS=(-f "$MAIN") +for f in "${EXTRAS[@]:-}"; do + [ -n "$f" ] && F_FLAGS+=(-f "$f") +done + +# ── Load env file if present ───────────────────────────────────────────────── + +HINT=" Hint: 'secret not found' means Woodpecker hasn't provisioned secrets yet.\n Trigger the pipeline: https://woodpecker.bryanmail.net\n" + +if [ -f "$ENVFILE" ]; then + echo " Env: $ENVFILE" + eval "$(python3 "$PY" export "$ENVFILE")" + VARS="$(python3 "$PY" vars "$ENVFILE")" + echo " Vars: $VARS" + + if [ "${#F_FLAGS[@]}" -gt 2 ]; then + # Folder mode with extras: use docker compose config to merge, then deploy + docker compose "${F_FLAGS[@]}" config \ + | python3 "$PY" strip \ + | envsubst "$VARS" \ + | docker stack deploy -c - "$STACK" \ + || { printf "\n%b" "$HINT"; exit 1; } + else + # Single file: envsubst directly (preserves existing behavior exactly) + envsubst "$VARS" < "$MAIN" \ + | python3 "$PY" strip \ + | docker stack deploy -c - "$STACK" \ + || { printf "\n%b" "$HINT"; exit 1; } + fi + +else + echo " Env: (none)" + + if [ "${#F_FLAGS[@]}" -gt 2 ]; then + docker compose "${F_FLAGS[@]}" config \ + | python3 "$PY" strip \ + | docker stack deploy -c - "$STACK" \ + || { printf "\n%b" "$HINT"; exit 1; } + else + docker stack deploy -c "$MAIN" "$STACK" \ + || { printf "\n%b" "$HINT"; exit 1; } + fi +fi + echo "==> Done: $STACK"