who_need_help/scripts/load-cycle.sh

178 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
umask 077
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
BASE_ENV=${WNH_LOAD_BASE_ENV_FILE:-"$ROOT/output/runtime/load.env"}
LABEL=${1:-"cycle-$(date -u +%Y%m%dT%H%M%SZ)"}
MODE=${2:-load}
if [[ ! "$LABEL" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Run label may contain only letters, numbers, dot, underscore, and dash." >&2
exit 1
fi
case "$MODE" in
load | resilience | both) ;;
*)
echo "Mode must be load, resilience, or both." >&2
exit 1
;;
esac
WNH_LOAD_ENV_FILE="$BASE_ENV" "$ROOT/scripts/ensure-local-load-env.sh"
if [[ ! -f "$BASE_ENV" ]]; then
echo "Missing base load environment: $BASE_ENV." >&2
exit 1
fi
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
safe_id=$(printf '%s' "$run_id" | tr -cd 'A-Za-z0-9')
project="who_need_help_load_$safe_id"
temporary_env=$(mktemp "${TMPDIR:-/tmp}/wnh-load-cycle.XXXXXX.env")
export WNH_LOAD_ENV_FILE=$temporary_env
export WNH_LOAD_TOOLS_IMAGE="who-need-help:load-tools-$safe_id"
export WNH_PYTHON_RUNTIME_IMAGE="who-need-help:python-runtime-$safe_id"
cp "$BASE_ENV" "$temporary_env"
chmod 600 "$temporary_env"
set_env_value() {
local name=$1
local value=$2
local replacement="$name=$value"
if grep -q "^${name}=" "$temporary_env"; then
sed -i "s|^${name}=.*|$replacement|" "$temporary_env"
else
printf '%s\n' "$replacement" >>"$temporary_env"
fi
}
set_env_value LOAD_PROJECT "$project"
set_env_value COMPOSE_PROJECT_NAME "$project"
set_env_value TRAEFIK_PROJECT_CONSTRAINT "$project"
set_env_value TRAEFIK_DOCKER_NETWORK "${project}_ingress"
set_env_value APP_IMAGE "who-need-help:load-$safe_id"
set_env_value SOCKET_PROXY_IMAGE "who-need-help:socket-proxy-load-$safe_id"
set_env_value POSTGIS_IMAGE "who-need-help:postgis-load-$safe_id"
apply_positive_integer_override() {
local override_name=$1
local target_name=$2
local value=${!override_name:-}
if [[ -z "$value" ]]; then
return
fi
if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then
echo "$override_name must be a positive integer." >&2
exit 1
fi
set_env_value "$target_name" "$value"
}
apply_positive_integer_override WNH_LOAD_WEB_REPLICAS_OVERRIDE LOAD_WEB_REPLICAS
apply_positive_integer_override WNH_LOAD_WORKER_REPLICAS_OVERRIDE LOAD_WORKER_REPLICAS
apply_positive_integer_override WNH_LOAD_HTTP_VUS_OVERRIDE LOAD_HTTP_VUS
apply_positive_integer_override WNH_LOAD_WS_VUS_OVERRIDE LOAD_WS_VUS
apply_positive_integer_override WNH_LOAD_AUTH_VUS_OVERRIDE LOAD_AUTH_VUS
set -a
# shellcheck source=/dev/null
. "$temporary_env"
set +a
for name in APP_IMAGE SOCKET_PROXY_IMAGE POSTGIS_IMAGE; do
if [[ -z "${!name:-}" ]]; then
echo "$name is missing from the generated cycle environment." >&2
exit 1
fi
done
if [[ -n "$(docker ps -aq --filter "label=com.docker.compose.project=$project")" ]] ||
[[ -n "$(docker network ls -q --filter "label=com.docker.compose.project=$project")" ]] ||
[[ -n "$(docker volume ls -q --filter "label=com.docker.compose.project=$project")" ]]; then
echo "Generated load-cycle project already owns Docker resources: $project." >&2
exit 1
fi
started=0
cleanup() {
local status=$?
local cleanup_status=0
trap - EXIT HUP INT TERM
if [[ "$started" -eq 1 ]] ||
[[ -n "$(docker ps -aq --filter "label=com.docker.compose.project=$project")" ]] ||
[[ -n "$(docker network ls -q --filter "label=com.docker.compose.project=$project")" ]]; then
"$ROOT/scripts/load-stack-stop.sh" >/dev/null 2>&1 || cleanup_status=1
fi
while IFS= read -r volume; do
[[ -n "$volume" ]] || continue
observed_project=$(docker volume inspect --format \
'{{index .Labels "com.docker.compose.project"}}' "$volume")
references=$(docker ps -aq --filter "volume=$volume")
if [[ "$observed_project" != "$project" ]] || [[ -n "$references" ]]; then
echo "Refusing unexpected or referenced load-cycle volume: $volume" >&2
cleanup_status=1
continue
fi
docker volume rm "$volume" >/dev/null || cleanup_status=1
done < <(docker volume ls -q --filter "label=com.docker.compose.project=$project")
for image in "$APP_IMAGE" "$SOCKET_PROXY_IMAGE" "$POSTGIS_IMAGE" \
"$WNH_LOAD_TOOLS_IMAGE" "$WNH_PYTHON_RUNTIME_IMAGE"; do
if image_id=$(docker image inspect --format '{{.Id}}' "$image" 2>/dev/null); then
if [[ -n "$(docker ps -aq --filter "ancestor=$image_id")" ]]; then
echo "Refusing referenced load-cycle image: $image" >&2
cleanup_status=1
else
docker image rm "$image" >/dev/null || cleanup_status=1
fi
fi
done
rm -f "$temporary_env"
if [[ -n "$(docker ps -aq --filter "label=com.docker.compose.project=$project")" ]] ||
[[ -n "$(docker network ls -q --filter "label=com.docker.compose.project=$project")" ]] ||
[[ -n "$(docker volume ls -q --filter "label=com.docker.compose.project=$project")" ]]; then
echo "Load-cycle cleanup left project resources behind: $project." >&2
cleanup_status=1
fi
if [[ "$status" -eq 0 && "$cleanup_status" -ne 0 ]]; then
status=$cleanup_status
fi
exit "$status"
}
trap cleanup EXIT HUP INT TERM
"$ROOT/scripts/load-stack-up.sh"
started=1
case "$MODE" in
load)
"$ROOT/scripts/load-run.sh" "$LABEL"
;;
resilience)
"$ROOT/scripts/load-resilience-run.sh" "$LABEL"
;;
both)
"$ROOT/scripts/load-run.sh" "$LABEL-load"
"$ROOT/scripts/load-resilience-run.sh" "$LABEL-resilience"
;;
esac
echo "Load cycle completed; its project resources will now be removed: $project"