Files
n8n-install/scripts/utils.sh
Yury Kossakovsky e0018f2b2d refactor: consolidate shared utilities and add script documentation
- move common functions to utils.sh: init_paths, read_env_var, write_env_var,
  is_profile_active, load_env, gen_password, gen_hex, gen_base64, generate_bcrypt_hash
- add documentation headers to all installation scripts
- replace duplicate code with shared utility calls
- consolidate bcrypt hash generation loop in 03_generate_secrets.sh
- add DEBIAN_FRONTEND save/restore helpers for whiptail scripts
- standardize path initialization across all scripts
2025-12-12 09:58:12 -07:00

357 lines
10 KiB
Bash

#!/bin/bash
# =============================================================================
# utils.sh - Shared utilities for n8n-install scripts
# =============================================================================
# Common functions and utilities used across all installation scripts.
#
# Provides:
# - Path initialization (init_paths): Sets SCRIPT_DIR, PROJECT_ROOT, ENV_FILE
# - Logging functions: log_info, log_success, log_warning, log_error
# - .env manipulation: read_env_var, write_env_var, load_env
# - Whiptail wrappers: wt_input, wt_yesno, require_whiptail
# - Validation helpers: require_command, require_file, ensure_file_exists
# - Profile management: is_profile_active, update_compose_profiles
# - Doctor output helpers: print_ok, print_warning, print_error
#
# Usage: source "$(dirname "$0")/utils.sh" && init_paths
# =============================================================================
#=============================================================================
# CONSTANTS
#=============================================================================
DOMAIN_PLACEHOLDER="yourdomain.com"
#=============================================================================
# PATH INITIALIZATION
#=============================================================================
# Initialize standard paths - call at start of each script
# WARNING: Must be called directly from script top-level, NOT from within functions.
# BASH_SOURCE[1] refers to the script that sourced utils.sh.
# Usage: source utils.sh && init_paths
init_paths() {
# BASH_SOURCE[1] = the script that called this function (not utils.sh itself)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
}
#=============================================================================
# LOGGING (Simplified)
#=============================================================================
# Internal logging function
_log() {
local level="$1"
local message="$2"
echo "[$level] $(date +%H:%M:%S): $message"
}
log_info() {
_log "INFO" "$1"
}
log_success() {
_log "OK" "$1"
}
log_warning() {
_log "WARN" "$1"
}
log_error() {
_log "ERROR" "$1" >&2
}
# Display a header for major sections
log_header() {
local message="$1"
echo ""
echo "========== $message =========="
}
#=============================================================================
# COLOR OUTPUT (for diagnostics and previews)
#=============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_ok() {
echo -e " ${GREEN}[OK]${NC} $1"
}
print_error() {
echo -e " ${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e " ${YELLOW}[WARNING]${NC} $1"
}
print_info() {
echo -e " ${BLUE}[INFO]${NC} $1"
}
#=============================================================================
# ENVIRONMENT MANAGEMENT
#=============================================================================
# Load .env file safely
# Usage: load_env [env_file_path]
load_env() {
local env_file="${1:-$ENV_FILE}"
if [[ ! -f "$env_file" ]]; then
log_error ".env file not found: $env_file"
return 1
fi
set -a
source "$env_file"
set +a
}
# Read a variable from .env file
# Usage: value=$(read_env_var "VAR_NAME" [env_file])
read_env_var() {
local var_name="$1"
local env_file="${2:-$ENV_FILE}"
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
grep "^${var_name}=" "$env_file" | cut -d'=' -f2- | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//"
fi
}
# Write/update a variable in .env file (with automatic .bak cleanup)
# Usage: write_env_var "VAR_NAME" "value" [env_file]
write_env_var() {
local var_name="$1"
local var_value="$2"
local env_file="${3:-$ENV_FILE}"
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
sed -i.bak "\|^${var_name}=|d" "$env_file"
rm -f "${env_file}.bak"
fi
echo "${var_name}=\"${var_value}\"" >> "$env_file"
}
# Check if a Docker Compose profile is active
# IMPORTANT: Requires COMPOSE_PROFILES to be set before calling (via load_env or direct assignment)
# Usage: is_profile_active "n8n" && echo "n8n is active"
is_profile_active() {
local profile="$1"
[[ -n "$COMPOSE_PROFILES" && ",$COMPOSE_PROFILES," == *",$profile,"* ]]
}
#=============================================================================
# UTILITIES
#=============================================================================
# Require a command to be available
# Usage: require_command "docker" "Install Docker: https://docs.docker.com/engine/install/"
require_command() {
local cmd="$1"
local install_hint="${2:-Please install $cmd}"
if ! command -v "$cmd" &> /dev/null; then
log_error "'$cmd' not found. $install_hint"
exit 1
fi
}
# Cleanup .bak files created by sed -i
# Usage: cleanup_bak_files [directory]
cleanup_bak_files() {
local directory="${1:-$PROJECT_ROOT}"
find "$directory" -maxdepth 1 -name "*.bak" -type f -delete 2>/dev/null || true
}
# Escape string for JSON output
# Usage: escaped=$(json_escape "string with \"quotes\"")
json_escape() {
local str="$1"
printf '%s' "$str" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r'
}
#=============================================================================
# FILE UTILITIES
#=============================================================================
# Require a file to exist, exit with error if not found
# Usage: require_file "/path/to/file" "Custom error message"
require_file() {
local file="$1"
local error_msg="${2:-File not found: $file}"
if [[ ! -f "$file" ]]; then
log_error "$error_msg"
exit 1
fi
}
# Ensure a file exists, create empty file if it doesn't
# Usage: ensure_file_exists "/path/to/file"
ensure_file_exists() {
local file="$1"
[[ ! -f "$file" ]] && touch "$file"
}
#=============================================================================
# COMPOSE PROFILES MANAGEMENT
#=============================================================================
# Update COMPOSE_PROFILES in .env file
# Usage: update_compose_profiles "n8n,monitoring,portainer" [env_file]
update_compose_profiles() {
local profiles="$1"
local env_file="${2:-$ENV_FILE}"
ensure_file_exists "$env_file"
if grep -q "^COMPOSE_PROFILES=" "$env_file"; then
sed -i.bak "\|^COMPOSE_PROFILES=|d" "$env_file"
rm -f "${env_file}.bak"
fi
echo "COMPOSE_PROFILES=${profiles}" >> "$env_file"
}
#=============================================================================
# DEBIAN_FRONTEND MANAGEMENT
#=============================================================================
ORIGINAL_DEBIAN_FRONTEND=""
# Save current DEBIAN_FRONTEND and set to dialog for whiptail
# Usage: save_debian_frontend
save_debian_frontend() {
ORIGINAL_DEBIAN_FRONTEND="$DEBIAN_FRONTEND"
export DEBIAN_FRONTEND=dialog
}
# Restore original DEBIAN_FRONTEND value
# Usage: restore_debian_frontend
restore_debian_frontend() {
if [[ -n "$ORIGINAL_DEBIAN_FRONTEND" ]]; then
export DEBIAN_FRONTEND="$ORIGINAL_DEBIAN_FRONTEND"
else
unset DEBIAN_FRONTEND
fi
}
#=============================================================================
# SECRET GENERATION
#=============================================================================
# Generate random string with specified characters
# Usage: gen_random 32 'A-Za-z0-9'
gen_random() {
local length="$1"
local characters="$2"
head /dev/urandom | tr -dc "$characters" | head -c "$length"
}
# Generate alphanumeric password
# Usage: gen_password 32
gen_password() {
gen_random "$1" 'A-Za-z0-9'
}
# Generate hex string
# Usage: gen_hex 64 (returns 64 hex characters)
gen_hex() {
local length="$1"
local bytes=$(( (length + 1) / 2 ))
openssl rand -hex "$bytes" | head -c "$length"
}
# Generate base64 string
# Usage: gen_base64 64 (returns 64 base64 characters)
gen_base64() {
local length="$1"
local bytes=$(( (length * 3 + 3) / 4 ))
openssl rand -base64 "$bytes" | head -c "$length"
}
# Generate bcrypt hash using Caddy
# Usage: hash=$(generate_bcrypt_hash "plaintext_password")
generate_bcrypt_hash() {
local plaintext="$1"
if [[ -n "$plaintext" ]]; then
caddy hash-password --algorithm bcrypt --plaintext "$plaintext" 2>/dev/null
fi
}
#=============================================================================
# VALIDATION
#=============================================================================
# Validate that a value is a positive integer
# Usage: validate_positive_integer "5" && echo "valid"
validate_positive_integer() {
local value="$1"
[[ "$value" =~ ^0*[1-9][0-9]*$ ]]
}
#=============================================================================
# WHIPTAIL HELPERS
#=============================================================================
# Ensure whiptail is available
require_whiptail() {
if ! command -v whiptail >/dev/null 2>&1; then
log_error "'whiptail' is not installed. Install with: sudo apt-get install -y whiptail"
exit 1
fi
}
# Input box
# Usage: result=$(wt_input "Title" "Prompt" "default")
# Returns 0 on OK, 1 on Cancel
wt_input() {
local title="$1"
local prompt="$2"
local default_value="$3"
local result
result=$(whiptail --title "$title" --inputbox "$prompt" 15 80 "$default_value" 3>&1 1>&2 2>&3)
local status=$?
if [ $status -ne 0 ]; then
return 1
fi
echo "$result"
return 0
}
# Password box
# Usage: result=$(wt_password "Title" "Prompt")
# Returns 0 on OK, 1 on Cancel
wt_password() {
local title="$1"
local prompt="$2"
local result
result=$(whiptail --title "$title" --passwordbox "$prompt" 15 80 3>&1 1>&2 2>&3)
local status=$?
if [ $status -ne 0 ]; then
return 1
fi
echo "$result"
return 0
}
# Yes/No box
# Usage: wt_yesno "Title" "Prompt" "default" (default: yes|no)
# Returns 0 for Yes, 1 for No/Cancel
wt_yesno() {
local title="$1"
local prompt="$2"
local default_choice="$3"
if [ "$default_choice" = "yes" ]; then
whiptail --title "$title" --yesno "$prompt" 10 80
else
whiptail --title "$title" --defaultno --yesno "$prompt" 10 80
fi
}
# Message box
# Usage: wt_msg "Title" "Message"
wt_msg() {
local title="$1"
local message="$2"
whiptail --title "$title" --msgbox "$message" 10 80
}