Files
k3s-gitops/sandbox/auto.sh
2026-01-13 13:44:06 +00:00

714 lines
20 KiB
Bash

#!/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 "$@"