#!/usr/bin/env bash set -euo pipefail PATH="/usr/sbin:/usr/bin:/sbin:/bin" export PATH umask 077 unset BASH_ENV ENV CDPATH GLOBIGNORE LD_PRELOAD LD_LIBRARY_PATH PYTHONPATH NODE_OPTIONS SERVICES=( hyb-oa-automatic-backup-daily.service hyb-oa-automatic-backup-watchdog.service hyb-oa-automatic-backup-drill-reminder.service hyb-oa-warch3-ops.service ) BACKUP_TIMERS=( hyb-oa-automatic-backup-daily.timer hyb-oa-automatic-backup-watchdog.timer hyb-oa-automatic-backup-drill-reminder.timer ) BACKUP_SERVICES=( hyb-oa-automatic-backup-daily.service hyb-oa-automatic-backup-watchdog.service hyb-oa-automatic-backup-drill-reminder.service ) OPS_TIMERS=( hyb-oa-warch3-ops.timer ) CONTRACT_SERVICE="hyb-oa-automatic-backup-daily.service" DROPIN_NAME="20-active-build.conf" ROOT_PREFIX="" TEST_MODE=0 fail() { local code="$1" local category="$2" echo "[deployment-contract-refresh][FAIL] category=${category}" >&2 exit "$code" } # Exit 255 is reserved for the SSH client transport layer. Do not use it for # helper failures, because callers classify 255 as root_identity_unavailable. if [[ "$(id -u)" != "0" ]]; then test_root="${HYB_CONTRACT_REFRESH_TEST_ROOT:-}" if [[ "$test_root" =~ ^/tmp/[A-Za-z0-9._/-]+$ && "$test_root" != */ \ && "$test_root" != *"//"* && "/$test_root/" != *"/../"* ]]; then TEST_MODE=1 ROOT_PREFIX="$test_root" PATH="$test_root/bin:/usr/sbin:/usr/bin:/sbin:/bin" export PATH else fail 70 root_required fi fi UNIT_ROOT="${ROOT_PREFIX}/etc/systemd/system" # Absent file = automatic backup mode (all backup timers must run). The only # accepted content is "manual_snapshot": backup timers must then be disabled # and inactive, because backups are manual Lighthouse snapshots before deploys. BACKUP_MODE_FILE="${ROOT_PREFIX}/etc/hyb-oa-backup-mode" BACKUP_MODE="" LOCK_FILE="${ROOT_PREFIX}/run/lock/hyb-oa-deployment-contract-refresh.lock" EXPECTED_TARGET="${ROOT_PREFIX}/usr/local/sbin/hyb-oa-deployment-contract-refresh" need_command() { command -v "$1" >/dev/null 2>&1 || fail 70 runtime_prerequisite_missing } for required_command in systemctl docker install mktemp mv mkdir chmod flock stat; do need_command "$required_command" done validate_self() { if [[ "$TEST_MODE" == "0" ]]; then [[ "$0" == "$EXPECTED_TARGET" && -f "$EXPECTED_TARGET" && ! -L "$EXPECTED_TARGET" \ && "$(stat -c %u:%g:%a "$EXPECTED_TARGET")" == "0:0:755" ]] \ || fail 70 refresh_program_integrity_invalid else [[ -f "$0" && ! -L "$0" ]] || fail 70 refresh_program_integrity_invalid fi } systemctl_value() { local unit="$1" local property="$2" systemctl show "$unit" -p "$property" --value } unit_environment_value() { local unit="$1" local wanted="$2" local entry value="" for entry in $(systemctl_value "$unit" Environment); do if [[ "${entry%%=*}" == "$wanted" ]]; then value="${entry#*=}" fi done [[ -n "$value" ]] || return 1 printf '%s' "$value" } validate_fixed_units() { local unit fragment owner for unit in "${SERVICES[@]}"; do [[ "$(systemctl_value "$unit" LoadState)" == "loaded" ]] \ || fail 70 service_not_loaded fragment="$(systemctl_value "$unit" FragmentPath)" [[ -n "$fragment" ]] || fail 70 service_fragment_missing if [[ "$TEST_MODE" == "0" ]]; then [[ "$fragment" == /etc/systemd/system/* && -f "$fragment" && ! -L "$fragment" ]] \ || fail 70 service_fragment_invalid owner="$(stat -c %u "$fragment")" [[ "$owner" == "0" ]] || fail 70 service_fragment_not_root_owned fi done } resolve_active_build() { local project local -a servers=() project="$(unit_environment_value "$CONTRACT_SERVICE" COMPOSE_PROJECT_NAME)" \ || fail 70 compose_project_missing [[ "$project" =~ ^[a-z0-9][a-z0-9_-]*$ ]] || fail 70 compose_project_invalid mapfile -t servers < <(docker ps \ --filter "label=com.docker.compose.project=$project" \ --filter "label=com.docker.compose.service=server" \ --format '{{.ID}}') [[ "${#servers[@]}" -eq 1 && -n "${servers[0]}" ]] \ || fail 70 active_server_not_unique ACTIVE_BUILD="$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "${servers[0]}" \ | sed -n 's/^BUILD_SHA=//p' | tail -n 1)" ACTIVE_BUILD="${ACTIVE_BUILD,,}" [[ "$ACTIVE_BUILD" =~ ^[0-9a-f]{40,64}$ ]] || fail 70 active_build_invalid ACTIVE_FRONTEND_BUILD="${ACTIVE_BUILD:0:12}" } write_build_dropin() { local service="$1" local dropin_dir="$UNIT_ROOT/${service}.d" local target="$dropin_dir/$DROPIN_NAME" local temporary if [[ "$TEST_MODE" == "0" ]]; then install -d -o root -g root -m 0755 "$dropin_dir" else mkdir -p "$dropin_dir" chmod 0755 "$dropin_dir" fi temporary="$(mktemp "$dropin_dir/.${DROPIN_NAME}.XXXXXX")" { printf '[Service]\n' printf 'Environment=BUILD_SHA=%s\n' "$ACTIVE_BUILD" printf 'Environment=VITE_BUILD_SHA=%s\n' "$ACTIVE_FRONTEND_BUILD" } >"$temporary" chmod 0644 "$temporary" if [[ "$TEST_MODE" == "0" ]]; then chown root:root "$temporary" fi mv -f -- "$temporary" "$target" } verify_builds() { local service service_build service_frontend_build for service in "${SERVICES[@]}"; do service_build="$(unit_environment_value "$service" BUILD_SHA)" \ || fail 42 unit_build_not_refreshed service_frontend_build="$(unit_environment_value "$service" VITE_BUILD_SHA)" \ || fail 42 unit_build_not_refreshed [[ "$service_build" == "$ACTIVE_BUILD" \ && "$service_frontend_build" == "$ACTIVE_FRONTEND_BUILD" ]] \ || fail 42 unit_build_not_refreshed done } resolve_backup_mode() { local content if [[ ! -e "$BACKUP_MODE_FILE" && ! -L "$BACKUP_MODE_FILE" ]]; then BACKUP_MODE="automatic" return 0 fi [[ -f "$BACKUP_MODE_FILE" && ! -L "$BACKUP_MODE_FILE" ]] \ || fail 44 backup_mode_invalid if [[ "$TEST_MODE" == "0" ]]; then [[ "$(stat -c %u:%g "$BACKUP_MODE_FILE")" == "0:0" ]] \ && [[ "$(stat -c %a "$BACKUP_MODE_FILE")" =~ ^(644|600|444|400)$ ]] \ || fail 44 backup_mode_invalid fi content="$(<"$BACKUP_MODE_FILE")" [[ "$content" == "manual_snapshot" ]] || fail 44 backup_mode_invalid BACKUP_MODE="manual_snapshot" } timer_state() { local timer="$1" printf '%s:%s' \ "$(systemctl is-enabled "$timer" 2>/dev/null || true)" \ "$(systemctl is-active "$timer" 2>/dev/null || true)" } verify_timers() { local timer for timer in "${OPS_TIMERS[@]}"; do [[ "$(timer_state "$timer")" == "enabled:active" ]] || fail 43 timer_not_active done for timer in "${BACKUP_TIMERS[@]}"; do if [[ "$BACKUP_MODE" == "manual_snapshot" ]]; then [[ "$(timer_state "$timer")" == "disabled:inactive" ]] \ || fail 45 backup_timer_not_disabled else [[ "$(timer_state "$timer")" == "enabled:active" ]] || fail 43 timer_not_active fi done [[ "$BACKUP_MODE" == "manual_snapshot" ]] || return 0 # A disabled timer does not stop an in-flight run; the switch is complete # only after every backup service has exited (a failed last run still counts). local service state for service in "${BACKUP_SERVICES[@]}"; do state="$(systemctl is-active "$service" 2>/dev/null || true)" [[ "$state" == "inactive" || "$state" == "failed" ]] \ || fail 45 backup_timer_not_disabled done } probe() { validate_self validate_fixed_units resolve_backup_mode resolve_active_build # Fail before any release write when timers disagree with the backup mode. verify_timers echo "[deployment-contract-refresh][PASS] probe" } refresh() { validate_self validate_fixed_units resolve_backup_mode resolve_active_build mkdir -p "$(dirname "$LOCK_FILE")" exec 9>"$LOCK_FILE" flock -n 9 || fail 42 refresh_already_running local service for service in "${SERVICES[@]}"; do write_build_dropin "$service" done systemctl daemon-reload verify_builds verify_timers echo "[deployment-contract-refresh][PASS] refreshed" } [[ "$#" -eq 0 ]] || fail 64 unsupported_forced_command case "${SSH_ORIGINAL_COMMAND:-}" in probe) probe ;; refresh) refresh ;; *) fail 64 unsupported_forced_command ;; esac