#!/bin/bash # ============================================================================== # WALLARM COMMON LIBRARY - shared functions for docker/ and native/ deployment # ============================================================================== # Purpose: Single source of truth for functionality shared by both deployment # types (Docker container vs native NGINX install). # Usage: Scripts source this file AFTER setting `set -euo pipefail` and before # defining their own functions: # source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../common/wallarm-lib.sh" # The library does NOT set the error trap itself; each script owns its # error handling configuration. # ============================================================================== # ------------------------------------------------------------------------------ # COLOR DEFINITIONS (for better UX) # ------------------------------------------------------------------------------ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[1;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' BOLD='\033[1m' NC='\033[0m' # No Color # ------------------------------------------------------------------------------ # SSL SECURITY SETTINGS # WALLARM_INSECURE_SSL=1 disables SSL certificate validation (for self-signed # certs). Kept as a default of 1 for backward compatibility with existing usage. # ------------------------------------------------------------------------------ INSECURE_SSL="${WALLARM_INSECURE_SSL:-1}" if [ "$INSECURE_SSL" = "1" ]; then CURL_INSECURE_FLAG="-k" else CURL_INSECURE_FLAG="" fi # ------------------------------------------------------------------------------ # EARLY ERROR HANDLER # Use with: trap early_error_handler ERR # Handles failures before log_message is available (or when logging is not set). # ------------------------------------------------------------------------------ early_error_handler() { echo -e "${RED}${BOLD}[ERROR]${NC} Script failed at line $LINENO. Command: $BASH_COMMAND" >&2 exit 1 } # ------------------------------------------------------------------------------ # LOGGING # ------------------------------------------------------------------------------ # Log a message to stderr (colored) and to $LOG_FILE (if set). log_message() { local level="$1" local message="$2" local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') local color="$NC" case "$level" in "INFO") color="${BLUE}" ;; "SUCCESS") color="${GREEN}" ;; "WARNING") color="${YELLOW}" ;; "ERROR") color="${RED}" ;; "DEBUG") color="${CYAN}" ;; esac echo -e "${color}[${timestamp}] ${level}: ${message}${NC}" >&2 if [ -n "${LOG_FILE:-}" ]; then echo "[${timestamp}] ${level}: ${message}" >> "$LOG_FILE" fi } # Log an ERROR, print a remediation banner, and exit non-zero. fail_with_remediation() { local error_msg="$1" local remediation="$2" log_message "ERROR" "$error_msg" echo -e "\n${RED}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${RED}${BOLD}║ DEPLOYMENT FAILED ║${NC}" echo -e "${RED}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}" echo -e "\n${YELLOW}${BOLD}Root Cause:${NC} $error_msg" echo -e "\n${YELLOW}${BOLD}How to Fix:${NC}" echo -e "$remediation" echo -e "\n${YELLOW}Check the full log for details:${NC} ${LOG_FILE:-stdout}" exit 1 } # ------------------------------------------------------------------------------ # MISC HELPERS # ------------------------------------------------------------------------------ # Extract hostname from a URL, stripping protocol and credentials for safe logging. extract_hostname_from_url() { local url="$1" local hostpart="${url#*://}" hostpart="${hostpart#*@}" hostpart="${hostpart%%[:/]*}" echo "$hostpart" } # Check whether a command exists (respects PATH + common system directories). command_exists() { local cmd="$1" if command -v "$cmd" >/dev/null 2>&1; then return 0 fi local system_dirs=("/usr/sbin" "/sbin" "/usr/local/sbin" "/usr/bin" "/bin" "/usr/local/bin") for dir in "${system_dirs[@]}"; do if [ -x "$dir/$cmd" ]; then return 0 fi done return 1 } # Validate an IPv4 address (basic format + octet range check). validate_ip_address() { local ip="$1" if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then return 1 fi IFS='.' read -r i1 i2 i3 i4 <<< "$ip" if [ "$i1" -gt 255 ] || [ "$i2" -gt 255 ] || [ "$i3" -gt 255 ] || [ "$i4" -gt 255 ]; then return 1 fi return 0 } # Validate an IP or CIDR entry (IPv4 with optional /prefix). Returns 0 if valid. validate_ip_or_cidr() { local entry="$1" if [[ ! "$entry" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then return 1 fi IFS='/' read -r ip cidr <<< "$entry" IFS='.' read -r o1 o2 o3 o4 <<< "$ip" if [ "$o1" -gt 255 ] || [ "$o2" -gt 255 ] || [ "$o3" -gt 255 ] || [ "$o4" -gt 255 ]; then return 1 fi if [ -n "$cidr" ] && { [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; }; then return 1 fi return 0 } # Check if a TCP/UDP port is currently in use. Returns 0 when available. check_port_available() { local port="$1" local protocol="${2:-tcp}" log_message "DEBUG" "Checking port $port/$protocol availability..." if command -v ss >/dev/null 2>&1; then if ss -"${protocol:0:1}"ln | grep -q ":$port "; then return 1 # Port in use fi elif command -v netstat >/dev/null 2>&1; then if netstat -tulpn 2>/dev/null | grep -E ":$port\s" >/dev/null 2>&1; then return 1 # Port in use fi else log_message "WARNING" "Neither ss nor netstat available, cannot check port $port" fi return 0 # Port available (or cannot check) } # ------------------------------------------------------------------------------ # SYSTEM DETECTION # ------------------------------------------------------------------------------ # Detect OS name and version. Prints "name:version". detect_os_and_version() { log_message "INFO" "Detecting OS and version..." local os_name="" local os_version="" if [ -f /etc/os-release ]; then . /etc/os-release os_name="$ID" os_version="$VERSION_ID" elif [ -f /etc/redhat-release ]; then os_name="rhel" os_version=$(sed -e 's/.*release \([0-9]\+\)\..*/\1/' /etc/redhat-release) elif [ -f /etc/alpine-release ]; then os_name="alpine" os_version=$(cat /etc/alpine-release) else os_name=$(uname -s | tr '[:upper:]' '[:lower:]') os_version=$(uname -r) fi os_name="${os_name//[$'\t\r\n']/}" os_version="${os_version//[$'\t\r\n']/}" case "$os_name" in "ubuntu"|"debian"|"centos"|"rhel"|"alpine"|"amzn"|"ol"|"rocky"|"almalinux") log_message "SUCCESS" "OS detected: $os_name $os_version (supported)" ;; *) log_message "WARNING" "OS '$os_name' not explicitly tested but may work" ;; esac echo "$os_name:$os_version" } # Detect architecture. Prints a normalized value (x86_64/aarch64/armhf/unknown). detect_architecture() { log_message "INFO" "Detecting system architecture..." local arch arch=$(uname -m) local docker_arch="" case "$arch" in x86_64|x64|amd64) docker_arch="x86_64" log_message "SUCCESS" "Architecture: x86_64 (Intel/AMD 64-bit)" ;; aarch64|arm64) docker_arch="aarch64" log_message "SUCCESS" "Architecture: aarch64 (ARM 64-bit)" ;; armv7l|armhf) docker_arch="armhf" log_message "SUCCESS" "Architecture: armhf (ARM 32-bit)" ;; *) log_message "ERROR" "Unsupported architecture: $arch" docker_arch="unknown" ;; esac echo "$docker_arch" } # Detect the init system. Prints one of systemd/openrc/sysvinit/upstart/unknown. detect_init_system() { log_message "INFO" "Detecting init system..." local init_system="unknown" if [ "$(uname -s)" = "Darwin" ]; then init_system="darwin" log_message "SUCCESS" "Init system: darwin (macOS)" elif command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then init_system="systemd" log_message "SUCCESS" "Init system: systemd" elif { [ -d /etc/init.d ] && [ -x /sbin/initctl ]; } || [ -x /sbin/init ]; then init_system="sysvinit" log_message "SUCCESS" "Init system: sysvinit" elif [ -d /etc/rc.d ] && [ -x /sbin/rc-service ]; then init_system="openrc" log_message "SUCCESS" "Init system: openrc (Alpine)" elif [ -x /sbin/upstart ]; then init_system="upstart" log_message "SUCCESS" "Init system: upstart" else log_message "WARNING" "Could not determine init system" fi echo "$init_system" } # ------------------------------------------------------------------------------ # NETWORK CONNECTIVITY # ------------------------------------------------------------------------------ # Test connectivity to a host/URL. Returns 0 when reachable. test_connectivity() { local host="$1" local description="$2" local timeout="${3:-10}" local display_host display_host=$(extract_hostname_from_url "$host") log_message "INFO" "Testing connectivity to $description ($display_host)..." local url="$host" if [[ ! "$host" =~ ^https?:// ]]; then url="https://$host" fi if curl -sL $CURL_INSECURE_FLAG --connect-timeout "$timeout" "$url" >/dev/null 2>&1; then log_message "SUCCESS" "$description is reachable" return 0 else log_message "ERROR" "$description is NOT reachable" return 1 fi } # Test a set of cloud endpoints. Prints "true" if all reachable, else "false". test_cloud_endpoints() { local cloud_name="$1" shift local endpoints=("$@") log_message "INFO" "Testing $cloud_name cloud endpoints..." local all_reachable=true local endpoint for endpoint in "${endpoints[@]}"; do if ! test_connectivity "$endpoint" "$cloud_name cloud endpoint $endpoint"; then all_reachable=false fi done if [ "$all_reachable" = "true" ]; then log_message "SUCCESS" "All $cloud_name cloud endpoints reachable" echo "true" else log_message "WARNING" "Some $cloud_name cloud endpoints unreachable" echo "false" fi } # ------------------------------------------------------------------------------ # ENVIRONMENT FILE HANDLING # ------------------------------------------------------------------------------ # Load a preflight .env file into global variables. Returns 1 if file missing. load_env_file() { local env_file="${1:-$ENV_FILE}" if [ ! -f "$env_file" ]; then log_message "ERROR" "Environment file not found: $env_file" return 1 fi local key value while IFS='=' read -r key value; do [[ "$key" =~ ^#.*$ ]] && continue [[ -z "$key" ]] && continue value="${value%\"}" value="${value#\"}" case "$key" in result) CHECK_RESULT="$value" ;; os_name) OS_NAME="$value" ;; os_version) OS_VERSION="$value" ;; architecture) ARCHITECTURE="$value" ;; init_system) INIT_SYSTEM="$value" ;; us_cloud_reachable) US_CLOUD_REACHABLE="$value" ;; eu_cloud_reachable) EU_CLOUD_REACHABLE="$value" ;; registry_reachable) REGISTRY_REACHABLE="$value" ;; download_reachable) DOWNLOAD_REACHABLE="$value" ;; git_reachable) GIT_REACHABLE="$value" ;; installer_reachable) INSTALLER_REACHABLE="$value" ;; esac done < "$env_file" log_message "SUCCESS" "Loaded preflight results from $env_file" return 0 } # ------------------------------------------------------------------------------ # CLOUD REGION SELECTION # Sets CLOUD_REGION and API_HOST based on reachability from the preflight check. # ------------------------------------------------------------------------------ select_cloud_region() { log_message "INFO" "Selecting Wallarm Cloud region..." echo -e "\n${CYAN}${BOLD}Wallarm Cloud Region Selection:${NC}" local available_options=() if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then echo -e "1. ${YELLOW}US Cloud${NC} (us1.api.wallarm.com) - For US-based deployments" available_options+=("1" "US") fi if [ "${EU_CLOUD_REACHABLE:-false}" = "true" ]; then echo -e "2. ${YELLOW}EU Cloud${NC} (api.wallarm.com) - For EU-based deployments" available_options+=("2" "EU") fi if [ ${#available_options[@]} -eq 0 ]; then fail_with_remediation "No cloud regions available" \ "Preflight check showed no reachable cloud regions. 1. Check network connectivity to Wallarm endpoints 2. Run the preflight check again 3. Contact network administrator if behind firewall" fi local pattern pattern="^($(IFS='|'; echo "${available_options[*]}"))$" local cloud_choice="" while [[ ! "$cloud_choice" =~ $pattern ]]; do if [ ${#available_options[@]} -eq 2 ]; then if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then cloud_choice="US" break else cloud_choice="EU" break fi fi read -r -p "$(echo -e "${YELLOW}Enter choice [1/US or 2/EU]: ${NC}")" cloud_choice cloud_choice=$(echo "$cloud_choice" | tr '[:lower:]' '[:upper:]') case "$cloud_choice" in 1|"US") if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then CLOUD_REGION="US" API_HOST="us1.api.wallarm.com" log_message "INFO" "Selected US Cloud" else echo -e "${RED}US Cloud is not reachable (per preflight check)${NC}" cloud_choice="" fi ;; 2|"EU") if [ "${EU_CLOUD_REACHABLE:-false}" = "true" ]; then CLOUD_REGION="EU" API_HOST="api.wallarm.com" log_message "INFO" "Selected EU Cloud" else echo -e "${RED}EU Cloud is not reachable (per preflight check)${NC}" cloud_choice="" fi ;; *) if [ -n "$cloud_choice" ]; then echo -e "${RED}Invalid choice. Select from available options above.${NC}" fi ;; esac done log_message "SUCCESS" "Cloud region selected: $CLOUD_REGION ($API_HOST)" } # ------------------------------------------------------------------------------ # ARTIFACT DOWNLOAD (Git Repositorys primary source) # ------------------------------------------------------------------------------ # Download a file from Git Repositorys. Returns 0 on success. download_from_git() { local url="$1" local output_path="$2" local description="$3" log_message "INFO" "Attempting to download $description from Git Repositorys..." log_message "DEBUG" "URL: $url" log_message "DEBUG" "Output path: $output_path" if curl -fL "$CURL_INSECURE_FLAG" --connect-timeout 30 --max-time 300 --progress-bar "$url" -o "$output_path"; then log_message "SUCCESS" "Downloaded $description to $output_path" return 0 else local curl_exit=$? log_message "ERROR" "Failed to download $description from Git Repositorys (curl exit: $curl_exit)" if [ -f "$output_path" ]; then rm -f "$output_path" log_message "DEBUG" "Removed partial download: $output_path" fi return 1 fi } # Verify a file against a checksum file or URL. Returns 0 on success; # skips verification (returns 0) when the checksum cannot be obtained. verify_checksum() { local file_path="$1" local checksum_file_or_url="$2" local description="$3" log_message "INFO" "Verifying $description checksum..." local checksum_file="" if [[ "$checksum_file_or_url" =~ ^https?:// ]]; then checksum_file="/tmp/$(basename "$checksum_file_or_url")" log_message "DEBUG" "Downloading checksum from URL: $checksum_file_or_url" if ! curl -fL "$CURL_INSECURE_FLAG" --connect-timeout 10 --max-time 30 -s "$checksum_file_or_url" -o "$checksum_file"; then log_message "WARNING" "Could not download checksum file, skipping verification" return 0 fi else checksum_file="$checksum_file_or_url" fi if [ ! -f "$checksum_file" ]; then log_message "WARNING" "Checksum file not found: $checksum_file, skipping verification" return 0 fi local expected_checksum expected_checksum=$(awk '{print $1}' "$checksum_file" 2>/dev/null) if [ -z "$expected_checksum" ]; then log_message "WARNING" "Could not read checksum from $checksum_file, skipping verification" return 0 fi log_message "DEBUG" "Computing SHA256 checksum of $file_path..." local actual_checksum if command -v sha256sum >/dev/null 2>&1; then actual_checksum=$(sha256sum "$file_path" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then actual_checksum=$(shasum -a 256 "$file_path" | awk '{print $1}') else log_message "WARNING" "sha256sum or shasum not available, skipping checksum verification" return 0 fi if [ "$expected_checksum" = "$actual_checksum" ]; then log_message "SUCCESS" "$description checksum verified successfully" return 0 else log_message "ERROR" "$description checksum verification FAILED" log_message "DEBUG" "Expected: $expected_checksum" log_message "DEBUG" "Actual: $actual_checksum" rm -f "$file_path" log_message "INFO" "Removed corrupted file: $file_path" return 1 fi }