#!/usr/bin/env bash # # #Bash script for safe COIN sandbox deployment that prepares new release directories, extracts the release from a Docker image, runs interactive pre-checks (Docker, SSH, DB migrations), #supports dry-run and node-specific deploys, and deploys to node-3 and node-4 with logging and safety guards # Features: # - Full/partial release deploy to node-3 / node-4 # - Manual EXPECTED_MIGRATION_ID check in DB # - Colorful output # - Logging to file # - Interactive SELF-TEST # - Manual stop for editing project.env # - CLI flags: # --dry-run # --self-test-only # --node3-only # --node4-only # --skip-db-check # --skip-self-test # --auto-yes # --deploy-only node3|node4|node3,node4 # --help # # #For testing use ./deploy.sh --dry-run --self-test-only # # #NAME DESCRIPTION DOCKER ENDPOINT ERROR #default Current DOCKER_HOST based configuration unix:///var/run/docker.sock #wlt-sbx-coinssm-ams tcp://10.95.81.151:2376 #wlt-sbx-dkapp3-ams tcp://10.95.81.131:2376 #wlt-sbx-dkapp4-ams * tcp://10.95.81.132:2376 # set -euo pipefail ############################################ # -------- COLORS (for console only) ------- ############################################ RED="\033[1;31m" GREEN="\033[1;32m" YELLOW="\033[1;33m" BLUE="\033[1;34m" RESET="\033[0m" ############################################ # -------- ARGUMENT PARSING FLAGS ---------- ############################################ CMD_DRY_RUN=false CMD_SELF_TEST_ONLY=false CMD_NODE3_ONLY=false CMD_NODE4_ONLY=false CMD_SKIP_DB_CHECK=false CMD_SKIP_SELF_TEST=false CMD_AUTO_YES=false CMD_ROLLBACK=false # NEW: deploy-only CMD_DEPLOY_ONLY=false DEPLOY_ONLY_NODES="" ############################################ # -------- STATUS FLAGS (summary) ---------- ############################################ PREPARED_NODE3=false PREPARED_NODE4=false SELECTED_NODE3=false SELECTED_NODE4=false DEPLOY_ATTEMPT_NODE3=false DEPLOY_ATTEMPT_NODE4=false show_help() { echo "" echo "COIN Sandbox Deployment Script — usage:" echo "" echo " --dry-run Run commands in simulation mode (no real changes)" echo " --self-test-only Run self-test checks and exit (no deploy)" echo " --node3-only Prepare BOTH nodes, but deploy only node-3 (no questions)" echo " --node4-only Prepare BOTH nodes, but deploy only node-4 (no questions)" echo " --skip-db-check Do not check migration ID in DB after node-3 deploy" echo " --skip-self-test Skip self-test before deployment" echo " --auto-yes Automatically answer YES to all confirmations" echo " --rollback Revert to previous release (stops stacks, redeploys previous version)" echo " --deploy-only node3|node4|node3,node4 Deploy only selected node(s), skip prepare & questions" echo " --help, -h Show this help message" echo "" exit 0 } # NEW: robust arg parsing to support --deploy-only value while [[ $# -gt 0 ]]; do case "$1" in --dry-run) CMD_DRY_RUN=true shift ;; --self-test-only) CMD_SELF_TEST_ONLY=true shift ;; --node3-only) CMD_NODE3_ONLY=true shift ;; --node4-only) CMD_NODE4_ONLY=true shift ;; --skip-db-check) CMD_SKIP_DB_CHECK=true shift ;; --skip-self-test) CMD_SKIP_SELF_TEST=true shift ;; --auto-yes) CMD_AUTO_YES=true shift ;; --rollback) CMD_ROLLBACK=true shift ;; --deploy-only) CMD_DEPLOY_ONLY=true DEPLOY_ONLY_NODES="${2:-}" shift 2 ;; --help|-h) show_help ;; *) echo "Unknown argument: $1" echo "Use --help for usage." exit 1 ;; esac done # NEW: validate deploy-only input if [ "$CMD_DEPLOY_ONLY" = true ]; then if [[ ! "$DEPLOY_ONLY_NODES" =~ ^(node3|node4|node3,node4|node4,node3)$ ]]; then echo "ERROR: --deploy-only requires: node3 | node4 | node3,node4" echo "Example: ./deploy.sh --deploy-only node3" exit 1 fi fi ############################################ # -------- INTERACTIVE INPUT --------------- ############################################ prompt_var() { local var_name="$1" local default_value="$2" local current_value="${!var_name:-}" if [ -n "$current_value" ]; then printf -v "$var_name" "%s" "$current_value" return fi read -r -p "${var_name} [${default_value}]: " input if [ -z "$input" ]; then printf -v "$var_name" "%s" "$default_value" else printf -v "$var_name" "%s" "$input" fi } ############################################ # -------- RELEASE INPUT ------------------- ############################################ prompt_var "TASK_ID" "41361" prompt_var "RELEASE_VERSION" "25.22" prompt_var "RELEASE_TAG" "2025-12-15-11eeef9e99" prompt_var "PREVIOUS_RELEASE_VERSION" "25.21" prompt_var "PREVIOUS_RELEASE_TAG" "2025-12-05-ecacdc6c25" prompt_var "EXPECTED_MIGRATION_ID" "565" ############################################ # -------- CONFIG (change per release) ----- ############################################ # Base directory for sandbox releases BASE_DIR="/home/dev-wltsbx/encrypted/sandbox" # Docker registry REGISTRY="wlt-sbx-hb-int.wltsbxinner.walletto.eu/coin/release" # Docker contexts NODE3_CONTEXT="wlt-sbx-dkapp3-ams" NODE4_CONTEXT="wlt-sbx-dkapp4-ams" # Docker stacks NODE3_STACK="sbxapp3" NODE4_STACK="sbxapp4" # DRY-RUN: default false, can be overridden by env or --dry-run DRY_RUN="${DRY_RUN:-false}" if [ "$CMD_DRY_RUN" = true ]; then DRY_RUN=true fi SSH_JUMP_HOST="${SSH_JUMP_HOST:-YOUR_JUMP_HOST}" DB_HOST="${DB_HOST:-YOUR_DB_HOST}" DB_PORT="${DB_PORT:-5432}" DB_NAME="${DB_NAME:-coin}" DB_USER="${DB_USER:-coin}" DB_PASSWORD="${DB_PASSWORD:-YOUR_DB_PASSWORD}" ############################################ # -------- DERIVED PATHS ------------------- ############################################ NEW_SUFFIX="_sbx_${RELEASE_TAG}" PREV_SUFFIX="_sbx_${PREVIOUS_RELEASE_TAG}" NODE4_NEW="${BASE_DIR}/${RELEASE_VERSION}${NEW_SUFFIX}-node-4" NODE3_NEW="${BASE_DIR}/${RELEASE_VERSION}${NEW_SUFFIX}-node-3" NODE4_PREV="${BASE_DIR}/${PREVIOUS_RELEASE_VERSION}${PREV_SUFFIX}-node-4" NODE3_PREV="${BASE_DIR}/${PREVIOUS_RELEASE_VERSION}${PREV_SUFFIX}-node-3" OLD_COIN="coin-${PREVIOUS_RELEASE_TAG}" NEW_COIN="coin-${RELEASE_TAG}" TARBALL="${RELEASE_TAG}.tar.gz" ############################################ # -------- LOGGING SETUP ------------------- ############################################ LOG_DIR="${BASE_DIR}/logs" mkdir -p "$LOG_DIR" TIMESTAMP="$(date '+%Y-%m-%d__%H-%M-%S')" LOGFILE="${LOG_DIR}/deploy_${RELEASE_TAG}__${TIMESTAMP}_task-${TASK_ID}.log" touch "$LOGFILE" ############################################ # -------- UTILS (log, run, confirm) ------- ############################################ log_msg() { # Write to logfile without ANSI codes printf "%s\n" "$(echo -e "$1" | sed 's/\x1B\[[0-9;]*[JKmsu]//g')" >> "$LOGFILE" # Print to console with colors echo -e "$1" } run() { log_msg "${BLUE}+ $*${RESET}" if [ "$DRY_RUN" != "true" ]; then "$@" fi } ensure_dir() { if [ ! -d "$1" ]; then log_msg "${RED}ERROR: directory not found: $1${RESET}" exit 1 fi } confirm() { local question="$1" if [ "$CMD_AUTO_YES" = true ]; then log_msg "${YELLOW}[AUTO-YES] ${question}: YES${RESET}" return 0 fi read -r -p "${question} (yes/no): " answer case "$answer" in yes|y|Y) return 0 ;; *) log_msg "${RED}Operation cancelled by user.${RESET}"; exit 1 ;; esac } # NEW: soft confirm (no => skip, not exit) confirm_optional() { local question="$1" if [ "$CMD_AUTO_YES" = true ]; then log_msg "${YELLOW}[AUTO-YES] ${question}: YES${RESET}" return 0 fi while true; do read -r -p "${question} (yes/no): " answer case "$answer" in yes|y|Y) return 0 ;; no|n|N) return 1 ;; *) echo "Please answer yes or no." ;; esac done } print_summary() { log_msg "" log_msg "${BLUE}======== DEPLOY SUMMARY ========${RESET}" if [ "$CMD_DEPLOY_ONLY" = true ]; then log_msg "Prepared:" log_msg " - node-4 : skipped (deploy-only)" log_msg " - node-3 : skipped (deploy-only)" else log_msg "Prepared:" log_msg " - node-4 : ${PREPARED_NODE4}" log_msg " - node-3 : ${PREPARED_NODE3}" fi log_msg "" log_msg "Selected:" log_msg " - node-3 : ${SELECTED_NODE3}" log_msg " - node-4 : ${SELECTED_NODE4}" log_msg "" log_msg "Deploy attempted:" log_msg " - node-3 : ${DEPLOY_ATTEMPT_NODE3}" log_msg " - node-4 : ${DEPLOY_ATTEMPT_NODE4}" log_msg "" log_msg "Mode:" if [ "$CMD_DEPLOY_ONLY" = true ]; then log_msg " - deploy-only : true (${DEPLOY_ONLY_NODES})" else log_msg " - deploy-only : false" fi log_msg "${BLUE}================================${RESET}" } ############################################ # -------- SELF-TEST (interactive) --------- ############################################ self_test() { log_msg "${BLUE}========== SELF-TEST ==========${RESET}" local issues=() # 1. Directories [ -d "$BASE_DIR" ] || issues+=("BASE_DIR does not exist: $BASE_DIR") [ -d "$NODE4_PREV" ] || issues+=("Previous node-4 release dir missing: $NODE4_PREV") [ -d "$NODE3_PREV" ] || issues+=("Previous node-3 release dir missing: $NODE3_PREV") # 2. Docker contexts if ! docker context ls >/dev/null 2>&1; then issues+=("docker context ls failed (docker not available?)") else docker context ls --format '{{.Name}}' | grep -qx "$NODE3_CONTEXT" || \ issues+=("docker context for node-3 not found: $NODE3_CONTEXT") docker context ls --format '{{.Name}}' | grep -qx "$NODE4_CONTEXT" || \ issues+=("docker context for node-4 not found: $NODE4_CONTEXT") fi # Config summary log_msg "${YELLOW}Release configuration:${RESET}" log_msg " Release version : ${RELEASE_VERSION}" log_msg " Release tag : ${RELEASE_TAG}" log_msg " Previous version: ${PREVIOUS_RELEASE_VERSION} (${PREVIOUS_RELEASE_TAG})" log_msg " Task ID : ${TASK_ID}" log_msg " BASE_DIR : ${BASE_DIR}" log_msg " NODE3 context : ${NODE3_CONTEXT}" log_msg " NODE4 context : ${NODE4_CONTEXT}" log_msg " EXPECTED MIG ID : ${EXPECTED_MIGRATION_ID}" log_msg " Log file : ${LOGFILE}" if [ "${#issues[@]}" -gt 0 ]; then log_msg "${RED}Potential issues detected:${RESET}" for item in "${issues[@]}"; do log_msg " - ${item}" done confirm "⚠ Continue deployment despite these issues?" else log_msg "${GREEN}SELF-TEST: no critical issues detected.${RESET}" confirm "Proceed with deployment?" fi log_msg "${BLUE}========== SELF-TEST DONE ======${RESET}" } ############################################ # -------- ROLLBACK TO PREVIOUS RELEASE ---- ############################################ rollback() { log_msg "${BLUE}=== ROLLBACK TO PREVIOUS RELEASE ===${RESET}" log_msg "${YELLOW}Rolling back from ${RELEASE_VERSION} (${RELEASE_TAG})${RESET}" log_msg "${YELLOW} to ${PREVIOUS_RELEASE_VERSION} (${PREVIOUS_RELEASE_TAG})${RESET}" confirm "⚠ This will stop current stacks and revert to previous release. Continue?" # Stop node-3 stack log_msg "${YELLOW}Stopping node-3 stack (${NODE3_STACK})…${RESET}" run docker context use "$NODE3_CONTEXT" run docker stack rm "$NODE3_STACK" || log_msg "${YELLOW}Stack not found or already removed.${RESET}" sleep 3 # Stop node-4 stack log_msg "${YELLOW}Stopping node-4 stack (${NODE4_STACK})…${RESET}" run docker context use "$NODE4_CONTEXT" run docker stack rm "$NODE4_STACK" || log_msg "${YELLOW}Stack not found or already removed.${RESET}" sleep 3 log_msg "${GREEN}Stacks stopped.${RESET}" # Deploy previous node-3 release log_msg "${YELLOW}Deploying previous node-3 release…${RESET}" ensure_dir "$NODE3_PREV" cd "$NODE3_PREV" run docker context use "$NODE3_CONTEXT" run ./deploy.sh deploy \ -n "$NODE3_CONTEXT" \ -w "$NODE3_STACK" \ -N node.env \ -P project.env \ -P project_node3.env \ -f docker-compose.yml \ -f custom.secrets.yml \ -f docker-compose-testshop.yaml \ -s secrets.override.env \ -u # Deploy previous node-4 release log_msg "${YELLOW}Deploying previous node-4 release…${RESET}" ensure_dir "$NODE4_PREV" cd "$NODE4_PREV" run docker context use "$NODE4_CONTEXT" run ./deploy.sh deploy \ -n "$NODE4_CONTEXT" \ -w "$NODE4_STACK" \ -N node.env \ -P project.env \ -P project_node4.env \ -f docker-compose.yml \ -f custom.secrets.yml \ -f docker-compose-testshop.yaml \ -s secrets.override.env \ -u log_msg "${GREEN}==================================================${RESET}" log_msg "${GREEN} ROLLBACK COMPLETED${RESET}" log_msg "${GREEN} Now running: ${PREVIOUS_RELEASE_VERSION} (${PREVIOUS_RELEASE_TAG})${RESET}" log_msg "${GREEN}==================================================${RESET}" } ############################################ # -------- NODE-4 PREPARATION -------------- ############################################ prepare_node4() { log_msg "${BLUE}=== PREPARE NODE-4 ===${RESET}" ensure_dir "$NODE4_PREV" ensure_dir "$BASE_DIR" run cp -r "$NODE4_PREV" "$NODE4_NEW" cd "$NODE4_NEW" # Remove old coin dir [ -d "$OLD_COIN" ] && run rm -rf "$OLD_COIN" log_msg "${YELLOW}Extracting release archive from Docker…${RESET}" if [ "$DRY_RUN" != "true" ]; then docker run -i --rm "${REGISTRY}:${RELEASE_TAG}" release | base64 -d > "$TARBALL" else log_msg "${BLUE}+ docker run … > $TARBALL (DRY RUN)${RESET}" fi run tar -xzf "$TARBALL" run rm -f "$TARBALL" ensure_dir "$NEW_COIN" # Copy deploy.sh and docker-compose.yml run cp "${NEW_COIN}/deploy.sh" ./ run cp "${NEW_COIN}/docker-compose.yml" ./ # Update TAG in node.env (TAG = release tag, not dir name) if grep -q '^TAG=' node.env; then run sed -i "s/^TAG=.*/TAG=${RELEASE_TAG}/" node.env else run bash -c "echo TAG=${RELEASE_TAG} >> node.env" fi # Comment out all export TAG_* patch lines if grep -q '^export TAG_' node.env; then run sed -i 's/^export TAG_/#export TAG_/' node.env fi # Manual edit of project.env if [ -f project.env ]; then log_msg "${YELLOW}Manual step required: review and edit project.env NOW.${RESET}" log_msg "${YELLOW}For example: vi ${NODE4_NEW}/project.env${RESET}" confirm "Continue after you have manually updated project.env?" else log_msg "${RED}project.env not found in ${NODE4_NEW}!${RESET}" exit 1 fi PREPARED_NODE4=true log_msg "${GREEN}Node-4 prepared: ${NODE4_NEW}${RESET}" } ############################################ # -------- NODE-3 PREPARATION -------------- ############################################ prepare_node3() { log_msg "${BLUE}=== PREPARE NODE-3 ===${RESET}" ensure_dir "$NODE3_PREV" ensure_dir "$NODE4_NEW" run cp -r "$NODE3_PREV" "$NODE3_NEW" cd "$NODE3_NEW" [ -d "$OLD_COIN" ] && run rm -rf "$OLD_COIN" run cp -r "$NODE4_NEW/${NEW_COIN}" ./ run cp "${NEW_COIN}/deploy.sh" ./ run cp "${NEW_COIN}/docker-compose.yml" ./ # Reuse already updated node.env and project.env from node-4 run cp "$NODE4_NEW/node.env" ./ run cp "$NODE4_NEW/project.env" ./ PREPARED_NODE3=true log_msg "${GREEN}Node-3 prepared: ${NODE3_NEW}${RESET}" } ############################################ # -------- NODE-3 DEPLOY ------------------- ############################################ deploy_node3() { log_msg "${BLUE}=== DEPLOY NODE-3 ===${RESET}" cd "$NODE3_NEW" run docker context use "$NODE3_CONTEXT" DEPLOY_ATTEMPT_NODE3=true run ./deploy.sh deploy \ -n "$NODE3_CONTEXT" \ -w "$NODE3_STACK" \ -N node.env \ -P project.env \ -P project_node3.env \ -f docker-compose.yml \ -f custom.secrets.yml \ -f docker-compose-testshop.yaml \ -s secrets.override.env \ -u run docker ps } ############################################ # -------- NODE-4 DEPLOY ------------------- ############################################ deploy_node4() { log_msg "${BLUE}=== DEPLOY NODE-4 ===${RESET}" cd "$NODE4_NEW" run docker context use "$NODE4_CONTEXT" DEPLOY_ATTEMPT_NODE4=true run ./deploy.sh deploy \ -n "$NODE4_CONTEXT" \ -w "$NODE4_STACK" \ -N node.env \ -P project.env \ -P project_node4.env \ -f docker-compose.yml \ -f custom.secrets.yml \ -f docker-compose-testshop.yaml \ -s secrets.override.env \ -u run docker ps log_msg "${YELLOW}Reminder: run regression tests manually.${RESET}" } ############################################ # -------- MAIN ---------------------------- ############################################ main() { log_msg "${BLUE}==================================================${RESET}" log_msg "${BLUE} COIN Sandbox Deployment Script${RESET}" log_msg "${BLUE} Release: ${RELEASE_VERSION} (${RELEASE_TAG})${RESET}" log_msg "${BLUE} Task ID: ${TASK_ID}${RESET}" log_msg "${BLUE} Logfile: ${LOGFILE}${RESET}" log_msg "${BLUE} DRY_RUN: ${DRY_RUN}${RESET}" log_msg "${BLUE}==================================================${RESET}" # Rollback mode takes priority if [ "$CMD_ROLLBACK" = true ]; then rollback print_summary exit 0 fi # Prevent conflicting flags if [ "$CMD_NODE3_ONLY" = true ] && [ "$CMD_NODE4_ONLY" = true ]; then log_msg "${RED}Cannot use --node3-only and --node4-only together.${RESET}" exit 1 fi # SELF-TEST if [ "$CMD_SKIP_SELF_TEST" = false ]; then self_test else log_msg "${YELLOW}SELF-TEST skipped (--skip-self-test).${RESET}" fi # Only self-test and exit if [ "$CMD_SELF_TEST_ONLY" = true ]; then log_msg "${YELLOW}Self-test only mode: no deployment will be performed.${RESET}" print_summary exit 0 fi ######################################################### # DEPLOY-ONLY MODE: skip prepare and skip questions ######################################################### if [ "$CMD_DEPLOY_ONLY" = true ]; then log_msg "${YELLOW}DEPLOY-ONLY mode enabled: ${DEPLOY_ONLY_NODES}${RESET}" log_msg "${YELLOW}Skipping prepare phase and all confirmations.${RESET}" if [[ "$DEPLOY_ONLY_NODES" == *"node3"* ]]; then SELECTED_NODE3=true ensure_dir "$NODE3_NEW" deploy_node3 fi if [[ "$DEPLOY_ONLY_NODES" == *"node4"* ]]; then SELECTED_NODE4=true ensure_dir "$NODE4_NEW" deploy_node4 fi print_summary exit 0 fi ######################################################### # NORMAL MODE: ALWAYS PREPARE BOTH NODES ######################################################### prepare_node4 prepare_node3 ######################################################### # DEPLOY SELECTION ######################################################### if [ "$CMD_NODE3_ONLY" = true ]; then log_msg "${YELLOW}Mode: node-3 only deployment.${RESET}" SELECTED_NODE3=true deploy_node3 print_summary exit 0 fi if [ "$CMD_NODE4_ONLY" = true ]; then log_msg "${YELLOW}Mode: node-4 only deployment.${RESET}" SELECTED_NODE4=true deploy_node4 print_summary exit 0 fi # Interactive deploy questions if confirm_optional "Запускать деплой node-3?"; then SELECTED_NODE3=true deploy_node3 else log_msg "${YELLOW}Node-3 deployment skipped by user choice.${RESET}" fi if confirm_optional "Запускать деплой node-4?"; then SELECTED_NODE4=true deploy_node4 else log_msg "${YELLOW}Node-4 deployment skipped by user choice.${RESET}" fi log_msg "${GREEN}==================================================${RESET}" log_msg "${GREEN} DEPLOYMENT FINISHED.${RESET}" log_msg "${GREEN} Expected DB last_id = ${EXPECTED_MIGRATION_ID}${RESET}" log_msg "${GREEN}==================================================${RESET}" print_summary } main "$@"