feat: accept multiple stack arguments

This commit is contained in:
lingkai5wu committed 2026-10-15 14:42:35 +08:00
1 parent c002ec3244
commit 58636aa06e
1 file changed
+111 -60
+111 -60
View File
@@ -28,6 +28,7 @@ RUN_SUCCESS=0
RUN_FAILED=0 RUN_FAILED=0
RUN_FAILED_STACKS=() RUN_FAILED_STACKS=()
RUN_SKIPPED_STAGES=() RUN_SKIPPED_STAGES=()
RUN_SKIPPED_STACKS=()
die() { die() {
echo "ERROR: $*" >&2 echo "ERROR: $*" >&2
@@ -69,28 +70,34 @@ handle_signal() {
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") COMMAND [STACK] Usage: $(basename "$0") COMMAND [STACK...]
STACK may be a plain stack name or a stack folder such as qbittorrent/ STACK may be a plain stack name or a stack folder such as qbittorrent/
or ./qbittorrent/. Folder references resolve against the stacks root; or ./qbittorrent/. Folder references resolve against the stacks root;
paths outside it are rejected. paths outside it are rejected.
With several STACK arguments, lifecycle, ps, and images run the stacks
one at a time in the order given. A lifecycle failure stops the run:
the remaining stacks are skipped and, with stop or down, left untouched.
ps continues inspecting the remaining stacks and reports failed stacks
at the end.
Lifecycle: Lifecycle:
up [STACK] Create and start containers up [STACK...] Create and start containers
start [STACK] Start existing containers start [STACK...] Start existing containers
stop [STACK] Stop containers stop [STACK...] Stop containers
restart [STACK] Restart containers restart [STACK...] Restart containers
down [STACK] Stop and remove containers and networks down [STACK...] Stop and remove containers and networks
pull [STACK] Pull images pull [STACK...] Pull images
update [STACK] Pull images and apply Compose changes update [STACK...] Pull images and apply Compose changes
Inspect: Inspect:
ps [STACK] Show container status ps [STACK...] Show container status
logs STACK Follow logs logs STACK Follow logs
config STACK Render Compose configuration config STACK Render Compose configuration
images [STACK] Show services, configured images, and versions images [STACK...] Show services, configured images, and versions
list List stacks, stages, and management status list List stacks, stages, and management status
help, -h, --help Show this help help, -h, --help Show this help
Stage configuration: Stage configuration:
max_parallel N Set concurrency, 0-65535 (0 = unlimited) max_parallel N Set concurrency, 0-65535 (0 = unlimited)
@@ -165,25 +172,25 @@ validate_name() {
# folder inside STACKS_DIR; paths to folders elsewhere are rejected. # folder inside STACKS_DIR; paths to folders elsewhere are rejected.
normalize_target() { normalize_target() {
local target_name="$1" local target_name="$1"
local -n target="$target_name" local -n value="$target_name"
local original="$target" local original="$value"
local name path resolved canonical root local name path resolved canonical root
while [[ "$target" == */ ]]; do while [[ "$value" == */ ]]; do
target="${target%/}" value="${value%/}"
done done
# Names without a path component are validated against the stacks. # Names without a path component are validated against the stacks.
if [[ "$target" != */* ]]; then if [[ "$value" != */* ]]; then
[[ -n "$target" ]] || die "invalid stack path: $original" [[ -n "$value" ]] || die "invalid stack path: $original"
return 0 return 0
fi fi
name="${target##*/}" name="${value##*/}"
[[ -n "$name" ]] || die "invalid stack path: $original" [[ -n "$name" ]] || die "invalid stack path: $original"
root="$(readlink -f -- "$STACKS_DIR" 2>/dev/null)" || root="$(readlink -f -- "$STACKS_DIR" 2>/dev/null)" ||
die "invalid STACKS_DIR: $STACKS_DIR" die "invalid STACKS_DIR: $STACKS_DIR"
path="$target" path="$value"
[[ "$path" == /* ]] || path="$STACKS_DIR/$path" [[ "$path" == /* ]] || path="$STACKS_DIR/$path"
resolved="$(readlink -f -- "$path" 2>/dev/null)" || resolved="$(readlink -f -- "$path" 2>/dev/null)" ||
die "stack folder not found: $original" die "stack folder not found: $original"
@@ -192,7 +199,7 @@ normalize_target() {
[[ "$resolved" == "$canonical" ]] || [[ "$resolved" == "$canonical" ]] ||
die "not a stack folder in $root: $original" die "not a stack folder in $root: $original"
target="$name" value="$name"
} }
# Set the maximum concurrency within each stage. # Set the maximum concurrency within each stage.
@@ -567,6 +574,10 @@ print_run_summary() {
echo "Failed stacks: ${RUN_FAILED_STACKS[*]}" echo "Failed stacks: ${RUN_FAILED_STACKS[*]}"
fi fi
if ((${#RUN_SKIPPED_STACKS[@]} > 0)); then
echo "Skipped stacks: ${RUN_SKIPPED_STACKS[*]}"
fi
if ((${#RUN_SKIPPED_STAGES[@]} > 0)); then if ((${#RUN_SKIPPED_STAGES[@]} > 0)); then
echo "Skipped stages: ${RUN_SKIPPED_STAGES[*]}" echo "Skipped stages: ${RUN_SKIPPED_STAGES[*]}"
fi fi
@@ -600,6 +611,7 @@ run_all_staged() {
RUN_FAILED=0 RUN_FAILED=0
RUN_FAILED_STACKS=() RUN_FAILED_STACKS=()
RUN_SKIPPED_STAGES=() RUN_SKIPPED_STAGES=()
RUN_SKIPPED_STACKS=()
if [[ "$action" == "stop" || "$action" == "down" ]]; then if [[ "$action" == "stop" || "$action" == "down" ]]; then
direction="reverse" direction="reverse"
start=$((end - 1)) start=$((end - 1))
@@ -618,11 +630,41 @@ run_all_staged() {
print_run_summary print_run_summary
} }
# Run the given stacks one at a time, in the given order. A failure stops
# the run; the remaining stacks are reported as skipped.
run_stacks_sequential() {
local action="$1"
local index stack
local -a stacks=("${@:2}")
RUN_SUCCESS=0
RUN_FAILED=0
RUN_FAILED_STACKS=()
RUN_SKIPPED_STAGES=()
RUN_SKIPPED_STACKS=()
for ((index = 0; index < ${#stacks[@]}; index++)); do
stack="${stacks[index]}"
if run_stack "$stack" "$action"; then
((RUN_SUCCESS += 1))
else
((RUN_FAILED += 1))
RUN_FAILED_STACKS+=("$stack")
RUN_SKIPPED_STACKS=("${stacks[@]:$((index + 1))}")
break
fi
done
print_run_summary
((RUN_FAILED == 0))
}
run_all_sequential() { run_all_sequential() {
local action="$1" local action="$1"
shift
local stack local stack
local -a failed_stacks=() local -a failed_stacks=()
for stack in "${MANAGED_STACKS[@]}"; do for stack in "$@"; do
run_stack "$stack" "$action" || failed_stacks+=("$stack") run_stack "$stack" "$action" || failed_stacks+=("$stack")
done done
@@ -678,17 +720,11 @@ list_stacks() {
} }
list_images() { list_images() {
local target="${1:-}"
local stack output service image local stack output service image
local failed=0 local failed=0
local -a stacks=("${MANAGED_STACKS[@]}") cells=(STACK SERVICE IMAGE) local -a cells=(STACK SERVICE IMAGE)
if [[ -n "$target" ]]; then for stack in "$@"; do
require_target "images" "$target"
stacks=("$target")
fi
for stack in "${stacks[@]}"; do
# Read service/image pairs from Compose's canonical YAML (two-space indent). # Read service/image pairs from Compose's canonical YAML (two-space indent).
if ! output="$(compose "$stack" config --format yaml | awk -v project="$stack" ' if ! output="$(compose "$stack" config --format yaml | awk -v project="$stack" '
{ gsub(/["\047]/, "") } { gsub(/["\047]/, "") }
@@ -728,6 +764,17 @@ require_target() {
is_managed "$target" || die "stack is excluded: $target" is_managed "$target" || die "stack is excluded: $target"
} }
# Validate every requested stack before acting on any of them.
require_targets() {
local command="$1"
local target
shift
for target in "$@"; do
require_target "$command" "$target"
done
}
acquire_lock() { acquire_lock() {
mkdir -p "$(dirname "$LOCK_FILE")" || mkdir -p "$(dirname "$LOCK_FILE")" ||
die "cannot create lock directory" die "cannot create lock directory"
@@ -739,10 +786,10 @@ acquire_lock() {
main() { main() {
local ACTION="${1:-}" local ACTION="${1:-}"
local TARGET="${2:-}" local target
local -a requested=("${@:2}")
local -a targets=() local -a targets=()
local -a stacks=()
[[ $# -le 2 ]] || die "too many arguments"
case "$ACTION" in case "$ACTION" in
help | -h | --help) help | -h | --help)
@@ -751,56 +798,60 @@ main() {
;; ;;
esac esac
if [[ -n "$TARGET" ]]; then for target in "${requested[@]}"; do
normalize_target TARGET normalize_target target
fi contains "$target" "${targets[@]}" &&
die "stack is specified more than once: $target"
targets+=("$target")
done
load_config load_config
discover_stacks discover_stacks
validate_config_stacks validate_config_stacks
build_execution_stages build_execution_stages
# Commands act on the requested stacks, or on every managed stack when
# none were requested.
stacks=("${targets[@]}")
((${#stacks[@]} > 0)) || stacks=("${MANAGED_STACKS[@]}")
case "$ACTION" in case "$ACTION" in
up | start | stop | restart | down | pull | update) up | start | stop | restart | down | pull | update)
acquire_lock acquire_lock
require_targets "$ACTION" "${targets[@]}"
if [[ -n "$TARGET" ]]; then
require_target "$ACTION" "$TARGET"
targets=("$TARGET")
else
targets=("${MANAGED_STACKS[@]}")
fi
if [[ "$ACTION" == "up" || "$ACTION" == "update" ]]; then if [[ "$ACTION" == "up" || "$ACTION" == "update" ]]; then
preflight_configs "${targets[@]}" || return 1 preflight_configs "${stacks[@]}" || return 1
fi fi
if [[ -n "$TARGET" ]]; then if ((${#targets[@]} == 0)); then
run_stack "$TARGET" "$ACTION"
else
run_all_staged "$ACTION" run_all_staged "$ACTION"
elif ((${#targets[@]} == 1)); then
run_stack "${stacks[0]}" "$ACTION"
else
run_stacks_sequential "$ACTION" "${stacks[@]}"
fi fi
;; ;;
ps) ps)
if [[ -n "$TARGET" ]]; then require_targets "$ACTION" "${targets[@]}"
require_target "$ACTION" "$TARGET" run_all_sequential "$ACTION" "${stacks[@]}"
run_stack "$TARGET" "$ACTION"
else
run_all_sequential "$ACTION"
fi
;; ;;
logs) logs)
require_target "$ACTION" "$TARGET" ((${#targets[@]} <= 1)) || die "$ACTION expects a single stack"
compose "$TARGET" logs -f --tail=100 require_target "$ACTION" "${targets[0]:-}"
compose "${targets[0]}" logs -f --tail=100
;; ;;
config) config)
require_target "$ACTION" "$TARGET" ((${#targets[@]} <= 1)) || die "$ACTION expects a single stack"
compose "$TARGET" config require_target "$ACTION" "${targets[0]:-}"
compose "${targets[0]}" config
;; ;;
images) images)
list_images "$TARGET" require_targets "$ACTION" "${targets[@]}"
list_images "${stacks[@]}"
;; ;;
list) list)
[[ -z "$TARGET" ]] || die "list does not accept a stack name" ((${#targets[@]} == 0)) || die "list does not accept a stack name"
list_stacks list_stacks
;; ;;
*) *)