From 5e20bc70af61ab047b12fe24f19968b0af8fdc0c Mon Sep 17 00:00:00 2001 From: lingkai5wu Date: Sat, 10 Oct 2026 15:55:29 +0800 Subject: [PATCH] feat: implement group management and parallel execution in manage.sh --- manage.conf | 26 ++++ manage.sh | 416 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 375 insertions(+), 67 deletions(-) create mode 100644 manage.conf diff --git a/manage.conf b/manage.conf new file mode 100644 index 0000000..23d21b7 --- /dev/null +++ b/manage.conf @@ -0,0 +1,26 @@ +# Global maximum number of stacks executed concurrently within a group. +# Use 0 for unlimited concurrency. +max_parallel 2 + +# Groups run from top to bottom for up/start/restart/pull/update. +# They run from bottom to top for stop/down. +group gateway --parallel 1 \ + nginx + +group network \ + adguardhome \ + ddns-go + +group services \ + samba \ + immich \ + gitea \ + openlist \ + homeassistant \ + qbittorrent + +group backup --parallel 1 \ + backrest + +# Excluded stacks are ignored by all bulk commands. +# exclude experimental-stack diff --git a/manage.sh b/manage.sh index 6c726dd..317c9c7 100755 --- a/manage.sh +++ b/manage.sh @@ -8,23 +8,30 @@ 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=() +DEFAULT_MAX_PARALLEL=2 + +CONFIG_GROUP_NAMES=() +CONFIG_GROUP_LIMITS=() +CONFIG_GROUP_STACKS=() +CONFIGURED_STACKS=() +EXCLUDED_STACKS=() DISCOVERED_STACKS=() MANAGED_STACKS=() +GROUP_NAMES=() +GROUP_LIMITS=() +GROUP_STACKS=() +UNASSIGNED_STACKS=() + +RUN_SUCCESS=0 +RUN_FAILED=0 +RUN_FAILED_STACKS=() die() { echo "ERROR: $*" >&2 exit 1 } -if [[ -f "$CONFIG_FILE" ]]; then - bash -n "$CONFIG_FILE" || die "Syntax error in config file: $CONFIG_FILE" - source "$CONFIG_FILE" -fi - usage() { cat </dev/null 2>&1; then + die "$legacy is no longer supported; migrate $CONFIG_FILE to group/exclude directives" + fi + done +} + discover_stacks() { local compose_file stack @@ -73,30 +179,76 @@ is_discovered() { } 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[@]}" + is_discovered "$1" && ! contains "$1" "${EXCLUDED_STACKS[@]}" } -build_managed_stacks() { +validate_config_stacks() { local stack - MANAGED_STACKS=() + for stack in "${CONFIGURED_STACKS[@]}"; do + is_discovered "$stack" || die "configured stack not found: $stack" + done - for stack in "${PRIORITY[@]}"; do - if is_managed "$stack" && ! contains "$stack" "${MANAGED_STACKS[@]}"; then - MANAGED_STACKS+=("$stack") - fi + for stack in "${EXCLUDED_STACKS[@]}"; do + is_discovered "$stack" || die "excluded stack not found: $stack" + done +} + +build_execution_groups() { + local i stack limit + local -a stacks=() + + GROUP_NAMES=() + GROUP_LIMITS=() + GROUP_STACKS=() + MANAGED_STACKS=() + UNASSIGNED_STACKS=() + + for ((i = 0; i < ${#CONFIG_GROUP_NAMES[@]}; i++)); do + limit="${CONFIG_GROUP_LIMITS[i]:-$DEFAULT_MAX_PARALLEL}" + GROUP_NAMES+=("${CONFIG_GROUP_NAMES[i]}") + GROUP_LIMITS+=("$limit") + GROUP_STACKS+=("${CONFIG_GROUP_STACKS[i]}") + + read -r -a stacks <<<"${CONFIG_GROUP_STACKS[i]}" + MANAGED_STACKS+=("${stacks[@]}") done for stack in "${DISCOVERED_STACKS[@]}"; do - if is_managed "$stack" && ! contains "$stack" "${MANAGED_STACKS[@]}"; then + if ! contains "$stack" "${CONFIGURED_STACKS[@]}" && + ! contains "$stack" "${EXCLUDED_STACKS[@]}"; then + UNASSIGNED_STACKS+=("$stack") MANAGED_STACKS+=("$stack") fi done + + if ((${#UNASSIGNED_STACKS[@]} > 0)); then + GROUP_NAMES+=("unassigned") + GROUP_LIMITS+=("$DEFAULT_MAX_PARALLEL") + GROUP_STACKS+=("${UNASSIGNED_STACKS[*]}") + fi +} + +warn_unassigned() { + ((${#UNASSIGNED_STACKS[@]} == 0)) && return 0 + + echo "WARNING: unassigned stacks will run in the final implicit group: ${UNASSIGNED_STACKS[*]}" >&2 +} + +stack_group() { + local target="$1" + local i + local -a stacks=() + + for ((i = 0; i < ${#GROUP_NAMES[@]}; i++)); do + read -r -a stacks <<<"${GROUP_STACKS[i]}" + if contains "$target" "${stacks[@]}"; then + echo "${GROUP_NAMES[i]}" + return 0 + fi + done + + return 1 } compose() { @@ -132,59 +284,186 @@ run_stack() { esac } -run_all() { - local action="$1" - local success=0 failed=0 stack i +run_stack_prefixed() { + local stack="$1" + local action="$2" + local line status - if [[ "$action" == "stop" || "$action" == "down" ]]; then - for ((i = ${#MANAGED_STACKS[@]} - 1; i >= 0; i--)); do - stack="${MANAGED_STACKS[i]}" + run_stack "$stack" "$action" 2>&1 | + while IFS= read -r line || [[ -n "$line" ]]; do + printf '[%s] %s\n' "$stack" "$line" + done + status="${PIPESTATUS[0]}" - 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 + return "$status" +} + +run_group() { + local group_index="$1" + local action="$2" + local group_name="${GROUP_NAMES[group_index]}" + local limit="${GROUP_LIMITS[group_index]}" + local semaphore_dir semaphore_fifo semaphore_fd + local stack pid i status + local failed=0 + local -a stacks=() pids=() pid_stacks=() + + read -r -a stacks <<<"${GROUP_STACKS[group_index]}" + ((${#stacks[@]} > 0)) || return 0 + + if ((limit == 0 || limit > ${#stacks[@]})); then + limit="${#stacks[@]}" fi echo - echo "Succeeded: $success" - echo "Failed: $failed" + echo "==> Group $group_name: $action (${#stacks[@]} stacks, parallel=$limit)" + + semaphore_dir="$(mktemp -d "${TMPDIR:-/tmp}/stacks-manage.XXXXXX")" || + die "cannot create semaphore directory" + semaphore_fifo="$semaphore_dir/tokens" + mkfifo "$semaphore_fifo" || die "cannot create semaphore" + exec {semaphore_fd}<>"$semaphore_fifo" || die "cannot open semaphore" + + for ((i = 0; i < limit; i++)); do + printf '\n' >&"$semaphore_fd" + done + + for stack in "${stacks[@]}"; do + IFS= read -r -u "$semaphore_fd" + + ( + status=0 + run_stack_prefixed "$stack" "$action" || status=$? + printf '\n' >&"$semaphore_fd" + exit "$status" + ) & + + pids+=("$!") + pid_stacks+=("$stack") + done + + for ((i = 0; i < ${#pids[@]}; i++)); do + pid="${pids[i]}" + stack="${pid_stacks[i]}" + + if wait "$pid"; then + ((RUN_SUCCESS += 1)) + else + ((RUN_FAILED += 1)) + RUN_FAILED_STACKS+=("$stack") + ((failed += 1)) + fi + done + + exec {semaphore_fd}>&- + rm -f -- "$semaphore_fifo" + rmdir -- "$semaphore_dir" ((failed == 0)) } -stack_status() { - local stack="$1" +print_run_summary() { + echo + echo "Succeeded: $RUN_SUCCESS" + echo "Failed: $RUN_FAILED" - 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" + if ((${#RUN_FAILED_STACKS[@]} > 0)); then + echo "Failures: ${RUN_FAILED_STACKS[*]}" fi } -list_stacks() { +print_skipped_groups() { + local failed_index="$1" + local direction="$2" + local i + local -a skipped=() + + if [[ "$direction" == "reverse" ]]; then + for ((i = failed_index - 1; i >= 0; i--)); do + skipped+=("${GROUP_NAMES[i]}") + done + else + for ((i = failed_index + 1; i < ${#GROUP_NAMES[@]}; i++)); do + skipped+=("${GROUP_NAMES[i]}") + done + fi + + if ((${#skipped[@]} > 0)); then + echo "Skipped groups after failure: ${skipped[*]}" >&2 + fi +} + +run_all_grouped() { + local action="$1" + local direction="forward" + local i + + RUN_SUCCESS=0 + RUN_FAILED=0 + RUN_FAILED_STACKS=() + warn_unassigned + + if [[ "$action" == "stop" || "$action" == "down" ]]; then + direction="reverse" + for ((i = ${#GROUP_NAMES[@]} - 1; i >= 0; i--)); do + if ! run_group "$i" "$action"; then + print_skipped_groups "$i" "$direction" + print_run_summary + return 1 + fi + done + else + for ((i = 0; i < ${#GROUP_NAMES[@]}; i++)); do + if ! run_group "$i" "$action"; then + print_skipped_groups "$i" "$direction" + print_run_summary + return 1 + fi + done + fi + + print_run_summary +} + +run_all_sequential() { + local action="$1" local stack - printf '%-24s %s\n' "STACK" "STATUS" - printf '%-24s %s\n' "------------------------" "---------------" + RUN_SUCCESS=0 + RUN_FAILED=0 + RUN_FAILED_STACKS=() + warn_unassigned + + for stack in "${MANAGED_STACKS[@]}"; do + if run_stack "$stack" "$action"; then + ((RUN_SUCCESS += 1)) + else + ((RUN_FAILED += 1)) + RUN_FAILED_STACKS+=("$stack") + fi + done + + print_run_summary + ((RUN_FAILED == 0)) +} + +list_stacks() { + local stack group_name status + + warn_unassigned + printf '%-24s %-16s %s\n' "STACK" "GROUP" "STATUS" + printf '%-24s %-16s %s\n' "------------------------" "----------------" "---------------" for stack in "${DISCOVERED_STACKS[@]}"; do - printf '%-24s %s\n' "$stack" "$(stack_status "$stack")" + if contains "$stack" "${EXCLUDED_STACKS[@]}"; then + group_name="-" + status="excluded" + else + group_name="$(stack_group "$stack")" + status="managed" + fi + + printf '%-24s %-16s %s\n' "$stack" "$group_name" "$status" done } @@ -214,6 +493,7 @@ list_images() { return fi + warn_unassigned for stack in "${MANAGED_STACKS[@]}"; do stack_images "$stack" || ((failed += 1)) done @@ -226,26 +506,28 @@ require_target() { 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" + 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" + die "cannot create lock directory" exec {LOCK_FD}>"$LOCK_FILE" || - die "Cannot open lock file: $LOCK_FILE" + die "cannot open lock file: $LOCK_FILE" flock -n "$LOCK_FD" || - die "Another manage.sh operation is already running" + die "another manage.sh operation is already running" } +load_config discover_stacks -build_managed_stacks +validate_config_stacks +build_execution_groups ACTION="${1:-}" TARGET="${2:-}" -[[ $# -le 2 ]] || die "Too many arguments" +[[ $# -le 2 ]] || die "too many arguments" case "$ACTION" in up | start | stop | restart | down | pull | update) @@ -255,7 +537,7 @@ up | start | stop | restart | down | pull | update) require_target "$ACTION" "$TARGET" run_stack "$TARGET" "$ACTION" else - run_all "$ACTION" + run_all_grouped "$ACTION" fi ;; ps) @@ -263,7 +545,7 @@ ps) require_target "$ACTION" "$TARGET" run_stack "$TARGET" "$ACTION" else - run_all "$ACTION" + run_all_sequential "$ACTION" fi ;; logs)