Files
stack/scripts/lib/common.sh
Jason Woltje e63c19d158
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
chore: Cleanup QA reports and improve setup scripts
Scripts:
- common.sh: Fix select_option to use /dev/tty for interactive prompts
- common.sh: Improve check_docker with detailed error messages
- setup.sh: Add Traefik configuration options
- setup.sh: Add argument validation for --mode, --external-authentik, etc.
- setup.sh: Add fun taglines

QA Reports:
- Remove stale remediation reports
- Keep current pending reports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 22:53:47 -06:00

562 lines
14 KiB
Bash
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 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=''
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[@]}
# Output UI to /dev/tty so it's visible even when function output is captured
echo "$prompt" >/dev/tty
for i in "${!options[@]}"; do
printf " %d) %s\n" "$((i + 1))" "${options[$i]}" >/dev/tty
done
echo "" >/dev/tty
local selection
while true; do
read -r -p "Enter selection [1-$num_options]: " selection </dev/tty >/dev/tty
if [[ "$selection" =~ ^[0-9]+$ ]] && \
[ "$selection" -ge 1 ] && \
[ "$selection" -le "$num_options" ]; then
# Only output the selected value to stdout (for capture)
echo "${options[$((selection - 1))]}"
return 0
else
print_error "Invalid selection. Please enter a number between 1 and $num_options." >/dev/tty
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() {
if ! check_command docker; then
return 1
fi
# Check if daemon is accessible
if docker info >/dev/null 2>&1; then
return 0
fi
# Docker exists but daemon not accessible
# This could be permission issue or daemon not running
local error_msg
error_msg=$(docker info 2>&1)
if [[ "$error_msg" =~ "permission denied" ]]; then
print_warning "Docker installed but permission denied"
print_info "You may need to add your user to the docker group:"
print_info " sudo usermod -aG docker \$USER"
print_info " Then log out and back in"
return 2 # Special code for permission issue
elif [[ "$error_msg" =~ "Cannot connect to the Docker daemon" ]]; then
print_warning "Docker installed but daemon not running"
print_info "Start it with: sudo systemctl start docker"
return 3 # Special code for daemon not running
else
print_warning "Docker installed but not accessible"
return 4 # Unknown issue
fi
}
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)
# Don't quote $package to allow multi-word package names
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"
# Allow single-character subdomains and properly validate domain structure
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
}
validate_ipv4() {
local ip="$1"
local IFS='.'
local -a octets
read -ra octets <<< "$ip"
# Must have exactly 4 octets
[[ ${#octets[@]} -eq 4 ]] || return 1
# Each octet must be 0-255
for octet in "${octets[@]}"; do
# Must be numeric
[[ "$octet" =~ ^[0-9]+$ ]] || return 1
# Must be in range 0-255
(( octet >= 0 && octet <= 255 )) || return 1
done
return 0
}
# ============================================================================
# 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))"
}