#!/usr/bin/env bash set -uo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" DEPLOY_SERVER="${DEPLOY_SERVER:-}" DEPLOY_REFRESH_SERVER="${DEPLOY_REFRESH_SERVER:-}" DEPLOY_CONTRACT_UNIT="${DEPLOY_CONTRACT_UNIT:-hyb-oa-automatic-backup-daily.service}" # shellcheck source=deployment-contract-lib.sh source "$ROOT_DIR/deploy/trialrun/deployment-contract-lib.sh" CHECK_NAMES=( deploy_ssh_identity contract_six_keys repository_root repository_writable env_readable data_directories runtime_unique runtime_build storage_mount pgdata_mount backup_build root_ssh_identity refresh_program ) declare -A CHECK_RESULTS=() declare -A CONTRACT=() for check_name in "${CHECK_NAMES[@]}"; do CHECK_RESULTS["$check_name"]="FAIL" done if deployment_contract_safe_ssh_target "$DEPLOY_SERVER" \ && ssh "$DEPLOY_SERVER" true >/dev/null 2>&1; then CHECK_RESULTS[deploy_ssh_identity]="PASS" fi if deployment_contract_load "$DEPLOY_SERVER" "$DEPLOY_CONTRACT_UNIT" CONTRACT; then CHECK_RESULTS[contract_six_keys]="PASS" remote_results="$(ssh "$DEPLOY_SERVER" \ "REMOTE_DIR='${CONTRACT[REMOTE_DIR]}' REMOTE_ENV_FILE='${CONTRACT[REMOTE_ENV_FILE]}' COMPOSE_FILE='${CONTRACT[COMPOSE_FILE]}' COMPOSE_PROJECT_NAME='${CONTRACT[COMPOSE_PROJECT_NAME]}' POSTGRES_DATA_DIR='${CONTRACT[POSTGRES_DATA_DIR]}' STORAGE_HOST_DIR='${CONTRACT[STORAGE_HOST_DIR]}' CONTRACT_UNIT='${DEPLOY_CONTRACT_UNIT}' bash -s" 2>/dev/null <<'REMOTE' set -uo pipefail emit() { local name="$1" shift if ("$@") >/dev/null 2>&1; then printf '%s=PASS\n' "$name" else printf '%s=FAIL\n' "$name" fi } repository_root() { [[ -d "$REMOTE_DIR" && -f "$REMOTE_DIR/server/package.json" \ && -f "$REMOTE_DIR/$COMPOSE_FILE" ]] } repository_writable() { [[ -w "$REMOTE_DIR" ]]; } env_readable() { [[ -f "$REMOTE_ENV_FILE" && ! -L "$REMOTE_ENV_FILE" && -r "$REMOTE_ENV_FILE" ]]; } data_directories() { [[ -f "$POSTGRES_DATA_DIR/PG_VERSION" && -d "$STORAGE_HOST_DIR" ]]; } container_ids() { local service="$1" docker ps \ --filter "label=com.docker.compose.project=$COMPOSE_PROJECT_NAME" \ --filter "label=com.docker.compose.service=$service" \ --format '{{.ID}}' } container_env() { local container="$1" local wanted="$2" docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$container" \ | sed -n "s/^${wanted}=//p" | tail -n 1 } mount_source() { local container="$1" local destination="$2" docker inspect -f '{{json .Mounts}}' "$container" | python3 -c ' import json, sys destination = sys.argv[1] mounts = json.load(sys.stdin) matches = [item.get("Source", "") for item in mounts if item.get("Destination") == destination] if len(matches) != 1 or not matches[0]: raise SystemExit(1) print(matches[0]) ' "$destination" } mapfile -t servers < <(container_ids server 2>/dev/null || true) mapfile -t dbs < <(container_ids db 2>/dev/null || true) server_id="${servers[0]:-}" db_id="${dbs[0]:-}" runtime_unique_ok=0 if [[ "${#servers[@]}" -eq 1 && "${#dbs[@]}" -eq 1 \ && -n "$server_id" && -n "$db_id" ]]; then runtime_unique_ok=1 fi runtime_unique() { [[ "$runtime_unique_ok" == 1 ]]; } runtime_build() { [[ "$runtime_unique_ok" == 1 ]] || return 1 active_build="$(container_env "$server_id" BUILD_SHA)" [[ "$active_build" =~ ^[0-9a-fA-F]{40,64}$ ]] } storage_mount() { [[ "$runtime_unique_ok" == 1 ]] || return 1 storage_destination="$(container_env "$server_id" STORAGE_DIR)" [[ "$storage_destination" == /* ]] || return 1 [[ "$(mount_source "$server_id" "$storage_destination")" == "$STORAGE_HOST_DIR" ]] } pgdata_mount() { [[ "$runtime_unique_ok" == 1 ]] || return 1 [[ "$(mount_source "$db_id" /var/lib/postgresql/data)" == "$POSTGRES_DATA_DIR" ]] } backup_build() { runtime_build || return 1 unit_env="$(systemctl show "$CONTRACT_UNIT" -p Environment --value)" unit_build="" for entry in $unit_env; do if [[ "${entry%%=*}" == "BUILD_SHA" ]]; then unit_build="${entry#*=}" fi done [[ "$unit_build" == "$active_build" ]] } emit repository_root repository_root emit repository_writable repository_writable emit env_readable env_readable emit data_directories data_directories emit runtime_unique runtime_unique emit runtime_build runtime_build emit storage_mount storage_mount emit pgdata_mount pgdata_mount emit backup_build backup_build REMOTE )" while IFS='=' read -r result_name result_value; do case "$result_name" in repository_root|repository_writable|env_readable|data_directories|runtime_unique|runtime_build|storage_mount|pgdata_mount|backup_build) [[ "$result_value" == "PASS" || "$result_value" == "FAIL" ]] \ && CHECK_RESULTS["$result_name"]="$result_value" ;; esac done <<<"$remote_results" fi refresh_output="" refresh_status=255 if deployment_contract_safe_ssh_target "$DEPLOY_REFRESH_SERVER"; then refresh_output="$(ssh "$DEPLOY_REFRESH_SERVER" probe 2>/dev/null)" refresh_status=$? fi if [[ "$refresh_status" -ne 255 ]]; then CHECK_RESULTS[root_ssh_identity]="PASS" fi if [[ "$refresh_status" -eq 0 \ && "$refresh_output" == "[deployment-contract-refresh][PASS] probe" ]]; then CHECK_RESULTS[refresh_program]="PASS" fi failure_count=0 for check_name in "${CHECK_NAMES[@]}"; do printf '[deployment-preflight] %s %s\n' "$check_name" "${CHECK_RESULTS[$check_name]}" if [[ "${CHECK_RESULTS[$check_name]}" != "PASS" ]]; then failure_count=$((failure_count + 1)) fi done printf '[deployment-preflight] summary PASS=%s FAIL=%s\n' \ "$((${#CHECK_NAMES[@]} - failure_count))" "$failure_count" [[ "$failure_count" -eq 0 ]]