chore: add Docker Compose stack management script

This commit is contained in:
lingkai5wu committed 2026-08-15 20:48:04 +08:00
1 parent a03633a5bb
commit 21721df7f5
1 file changed
+254
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env bash
set -uo pipefail
shopt -s nullglob
SCRIPT_DIR="$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")"
STACKS_DIR="${STACKS_DIR:-$SCRIPT_DIR}"
CONFIG_FILE="${STACKS_CONFIG:-$STACKS_DIR/manage.conf}"
LOCK_FILE="${STACKS_LOCK_FILE:-/run/lock/stacks-manage.lock}"
WHITELIST=()
BLACKLIST=()
PRIORITY=()
if [[ -f "$CONFIG_FILE" ]]; then
bash -n "$CONFIG_FILE" || die "Syntax error in config file"
source "$CONFIG_FILE"
fi
DISCOVERED_STACKS=()
MANAGED_STACKS=()
die() {
echo "ERROR: $*" >&2
exit 1
}
usage() {
cat <<EOF
Usage: $(basename "$0") COMMAND [STACK]
Commands:
up Create and start containers
start Start existing containers
stop Stop containers
restart Restart containers
down Stop and remove containers and networks
pull Pull images
update Pull images and apply Compose changes
ps Show container status
logs Follow logs for a stack
config Render Compose configuration for a stack
list List discovered stacks and their status
EOF
}
contains() {
local target="$1"
shift
local item
for item in "$@"; do
[[ "$item" == "$target" ]] && return 0
done
return 1
}
discover_stacks() {
local compose_file stack
DISCOVERED_STACKS=()
for compose_file in "$STACKS_DIR"/*/compose.yaml; do
stack="${compose_file%/compose.yaml}"
DISCOVERED_STACKS+=("${stack##*/}")
done
}
is_discovered() {
contains "$1" "${DISCOVERED_STACKS[@]}"
}
is_managed() {
local stack="$1"
is_discovered "$stack" || return 1
[[ ! -e "$STACKS_DIR/$stack/.disabled" ]] || return 1
contains "$stack" "${BLACKLIST[@]}" && return 1
((${#WHITELIST[@]} == 0)) || contains "$stack" "${WHITELIST[@]}"
}
build_managed_stacks() {
local stack
MANAGED_STACKS=()
for stack in "${PRIORITY[@]}"; do
if is_managed "$stack" && ! contains "$stack" "${MANAGED_STACKS[@]}"; then
MANAGED_STACKS+=("$stack")
fi
done
for stack in "${DISCOVERED_STACKS[@]}"; do
if is_managed "$stack" && ! contains "$stack" "${MANAGED_STACKS[@]}"; then
MANAGED_STACKS+=("$stack")
fi
done
}
compose() {
local stack="$1"
shift
(
cd -- "$STACKS_DIR/$stack" || exit 1
docker compose "$@"
)
}
run_stack() {
local stack="$1"
local action="$2"
echo
echo "==> $stack: $action"
case "$action" in
up)
compose "$stack" config -q &&
compose "$stack" up -d
;;
update)
compose "$stack" config -q &&
compose "$stack" pull &&
compose "$stack" up -d
;;
*)
compose "$stack" "$action"
;;
esac
}
run_all() {
local action="$1"
local success=0 failed=0 stack i
if [[ "$action" == "stop" || "$action" == "down" ]]; then
for ((i = ${#MANAGED_STACKS[@]} - 1; i >= 0; i--)); do
stack="${MANAGED_STACKS[i]}"
if run_stack "$stack" "$action"; then
((success += 1))
else
((failed += 1))
fi
done
else
for stack in "${MANAGED_STACKS[@]}"; do
if run_stack "$stack" "$action"; then
((success += 1))
else
((failed += 1))
fi
done
fi
echo
echo "Succeeded: $success"
echo "Failed: $failed"
((failed == 0))
}
stack_status() {
local stack="$1"
if [[ -e "$STACKS_DIR/$stack/.disabled" ]]; then
echo "disabled"
elif contains "$stack" "${BLACKLIST[@]}"; then
echo "blacklisted"
elif ((${#WHITELIST[@]} > 0)) && ! contains "$stack" "${WHITELIST[@]}"; then
echo "not whitelisted"
else
echo "managed"
fi
}
list_stacks() {
local stack
printf '%-24s %s\n' "STACK" "STATUS"
printf '%-24s %s\n' "------------------------" "---------------"
for stack in "${DISCOVERED_STACKS[@]}"; do
printf '%-24s %s\n' "$stack" "$(stack_status "$stack")"
done
}
require_target() {
local command="$1"
local target="$2"
[[ -n "$target" ]] || die "$command requires a stack name"
is_discovered "$target" || die "Stack not found: $target"
is_managed "$target" || die "Stack is excluded: $target"
}
acquire_lock() {
mkdir -p "$(dirname "$LOCK_FILE")" ||
die "Cannot create lock directory"
exec {LOCK_FD}>"$LOCK_FILE" ||
die "Cannot open lock file: $LOCK_FILE"
flock -n "$LOCK_FD" ||
die "Another manage.sh operation is already running"
}
discover_stacks
build_managed_stacks
ACTION="${1:-}"
TARGET="${2:-}"
[[ $# -le 2 ]] || die "Too many arguments"
case "$ACTION" in
up | start | stop | restart | down | pull | update)
acquire_lock
if [[ -n "$TARGET" ]]; then
require_target "$ACTION" "$TARGET"
run_stack "$TARGET" "$ACTION"
else
run_all "$ACTION"
fi
;;
ps)
if [[ -n "$TARGET" ]]; then
require_target "$ACTION" "$TARGET"
run_stack "$TARGET" "$ACTION"
else
run_all "$ACTION"
fi
;;
logs)
require_target "$ACTION" "$TARGET"
compose "$TARGET" logs -f --tail=100
;;
config)
require_target "$ACTION" "$TARGET"
compose "$TARGET" config
;;
list)
[[ -z "$TARGET" ]] || die "list does not accept a stack name"
list_stacks
;;
help | -h | --help)
usage
;;
*)
usage
exit 1
;;
esac