feat: accept multiple stack arguments
This commit is contained in:
1 parent
c002ec3244
commit
58636aa06e
1 file changed
+111
-60
@@ -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
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
Reference in new issue
Block a user