#!/usr/bin/env bash # P0-OBS-002 - controlled deployment of the host110 ClickHouse native BACKUP disk. # # This script owns only an additive Compose override and one ClickHouse config # fragment. It never runs BACKUP/RESTORE, reads a dotenv/secret source, pulls an # image, or recreates another service. set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" LOCAL_OVERRIDE="${ROOT_DIR}/ops/signoz/docker-compose.clickhouse-backup.override.yaml" LOCAL_CONFIG="${ROOT_DIR}/ops/signoz/clickhouse/config.d/backup_disk.xml" TARGET_HOST="wooo@192.168.0.110" REMOTE_DIR="/home/wooo/signoz/deploy/docker" REMOTE_BASE="${REMOTE_DIR}/docker-compose.yaml" REMOTE_OVERRIDE="${REMOTE_DIR}/docker-compose.awoooi-clickhouse-backup.yaml" REMOTE_CONFIG="${REMOTE_DIR}/awoooi-clickhouse-backup-disk.xml" REMOTE_PROJECT="signoz" REMOTE_SERVICE="clickhouse" CLICKHOUSE_CONTAINER="signoz-clickhouse" COLLECTOR_CONTAINER="signoz-otel-collector" SIGNOZ_CONTAINER="signoz" ZOOKEEPER_CONTAINER="signoz-zookeeper-1" HOST_BACKUP_DIR="/backup/staging/signoz-clickhouse" CONTAINER_BACKUP_DIR="/backups" CLICKHOUSE_DISK="backups" CLICKHOUSE_UID="101" CLICKHOUSE_GID="101" RECEIPT_ROOT="/backup/deploy-receipts/signoz-clickhouse-backup-disk" API_URL="http://127.0.0.1:8080/api/v1/health" SAFE_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" SAFE_HOME="/home/wooo" LOCAL_KILL_AFTER_SECONDS=15 REMOTE_CHECK_TIMEOUT_SECONDS=180 REMOTE_STAGE_TIMEOUT_SECONDS=60 REMOTE_TRANSFER_TIMEOUT_SECONDS=60 REMOTE_APPLY_TIMEOUT_SECONDS=3600 REMOTE_CLEANUP_TIMEOUT_SECONDS=60 SSH_OPTS=( -o BatchMode=yes -o ConnectTimeout=8 -o ConnectionAttempts=1 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 ) usage() { cat <<'USAGE' Usage: deploy-signoz-clickhouse-backup-disk.sh [--check|--apply] Required environment: TRACE_ID Path-safe controlled-apply trace identifier RUN_ID Unique path-safe execution identifier WORK_ITEM_ID Path-safe work-item identifier --check performs local source validation and remote read-only Compose/runtime checks. --apply atomically installs the two repo-owned artifacts, recreates only the ClickHouse service with --pull never, independently verifies runtime state, and restores the exact prior managed Compose state on failure. USAGE } MODE="${1:---check}" case "${MODE}" in --check|--apply) ;; -h|--help) usage; exit 0 ;; *) usage >&2; exit 64 ;; esac [ "$#" -le 1 ] || { usage >&2; exit 64; } TRACE_ID="${TRACE_ID:-}" RUN_ID="${RUN_ID:-}" WORK_ITEM_ID="${WORK_ITEM_ID:-}" valid_identity() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$ ]] } for value in "${TRACE_ID}" "${RUN_ID}" "${WORK_ITEM_ID}"; do valid_identity "${value}" || { printf 'TRACE_ID, RUN_ID, and WORK_ITEM_ID are required and must be path-safe\n' >&2 exit 64 } done for command_name in python3 scp ssh timeout; do command -v "${command_name}" >/dev/null 2>&1 || { printf 'required local command missing: %s\n' "${command_name}" >&2 exit 69 } done hash_file() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' else shasum -a 256 "$1" | awk '{print $1}' fi } bounded_ssh() { local timeout_seconds="$1" shift timeout --signal=TERM --kill-after="${LOCAL_KILL_AFTER_SECONDS}" \ "${timeout_seconds}" ssh "${SSH_OPTS[@]}" "${TARGET_HOST}" "$@" } bounded_scp() { timeout --signal=TERM --kill-after="${LOCAL_KILL_AFTER_SECONDS}" \ "${REMOTE_TRANSFER_TIMEOUT_SECONDS}" scp -q "${SSH_OPTS[@]}" "$@" } validate_local_sources() { [ -f "${LOCAL_OVERRIDE}" ] && [ ! -L "${LOCAL_OVERRIDE}" ] [ -f "${LOCAL_CONFIG}" ] && [ ! -L "${LOCAL_CONFIG}" ] python3 - "${LOCAL_CONFIG}" <<'PY' from pathlib import Path import sys import xml.etree.ElementTree as ET path = Path(sys.argv[1]) root = ET.parse(path).getroot() assert root.tag == "clickhouse" disk = root.find("./storage_configuration/disks/backups") assert disk is not None assert disk.findtext("type") == "local" assert disk.findtext("path") == "/backups/" backups = root.find("./backups") assert backups is not None assert backups.findtext("allowed_disk") == "backups" assert backups.findtext("allowed_path") == "/backups/" assert backups.findtext("allow_concurrent_backups") == "false" assert backups.findtext("allow_concurrent_restores") == "false" PY [ "$(grep -F -c ' clickhouse:' "${LOCAL_OVERRIDE}")" -eq 1 ] [ "$(grep -F -c 'source: /backup/staging/signoz-clickhouse' "${LOCAL_OVERRIDE}")" -eq 1 ] [ "$(grep -F -c 'target: /backups' "${LOCAL_OVERRIDE}")" -eq 1 ] [ "$(grep -F -c 'target: /etc/clickhouse-server/config.d/awoooi-backup-disk.xml' "${LOCAL_OVERRIDE}")" -eq 1 ] ! grep -Eq '^[[:space:]]*(image|build|command|environment):' "${LOCAL_OVERRIDE}" } validate_local_sources OVERRIDE_SHA="$(hash_file "${LOCAL_OVERRIDE}")" CONFIG_SHA="$(hash_file "${LOCAL_CONFIG}")" local_receipt() { local phase="$1" result="$2" detail="$3" printf '{"schema":"awoooi_controlled_apply_receipt_v1","trace_id":"%s","run_id":"%s","work_item_id":"%s","asset_id":"host110-signoz-clickhouse-backup-disk","phase":"%s","result":"%s","detail":"%s"}\n' \ "${TRACE_ID}" "${RUN_ID}" "${WORK_ITEM_ID}" "${phase}" "${result}" "${detail}" } remote_candidate_compose_check() { bounded_ssh "${REMOTE_CHECK_TIMEOUT_SECONDS}" \ "timeout --signal=TERM --kill-after=15 120 env -i PATH='${SAFE_PATH}' HOME='${SAFE_HOME}' docker compose --env-file /dev/null -p '${REMOTE_PROJECT}' -f '${REMOTE_BASE}' -f - config -q >/dev/null" \ < "${LOCAL_OVERRIDE}" } remote_read_only_check() { bounded_ssh "${REMOTE_CHECK_TIMEOUT_SECONDS}" bash -s -- \ "${TRACE_ID}" "${RUN_ID}" "${WORK_ITEM_ID}" \ "${OVERRIDE_SHA}" "${CONFIG_SHA}" <<'REMOTE_CHECK' set -euo pipefail TRACE_ID="$1" RUN_ID="$2" WORK_ITEM_ID="$3" EXPECTED_OVERRIDE_SHA="$4" EXPECTED_CONFIG_SHA="$5" REMOTE_DIR="/home/wooo/signoz/deploy/docker" REMOTE_BASE="${REMOTE_DIR}/docker-compose.yaml" REMOTE_OVERRIDE="${REMOTE_DIR}/docker-compose.awoooi-clickhouse-backup.yaml" REMOTE_CONFIG="${REMOTE_DIR}/awoooi-clickhouse-backup-disk.xml" HOST_BACKUP_DIR="/backup/staging/signoz-clickhouse" SAFE_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" SAFE_HOME="/home/wooo" COMMAND_TIMEOUT_SECONDS=30 COMPOSE_TIMEOUT_SECONDS=120 KILL_AFTER_SECONDS=15 for command_name in docker timeout; do command -v "${command_name}" >/dev/null 2>&1 done docker_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMMAND_TIMEOUT_SECONDS}" docker "$@" } compose_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMPOSE_TIMEOUT_SECONDS}" env -i PATH="${SAFE_PATH}" HOME="${SAFE_HOME}" \ docker compose --env-file /dev/null -p signoz "$@" } file_hash_or_absent() { if [ -f "$1" ] && [ ! -L "$1" ]; then sha256sum "$1" | awk '{print $1}' elif [ ! -e "$1" ]; then printf 'absent\n' else printf 'unsafe\n' fi } [ -f "${REMOTE_BASE}" ] && [ ! -L "${REMOTE_BASE}" ] for container in signoz-clickhouse signoz-otel-collector signoz signoz-zookeeper-1; do docker_cmd inspect "${container}" >/dev/null done compose_cmd -f "${REMOTE_BASE}" config -q >/dev/null compose_cmd -f "${REMOTE_BASE}" config --services | grep -qx clickhouse override_hash="$(file_hash_or_absent "${REMOTE_OVERRIDE}")" config_hash="$(file_hash_or_absent "${REMOTE_CONFIG}")" override_match=false config_match=false [ "${override_hash}" = "${EXPECTED_OVERRIDE_SHA}" ] && override_match=true [ "${config_hash}" = "${EXPECTED_CONFIG_SHA}" ] && config_match=true host_dir=absent if [ -d "${HOST_BACKUP_DIR}" ] && [ ! -L "${HOST_BACKUP_DIR}" ]; then host_dir=present elif [ -e "${HOST_BACKUP_DIR}" ]; then host_dir=unsafe fi disk_count="$(docker_cmd exec signoz-clickhouse clickhouse-client --query \ "SELECT count() FROM system.disks WHERE name='backups' FORMAT TSVRaw" 2>/dev/null)" mount_count="$(docker_cmd inspect --format \ '{{range .Mounts}}{{if eq .Destination "/backups"}}1{{end}}{{end}}' \ signoz-clickhouse | tr -cd '1' | wc -c | xargs)" collector_running="$(docker_cmd inspect --format '{{.State.Running}}' signoz-otel-collector)" collector_restarts="$(docker_cmd inspect --format '{{.RestartCount}}' signoz-otel-collector)" printf '{"schema":"awoooi_controlled_apply_receipt_v1","trace_id":"%s","run_id":"%s","work_item_id":"%s","asset_id":"host110-signoz-clickhouse-backup-disk","phase":"source_of_truth_diff","result":"pass","detail":"override_match_%s_config_match_%s_host_dir_%s_live_disk_count_%s_live_mount_count_%s_collector_running_%s_collector_restarts_%s"}\n' \ "${TRACE_ID}" "${RUN_ID}" "${WORK_ITEM_ID}" "${override_match}" \ "${config_match}" "${host_dir}" "${disk_count}" "${mount_count}" \ "${collector_running}" "${collector_restarts}" REMOTE_CHECK } local_receipt sensor_source pass "override_sha_${OVERRIDE_SHA}_config_sha_${CONFIG_SHA}" local_receipt normalized_asset_identity pass "host_110_project_signoz_service_clickhouse_disk_backups" remote_candidate_compose_check remote_read_only_check local_receipt check pass "compose_config_q_no_output_no_write" if [ "${MODE}" = "--check" ]; then local_receipt terminal check_pass_no_write "source_and_remote_preflight_valid" exit 0 fi STAGE_DIR="/tmp/awoooi-signoz-clickhouse-backup.${RUN_ID}" STAGE_CREATED=0 LOCAL_APPLY_VERIFIED=0 cleanup_stage() { local cleanup_rc [ "${STAGE_CREATED}" -eq 1 ] || return 0 bounded_ssh "${REMOTE_CLEANUP_TIMEOUT_SECONDS}" bash -s -- "${STAGE_DIR}" <<'REMOTE_CLEANUP' >/dev/null 2>&1 set -euo pipefail stage_dir="$1" if [ ! -e "${stage_dir}" ]; then exit 0 fi [ -d "${stage_dir}" ] && [ ! -L "${stage_dir}" ] rm -f "${stage_dir}/override.yaml" "${stage_dir}/backup_disk.xml" rmdir "${stage_dir}" [ ! -e "${stage_dir}" ] REMOTE_CLEANUP cleanup_rc=$? [ "${cleanup_rc}" -eq 0 ] || return "${cleanup_rc}" STAGE_CREATED=0 } on_local_exit() { local rc=$? local cleanup_rc=0 cleanup_terminal=not_created terminal=failed trap - EXIT HUP INT TERM set +e if [ "${STAGE_CREATED}" -eq 1 ]; then cleanup_stage cleanup_rc=$? if [ "${cleanup_rc}" -eq 0 ]; then cleanup_terminal=pass_absence_verified else cleanup_terminal=failed_or_residue_present rc=92 fi fi local_receipt cleanup "$([ "${cleanup_rc}" -eq 0 ] && printf pass || printf failed)" \ "remote_stage_${cleanup_terminal}" if [ "${rc}" -eq 0 ] && [ "${LOCAL_APPLY_VERIFIED}" -eq 1 ]; then terminal=pass fi local_receipt terminal "${terminal}" \ "remote_controlled_apply_${terminal}_stage_${cleanup_terminal}_exit_${rc}" exit "${rc}" } trap on_local_exit EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM bounded_ssh "${REMOTE_STAGE_TIMEOUT_SECONDS}" bash -s -- "${STAGE_DIR}" <<'REMOTE_STAGE' set -euo pipefail stage_dir="$1" [ ! -e "${stage_dir}" ] umask 077 mkdir -m 0700 "${stage_dir}" REMOTE_STAGE STAGE_CREATED=1 bounded_scp "${LOCAL_OVERRIDE}" \ "${TARGET_HOST}:${STAGE_DIR}/override.yaml" bounded_scp "${LOCAL_CONFIG}" \ "${TARGET_HOST}:${STAGE_DIR}/backup_disk.xml" bounded_ssh "${REMOTE_APPLY_TIMEOUT_SECONDS}" bash -s -- \ "${TRACE_ID}" "${RUN_ID}" "${WORK_ITEM_ID}" \ "${OVERRIDE_SHA}" "${CONFIG_SHA}" "${STAGE_DIR}" <<'REMOTE_APPLY' set -euo pipefail TRACE_ID="$1" RUN_ID="$2" WORK_ITEM_ID="$3" EXPECTED_OVERRIDE_SHA="$4" EXPECTED_CONFIG_SHA="$5" STAGE_DIR="$6" REMOTE_DIR="/home/wooo/signoz/deploy/docker" REMOTE_BASE="${REMOTE_DIR}/docker-compose.yaml" REMOTE_OVERRIDE="${REMOTE_DIR}/docker-compose.awoooi-clickhouse-backup.yaml" REMOTE_CONFIG="${REMOTE_DIR}/awoooi-clickhouse-backup-disk.xml" STAGE_OVERRIDE="${STAGE_DIR}/override.yaml" STAGE_CONFIG="${STAGE_DIR}/backup_disk.xml" REMOTE_PROJECT="signoz" REMOTE_SERVICE="clickhouse" CLICKHOUSE_CONTAINER="signoz-clickhouse" COLLECTOR_CONTAINER="signoz-otel-collector" SIGNOZ_CONTAINER="signoz" ZOOKEEPER_CONTAINER="signoz-zookeeper-1" HOST_BACKUP_DIR="/backup/staging/signoz-clickhouse" RECEIPT_ROOT="/backup/deploy-receipts/signoz-clickhouse-backup-disk" RECEIPT_DIR="${RECEIPT_ROOT}/${RUN_ID}" RECEIPT_LOG="${RECEIPT_DIR}/receipts.jsonl" API_URL="http://127.0.0.1:8080/api/v1/health" SAFE_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" SAFE_HOME="/home/wooo" LOCK_FILE="/tmp/awoooi-signoz-clickhouse-backup-disk.lock" BACKUP_OPERATION_LOCK_FILE="/tmp/awoooi-signoz-backup-operation.lock" CANARY_LOCK_FILE="/tmp/awoooi-signoz-backup-canary.lock" COMMAND_TIMEOUT_SECONDS=30 COMPOSE_CONFIG_TIMEOUT_SECONDS=120 COMPOSE_APPLY_TIMEOUT_SECONDS=300 KILL_AFTER_SECONDS=15 APPLY_STARTED=0 DEPLOY_VERIFIED=0 HOST_DIR_CREATED=0 PRIOR_OVERRIDE_PRESENT=0 PRIOR_CONFIG_PRESENT=0 PRIOR_OVERRIDE_HASH="absent" PRIOR_OVERRIDE_MODE="absent" PRIOR_OVERRIDE_UID="absent" PRIOR_OVERRIDE_GID="absent" PRIOR_CONFIG_HASH="absent" PRIOR_CONFIG_MODE="absent" PRIOR_CONFIG_UID="absent" PRIOR_CONFIG_GID="absent" PRIOR_DISK_COUNT=0 BEFORE_IMAGE_ID="" BEFORE_DATA_VOLUME="" BEFORE_COLLECTOR_ID="" BEFORE_COLLECTOR_RESTARTS="" BEFORE_SIGNOZ_ID="" BEFORE_SIGNOZ_RESTARTS="" BEFORE_ZOOKEEPER_ID="" BEFORE_ZOOKEEPER_RESTARTS="" receipt() { local phase="$1" result="$2" detail="$3" local line line="$(printf '{"schema":"awoooi_controlled_apply_receipt_v1","trace_id":"%s","run_id":"%s","work_item_id":"%s","asset_id":"host110-signoz-clickhouse-backup-disk","phase":"%s","result":"%s","detail":"%s"}' \ "${TRACE_ID}" "${RUN_ID}" "${WORK_ITEM_ID}" "${phase}" "${result}" "${detail}")" printf '%s\n' "${line}" printf '%s\n' "${line}" >> "${RECEIPT_LOG}" } docker_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMMAND_TIMEOUT_SECONDS}" docker "$@" } bounded_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMMAND_TIMEOUT_SECONDS}" "$@" } compose_config_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMPOSE_CONFIG_TIMEOUT_SECONDS}" env -i PATH="${SAFE_PATH}" HOME="${SAFE_HOME}" \ docker compose --env-file /dev/null -p "${REMOTE_PROJECT}" "$@" } compose_apply_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMPOSE_APPLY_TIMEOUT_SECONDS}" env -i PATH="${SAFE_PATH}" HOME="${SAFE_HOME}" \ docker compose --env-file /dev/null -p "${REMOTE_PROJECT}" "$@" } pgrep_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMMAND_TIMEOUT_SECONDS}" pgrep "$@" } ss_cmd() { timeout --signal=TERM --kill-after="${KILL_AFTER_SECONDS}" \ "${COMMAND_TIMEOUT_SECONDS}" ss "$@" } compose_base_config() { compose_config_cmd -f "${REMOTE_BASE}" config -q >/dev/null 2>&1 } compose_managed_config() { compose_config_cmd -f "${REMOTE_BASE}" -f "${REMOTE_OVERRIDE}" \ config -q >/dev/null 2>&1 } compose_stage_config() { compose_config_cmd -f "${REMOTE_BASE}" -f "${STAGE_OVERRIDE}" \ config -q >/dev/null 2>&1 } recreate_managed() { compose_apply_cmd -f "${REMOTE_BASE}" -f "${REMOTE_OVERRIDE}" \ up -d --no-deps --pull never --force-recreate "${REMOTE_SERVICE}" \ >/dev/null 2>&1 } recreate_base() { compose_apply_cmd -f "${REMOTE_BASE}" \ up -d --no-deps --pull never --force-recreate "${REMOTE_SERVICE}" \ >/dev/null 2>&1 } wait_clickhouse() { local attempt running health for attempt in $(seq 1 36); do running="$(docker_cmd inspect --format '{{.State.Running}}' "${CLICKHOUSE_CONTAINER}" 2>/dev/null || true)" health="$(docker_cmd inspect --format '{{if (index .State "Health")}}{{(index .State "Health").Status}}{{else}}not_configured{{end}}' \ "${CLICKHOUSE_CONTAINER}" 2>/dev/null || true)" if [ "${running}" = true ] && [ "${health}" = healthy ] \ && docker_cmd exec "${CLICKHOUSE_CONTAINER}" \ clickhouse-client --query 'SELECT 1' >/dev/null 2>&1; then return 0 fi sleep 5 done return 1 } listener_up() { local port="$1" ss_cmd -H -lnt | awk -v port="${port}" '$4 ~ (":" port "$") { found = 1 } END { exit(found ? 0 : 1) }' } wait_api_and_listeners() { local attempt api_code for attempt in $(seq 1 30); do api_code="$(curl -sS --max-time 5 -o /dev/null -w '%{http_code}' "${API_URL}" 2>/dev/null || true)" if [ "${api_code}" = 200 ] && listener_up 4317 && listener_up 4318 && listener_up 8080; then return 0 fi sleep 5 done return 1 } file_matches_metadata() { local path="$1" expected_hash="$2" expected_mode="$3" expected_uid="$4" expected_gid="$5" [ -f "${path}" ] && [ ! -L "${path}" ] \ && [ "$(sha256sum "${path}" | awk '{print $1}')" = "${expected_hash}" ] \ && [ "$(stat -c '%a' "${path}")" = "${expected_mode}" ] \ && [ "$(stat -c '%u' "${path}")" = "${expected_uid}" ] \ && [ "$(stat -c '%g' "${path}")" = "${expected_gid}" ] } restore_exact_file() { local source="$1" target="$2" expected_hash="$3" expected_mode="$4" local expected_uid="$5" expected_gid="$6" tmp="$7" local current_uid current_gid bounded_cmd install -m "${expected_mode}" "${source}" "${tmp}" || return 1 current_uid="$(stat -c '%u' "${tmp}")" || return 1 current_gid="$(stat -c '%g' "${tmp}")" || return 1 if [ "${current_uid}" != "${expected_uid}" ] || [ "${current_gid}" != "${expected_gid}" ]; then bounded_cmd sudo -n chown "${expected_uid}:${expected_gid}" "${tmp}" || return 1 fi bounded_cmd mv -f "${tmp}" "${target}" || return 1 file_matches_metadata "${target}" "${expected_hash}" "${expected_mode}" \ "${expected_uid}" "${expected_gid}" } restore_managed_files() { local tmp if [ "${PRIOR_OVERRIDE_PRESENT}" -eq 1 ]; then tmp="${REMOTE_OVERRIDE}.rollback.${RUN_ID}" restore_exact_file "${RECEIPT_DIR}/prior-override.yaml" "${REMOTE_OVERRIDE}" \ "${PRIOR_OVERRIDE_HASH}" "${PRIOR_OVERRIDE_MODE}" \ "${PRIOR_OVERRIDE_UID}" "${PRIOR_OVERRIDE_GID}" "${tmp}" || return 1 else bounded_cmd rm -f "${REMOTE_OVERRIDE}" || return 1 [ ! -e "${REMOTE_OVERRIDE}" ] || return 1 fi if [ "${PRIOR_CONFIG_PRESENT}" -eq 1 ]; then tmp="${REMOTE_CONFIG}.rollback.${RUN_ID}" restore_exact_file "${RECEIPT_DIR}/prior-backup-disk.xml" "${REMOTE_CONFIG}" \ "${PRIOR_CONFIG_HASH}" "${PRIOR_CONFIG_MODE}" \ "${PRIOR_CONFIG_UID}" "${PRIOR_CONFIG_GID}" "${tmp}" || return 1 else bounded_cmd rm -f "${REMOTE_CONFIG}" || return 1 [ ! -e "${REMOTE_CONFIG}" ] || return 1 fi } cleanup_owned_residue() { local path for path in \ "${REMOTE_OVERRIDE}.candidate.${RUN_ID}" \ "${REMOTE_CONFIG}.candidate.${RUN_ID}" \ "${REMOTE_OVERRIDE}.rollback.${RUN_ID}" \ "${REMOTE_CONFIG}.rollback.${RUN_ID}"; do bounded_cmd rm -f "${path}" || return 1 [ ! -e "${path}" ] || return 1 done if [ -e "${STAGE_DIR}" ]; then [ -d "${STAGE_DIR}" ] && [ ! -L "${STAGE_DIR}" ] || return 1 bounded_cmd rm -f "${STAGE_OVERRIDE}" "${STAGE_CONFIG}" || return 1 bounded_cmd rmdir "${STAGE_DIR}" || return 1 fi [ ! -e "${STAGE_DIR}" ] || return 1 } rollback_deploy() { local disk_count rollback_dir_terminal=retained restore_managed_files || return 1 if [ "${PRIOR_OVERRIDE_PRESENT}" -eq 1 ]; then compose_managed_config || return 1 recreate_managed || return 1 else compose_base_config || return 1 recreate_base || return 1 fi wait_clickhouse || return 1 wait_api_and_listeners || return 1 disk_count="$(docker_cmd exec "${CLICKHOUSE_CONTAINER}" clickhouse-client --query \ "SELECT count() FROM system.disks WHERE name='backups' FORMAT TSVRaw" 2>/dev/null)" || return 1 [ "${disk_count}" = "${PRIOR_DISK_COUNT}" ] || return 1 [ "$(docker_cmd inspect --format '{{.Image}}' "${CLICKHOUSE_CONTAINER}")" = "${BEFORE_IMAGE_ID}" ] || return 1 [ "$(docker_cmd inspect --format '{{range .Mounts}}{{if eq .Destination "/var/lib/clickhouse"}}{{.Name}}{{end}}{{end}}' "${CLICKHOUSE_CONTAINER}")" = "${BEFORE_DATA_VOLUME}" ] || return 1 [ "$(docker_cmd inspect --format '{{.Id}}' "${COLLECTOR_CONTAINER}")" = "${BEFORE_COLLECTOR_ID}" ] || return 1 [ "$(docker_cmd inspect --format '{{.RestartCount}}' "${COLLECTOR_CONTAINER}")" = "${BEFORE_COLLECTOR_RESTARTS}" ] || return 1 if [ "${HOST_DIR_CREATED}" -eq 1 ]; then if bounded_cmd sudo -n rmdir "${HOST_BACKUP_DIR}" 2>/dev/null; then rollback_dir_terminal=removed_empty else rollback_dir_terminal=retained_nonempty_no_delete fi fi receipt rollback pass \ "prior_compose_state_restored_exact_hash_mode_uid_gid_disk_count_${disk_count}_host_dir_${rollback_dir_terminal}" } on_exit() { local rc=$? rollback_rc=0 cleanup_rc=0 terminal=failed trap - EXIT HUP INT TERM set +e if [ "${rc}" -ne 0 ] && [ "${APPLY_STARTED}" -eq 1 ] && [ "${DEPLOY_VERIFIED}" -eq 0 ]; then rollback_deploy rollback_rc=$? if [ "${rollback_rc}" -ne 0 ]; then receipt rollback failed "prior_compose_state_restore_failed" rc=91 fi fi cleanup_owned_residue cleanup_rc=$? if [ "${cleanup_rc}" -ne 0 ]; then receipt cleanup failed "exact_stage_candidate_or_rollback_residue_cleanup_failed" rc=92 else receipt cleanup pass "exact_stage_candidate_and_rollback_residue_absence_verified" fi if [ "${rc}" -eq 0 ] && [ "${DEPLOY_VERIFIED}" -eq 1 ] && [ "${cleanup_rc}" -eq 0 ]; then terminal=pass fi receipt terminal "${terminal}" "exit_${rc}_receipt_ref_${RECEIPT_LOG}" exit "${rc}" } mkdir -p "${RECEIPT_ROOT}" [ ! -e "${RECEIPT_DIR}" ] mkdir -m 0700 "${RECEIPT_DIR}" : > "${RECEIPT_LOG}" trap on_exit EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM for lock_path in "${LOCK_FILE}" "${BACKUP_OPERATION_LOCK_FILE}" "${CANARY_LOCK_FILE}"; do [ ! -L "${lock_path}" ] \ || { receipt check failed operation_lock_symlink_unsafe; exit 75; } if [ -e "${lock_path}" ]; then [ -f "${lock_path}" ] \ || { receipt check failed operation_lock_not_regular; exit 75; } fi done exec 9>>"${LOCK_FILE}" flock -n 9 || { receipt check failed another_deploy_holds_lock; exit 75; } exec 7>>"${CANARY_LOCK_FILE}" flock -n 7 || { receipt check failed backup_canary_lock_held; exit 75; } exec 8>>"${BACKUP_OPERATION_LOCK_FILE}" flock -n 8 || { receipt check failed backup_operation_lock_held; exit 75; } for command_name in awk curl docker flock install mv pgrep python3 seq sha256sum ss stat sudo timeout; do command -v "${command_name}" >/dev/null 2>&1 \ || { receipt check failed "required_command_missing_${command_name}"; exit 69; } done [ -d "${STAGE_DIR}" ] && [ ! -L "${STAGE_DIR}" ] [ -f "${STAGE_OVERRIDE}" ] && [ ! -L "${STAGE_OVERRIDE}" ] [ -f "${STAGE_CONFIG}" ] && [ ! -L "${STAGE_CONFIG}" ] [ "$(sha256sum "${STAGE_OVERRIDE}" | awk '{print $1}')" = "${EXPECTED_OVERRIDE_SHA}" ] [ "$(sha256sum "${STAGE_CONFIG}" | awk '{print $1}')" = "${EXPECTED_CONFIG_SHA}" ] [ -f "${REMOTE_BASE}" ] && [ ! -L "${REMOTE_BASE}" ] [ -d "${REMOTE_DIR}" ] && [ ! -L "${REMOTE_DIR}" ] && [ -w "${REMOTE_DIR}" ] [ -d /backup ] && [ ! -L /backup ] compose_base_config compose_stage_config compose_config_cmd -f "${REMOTE_BASE}" -f "${STAGE_OVERRIDE}" config --services \ | grep -qx "${REMOTE_SERVICE}" require_backup_idle() { local phase="$1" active_native_backups process_rc set +e pgrep_cmd -af '(^|/|[[:space:]])(backup-signoz|clickhouse-native-backup|clickhouse-native-restore-drill|run-signoz-backup-canary)[.]sh([[:space:]]|$)' \ >/dev/null 2>&1 process_rc=$? set -e case "${process_rc}" in 0) receipt check failed "active_signoz_backup_or_restore_process_detected_${phase}" return 75 ;; 1) ;; *) receipt check failed "signoz_backup_process_probe_failed_${phase}_exit_${process_rc}" return 69 ;; esac active_native_backups="$(docker_cmd exec "${CLICKHOUSE_CONTAINER}" clickhouse-client --query \ "SELECT count() FROM system.backups WHERE status IN ('CREATING_BACKUP','RESTORING') FORMAT TSVRaw" 2>/dev/null)" \ || return 1 [ "${active_native_backups}" = 0 ] \ || { receipt check failed "active_native_backup_or_restore_${phase}"; return 75; } receipt check pass "backup_idle_${phase}_shared_operation_and_canary_locks_held" } require_backup_idle preflight for container in "${CLICKHOUSE_CONTAINER}" "${COLLECTOR_CONTAINER}" "${SIGNOZ_CONTAINER}" "${ZOOKEEPER_CONTAINER}"; do [ "$(docker_cmd inspect --format '{{.State.Running}}' "${container}")" = true ] done wait_clickhouse wait_api_and_listeners if [ -e "${REMOTE_OVERRIDE}" ]; then [ -f "${REMOTE_OVERRIDE}" ] && [ ! -L "${REMOTE_OVERRIDE}" ] PRIOR_OVERRIDE_PRESENT=1 fi if [ -e "${REMOTE_CONFIG}" ]; then [ -f "${REMOTE_CONFIG}" ] && [ ! -L "${REMOTE_CONFIG}" ] PRIOR_CONFIG_PRESENT=1 fi [ "${PRIOR_OVERRIDE_PRESENT}" -eq "${PRIOR_CONFIG_PRESENT}" ] \ || { receipt check failed partial_managed_file_drift; exit 78; } if [ "${PRIOR_OVERRIDE_PRESENT}" -eq 1 ]; then compose_managed_config PRIOR_OVERRIDE_HASH="$(sha256sum "${REMOTE_OVERRIDE}" | awk '{print $1}')" PRIOR_OVERRIDE_MODE="$(stat -c '%a' "${REMOTE_OVERRIDE}")" PRIOR_OVERRIDE_UID="$(stat -c '%u' "${REMOTE_OVERRIDE}")" PRIOR_OVERRIDE_GID="$(stat -c '%g' "${REMOTE_OVERRIDE}")" PRIOR_CONFIG_HASH="$(sha256sum "${REMOTE_CONFIG}" | awk '{print $1}')" PRIOR_CONFIG_MODE="$(stat -c '%a' "${REMOTE_CONFIG}")" PRIOR_CONFIG_UID="$(stat -c '%u' "${REMOTE_CONFIG}")" PRIOR_CONFIG_GID="$(stat -c '%g' "${REMOTE_CONFIG}")" bounded_cmd cp -p "${REMOTE_OVERRIDE}" "${RECEIPT_DIR}/prior-override.yaml" bounded_cmd cp -p "${REMOTE_CONFIG}" "${RECEIPT_DIR}/prior-backup-disk.xml" [ "$(sha256sum "${RECEIPT_DIR}/prior-override.yaml" | awk '{print $1}')" = "${PRIOR_OVERRIDE_HASH}" ] [ "$(sha256sum "${RECEIPT_DIR}/prior-backup-disk.xml" | awk '{print $1}')" = "${PRIOR_CONFIG_HASH}" ] fi PRIOR_DISK_COUNT="$(docker_cmd exec "${CLICKHOUSE_CONTAINER}" clickhouse-client --query \ "SELECT count() FROM system.disks WHERE name='backups' FORMAT TSVRaw" 2>/dev/null)" BEFORE_IMAGE_ID="$(docker_cmd inspect --format '{{.Image}}' "${CLICKHOUSE_CONTAINER}")" BEFORE_DATA_VOLUME="$(docker_cmd inspect --format '{{range .Mounts}}{{if eq .Destination "/var/lib/clickhouse"}}{{.Name}}{{end}}{{end}}' "${CLICKHOUSE_CONTAINER}")" [ -n "${BEFORE_DATA_VOLUME}" ] BEFORE_COLLECTOR_ID="$(docker_cmd inspect --format '{{.Id}}' "${COLLECTOR_CONTAINER}")" BEFORE_COLLECTOR_RESTARTS="$(docker_cmd inspect --format '{{.RestartCount}}' "${COLLECTOR_CONTAINER}")" BEFORE_SIGNOZ_ID="$(docker_cmd inspect --format '{{.Id}}' "${SIGNOZ_CONTAINER}")" BEFORE_SIGNOZ_RESTARTS="$(docker_cmd inspect --format '{{.RestartCount}}' "${SIGNOZ_CONTAINER}")" BEFORE_ZOOKEEPER_ID="$(docker_cmd inspect --format '{{.Id}}' "${ZOOKEEPER_CONTAINER}")" BEFORE_ZOOKEEPER_RESTARTS="$(docker_cmd inspect --format '{{.RestartCount}}' "${ZOOKEEPER_CONTAINER}")" receipt sensor_source pass "override_sha_${EXPECTED_OVERRIDE_SHA}_config_sha_${EXPECTED_CONFIG_SHA}_base_compose_valid" receipt normalized_asset_identity pass "host_110_project_signoz_service_clickhouse_data_volume_${BEFORE_DATA_VOLUME}" receipt source_of_truth_diff pass \ "prior_override_${PRIOR_OVERRIDE_PRESENT}_hash_${PRIOR_OVERRIDE_HASH}_mode_${PRIOR_OVERRIDE_MODE}_uid_${PRIOR_OVERRIDE_UID}_gid_${PRIOR_OVERRIDE_GID}_prior_config_${PRIOR_CONFIG_PRESENT}_hash_${PRIOR_CONFIG_HASH}_mode_${PRIOR_CONFIG_MODE}_uid_${PRIOR_CONFIG_UID}_gid_${PRIOR_CONFIG_GID}_prior_disk_count_${PRIOR_DISK_COUNT}" receipt ai_decision candidate "install_dedicated_backups_disk_recreate_clickhouse_only" receipt risk_policy pass "risk_high_canary_single_service_no_pull_bounded_timeout_rollback" receipt check pass \ "compose_config_q_stage_hashes_runtime_preflight_dual_locks_backup_idle" require_backup_idle pre_mutation APPLY_STARTED=1 if [ -e "${HOST_BACKUP_DIR}" ]; then [ -d "${HOST_BACKUP_DIR}" ] && [ ! -L "${HOST_BACKUP_DIR}" ] [ "$(stat -c '%u:%g:%a' "${HOST_BACKUP_DIR}")" = "101:101:750" ] else bounded_cmd sudo -n install -d -o 101 -g 101 -m 0750 "${HOST_BACKUP_DIR}" HOST_DIR_CREATED=1 fi bounded_cmd install -m 0644 "${STAGE_OVERRIDE}" "${REMOTE_OVERRIDE}.candidate.${RUN_ID}" bounded_cmd install -m 0644 "${STAGE_CONFIG}" "${REMOTE_CONFIG}.candidate.${RUN_ID}" bounded_cmd mv -f "${REMOTE_OVERRIDE}.candidate.${RUN_ID}" "${REMOTE_OVERRIDE}" bounded_cmd mv -f "${REMOTE_CONFIG}.candidate.${RUN_ID}" "${REMOTE_CONFIG}" [ "$(sha256sum "${REMOTE_OVERRIDE}" | awk '{print $1}')" = "${EXPECTED_OVERRIDE_SHA}" ] [ "$(sha256sum "${REMOTE_CONFIG}" | awk '{print $1}')" = "${EXPECTED_CONFIG_SHA}" ] compose_managed_config recreate_managed wait_clickhouse AFTER_IMAGE_ID="$(docker_cmd inspect --format '{{.Image}}' "${CLICKHOUSE_CONTAINER}")" AFTER_DATA_VOLUME="$(docker_cmd inspect --format '{{range .Mounts}}{{if eq .Destination "/var/lib/clickhouse"}}{{.Name}}{{end}}{{end}}' "${CLICKHOUSE_CONTAINER}")" [ "${AFTER_IMAGE_ID}" = "${BEFORE_IMAGE_ID}" ] [ "${AFTER_DATA_VOLUME}" = "${BEFORE_DATA_VOLUME}" ] backup_mount="$(docker_cmd inspect --format '{{range .Mounts}}{{if eq .Destination "/backups"}}{{.Source}}|{{.RW}}{{end}}{{end}}' "${CLICKHOUSE_CONTAINER}")" config_mount="$(docker_cmd inspect --format '{{range .Mounts}}{{if eq .Destination "/etc/clickhouse-server/config.d/awoooi-backup-disk.xml"}}{{.RW}}{{end}}{{end}}' "${CLICKHOUSE_CONTAINER}")" [ "${backup_mount}" = "${HOST_BACKUP_DIR}|true" ] [ "${config_mount}" = false ] [ "$(stat -c '%u:%g:%a' "${HOST_BACKUP_DIR}")" = "101:101:750" ] server_uid="$(docker_cmd exec "${CLICKHOUSE_CONTAINER}" \ awk '/^Uid:/{print $2}' /proc/1/status 2>/dev/null)" server_gid="$(docker_cmd exec "${CLICKHOUSE_CONTAINER}" \ awk '/^Gid:/{print $2}' /proc/1/status 2>/dev/null)" [ "${server_uid}" = 101 ] [ "${server_gid}" = 101 ] docker_cmd exec -u 101:101 "${CLICKHOUSE_CONTAINER}" sh -c \ 'test -d /backups && test -w /backups' disk_state="$(docker_cmd exec "${CLICKHOUSE_CONTAINER}" clickhouse-client --query \ "SELECT concat(type,'|',toString(is_read_only),'|',toString(is_remote),'|',toString(is_broken),'|',toString(free_space>0)) FROM system.disks WHERE name='backups' FORMAT TSVRaw" 2>/dev/null)" [ "${disk_state}" = "Local|0|0|0|1" ] [ "$(docker_cmd exec "${CLICKHOUSE_CONTAINER}" clickhouse-client --query \ "EXISTS TABLE system.backups FORMAT TSVRaw" 2>/dev/null)" = 1 ] docker_cmd exec "${CLICKHOUSE_CONTAINER}" clickhouse-client --query 'SHOW GRANTS' 2>/dev/null \ | grep -Eqi '(^|[,[:space:]])BACKUP([,[:space:]]|$)' [ "$(docker_cmd inspect --format '{{.Id}}' "${COLLECTOR_CONTAINER}")" = "${BEFORE_COLLECTOR_ID}" ] [ "$(docker_cmd inspect --format '{{.RestartCount}}' "${COLLECTOR_CONTAINER}")" = "${BEFORE_COLLECTOR_RESTARTS}" ] [ "$(docker_cmd inspect --format '{{.Id}}' "${SIGNOZ_CONTAINER}")" = "${BEFORE_SIGNOZ_ID}" ] [ "$(docker_cmd inspect --format '{{.RestartCount}}' "${SIGNOZ_CONTAINER}")" = "${BEFORE_SIGNOZ_RESTARTS}" ] [ "$(docker_cmd inspect --format '{{.Id}}' "${ZOOKEEPER_CONTAINER}")" = "${BEFORE_ZOOKEEPER_ID}" ] [ "$(docker_cmd inspect --format '{{.RestartCount}}' "${ZOOKEEPER_CONTAINER}")" = "${BEFORE_ZOOKEEPER_RESTARTS}" ] wait_api_and_listeners receipt execution pass "clickhouse_recreated_no_pull_image_and_data_volume_unchanged" receipt post_verifier pass "disk_backups_local_rw_healthy_server_uid101_write_access_concurrency_disabled_collector_unchanged_listeners_4317_4318_8080_api_200" receipt closure_writeback pass "durable_receipt_ack_no_backup_or_restore_executed" DEPLOY_VERIFIED=1 REMOTE_APPLY LOCAL_APPLY_VERIFIED=1