463 lines
12 KiB
Bash
Executable File
463 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# =============================================================================
|
||
# Common Utilities Library for Meldestelle Shell Scripts
|
||
# =============================================================================
|
||
# This library provides common functions for logging, error handling, cleanup,
|
||
# and other utilities used across all shell scripts in the project.
|
||
#
|
||
# Usage: source "$(dirname "$0")/utils/common.sh" || source "scripts/utils/common.sh"
|
||
# =============================================================================
|
||
|
||
# Prevent multiple sourcing
|
||
if [[ "${COMMON_UTILS_LOADED:-}" == "true" ]]; then
|
||
return 0
|
||
fi
|
||
COMMON_UTILS_LOADED=true
|
||
|
||
# =============================================================================
|
||
# Configuration and Constants
|
||
# =============================================================================
|
||
|
||
# Colors for output
|
||
readonly RED='\033[0;31m'
|
||
readonly GREEN='\033[0;32m'
|
||
readonly YELLOW='\033[1;33m'
|
||
readonly BLUE='\033[0;34m'
|
||
readonly PURPLE='\033[0;35m'
|
||
readonly CYAN='\033[0;36m'
|
||
readonly WHITE='\033[1;37m'
|
||
readonly NC='\033[0m' # No Color
|
||
|
||
# Symbols
|
||
readonly CHECK_MARK="✓"
|
||
readonly CROSS_MARK="✗"
|
||
readonly WARNING_MARK="⚠"
|
||
readonly INFO_MARK="ℹ"
|
||
readonly ARROW_MARK="→"
|
||
|
||
# Global counters
|
||
ERRORS=0
|
||
WARNINGS=0
|
||
CHECKS=0
|
||
START_TIME=$(date +%s)
|
||
|
||
# =============================================================================
|
||
# Error Handling and Cleanup
|
||
# =============================================================================
|
||
|
||
# Enhanced error handling
|
||
set -euo pipefail
|
||
|
||
# Error trap function
|
||
error_trap() {
|
||
local exit_code=$?
|
||
local line_number=$1
|
||
log_error "Script failed at line $line_number with exit code $exit_code"
|
||
cleanup_on_exit
|
||
exit $exit_code
|
||
}
|
||
|
||
# Set error trap
|
||
trap 'error_trap $LINENO' ERR
|
||
|
||
# Cleanup function (can be overridden by scripts)
|
||
cleanup_on_exit() {
|
||
if declare -f cleanup > /dev/null; then
|
||
log_info "Running cleanup..."
|
||
cleanup
|
||
fi
|
||
}
|
||
|
||
# Set exit trap
|
||
trap cleanup_on_exit EXIT
|
||
|
||
# =============================================================================
|
||
# Logging Functions
|
||
# =============================================================================
|
||
|
||
# Get timestamp
|
||
get_timestamp() {
|
||
date '+%Y-%m-%d %H:%M:%S'
|
||
}
|
||
|
||
# Base logging function
|
||
log_base() {
|
||
local level=$1
|
||
local color=$2
|
||
local symbol=$3
|
||
local message=$4
|
||
local timestamp=$(get_timestamp)
|
||
|
||
echo -e "${color}[${timestamp}] ${symbol} [${level}]${NC} ${message}" >&2
|
||
}
|
||
|
||
# Info logging
|
||
log_info() {
|
||
log_base "INFO" "$BLUE" "$INFO_MARK" "$1"
|
||
}
|
||
|
||
# Success logging
|
||
log_success() {
|
||
log_base "SUCCESS" "$GREEN" "$CHECK_MARK" "$1"
|
||
}
|
||
|
||
# Warning logging
|
||
log_warning() {
|
||
log_base "WARNING" "$YELLOW" "$WARNING_MARK" "$1"
|
||
((WARNINGS++))
|
||
}
|
||
|
||
# Error logging
|
||
log_error() {
|
||
log_base "ERROR" "$RED" "$CROSS_MARK" "$1"
|
||
((ERRORS++))
|
||
}
|
||
|
||
# Debug logging (only if DEBUG=true)
|
||
log_debug() {
|
||
if [[ "${DEBUG:-false}" == "true" ]]; then
|
||
log_base "DEBUG" "$PURPLE" "🐛" "$1"
|
||
fi
|
||
}
|
||
|
||
# Progress logging
|
||
log_progress() {
|
||
log_base "PROGRESS" "$CYAN" "$ARROW_MARK" "$1"
|
||
}
|
||
|
||
# Section header
|
||
log_section() {
|
||
local title=$1
|
||
local line=$(printf '=%.0s' {1..80})
|
||
echo -e "\n${BLUE}${line}${NC}"
|
||
echo -e "${BLUE}${title}${NC}"
|
||
echo -e "${BLUE}${line}${NC}\n"
|
||
}
|
||
|
||
# =============================================================================
|
||
# Status and Validation Functions
|
||
# =============================================================================
|
||
|
||
# Print status with counter increment
|
||
print_status() {
|
||
local status=$1
|
||
local message=$2
|
||
((CHECKS++))
|
||
|
||
case $status in
|
||
"OK"|"SUCCESS")
|
||
log_success "$message"
|
||
;;
|
||
"WARNING"|"WARN")
|
||
log_warning "$message"
|
||
;;
|
||
"ERROR"|"FAIL")
|
||
log_error "$message"
|
||
;;
|
||
"INFO")
|
||
log_info "$message"
|
||
;;
|
||
*)
|
||
log_info "$message"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Check if command exists
|
||
command_exists() {
|
||
command -v "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
# Check if file exists with logging
|
||
check_file() {
|
||
local file=$1
|
||
local description=${2:-"File"}
|
||
|
||
if [[ -f "$file" ]]; then
|
||
print_status "OK" "$description exists: $file"
|
||
return 0
|
||
else
|
||
print_status "ERROR" "$description not found: $file"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Check if directory exists with logging
|
||
check_directory() {
|
||
local dir=$1
|
||
local description=${2:-"Directory"}
|
||
|
||
if [[ -d "$dir" ]]; then
|
||
print_status "OK" "$description exists: $dir"
|
||
return 0
|
||
else
|
||
print_status "ERROR" "$description not found: $dir"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Check if service is running on port
|
||
check_service_port() {
|
||
local port=$1
|
||
local service_name=${2:-"Service"}
|
||
local timeout=${3:-30}
|
||
|
||
log_info "Checking if $service_name is running on port $port..."
|
||
|
||
if timeout "$timeout" bash -c "until nc -z localhost $port; do sleep 1; done" 2>/dev/null; then
|
||
print_status "OK" "$service_name is running on port $port"
|
||
return 0
|
||
else
|
||
print_status "ERROR" "$service_name is not running on port $port (timeout: ${timeout}s)"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Check HTTP endpoint with retry
|
||
check_http_endpoint() {
|
||
local url=$1
|
||
local service_name=${2:-"Service"}
|
||
local timeout=${3:-30}
|
||
local retry_count=${4:-3}
|
||
|
||
log_info "Checking HTTP endpoint: $url"
|
||
|
||
for ((i=1; i<=retry_count; i++)); do
|
||
if timeout "$timeout" curl -sf "$url" >/dev/null 2>&1; then
|
||
print_status "OK" "$service_name endpoint is healthy: $url"
|
||
return 0
|
||
else
|
||
if [[ $i -lt $retry_count ]]; then
|
||
log_warning "Attempt $i/$retry_count failed, retrying in 5 seconds..."
|
||
sleep 5
|
||
fi
|
||
fi
|
||
done
|
||
|
||
print_status "ERROR" "$service_name endpoint is not healthy: $url (after $retry_count attempts)"
|
||
return 1
|
||
}
|
||
|
||
# =============================================================================
|
||
# Utility Functions
|
||
# =============================================================================
|
||
|
||
# Wait for service with timeout
|
||
wait_for_service() {
|
||
local check_command=$1
|
||
local service_name=$2
|
||
local timeout=${3:-60}
|
||
local interval=${4:-5}
|
||
|
||
log_info "Waiting for $service_name to be ready (timeout: ${timeout}s)..."
|
||
|
||
local elapsed=0
|
||
while [[ $elapsed -lt $timeout ]]; do
|
||
if eval "$check_command" >/dev/null 2>&1; then
|
||
log_success "$service_name is ready"
|
||
return 0
|
||
fi
|
||
|
||
sleep "$interval"
|
||
elapsed=$((elapsed + interval))
|
||
log_progress "Waiting for $service_name... (${elapsed}s/${timeout}s)"
|
||
done
|
||
|
||
log_error "$service_name failed to become ready within ${timeout}s"
|
||
return 1
|
||
}
|
||
|
||
# Create directory with logging
|
||
create_directory() {
|
||
local dir=$1
|
||
local description=${2:-"Directory"}
|
||
|
||
if [[ ! -d "$dir" ]]; then
|
||
if mkdir -p "$dir"; then
|
||
log_success "$description created: $dir"
|
||
else
|
||
log_error "Failed to create $description: $dir"
|
||
return 1
|
||
fi
|
||
else
|
||
log_info "$description already exists: $dir"
|
||
fi
|
||
}
|
||
|
||
# Backup file with timestamp
|
||
backup_file() {
|
||
local file=$1
|
||
local backup_dir=${2:-"./backups"}
|
||
|
||
if [[ -f "$file" ]]; then
|
||
create_directory "$backup_dir" "Backup directory"
|
||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||
local backup_file="$backup_dir/$(basename "$file").backup.$timestamp"
|
||
|
||
if cp "$file" "$backup_file"; then
|
||
log_success "File backed up: $file → $backup_file"
|
||
echo "$backup_file"
|
||
else
|
||
log_error "Failed to backup file: $file"
|
||
return 1
|
||
fi
|
||
else
|
||
log_warning "File not found for backup: $file"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Run command with timeout and logging
|
||
run_with_timeout() {
|
||
local timeout_duration=$1
|
||
local description=$2
|
||
shift 2
|
||
local command=("$@")
|
||
|
||
log_info "Running: $description"
|
||
log_debug "Command: ${command[*]}"
|
||
|
||
if timeout "$timeout_duration" "${command[@]}"; then
|
||
log_success "$description completed successfully"
|
||
return 0
|
||
else
|
||
local exit_code=$?
|
||
if [[ $exit_code -eq 124 ]]; then
|
||
log_error "$description timed out after ${timeout_duration}s"
|
||
else
|
||
log_error "$description failed with exit code $exit_code"
|
||
fi
|
||
return $exit_code
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# Summary and Reporting Functions
|
||
# =============================================================================
|
||
|
||
# Print execution summary
|
||
print_summary() {
|
||
local script_name=${1:-"Script"}
|
||
local end_time=$(date +%s)
|
||
local duration=$((end_time - START_TIME))
|
||
|
||
log_section "Execution Summary"
|
||
|
||
echo -e "Script: ${WHITE}$script_name${NC}"
|
||
echo -e "Duration: ${WHITE}${duration}s${NC}"
|
||
echo -e "Total checks: ${WHITE}$CHECKS${NC}"
|
||
echo -e "${GREEN}Successful: $((CHECKS - ERRORS - WARNINGS))${NC}"
|
||
echo -e "${YELLOW}Warnings: $WARNINGS${NC}"
|
||
echo -e "${RED}Errors: $ERRORS${NC}"
|
||
echo
|
||
|
||
if [[ $ERRORS -eq 0 ]]; then
|
||
if [[ $WARNINGS -eq 0 ]]; then
|
||
log_success "All checks passed! $script_name completed successfully."
|
||
return 0
|
||
else
|
||
log_warning "$script_name completed with warnings. Please review the warnings above."
|
||
return 0
|
||
fi
|
||
else
|
||
log_error "$script_name failed with $ERRORS errors. Please fix the errors above."
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# Environment and Configuration
|
||
# =============================================================================
|
||
|
||
# Load environment file if it exists
|
||
load_env_file() {
|
||
local env_file=${1:-.env}
|
||
|
||
if [[ -f "$env_file" ]]; then
|
||
log_info "Loading environment from: $env_file"
|
||
set -a
|
||
# shellcheck source=/dev/null
|
||
source "$env_file"
|
||
set +a
|
||
log_success "Environment loaded successfully"
|
||
else
|
||
log_warning "Environment file not found: $env_file"
|
||
fi
|
||
}
|
||
|
||
# Validate required environment variables
|
||
validate_env_vars() {
|
||
local vars=("$@")
|
||
local missing_vars=()
|
||
|
||
for var in "${vars[@]}"; do
|
||
if [[ -z "${!var:-}" ]]; then
|
||
missing_vars+=("$var")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#missing_vars[@]} -gt 0 ]]; then
|
||
log_error "Missing required environment variables: ${missing_vars[*]}"
|
||
return 1
|
||
else
|
||
log_success "All required environment variables are set"
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# Docker and Service Management
|
||
# =============================================================================
|
||
|
||
# Check if Docker is running
|
||
check_docker() {
|
||
if command_exists docker && docker info >/dev/null 2>&1; then
|
||
print_status "OK" "Docker is running"
|
||
return 0
|
||
else
|
||
print_status "ERROR" "Docker is not running or not accessible"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Check if docker-compose is available
|
||
check_docker_compose() {
|
||
if command_exists docker-compose; then
|
||
print_status "OK" "docker-compose is available"
|
||
return 0
|
||
elif docker compose version >/dev/null 2>&1; then
|
||
print_status "OK" "docker compose (plugin) is available"
|
||
return 0
|
||
else
|
||
print_status "ERROR" "Neither docker-compose nor docker compose is available"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Start Docker services with health check wait
|
||
start_docker_services() {
|
||
local services=("$@")
|
||
local compose_file=${COMPOSE_FILE:-docker-compose.yml}
|
||
|
||
log_info "Starting Docker services: ${services[*]}"
|
||
|
||
if docker-compose -f "$compose_file" up -d "${services[@]}"; then
|
||
log_success "Docker services started"
|
||
|
||
# Wait for services to be healthy
|
||
for service in "${services[@]}"; do
|
||
wait_for_service "docker-compose -f $compose_file ps $service | grep -q 'healthy\\|Up'" "$service" 120 10
|
||
done
|
||
else
|
||
log_error "Failed to start Docker services"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# Initialization
|
||
# =============================================================================
|
||
|
||
log_debug "Common utilities library loaded successfully"
|