All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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>
562 lines
14 KiB
Bash
562 lines
14 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=''
|
||
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))"
|
||
}
|