feat: improve staged execution safety

This commit is contained in:
lingkai5wu committed 2026-10-10 19:10:25 +08:00
1 parent ebab7d3213
commit 1143ae9f62
1 file changed
+84 -64
+84 -64
View File
@@ -10,29 +10,49 @@ LOCK_FILE="${STACKS_LOCK_FILE:-/run/lock/stacks-manage.lock}"
DEFAULT_MAX_PARALLEL=2 DEFAULT_MAX_PARALLEL=2
BEFORE_GROUPS=() BEFORE_STAGES=()
BEFORE_LIMITS=() BEFORE_LIMITS=()
AFTER_GROUPS=() AFTER_STAGES=()
AFTER_LIMITS=() AFTER_LIMITS=()
CONFIGURED_STACKS=() CONFIGURED_STACKS=()
EXCLUDED_STACKS=() EXCLUDED_STACKS=()
DISCOVERED_STACKS=() DISCOVERED_STACKS=()
MANAGED_STACKS=() MANAGED_STACKS=()
GROUP_NAMES=() STAGE_NAMES=()
GROUP_LIMITS=() STAGE_LIMITS=()
GROUP_STACKS=() STAGE_STACKS=()
RUN_SUCCESS=0 RUN_SUCCESS=0
RUN_FAILED=0 RUN_FAILED=0
RUN_FAILED_STACKS=() RUN_FAILED_STACKS=()
RUN_SKIPPED_GROUPS=() RUN_SKIPPED_STAGES=()
die() { die() {
echo "ERROR: $*" >&2 echo "ERROR: $*" >&2
exit 1 exit 1
} }
if ((BASH_VERSINFO[0] < 5 ||
(BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] < 1))); then
die "Bash 5.1 or newer is required"
fi
handle_signal() {
local status="$1"
local -a pids=()
trap - INT TERM
mapfile -t pids < <(jobs -pr)
((${#pids[@]} == 0)) || kill -TERM "${pids[@]}" 2>/dev/null
wait 2>/dev/null
printf '\n'
exit "$status"
}
trap 'handle_signal 130' INT
trap 'handle_signal 143' TERM
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") COMMAND [STACK] Usage: $(basename "$0") COMMAND [STACK]
@@ -49,13 +69,13 @@ Commands:
logs Follow logs for a stack logs Follow logs for a stack
config Render Compose configuration for a stack config Render Compose configuration for a stack
images Show configured images and versions images Show configured images and versions
list List discovered stacks and their group/status list List discovered stacks and their stage/status
Bulk execution: Bulk execution:
up/start/restart/pull/update run before, default, then after. up/start/restart/pull/update run before, default, then after.
stop/down run the same groups in reverse order. stop/down run the same stages in reverse order.
Each before/after declaration creates an ordered group. Each before/after declaration creates an ordered stage.
Stacks within a group run in parallel up to its configured limit. Stacks within a stage run in parallel up to its configured limit.
ps/images/list keep ordered output. ps/images/list keep ordered output.
Configuration: $CONFIG_FILE Configuration: $CONFIG_FILE
@@ -96,13 +116,13 @@ max_parallel() {
DEFAULT_MAX_PARALLEL="$1" DEFAULT_MAX_PARALLEL="$1"
} }
add_stage_group() { add_stage() {
local groups_name="$1" local stages_name="$1"
local limits_name="$2" local limits_name="$2"
local stage="$3" local stage="$3"
local limit="" local limit=""
local stack local stack
local -n groups="$groups_name" local -n stages="$stages_name"
local -n limits="$limits_name" local -n limits="$limits_name"
shift 3 shift 3
@@ -124,12 +144,12 @@ add_stage_group() {
CONFIGURED_STACKS+=("$stack") CONFIGURED_STACKS+=("$stack")
done done
groups+=("$*") stages+=("$*")
limits+=("$limit") limits+=("$limit")
} }
before() { add_stage_group BEFORE_GROUPS BEFORE_LIMITS before "$@"; } before() { add_stage BEFORE_STAGES BEFORE_LIMITS before "$@"; }
after() { add_stage_group AFTER_GROUPS AFTER_LIMITS after "$@"; } after() { add_stage AFTER_STAGES AFTER_LIMITS after "$@"; }
# Exclude stacks from all bulk operations. # Exclude stacks from all bulk operations.
exclude() { exclude() {
@@ -184,36 +204,36 @@ validate_config_stacks() {
done done
} }
append_stage_groups() { append_stages() {
local stage="$1" local stage="$1"
local groups_name="$2" local stages_name="$2"
local limits_name="$3" local limits_name="$3"
local i name local i name
local -a stacks=() local -a stacks=()
local -n groups="$groups_name" local -n stages="$stages_name"
local -n limits="$limits_name" local -n limits="$limits_name"
for ((i = 0; i < ${#groups[@]}; i++)); do for ((i = 0; i < ${#stages[@]}; i++)); do
name="$stage" name="$stage"
((${#groups[@]} > 1)) && name+="-$((i + 1))" ((${#stages[@]} > 1)) && name+="-$((i + 1))"
GROUP_NAMES+=("$name") STAGE_NAMES+=("$name")
GROUP_LIMITS+=("${limits[i]:-$DEFAULT_MAX_PARALLEL}") STAGE_LIMITS+=("${limits[i]:-$DEFAULT_MAX_PARALLEL}")
GROUP_STACKS+=("${groups[i]}") STAGE_STACKS+=("${stages[i]}")
read -r -a stacks <<<"${groups[i]}" read -r -a stacks <<<"${stages[i]}"
MANAGED_STACKS+=("${stacks[@]}") MANAGED_STACKS+=("${stacks[@]}")
done done
} }
build_execution_groups() { build_execution_stages() {
local stack local stack
local -a default_stacks=() local -a default_stacks=()
GROUP_NAMES=() STAGE_NAMES=()
GROUP_LIMITS=() STAGE_LIMITS=()
GROUP_STACKS=() STAGE_STACKS=()
MANAGED_STACKS=() MANAGED_STACKS=()
append_stage_groups before BEFORE_GROUPS BEFORE_LIMITS append_stages before BEFORE_STAGES BEFORE_LIMITS
for stack in "${DISCOVERED_STACKS[@]}"; do for stack in "${DISCOVERED_STACKS[@]}"; do
if ! contains "$stack" "${CONFIGURED_STACKS[@]}" && if ! contains "$stack" "${CONFIGURED_STACKS[@]}" &&
@@ -223,24 +243,24 @@ build_execution_groups() {
done done
if ((${#default_stacks[@]} > 0)); then if ((${#default_stacks[@]} > 0)); then
GROUP_NAMES+=("default") STAGE_NAMES+=("default")
GROUP_LIMITS+=("$DEFAULT_MAX_PARALLEL") STAGE_LIMITS+=("$DEFAULT_MAX_PARALLEL")
GROUP_STACKS+=("${default_stacks[*]}") STAGE_STACKS+=("${default_stacks[*]}")
MANAGED_STACKS+=("${default_stacks[@]}") MANAGED_STACKS+=("${default_stacks[@]}")
fi fi
append_stage_groups after AFTER_GROUPS AFTER_LIMITS append_stages after AFTER_STAGES AFTER_LIMITS
} }
stack_group() { stack_stage() {
local target="$1" local target="$1"
local i local i
local -a stacks=() local -a stacks=()
for ((i = 0; i < ${#GROUP_NAMES[@]}; i++)); do for ((i = 0; i < ${#STAGE_NAMES[@]}; i++)); do
read -r -a stacks <<<"${GROUP_STACKS[i]}" read -r -a stacks <<<"${STAGE_STACKS[i]}"
if contains "$target" "${stacks[@]}"; then if contains "$target" "${stacks[@]}"; then
echo "${GROUP_NAMES[i]}" echo "${STAGE_NAMES[i]}"
return 0 return 0
fi fi
done done
@@ -289,11 +309,11 @@ run_stack() {
esac esac
} }
run_group() { run_stage() {
local group_index="$1" local stage_index="$1"
local action="$2" local action="$2"
local group_name="${GROUP_NAMES[group_index]}" local stage_name="${STAGE_NAMES[stage_index]}"
local limit="${GROUP_LIMITS[group_index]}" local limit="${STAGE_LIMITS[stage_index]}"
local stack pid finished_pid status success_result working_result details local stack pid finished_pid status success_result working_result details
local index finished_index total local index finished_index total
local next=0 running=0 completed=0 failed=0 stack_width=0 interactive=0 rendered=0 local next=0 running=0 completed=0 failed=0 stack_width=0 interactive=0 rendered=0
@@ -310,7 +330,7 @@ run_group() {
fi fi
fi fi
read -r -a stacks <<<"${GROUP_STACKS[group_index]}" read -r -a stacks <<<"${STAGE_STACKS[stage_index]}"
total="${#stacks[@]}" total="${#stacks[@]}"
((total > 0)) || return 0 ((total > 0)) || return 0
for ((index = 0; index < total; index++)); do for ((index = 0; index < total; index++)); do
@@ -335,7 +355,7 @@ run_group() {
echo echo
if ((!interactive)); then if ((!interactive)); then
echo "==> Group $group_name: $action ($details)" echo "==> Stage $stage_name: $action ($details)"
fi fi
case "$action" in case "$action" in
@@ -378,7 +398,7 @@ run_group() {
if ((interactive)); then if ((interactive)); then
((rendered)) && printf '\033[%dA' "$((total + 1))" ((rendered)) && printf '\033[%dA' "$((total + 1))"
printf '\r\033[2K[+] %s: %s %d/%d\n' \ printf '\r\033[2K[+] %s: %s %d/%d\n' \
"$group_name" "$action" "$completed" "$total" "$stage_name" "$action" "$completed" "$total"
for ((index = 0; index < total; index++)); do for ((index = 0; index < total; index++)); do
printf '\r\033[2K %s%s%s Stack %-*s %s%s%s\n' \ printf '\r\033[2K %s%s%s Stack %-*s %s%s%s\n' \
"${state_colors[index]}" "${markers[index]}" "$color_reset" \ "${state_colors[index]}" "${markers[index]}" "$color_reset" \
@@ -434,39 +454,39 @@ print_run_summary() {
echo "Failed stacks: ${RUN_FAILED_STACKS[*]}" echo "Failed stacks: ${RUN_FAILED_STACKS[*]}"
fi fi
if ((${#RUN_SKIPPED_GROUPS[@]} > 0)); then if ((${#RUN_SKIPPED_STAGES[@]} > 0)); then
echo "Skipped groups: ${RUN_SKIPPED_GROUPS[*]}" echo "Skipped stages: ${RUN_SKIPPED_STAGES[*]}"
fi fi
} }
collect_skipped_groups() { collect_skipped_stages() {
local failed_index="$1" local failed_index="$1"
local direction="$2" local direction="$2"
local i local i
RUN_SKIPPED_GROUPS=() RUN_SKIPPED_STAGES=()
if [[ "$direction" == "reverse" ]]; then if [[ "$direction" == "reverse" ]]; then
for ((i = failed_index - 1; i >= 0; i--)); do for ((i = failed_index - 1; i >= 0; i--)); do
RUN_SKIPPED_GROUPS+=("${GROUP_NAMES[i]}") RUN_SKIPPED_STAGES+=("${STAGE_NAMES[i]}")
done done
else else
for ((i = failed_index + 1; i < ${#GROUP_NAMES[@]}; i++)); do for ((i = failed_index + 1; i < ${#STAGE_NAMES[@]}; i++)); do
RUN_SKIPPED_GROUPS+=("${GROUP_NAMES[i]}") RUN_SKIPPED_STAGES+=("${STAGE_NAMES[i]}")
done done
fi fi
} }
run_all_grouped() { run_all_staged() {
local action="$1" local action="$1"
local direction="forward" local direction="forward"
local start=0 end="${#GROUP_NAMES[@]}" step=1 local start=0 end="${#STAGE_NAMES[@]}" step=1
local i local i
RUN_SUCCESS=0 RUN_SUCCESS=0
RUN_FAILED=0 RUN_FAILED=0
RUN_FAILED_STACKS=() RUN_FAILED_STACKS=()
RUN_SKIPPED_GROUPS=() RUN_SKIPPED_STAGES=()
if [[ "$action" == "stop" || "$action" == "down" ]]; then if [[ "$action" == "stop" || "$action" == "down" ]]; then
direction="reverse" direction="reverse"
start=$((end - 1)) start=$((end - 1))
@@ -475,8 +495,8 @@ run_all_grouped() {
fi fi
for ((i = start; i != end; i += step)); do for ((i = start; i != end; i += step)); do
if ! run_group "$i" "$action"; then if ! run_stage "$i" "$action"; then
collect_skipped_groups "$i" "$direction" collect_skipped_stages "$i" "$direction"
print_run_summary print_run_summary
return 1 return 1
fi fi
@@ -501,21 +521,21 @@ run_all_sequential() {
} }
list_stacks() { list_stacks() {
local stack group_name status local stack stage_name status
printf '%-24s %-16s %s\n' "STACK" "GROUP" "STATUS" printf '%-24s %-16s %s\n' "STACK" "STAGE" "STATUS"
printf '%-24s %-16s %s\n' "------------------------" "----------------" "---------------" printf '%-24s %-16s %s\n' "------------------------" "----------------" "---------------"
for stack in "${DISCOVERED_STACKS[@]}"; do for stack in "${DISCOVERED_STACKS[@]}"; do
if contains "$stack" "${EXCLUDED_STACKS[@]}"; then if contains "$stack" "${EXCLUDED_STACKS[@]}"; then
group_name="-" stage_name="-"
status="excluded" status="excluded"
else else
group_name="$(stack_group "$stack")" stage_name="$(stack_stage "$stack")"
status="managed" status="managed"
fi fi
printf '%-24s %-16s %s\n' "$stack" "$group_name" "$status" printf '%-24s %-16s %s\n' "$stack" "$stage_name" "$status"
done done
} }
@@ -573,7 +593,7 @@ acquire_lock() {
load_config load_config
discover_stacks discover_stacks
validate_config_stacks validate_config_stacks
build_execution_groups build_execution_stages
ACTION="${1:-}" ACTION="${1:-}"
TARGET="${2:-}" TARGET="${2:-}"
@@ -588,7 +608,7 @@ up | start | stop | restart | down | pull | update)
require_target "$ACTION" "$TARGET" require_target "$ACTION" "$TARGET"
run_stack "$TARGET" "$ACTION" run_stack "$TARGET" "$ACTION"
else else
run_all_grouped "$ACTION" run_all_staged "$ACTION"
fi fi
;; ;;
ps) ps)