refactor(deploy/stack-deploy.sh): add folder-based multi-compose support
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Detect <stack>/ folder vs flat <stack>.yaml automatically - Main file must be named <stack>.yml or <stack>.yaml inside folder - Extra *.yml/*.yaml files in folder merged via docker compose config - Env file resolved from folder/<stack>.env or flat <stack>.env - Flat path unchanged — zero regression for existing stacks - envsubst applied after compose config merge in folder mode"
This commit is contained in:
+111
-20
@@ -1,26 +1,117 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# stack-deploy.sh - Safe Docker Swarm stack deployer
|
# stack-deploy.sh — Safe Docker Swarm stack deployer
|
||||||
# Handles special chars in .env (||, backticks, %, *) via Python
|
# Supports:
|
||||||
|
# - Flat: <stack>.yaml (legacy, all existing stacks)
|
||||||
|
# - Folder: <stack>/ (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 (<stack>.yml or <stack>.yaml) is always passed FIRST.
|
||||||
|
# Remaining files are sorted and appended.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
STACK="${1:?Usage: stack-deploy.sh <stack-name>}"
|
STACK="${1:?Usage: stack-deploy.sh <stack-name>}"
|
||||||
DIR="/volume1/docker/compose-files"
|
DIR="/volume1/docker/compose-files"
|
||||||
YAML="$DIR/${STACK}.yaml"
|
PY="$DIR/deploy/envparse.py"
|
||||||
ENVFILE="$DIR/${STACK}.env"
|
|
||||||
PY="/volume1/docker/compose-files/deploy/envparse.py"
|
# ── Locate compose file(s) ──────────────────────────────────────────────────
|
||||||
[ -f "$YAML" ] || { echo "ERROR: $YAML not found"; exit 1; }
|
|
||||||
echo "==> Deploying stack: $STACK"
|
FOLDER="$DIR/$STACK"
|
||||||
if [ -f "$ENVFILE" ]; then
|
FLAT_YAML="$DIR/${STACK}.yaml"
|
||||||
echo " Loading: $ENVFILE"
|
FLAT_YML="$DIR/${STACK}.yml"
|
||||||
eval "$(python3 $PY export "$ENVFILE")"
|
|
||||||
VARS="$(python3 $PY vars "$ENVFILE")"
|
if [ -d "$FOLDER" ]; then
|
||||||
echo " Vars: $VARS"
|
MODE="folder"
|
||||||
envsubst "$VARS" < "$YAML" \
|
|
||||||
| python3 $PY strip \
|
# Main file must be named <stack>.yml or <stack>.yaml
|
||||||
| docker stack deploy -c - "$STACK"\
|
if [ -f "$FOLDER/${STACK}.yml" ]; then MAIN="$FOLDER/${STACK}.yml"
|
||||||
|| { 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; }
|
elif [ -f "$FOLDER/${STACK}.yaml" ]; then MAIN="$FOLDER/${STACK}.yaml"
|
||||||
else
|
else echo "ERROR: No main compose file ($STACK.yml or $STACK.yaml) in $FOLDER"; exit 1
|
||||||
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; }
|
|
||||||
fi
|
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 "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"
|
echo "==> Done: $STACK"
|
||||||
|
|||||||
Reference in New Issue
Block a user