175 lines
5.2 KiB
Bash
Executable File
175 lines
5.2 KiB
Bash
Executable File
#!/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
|