meldestelle/scripts/utils/common.sh
2025-07-25 23:16:16 +02:00

463 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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