captaincore/app/bulk
Austin Ginder f1fcef1dfa 📦 NEW: Bulk runner
2026-03-16 06:57:17 -04:00

131 lines
4.3 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Bulk runs a command to many sites.
#
# `captaincore bulk <command>`
#
# [<site>...]
# One or more sites.
#
# [@<target>]
# Target groups of sites like @all @production or @staging.
#
# [--parallel=<number>]
# Number of sites to backup at same time
#
# Prevent infinite recursion from re-entering bulk mode
if [[ "$CC_BULK_RUNNING" == "true" ]]; then
echo -e "${COLOR_RED}Error:${COLOR_NORMAL} Recursive bulk execution detected. Aborting."
exit 1
fi
export CC_BULK_RUNNING=true
while read config; do
if [[ "$config" == "Error:"* ]]; then
continue
fi
declare "$config"
done <<< "$(captaincore config fetch --captain-id=$CAPTAIN_ID)"
# Error if no sites specifed
if [ ${#@} -eq 0 ]; then
echo -e "${COLOR_RED}Error:${COLOR_NORMAL} Please specify <command>."
exit
fi
cmd=$1
cmd=${cmd/\// }
shift # remove command name
target=$CAPTAINCORE_ARGS
count=( $target )
parallel=$FLAG_PARALLEL
# Remove target arguments, leaving only flags
for ((i = 0; i < ${#count[@]}; i++)); do shift; done
# Shell-quote remaining flags for safe transport through env var
cc_args_quoted=""
if [[ $# -gt 0 ]]; then
cc_args_quoted=$(printf '%q ' "$@")
fi
# Error if no sites specifed
if [[ $cmd == "" ]] || [[ $target == "" ]]; then
echo -e "${COLOR_RED}Error:${COLOR_NORMAL} Please specify one or more sites, or use a target @all, @production or @staging."
exit
fi
run_command() {
if [[ $target == "@all"* ]] || [[ $target == "@production"* ]] || [[ $target == "@staging"* ]]; then
target=$( captaincore site list $target --captain-id=$CAPTAIN_ID )
fi
if [[ "$parallel" == "" ]]; then
parallel=10
fi
site_count=( $target )
echo "Running '$cmd' on ${#site_count[@]} sites (parallel: $parallel)..."
# Set up progress tracking
progress_dir="$HOME/.captaincore/data/progress"
mkdir -p "$progress_dir"
bulk_pid=$$
progress_meta="$progress_dir/${bulk_pid}.json"
progress_log="$progress_dir/${bulk_pid}.log"
# JSON-escape the args string
args_escaped=$(printf '%s' "$cc_args_quoted" | sed 's/\\/\\\\/g; s/"/\\"/g')
target_escaped=$(printf '%s' "$target" | tr '\n' ' ' | sed 's/\\/\\\\/g; s/"/\\"/g')
printf '{"command":"%s","total":%d,"pid":%d,"started_at":%d,"captain_id":"%s","parallel":%d,"target":"%s","args":"%s"}\n' \
"$cmd" "${#site_count[@]}" "$bulk_pid" "$(date +%s)" "$CAPTAIN_ID" "$parallel" "$target_escaped" "$args_escaped" \
> "$progress_meta"
> "$progress_log"
export CC_PROGRESS_LOG="$progress_log"
cleanup_progress() { rm -f "$progress_meta" "$progress_log"; rmdir "$progress_dir/${bulk_pid}.lock" 2>/dev/null; }
trap cleanup_progress EXIT
export CC_OUTPUT_LOCK="$progress_dir/${bulk_pid}.lock"
if [[ "$FLAG_LABEL" == "true" ]]; then
export CC_CMD="$cmd"
export CC_ARGS_QUOTED="$cc_args_quoted"
export CC_CAPTAIN_ID="$CAPTAIN_ID"
labeled_run() {
local site="$1"
local raw_output output exit_code
raw_output=$(eval captaincore "$CC_CMD" $CC_ARGS_QUOTED --captain-id="$CC_CAPTAIN_ID" "$site" 2>/dev/null)
exit_code=$?
# Extract content between markers, stripping SSH MOTD/banner
output=$(echo "$raw_output" | sed -n '/____CC_OUTPUT_START____/,/____CC_OUTPUT_END____/{/____CC_OUTPUT_START____/d;/____CC_OUTPUT_END____/d;p;}' | sed '/^$/d')
if [[ -n "$output" ]]; then
while ! mkdir "$CC_OUTPUT_LOCK" 2>/dev/null; do sleep 0.05; done
printf "${COLOR_GREEN}== %s ==${COLOR_NORMAL}\n%s\n\n" "$site" "$output"
rmdir "$CC_OUTPUT_LOCK"
fi
printf '%s %d %d\n' "$site" "$exit_code" "$(date +%s)" >> "$CC_PROGRESS_LOG"
}
export -f labeled_run
export COLOR_GREEN COLOR_NORMAL
printf '%s\n' $target | xargs -P "$parallel" -n 1 bash -c 'labeled_run "$@"' _
else
export CC_CMD="$cmd"
export CC_ARGS_QUOTED="$cc_args_quoted"
export CC_CAPTAIN_ID="$CAPTAIN_ID"
tracked_run() {
local site="$1"
eval captaincore "$CC_CMD" $CC_ARGS_QUOTED --captain-id="$CC_CAPTAIN_ID" "$site" 2>&1
printf '%s %d %d\n' "$site" "$?" "$(date +%s)" >> "$CC_PROGRESS_LOG"
}
export -f tracked_run
printf '%s\n' $target | xargs -P "$parallel" -n 1 bash -c 'tracked_run "$@"' _
fi
}
run_command