179 lines
6.0 KiB
Bash
Executable File
179 lines
6.0 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
cd "${1:-.}"
|
|
|
|
env_value_from() {
|
|
env_file="$1"
|
|
key="$2"
|
|
default_value="${3:-}"
|
|
value="$(grep -E "^${key}=" "$env_file" | tail -n 1 | cut -d= -f2- || true)"
|
|
if [ -n "$value" ]; then
|
|
printf '%s' "$value"
|
|
else
|
|
printf '%s' "$default_value"
|
|
fi
|
|
}
|
|
|
|
env_value() {
|
|
key="$1"
|
|
default_value="${2:-}"
|
|
env_value_from .env "$key" "$default_value"
|
|
}
|
|
|
|
cache_backend="$(env_value CRANK_CACHE_BACKEND memory)"
|
|
compose_profiles=""
|
|
if [ "$cache_backend" = "valkey" ] || [ "$cache_backend" = "redis" ]; then
|
|
compose_profiles="--profile cache"
|
|
fi
|
|
compose() {
|
|
# Intentional word splitting: compose_profiles is either empty or two arguments.
|
|
# shellcheck disable=SC2086
|
|
docker compose $compose_profiles "$@"
|
|
}
|
|
|
|
compose_up() {
|
|
postgres_scale=""
|
|
if [ "$(env_value POSTGRES_HOST postgres)" != "postgres" ] \
|
|
&& compose config --services | grep -Fxq postgres; then
|
|
postgres_scale="--scale postgres=0"
|
|
fi
|
|
# Intentional word splitting: postgres_scale is either empty or two arguments.
|
|
# shellcheck disable=SC2086
|
|
compose up -d --remove-orphans $postgres_scale
|
|
}
|
|
|
|
show_failure_diagnostics() {
|
|
compose ps >&2 || true
|
|
compose logs --no-color migrate >&2 || true
|
|
}
|
|
|
|
wait_for_stack() {
|
|
readiness_path="$1"
|
|
attempt=1
|
|
while [ "$attempt" -le 45 ]; do
|
|
if curl --fail --silent http://127.0.0.1:3000/ >/dev/null \
|
|
&& curl --fail --silent "http://127.0.0.1:3001/${readiness_path}" >/dev/null \
|
|
&& curl --fail --silent "http://127.0.0.1:3002/${readiness_path}" >/dev/null; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
attempt=$((attempt + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
find_artifact_container() {
|
|
expected_root="$1"
|
|
expected_mount="$(printf 'volume\t%s' "$expected_root")"
|
|
|
|
for service in admin-api artifact-storage-init; do
|
|
candidates="$(compose ps -aq "$service" 2>/dev/null || true)"
|
|
# Intentional word splitting: Docker container IDs cannot contain whitespace.
|
|
# shellcheck disable=SC2086
|
|
set -- $candidates
|
|
if [ "$#" -gt 1 ]; then
|
|
echo "Artifact backup found multiple $service containers; refusing an ambiguous volume source" >&2
|
|
return 1
|
|
fi
|
|
if [ "$#" -eq 1 ]; then
|
|
candidate="$1"
|
|
oneoff="$(docker inspect --format '{{index .Config.Labels "com.docker.compose.oneoff"}}' "$candidate" 2>/dev/null || true)"
|
|
mounts="$(docker inspect --format '{{range .Mounts}}{{printf "%s\t%s\n" .Type .Destination}}{{end}}' "$candidate" 2>/dev/null || true)"
|
|
if [ "$oneoff" != "True" ] && [ "$oneoff" != "true" ] \
|
|
&& printf '%s\n' "$mounts" | grep -Fxq "$expected_mount"; then
|
|
printf '%s' "$candidate"
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
create_backup() {
|
|
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
backup_dir="$(pwd)/backups/${timestamp}"
|
|
mkdir -p "$backup_dir"
|
|
chmod 700 "$backup_dir"
|
|
|
|
backup_env_file=.env
|
|
previous_deployment=false
|
|
if [ -f .env.previous ]; then
|
|
backup_env_file=.env.previous
|
|
previous_deployment=true
|
|
cp "$backup_env_file" "$backup_dir/runtime.env"
|
|
else
|
|
cp .env "$backup_dir/runtime.env"
|
|
fi
|
|
if [ -f docker-compose.previous.yml ]; then
|
|
cp docker-compose.previous.yml "$backup_dir/docker-compose.yml"
|
|
else
|
|
cp docker-compose.yml "$backup_dir/docker-compose.yml"
|
|
fi
|
|
|
|
postgres_host="$(env_value_from "$backup_env_file" POSTGRES_HOST)"
|
|
postgres_port="$(env_value_from "$backup_env_file" POSTGRES_PORT 5432)"
|
|
postgres_db="$(env_value_from "$backup_env_file" POSTGRES_DB crank)"
|
|
postgres_user="$(env_value_from "$backup_env_file" POSTGRES_USER crank)"
|
|
postgres_password="$(env_value_from "$backup_env_file" POSTGRES_PASSWORD)"
|
|
if [ -z "$postgres_host" ] || [ -z "$postgres_password" ]; then
|
|
echo "PostgreSQL credentials are required for the pre-update backup" >&2
|
|
return 1
|
|
fi
|
|
docker run --rm --network host \
|
|
-e PGPASSWORD="$postgres_password" \
|
|
-v "$backup_dir:/backup" \
|
|
postgres:16-alpine \
|
|
pg_dump --host "$postgres_host" --port "$postgres_port" \
|
|
--username "$postgres_user" --dbname "$postgres_db" \
|
|
--format custom --file /backup/postgres.dump
|
|
|
|
storage_root="$(env_value_from "$backup_env_file" CRANK_STORAGE_ROOT /var/lib/crank/storage)"
|
|
if artifact_container="$(find_artifact_container "$storage_root")"; then
|
|
docker run --rm --volumes-from "$artifact_container:ro" \
|
|
-v "$backup_dir:/backup" alpine:3.21 \
|
|
tar -C "$storage_root" -czf /backup/artifacts.tar.gz .
|
|
elif [ "$previous_deployment" = true ]; then
|
|
echo "Existing deployment container is unavailable; artifact backup cannot be verified" >&2
|
|
return 1
|
|
else
|
|
tar -czf "$backup_dir/artifacts.tar.gz" --files-from /dev/null
|
|
fi
|
|
|
|
(cd "$backup_dir" && sha256sum postgres.dump artifacts.tar.gz runtime.env docker-compose.yml > SHA256SUMS)
|
|
find backups -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' \
|
|
| sort -nr | awk 'NR > 5 { print $2 }' | xargs -r rm -rf
|
|
}
|
|
|
|
rollback() {
|
|
echo "New release failed readiness; restoring previous deployment" >&2
|
|
if [ ! -f .env.previous ] || [ ! -f docker-compose.previous.yml ]; then
|
|
echo "Previous deployment metadata is unavailable" >&2
|
|
return 1
|
|
fi
|
|
cp .env .env.failed
|
|
cp docker-compose.yml docker-compose.failed.yml
|
|
cp .env.previous .env
|
|
cp docker-compose.previous.yml docker-compose.yml
|
|
cache_backend="$(env_value CRANK_CACHE_BACKEND memory)"
|
|
compose_profiles=""
|
|
if [ "$cache_backend" = "valkey" ] || [ "$cache_backend" = "redis" ]; then
|
|
compose_profiles="--profile cache"
|
|
fi
|
|
compose_up
|
|
wait_for_stack health
|
|
}
|
|
|
|
compose config -q
|
|
create_backup
|
|
compose pull
|
|
|
|
if ! compose_up || ! wait_for_stack ready; then
|
|
show_failure_diagnostics
|
|
rollback
|
|
exit 1
|
|
fi
|
|
|
|
compose ps
|