Modeled after Calibr setup.sh pattern (~/src/calibr/scripts/setup.sh). Implemented (Foundation): - Platform detection (Ubuntu, Arch, macOS, Fedora) - Dependency checking and installation - Mode selection (Docker vs Native) - Interactive + non-interactive modes - Comprehensive logging (clean console + full trace to log file) - Common utility functions library (450+ lines) Features in common.sh: - Output formatting (colors, headers, success/error/warning) - User input (confirm, select_option) - Platform detection - Dependency checking (Docker, Node, pnpm, PostgreSQL) - Package installation (apt, pacman, dnf, brew) - Validation (URL, email, port, domain) - Secret generation (cryptographically secure) - .env file parsing and management - Port conflict detection - File backup with timestamps To Be Implemented (See scripts/README.md): - Complete configuration collection - .env generation with smart preservation - Port conflict detection - Password/secret generation - Authentik blueprint auto-configuration - Docker deployment execution - Post-install instructions Usage: ./scripts/setup.sh # Interactive ./scripts/setup.sh --help # Show options ./scripts/setup.sh --dry-run # Preview ./scripts/setup.sh --non-interactive # CI/CD Refs: Setup wizard issue (created) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
511 lines
12 KiB
Bash
511 lines
12 KiB
Bash
#!/bin/bash
|
||
# Common utility functions for Mosaic Stack setup
|
||
# Adapted from Calibr setup pattern
|
||
|
||
# ============================================================================
|
||
# Output Formatting
|
||
# ============================================================================
|
||
|
||
# Colors
|
||
if [[ -t 1 ]]; then
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m' # No Color
|
||
else
|
||
RED=''
|
||
GREEN=''
|
||
YELLOW=''
|
||
BLUE=''
|
||
CYAN='\033[0;36m'
|
||
BOLD=''
|
||
NC=''
|
||
fi
|
||
|
||
print_header() {
|
||
echo ""
|
||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${BOLD}${CYAN} $1${NC}"
|
||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
}
|
||
|
||
print_success() {
|
||
echo -e "${GREEN}✓${NC} $1"
|
||
}
|
||
|
||
print_error() {
|
||
echo -e "${RED}✗${NC} $1" >&2
|
||
}
|
||
|
||
print_warning() {
|
||
echo -e "${YELLOW}⚠${NC} $1"
|
||
}
|
||
|
||
print_info() {
|
||
echo -e "${BLUE}ℹ${NC} $1"
|
||
}
|
||
|
||
print_step() {
|
||
echo -e "${CYAN}→${NC} $1"
|
||
}
|
||
|
||
# ============================================================================
|
||
# User Input
|
||
# ============================================================================
|
||
|
||
confirm() {
|
||
local prompt="$1"
|
||
local default="${2:-n}"
|
||
local response
|
||
|
||
if [[ "$default" == "y" ]]; then
|
||
prompt="$prompt [Y/n]: "
|
||
else
|
||
prompt="$prompt [y/N]: "
|
||
fi
|
||
|
||
while true; do
|
||
read -r -p "$prompt" response
|
||
response=${response:-$default}
|
||
case "$response" in
|
||
[Yy]|[Yy][Ee][Ss])
|
||
return 0
|
||
;;
|
||
[Nn]|[Nn][Oo])
|
||
return 1
|
||
;;
|
||
*)
|
||
echo "Please answer yes or no."
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
select_option() {
|
||
local prompt="$1"
|
||
shift
|
||
local options=("$@")
|
||
local num_options=${#options[@]}
|
||
|
||
echo "$prompt"
|
||
for i in "${!options[@]}"; do
|
||
printf " %d) %s\n" "$((i + 1))" "${options[$i]}"
|
||
done
|
||
echo ""
|
||
|
||
local selection
|
||
while true; do
|
||
read -r -p "Enter selection [1-$num_options]: " selection
|
||
if [[ "$selection" =~ ^[0-9]+$ ]] && \
|
||
[ "$selection" -ge 1 ] && \
|
||
[ "$selection" -le "$num_options" ]; then
|
||
echo "${options[$((selection - 1))]}"
|
||
return 0
|
||
else
|
||
print_error "Invalid selection. Please enter a number between 1 and $num_options."
|
||
fi
|
||
done
|
||
}
|
||
|
||
# ============================================================================
|
||
# Platform Detection
|
||
# ============================================================================
|
||
|
||
detect_os() {
|
||
case "$OSTYPE" in
|
||
linux-gnu*)
|
||
if [[ -f /etc/os-release ]]; then
|
||
source /etc/os-release
|
||
case "$ID" in
|
||
ubuntu|debian)
|
||
echo "debian"
|
||
;;
|
||
arch|manjaro|endeavouros)
|
||
echo "arch"
|
||
;;
|
||
fedora|rhel|centos)
|
||
echo "fedora"
|
||
;;
|
||
*)
|
||
echo "linux"
|
||
;;
|
||
esac
|
||
else
|
||
echo "linux"
|
||
fi
|
||
;;
|
||
darwin*)
|
||
echo "macos"
|
||
;;
|
||
*)
|
||
echo "unknown"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
detect_package_manager() {
|
||
local os="$1"
|
||
|
||
case "$os" in
|
||
debian)
|
||
echo "apt"
|
||
;;
|
||
arch)
|
||
echo "pacman"
|
||
;;
|
||
fedora)
|
||
echo "dnf"
|
||
;;
|
||
macos)
|
||
if command -v brew >/dev/null 2>&1; then
|
||
echo "brew"
|
||
else
|
||
echo "none"
|
||
fi
|
||
;;
|
||
*)
|
||
echo "unknown"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
get_os_name() {
|
||
local os="$1"
|
||
|
||
case "$os" in
|
||
debian)
|
||
echo "Debian/Ubuntu"
|
||
;;
|
||
arch)
|
||
echo "Arch Linux"
|
||
;;
|
||
fedora)
|
||
echo "Fedora/RHEL"
|
||
;;
|
||
macos)
|
||
echo "macOS"
|
||
;;
|
||
*)
|
||
echo "Unknown OS"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================================
|
||
# Command and Dependency Checking
|
||
# ============================================================================
|
||
|
||
check_command() {
|
||
command -v "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
check_docker() {
|
||
check_command docker && docker info >/dev/null 2>&1
|
||
}
|
||
|
||
check_docker_compose() {
|
||
if docker compose version >/dev/null 2>&1; then
|
||
return 0
|
||
elif check_command docker-compose; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
check_docker_buildx() {
|
||
docker buildx version >/dev/null 2>&1
|
||
}
|
||
|
||
check_node() {
|
||
local min_version="${1:-18}"
|
||
if ! check_command node; then
|
||
return 1
|
||
fi
|
||
|
||
local node_version
|
||
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
|
||
|
||
if [[ "$node_version" -ge "$min_version" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
check_pnpm() {
|
||
check_command pnpm
|
||
}
|
||
|
||
check_postgres() {
|
||
check_command psql
|
||
}
|
||
|
||
check_sudo() {
|
||
if ! command -v sudo >/dev/null 2>&1; then
|
||
print_error "sudo is not installed"
|
||
return 1
|
||
fi
|
||
|
||
if ! sudo -n true 2>/dev/null; then
|
||
print_warning "This script may need elevated privileges"
|
||
print_info "You may be prompted for your password"
|
||
sudo -v
|
||
fi
|
||
}
|
||
|
||
# ============================================================================
|
||
# Package Installation
|
||
# ============================================================================
|
||
|
||
get_package_name() {
|
||
local pkg_manager="$1"
|
||
local package="$2"
|
||
|
||
case "$pkg_manager" in
|
||
apt)
|
||
case "$package" in
|
||
docker) echo "docker.io" ;;
|
||
docker-compose) echo "docker-compose" ;;
|
||
node) echo "nodejs" ;;
|
||
postgres) echo "postgresql postgresql-contrib" ;;
|
||
*) echo "$package" ;;
|
||
esac
|
||
;;
|
||
pacman)
|
||
case "$package" in
|
||
docker) echo "docker" ;;
|
||
docker-compose) echo "docker-compose" ;;
|
||
node) echo "nodejs" ;;
|
||
postgres) echo "postgresql" ;;
|
||
*) echo "$package" ;;
|
||
esac
|
||
;;
|
||
dnf)
|
||
case "$package" in
|
||
docker) echo "docker" ;;
|
||
docker-compose) echo "docker-compose" ;;
|
||
node) echo "nodejs" ;;
|
||
postgres) echo "postgresql-server" ;;
|
||
*) echo "$package" ;;
|
||
esac
|
||
;;
|
||
brew)
|
||
case "$package" in
|
||
docker) echo "docker" ;;
|
||
node) echo "node" ;;
|
||
postgres) echo "postgresql@17" ;;
|
||
*) echo "$package" ;;
|
||
esac
|
||
;;
|
||
*)
|
||
echo "$package"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
install_package() {
|
||
local pkg_manager="$1"
|
||
local package="$2"
|
||
|
||
print_step "Installing $package..."
|
||
|
||
case "$pkg_manager" in
|
||
apt)
|
||
sudo apt update && sudo apt install -y "$package"
|
||
;;
|
||
pacman)
|
||
sudo pacman -Sy --noconfirm "$package"
|
||
;;
|
||
dnf)
|
||
sudo dnf install -y "$package"
|
||
;;
|
||
brew)
|
||
brew install "$package"
|
||
;;
|
||
*)
|
||
print_error "Unknown package manager: $pkg_manager"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ============================================================================
|
||
# Validation Functions
|
||
# ============================================================================
|
||
|
||
validate_url() {
|
||
local url="$1"
|
||
if [[ "$url" =~ ^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$ ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
validate_email() {
|
||
local email="$1"
|
||
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
validate_port() {
|
||
local port="$1"
|
||
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
validate_domain() {
|
||
local domain="$1"
|
||
if [[ "$domain" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]{0,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$ ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ============================================================================
|
||
# Secret and Password Generation
|
||
# ============================================================================
|
||
|
||
generate_secret() {
|
||
local length="${1:-32}"
|
||
# Use /dev/urandom for cryptographically secure random
|
||
LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*()-_=+' </dev/urandom | head -c "$length"
|
||
}
|
||
|
||
generate_password() {
|
||
local length="${1:-16}"
|
||
# Passwords without special chars for easier manual entry
|
||
LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c "$length"
|
||
}
|
||
|
||
mask_value() {
|
||
local value="$1"
|
||
local length=${#value}
|
||
|
||
if [ "$length" -le 8 ]; then
|
||
echo "***${value: -2}"
|
||
else
|
||
echo "${value:0:3}...${value: -3}"
|
||
fi
|
||
}
|
||
|
||
is_placeholder() {
|
||
local value="$1"
|
||
|
||
# Check for common placeholder patterns
|
||
if [[ "$value" =~ ^\$\{.*\}$ ]] || \
|
||
[[ "$value" =~ ^(change-me|changeme|your-.*|example|placeholder|TODO|FIXME|xxx+)$ ]] || \
|
||
[[ "$value" =~ ^<.*>$ ]] || \
|
||
[[ -z "$value" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ============================================================================
|
||
# .env File Management
|
||
# ============================================================================
|
||
|
||
# Global associative array to store env values
|
||
declare -gA ENV_VALUES
|
||
|
||
parse_env_file() {
|
||
local env_file="$1"
|
||
|
||
if [[ ! -f "$env_file" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# Clear existing values
|
||
ENV_VALUES=()
|
||
|
||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||
# Skip comments and empty lines
|
||
[[ "$key" =~ ^#.*$ ]] && continue
|
||
[[ -z "$key" ]] && continue
|
||
|
||
# Remove leading/trailing whitespace
|
||
key=$(echo "$key" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||
value=$(echo "$value" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||
|
||
# Remove quotes if present
|
||
value="${value%\"}"
|
||
value="${value#\"}"
|
||
value="${value%\'}"
|
||
value="${value#\'}"
|
||
|
||
ENV_VALUES["$key"]="$value"
|
||
done < "$env_file"
|
||
}
|
||
|
||
get_env_value() {
|
||
local key="$1"
|
||
echo "${ENV_VALUES[$key]:-}"
|
||
}
|
||
|
||
set_env_value() {
|
||
local key="$1"
|
||
local value="$2"
|
||
ENV_VALUES["$key"]="$value"
|
||
}
|
||
|
||
# ============================================================================
|
||
# File Operations
|
||
# ============================================================================
|
||
|
||
backup_file() {
|
||
local file="$1"
|
||
if [[ -f "$file" ]]; then
|
||
local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||
cp "$file" "$backup"
|
||
print_success "Backed up $file to $backup"
|
||
fi
|
||
}
|
||
|
||
# ============================================================================
|
||
# Port Checking
|
||
# ============================================================================
|
||
|
||
check_port_in_use() {
|
||
local port="$1"
|
||
|
||
if command -v ss >/dev/null 2>&1; then
|
||
ss -tuln | grep -q ":${port} "
|
||
elif command -v netstat >/dev/null 2>&1; then
|
||
netstat -tuln | grep -q ":${port} "
|
||
elif command -v lsof >/dev/null 2>&1; then
|
||
lsof -i ":${port}" >/dev/null 2>&1
|
||
else
|
||
# Can't check, assume available
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
suggest_alternative_port() {
|
||
local base_port="$1"
|
||
local offset=100
|
||
|
||
for i in {1..10}; do
|
||
local alt_port=$((base_port + offset * i))
|
||
if ! check_port_in_use "$alt_port"; then
|
||
echo "$alt_port"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
echo "$((base_port + 10000))"
|
||
}
|