#!/usr/bin/env bash set -Eeuo pipefail log() { printf '{"event":"%s","environment":"%s","stack":"%s","releaseId":"%s","commitSha":"%s"}\n' \ "$1" "${ENVIRONMENT:-unknown}" "${STACK_NAME:-unknown}" "${RELEASE_ID:-unknown}" "${COMMIT_SHA:-unknown}" } require_var() { local name="$1" if [ -z "${!name:-}" ]; then printf 'Required environment variable %s is missing\n' "$name" >&2 exit 2 fi } require_var ENVIRONMENT require_var STACK_NAME require_var RELEASE_ID require_var COMMIT_SHA require_var WEB_IMAGE require_var PUBLIC_HOST require_var TRAEFIK_NETWORK REMOTE_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}" LOCK_FILE="${RELEASE_LOCK_FILE:-/tmp/${STACK_NAME}-${ENVIRONMENT}.lock}" exec 9>"$LOCK_FILE" if ! flock -n 9; then echo "Another deployment is already running for ${STACK_NAME}/${ENVIRONMENT}" >&2 exit 75 fi cleanup_release_images() { if [ "${DEPLOY_CLEANUP_FAILED_IMAGES:-true}" != "true" ]; then return 0 fi echo "Cleaning up unused release image after failed deploy." >&2 docker image rm "$WEB_IMAGE" >/dev/null 2>&1 || true if [ -n "${COMMIT_SHA:-}" ]; then docker image prune -af --filter "label=org.opencontainers.image.revision=${COMMIT_SHA}" >&2 || true fi } cleanup_on_exit() { local status=$? trap - EXIT if [ "$status" -ne 0 ]; then cleanup_release_images fi exit "$status" } trap cleanup_on_exit EXIT docker_disk_paths() { local docker_root docker_root="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || true)" if [ -n "$docker_root" ] && [ -d "$docker_root" ]; then printf '%s\n' "$docker_root" fi if [ -d /var/lib/containerd ]; then printf '%s\n' /var/lib/containerd fi } prune_docker_before_pull() { if [ "${DEPLOY_PRUNE_BEFORE_PULL:-false}" != "true" ]; then return 0 fi local prune_until="${DEPLOY_PRUNE_UNTIL:-168h}" echo "Pruning unused Docker images older than ${prune_until} before pulling release image." >&2 docker image prune -af --filter "until=${prune_until}" >&2 || true docker builder prune -af --filter "until=${prune_until}" >&2 || true } check_docker_disk_space() { local min_mb="${DEPLOY_MIN_FREE_MB:-2048}" if [[ ! "$min_mb" =~ ^[0-9]+$ ]]; then echo "DEPLOY_MIN_FREE_MB must be an integer MiB value, got: ${min_mb}" >&2 exit 2 fi local min_kb=$((min_mb * 1024)) local failed=0 local path available_kb available_mb while IFS= read -r path; do available_kb="$(df -Pk "$path" | awk 'NR == 2 { print $4 }')" if [ -z "$available_kb" ]; then continue fi if [ "$available_kb" -lt "$min_kb" ]; then available_mb=$((available_kb / 1024)) echo "Docker storage path ${path} has ${available_mb} MiB free; need at least ${min_mb} MiB before pulling release image." >&2 failed=1 fi done < <(docker_disk_paths | awk '!seen[$0]++') if [ "$failed" -ne 0 ]; then docker system df >&2 || true exit 1 fi } wait_service() { local service="$1" local timeout="${DEPLOY_WAIT_TIMEOUT_SECONDS:-300}" local started_at started_at="$(date +%s)" while true; do local update_state update_state="$(docker service inspect --format '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{end}}' "$service" 2>/dev/null || true)" if [ "$update_state" = "rollback_started" ] || [ "$update_state" = "rollback_completed" ] || [ "$update_state" = "paused" ]; then docker service ps --no-trunc "$service" >&2 || true exit 1 fi local desired running failed desired="$(docker service inspect --format '{{if .Spec.Mode.Replicated}}{{.Spec.Mode.Replicated.Replicas}}{{else}}1{{end}}' "$service")" running="$(docker service ps "$service" --filter desired-state=running --format '{{.CurrentState}}' | grep -c '^Running' || true)" failed="$(docker service ps "$service" --no-trunc --format '{{.CurrentState}} {{.Error}}' | grep -Ec 'Rejected|Failed|No such image|unhealthy' || true)" if [ "$failed" -gt 0 ]; then docker service ps --no-trunc "$service" >&2 || true exit 1 fi if [ "$running" -ge "$desired" ]; then log "service_converged" return 0 fi if [ "$(( $(date +%s) - started_at ))" -ge "$timeout" ]; then docker service ps --no-trunc "$service" >&2 || true echo "Timed out waiting for ${service} convergence" >&2 exit 1 fi sleep 5 done } log deploy_start if [ -n "${REGISTRY:-}" ] && [ -n "${REGISTRY_USERNAME:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" --username "$REGISTRY_USERNAME" --password-stdin fi prune_docker_before_pull check_docker_disk_space docker image pull "$WEB_IMAGE" export ENVIRONMENT STACK_NAME RELEASE_ID COMMIT_SHA WEB_IMAGE PUBLIC_HOST TRAEFIK_NETWORK export REGION="${REGION:-}" TEAM_ID="${TEAM_ID:-}" WEB_REPLICAS="${WEB_REPLICAS:-1}" export WEB_PLACEMENT_CONSTRAINT="${WEB_PLACEMENT_CONSTRAINT:-node.role == manager}" export TRAEFIK_ENTRYPOINTS="${TRAEFIK_ENTRYPOINTS:-websecure}" TRAEFIK_TLS="${TRAEFIK_TLS:-true}" export VIDEOREG_DB_MEMORY_TTL_MS="${VIDEOREG_DB_MEMORY_TTL_MS:-3600000}" export VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS="${VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS:-3600}" docker stack deploy --with-registry-auth --prune -c "$REMOTE_DIR/docker-stack.yml" "$STACK_NAME" wait_service "${STACK_NAME}_web" log deploy_converged