Files
hops/lib/system.sh
T
Stephen Klein cd30d45fbf Add Caddy reverse proxy support to HOPS
### New Features
- Added Caddy reverse proxy as a service option
- Proper Docker container configuration with ports 80, 443, 2019
- Health check monitoring via Caddy admin API
- Volume mounts for Caddyfile, site content, and data persistence
- Integration with existing service selection and categorization

### Configuration Scope
- HOPS provides: Container setup, volume mounts, networking, health checks
- User provides: Caddyfile configuration, routing rules, SSL settings
- Clear documentation about configuration responsibilities
- Example Caddyfile provided in README

### Documentation Updates
- Updated README.md with Caddy service listing and configuration guide
- Updated CLAUDE.md with Caddy in supported services
- Added comprehensive configuration scope documentation
- Updated version references to 3.2.0

### Technical Implementation
- Added generate_caddy() function to services file
- Integrated Caddy into service selection switch
- Added port mapping for conflict detection (80, 443, 2019)
- Categorized under proxy & security services
- Added to available services listing

This addition provides users with another reverse proxy option while
maintaining HOPS' philosophy of providing infrastructure while allowing
users to maintain control over their specific configuration needs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 05:29:25 -04:00

1164 lines
39 KiB
Bash

#!/bin/bash
# HOPS - System Validation Functions
# Functions for system checks, OS detection, and requirements validation
# Version: 3.2.0
# Source common functions
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$LIB_DIR/common.sh"
# Global variables for system info
OS_NAME=""
OS_VERSION=""
OS_NAME_LOWER=""
# Detect operating system
detect_os() {
info "🔍 Detecting operating system..."
# Check if we're on macOS
if [[ "$(uname -s)" == "Darwin" ]]; then
OS_NAME="macOS"
OS_VERSION=$(sw_vers -productVersion)
OS_NAME_LOWER="macos"
success "Detected supported OS: $OS_NAME $OS_VERSION"
return 0
fi
# Linux detection
if command_exists lsb_release; then
OS_NAME=$(lsb_release -is)
OS_VERSION=$(lsb_release -rs)
elif [[ -f /etc/os-release ]]; then
OS_NAME=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
else
error_exit "Unable to detect operating system"
fi
OS_NAME_LOWER=$(echo "$OS_NAME" | tr '[:upper:]' '[:lower:]')
# Validate supported OS
case "$OS_NAME_LOWER" in
ubuntu|debian|linuxmint|mint)
success "Detected supported OS: $OS_NAME $OS_VERSION"
;;
*)
error_exit "Unsupported OS: $OS_NAME $OS_VERSION. Only Ubuntu/Debian/Linux Mint/macOS are supported."
;;
esac
}
# Check system requirements
check_system_requirements() {
local min_ram_gb=${1:-2}
local min_disk_gb=${2:-10}
local target_dir="${3:-/}"
info "🔍 Checking system requirements..."
# Check architecture
local arch=$(uname -m)
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS supports both x86_64 and arm64 (Apple Silicon)
if [[ "$arch" != "x86_64" && "$arch" != "arm64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 and arm64 are supported on macOS."
fi
else
# Linux only supports x86_64
if [[ "$arch" != "x86_64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 is supported."
fi
fi
# Check RAM
local ram_gb
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS memory check
ram_gb=$(sysctl -n hw.memsize | awk '{print int($1/1024/1024/1024)}')
elif command_exists free; then
ram_gb=$(free -g | awk '/^Mem:/{print $2}')
else
ram_gb=$(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo)
fi
if [[ $ram_gb -lt $min_ram_gb ]]; then
error_exit "Insufficient RAM: ${ram_gb}GB detected, ${min_ram_gb}GB required"
fi
# Check disk space
local disk_avail_gb
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS disk space check
disk_avail_gb=$(df -g "$target_dir" | tail -n 1 | awk '{print $4}')
elif command_exists df; then
disk_avail_gb=$(df -BG --output=avail "$target_dir" | tail -n 1 | tr -d 'G')
else
error_exit "Unable to check disk space - 'df' command not available"
fi
if [[ $disk_avail_gb -lt $min_disk_gb ]]; then
error_exit "Insufficient disk space: ${disk_avail_gb}GB available in $target_dir, ${min_disk_gb}GB required"
fi
# Check CPU cores
local cpu_cores
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
cpu_cores=$(sysctl -n hw.ncpu)
else
cpu_cores=$(nproc)
fi
if [[ $cpu_cores -lt 2 ]]; then
warning "Only ${cpu_cores} CPU core(s) detected. 2+ cores recommended for optimal performance."
fi
success "System requirements met: ${ram_gb}GB RAM, ${disk_avail_gb}GB disk space, ${cpu_cores} CPU cores"
}
# Check if running in a container
check_container_environment() {
if [[ -f /.dockerenv ]] || grep -q 'container=docker' /proc/1/environ 2>/dev/null; then
warning "Running inside a container. Some features may not work correctly."
return 0
fi
return 1
}
# Check internet connectivity
check_internet() {
local test_urls=(
"google.com"
"github.com"
"docker.com"
)
info "🌐 Checking internet connectivity..."
for url in "${test_urls[@]}"; do
if ping -c 1 -W 5 "$url" >/dev/null 2>&1; then
success "Internet connectivity verified"
return 0
fi
done
error_exit "No internet connectivity detected. Please check your network connection."
}
# Check Docker requirements
check_docker_requirements() {
info "🐳 Checking Docker requirements..."
# Check if Docker is installed
if ! command_exists docker; then
warning "Docker not installed. Will be installed automatically."
return 1
fi
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
warning "Docker daemon not running. Will be started automatically."
return 1
fi
# Check Docker version
local docker_version=$(docker --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
local min_version="20.10.0"
if ! version_compare "$docker_version" "$min_version"; then
error_exit "Docker version $docker_version is too old. Minimum required: $min_version"
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available. Please install Docker Compose v2+"
fi
success "Docker requirements met"
return 0
}
# Compare version strings (returns 0 if version1 >= version2)
version_compare() {
local version1="$1"
local version2="$2"
# Convert versions to arrays
local IFS='.'
local -a ver1=($version1)
local -a ver2=($version2)
# Compare each component
for i in {0..2}; do
local v1=${ver1[$i]:-0}
local v2=${ver2[$i]:-0}
if [[ $v1 -gt $v2 ]]; then
return 0
elif [[ $v1 -lt $v2 ]]; then
return 1
fi
done
return 0
}
# Check if user has sudo privileges
check_sudo() {
if [[ $EUID -eq 0 ]]; then
return 0
fi
if ! sudo -n true 2>/dev/null; then
error_exit "This script requires sudo privileges. Please run with sudo or as root."
fi
return 0
}
# Get system timezone
get_system_timezone() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone detection
readlink /etc/localtime | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
ls -la /etc/localtime | awk '{print $NF}' | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
echo "UTC"
elif [[ -f /etc/timezone ]]; then
cat /etc/timezone
elif [[ -L /etc/localtime ]]; then
readlink /etc/localtime | sed 's|/usr/share/zoneinfo/||'
else
timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC"
fi
}
# Validate timezone
validate_timezone() {
local timezone="$1"
if [[ -z "$timezone" ]]; then
return 1
fi
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone validation
if [[ -f "/var/db/timezone/zoneinfo/$timezone" ]]; then
return 0
fi
else
# Linux timezone validation
if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then
return 0
fi
fi
return 1
}
# Check available storage space for specific path
check_storage_space() {
local path="$1"
local required_gb="$2"
# Create directory if it doesn't exist
mkdir -p "$path" 2>/dev/null || true
local available_gb
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
available_gb=$(df -g "$path" | tail -n 1 | awk '{print $4}')
else
available_gb=$(df -BG --output=avail "$path" | tail -n 1 | tr -d 'G')
fi
if [[ $available_gb -lt $required_gb ]]; then
error_exit "Insufficient storage space in $path: ${available_gb}GB available, ${required_gb}GB required"
fi
success "Storage space check passed: ${available_gb}GB available in $path"
}
# Check if directory is writable
check_directory_writable() {
local dir="$1"
# Try to create directory if it doesn't exist
if ! mkdir -p "$dir" 2>/dev/null; then
error_exit "Cannot create directory: $dir"
fi
# Check if writable
if ! [[ -w "$dir" ]]; then
error_exit "Directory not writable: $dir"
fi
return 0
}
# Get current user info (handles sudo correctly)
get_user_info() {
local -A user_info
if [[ -n "$SUDO_USER" ]]; then
user_info["username"]="$SUDO_USER"
user_info["uid"]=$(id -u "$SUDO_USER")
user_info["gid"]=$(id -g "$SUDO_USER")
user_info["home"]=$(eval echo "~$SUDO_USER")
else
user_info["username"]="$USER"
user_info["uid"]=$(id -u)
user_info["gid"]=$(id -g)
user_info["home"]="$HOME"
fi
# Return as key=value pairs
for key in "${!user_info[@]}"; do
echo "${key}=${user_info[$key]}"
done
}
# Check if firewall is available and configured
check_firewall() {
info "🔥 Checking firewall status..."
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS uses pfctl/firewall, but we'll skip automatic configuration
warning "macOS firewall detected. Automatic firewall configuration skipped."
info "💡 You may need to manually configure firewall rules if needed."
return 0
fi
if command_exists ufw; then
local ufw_status=$(ufw status | head -n1 | awk '{print $2}')
case "$ufw_status" in
"active")
success "UFW firewall is active"
return 0
;;
"inactive")
warning "UFW firewall is inactive. Will be configured automatically."
return 1
;;
*)
warning "UFW firewall status unknown: $ufw_status"
return 1
;;
esac
else
warning "UFW not installed. Will be installed automatically."
return 1
fi
}
# Comprehensive system check
run_system_checks() {
local min_ram_gb=${1:-2}
local min_disk_gb=${2:-10}
local target_dir="${3:-/}"
info "🔍 Running comprehensive system checks..."
check_root
detect_os
check_system_requirements "$min_ram_gb" "$min_disk_gb" "$target_dir"
check_internet
check_docker_requirements
# Skip firewall check for macOS (handled differently)
if [[ "$OS_NAME_LOWER" != "macos" ]]; then
check_firewall
fi
# Check for container environment (warning only)
check_container_environment
success "All system checks passed"
}
# Get platform-specific default paths
get_default_media_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# Use actual user, not root when running via sudo
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$USER"
fi
echo "/Users/$actual_user/hops/media"
else
# Use actual user, not root when running via sudo
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$USER"
fi
echo "/home/$actual_user/hops/media"
fi
}
get_default_config_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# Use actual user, not root when running via sudo
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$USER"
fi
echo "/Users/$actual_user/hops/config"
else
echo "/opt/appdata"
fi
}
get_default_homelab_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# Use actual user, not root when running via sudo
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$USER"
fi
echo "/Users/$actual_user/hops"
else
# Use actual user, not root when running via sudo
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$USER"
fi
echo "/home/$actual_user/hops"
fi
}
# Get Docker socket path for current platform
get_docker_socket_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/var/run/docker.sock"
else
echo "/var/run/docker.sock"
fi
}
# Package management abstraction
install_package() {
local package="$1"
if [[ -z "$package" ]]; then
error_exit "install_package requires a package name"
fi
info "📦 Installing package: $package"
case "$OS_NAME_LOWER" in
"macos")
if ! command_exists brew; then
error_exit "Homebrew not found. Please install Homebrew first: https://brew.sh/"
fi
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
sudo -u "$actual_user" brew install "$package"
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
apt-get update && apt-get install -y "$package"
;;
*)
error_exit "Unsupported OS for package installation: $OS_NAME"
;;
esac
success "Package installed: $package"
}
# Service management abstraction
start_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "start_service requires a service name"
fi
info "🚀 Starting service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# On macOS, Docker Desktop handles this
info "Docker Desktop should be started manually or via Docker Desktop app"
else
# Use launchctl for other services
launchctl start "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl start "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service started: $service"
}
# Check if service is running
is_service_running() {
local service="$1"
if [[ -z "$service" ]]; then
return 1
fi
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# Check if Docker daemon is responding
docker info >/dev/null 2>&1
else
# Check with launchctl
launchctl list | grep -q "$service"
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl is-active --quiet "$service"
;;
*)
return 1
;;
esac
}
# Enable service to start on boot
enable_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "enable_service requires a service name"
fi
info "⚙️ Enabling service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
info "Docker Desktop auto-start should be configured in Docker Desktop settings"
else
# Use launchctl for other services
launchctl enable "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl enable "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service enabled: $service"
}
# Get network interface IP address
get_primary_ip() {
local ip=""
case "$OS_NAME_LOWER" in
"macos")
# macOS network interface detection
ip=$(route get default | grep interface | awk '{print $2}' | head -1)
if [[ -n "$ip" ]]; then
ip=$(ifconfig "$ip" | grep 'inet ' | awk '{print $2}' | head -1)
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Linux network interface detection
ip=$(hostname -I | awk '{print $1}')
;;
*)
# Fallback method
ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
;;
esac
# Validate IP address
if is_valid_ip "$ip"; then
echo "$ip"
else
echo "localhost"
fi
}
# Remove existing Docker installation on Linux
remove_docker_linux() {
info "🗑️ Removing existing Docker installation..."
# Stop Docker service if running
if systemctl is-active --quiet docker; then
info "🛑 Stopping Docker service..."
systemctl stop docker
fi
# Stop Docker socket if running
if systemctl is-active --quiet docker.socket; then
info "🛑 Stopping Docker socket..."
systemctl stop docker.socket
fi
# Disable Docker service
if systemctl is-enabled --quiet docker; then
info "🔧 Disabling Docker service..."
systemctl disable docker
fi
# Remove Docker packages
info "🗑️ Removing Docker packages..."
apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
apt-get purge -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
# Remove Docker Compose standalone if installed
if [[ -f "/usr/local/bin/docker-compose" ]]; then
info "🗑️ Removing Docker Compose standalone..."
rm -f "/usr/local/bin/docker-compose"
fi
# Remove Docker data directories
info "🗑️ Removing Docker data directories..."
local docker_dirs=(
"/var/lib/docker"
"/var/lib/containerd"
"/etc/docker"
"/etc/containerd"
"/run/docker"
"/run/containerd"
"/opt/containerd"
)
for dir in "${docker_dirs[@]}"; do
if [[ -d "$dir" ]]; then
rm -rf "$dir"
fi
done
# Remove Docker group
if getent group docker >/dev/null 2>&1; then
info "🗑️ Removing Docker group..."
groupdel docker 2>/dev/null || true
fi
# Remove Docker repository
if [[ -f "/etc/apt/sources.list.d/docker.list" ]]; then
info "🗑️ Removing Docker repository..."
rm -f "/etc/apt/sources.list.d/docker.list"
fi
# Remove Docker GPG key
if [[ -f "/etc/apt/keyrings/docker.gpg" ]]; then
rm -f "/etc/apt/keyrings/docker.gpg"
fi
# Remove any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
# Clean up package manager cache
apt-get autoremove -y 2>/dev/null || true
apt-get autoclean 2>/dev/null || true
success "Docker removal completed"
}
# Remove existing Docker installation on macOS
remove_docker_macos() {
info "🗑️ Removing existing Docker installation..."
# Get the actual user (not root) for operations
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Stop Docker Desktop if running
if pgrep -f "Docker Desktop" >/dev/null 2>&1; then
info "🛑 Stopping Docker Desktop..."
sudo -u "$actual_user" osascript -e 'quit app "Docker Desktop"' 2>/dev/null || true
sleep 3
fi
# Remove Docker Desktop application
if [[ -d "/Applications/Docker.app" ]]; then
info "🗑️ Removing Docker Desktop application..."
rm -rf "/Applications/Docker.app"
fi
# Remove Docker CLI tools installed via Homebrew
if command_exists brew; then
info "🗑️ Removing Docker via Homebrew..."
sudo -u "$actual_user" brew uninstall --cask docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-compose 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-machine 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-buildx 2>/dev/null || true
sudo -u "$actual_user" brew uninstall containerd 2>/dev/null || true
fi
# Remove Docker data directories
local docker_dirs=(
"/Users/$actual_user/.docker"
"/Users/$actual_user/Library/Preferences/com.docker.docker.plist"
"/Users/$actual_user/Library/Saved Application State/com.electron.docker-frontend.savedState"
"/Users/$actual_user/Library/Group Containers/group.com.docker"
"/Users/$actual_user/Library/Containers/com.docker.docker"
"/Users/$actual_user/Library/Application Support/Docker Desktop"
"/Users/$actual_user/Library/Logs/Docker Desktop"
"/Users/$actual_user/Library/Preferences/com.electron.docker-frontend.plist"
"/Users/$actual_user/Library/Caches/com.docker.docker"
)
for dir in "${docker_dirs[@]}"; do
if [[ -e "$dir" ]]; then
info "🗑️ Removing: $dir"
rm -rf "$dir"
fi
done
# Remove Docker symlinks and binaries
local docker_links=(
"/usr/local/bin/docker"
"/usr/local/bin/docker-compose"
"/usr/local/bin/docker-machine"
"/usr/local/bin/docker-buildx"
"/usr/local/bin/containerd"
"/usr/local/bin/containerd-shim"
"/usr/local/bin/containerd-shim-runc-v2"
"/usr/local/bin/ctr"
"/usr/local/bin/runc"
"/usr/local/bin/docker-credential-desktop"
"/usr/local/bin/docker-credential-ecr-login"
"/usr/local/bin/docker-credential-osxkeychain"
"/usr/local/bin/kubectl"
"/usr/local/bin/kubectl.docker"
"/usr/local/bin/vpnkit"
"/usr/local/bin/com.docker.cli"
)
for link in "${docker_links[@]}"; do
if [[ -L "$link" ]] || [[ -f "$link" ]]; then
info "🗑️ Removing: $link"
rm -f "$link"
fi
done
# Kill any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f com.docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
success "Docker removal completed"
}
# Check existing Docker installation on macOS
check_existing_docker_macos() {
local docker_version=""
local docker_desktop_version=""
local installation_method=""
# Check for Docker command
if command_exists docker; then
docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
fi
# Check for Docker Desktop
if [[ -d "/Applications/Docker.app" ]]; then
docker_desktop_version=$(defaults read /Applications/Docker.app/Contents/Info.plist CFBundleShortVersionString 2>/dev/null || echo "unknown")
installation_method="Docker Desktop"
fi
# Check if installed via Homebrew
if command_exists brew; then
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
if sudo -u "$actual_user" brew list --cask docker >/dev/null 2>&1; then
installation_method="Homebrew Cask"
elif sudo -u "$actual_user" brew list docker >/dev/null 2>&1; then
installation_method="Homebrew"
fi
fi
# Return results
echo "docker_version=$docker_version"
echo "docker_desktop_version=$docker_desktop_version"
echo "installation_method=$installation_method"
}
# Install Docker for the current platform
install_docker() {
info "🐳 Installing Docker..."
case "$OS_NAME_LOWER" in
"macos")
# Check for existing Docker installation
local docker_info
docker_info=$(check_existing_docker_macos)
local docker_version=$(echo "$docker_info" | grep "docker_version=" | cut -d'=' -f2)
local docker_desktop_version=$(echo "$docker_info" | grep "docker_desktop_version=" | cut -d'=' -f2)
local installation_method=$(echo "$docker_info" | grep "installation_method=" | cut -d'=' -f2)
# If Docker is already installed, ask for confirmation to reinstall
if [[ -n "$docker_version" ]] || [[ -n "$docker_desktop_version" ]] || [[ -n "$installation_method" ]]; then
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker CLI version: $docker_version"
fi
if [[ -n "$docker_desktop_version" ]]; then
info " Docker Desktop version: $docker_desktop_version"
fi
if [[ -n "$installation_method" ]]; then
info " Installation method: $installation_method"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_macos
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker Desktop manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker Desktop."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Install fresh Docker Desktop
info "📦 Installing Docker Desktop for Mac..."
# Check if Homebrew is available
if ! command_exists brew; then
warning "Homebrew not found. Installing Homebrew first..."
# Get the actual user (not root) to install Homebrew
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Install Homebrew as the actual user, not root
sudo -u "$actual_user" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH for current session
if [[ -f "/opt/homebrew/bin/brew" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -f "/usr/local/bin/brew" ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
fi
# Install Docker Desktop via Homebrew Cask
info "📦 Installing Docker Desktop via Homebrew..."
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Check for and handle conflicting files
local conflicting_files=()
local potential_conflicts=(
"/usr/local/bin/compose-bridge"
"/usr/local/bin/docker"
"/usr/local/bin/docker-compose"
"/usr/local/bin/docker-credential-desktop"
"/usr/local/bin/docker-credential-osxkeychain"
"/usr/local/bin/hub-tool"
"/usr/local/bin/kubectl"
"/Applications/Docker.app"
"/usr/local/Caskroom/docker"
"/usr/local/Caskroom/docker-desktop"
"/opt/homebrew/Caskroom/docker"
"/opt/homebrew/Caskroom/docker-desktop"
)
for file in "${potential_conflicts[@]}"; do
if [[ -e "$file" ]]; then
conflicting_files+=("$file")
fi
done
if [[ ${#conflicting_files[@]} -gt 0 ]]; then
warning "⚠️ Conflicting Docker files detected:"
for file in "${conflicting_files[@]}"; do
info " - $file"
done
echo
read -p "❓ Do you want to remove these conflicting files before installing Docker? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
for file in "${conflicting_files[@]}"; do
info "🗑️ Removing conflicting file: $file"
if [[ -d "$file" ]]; then
rm -rf "$file" 2>/dev/null || {
warning "Failed to remove $file - you may need to remove it manually"
}
else
rm -f "$file" 2>/dev/null || {
warning "Failed to remove $file - you may need to remove it manually"
}
fi
done
success "Conflicting files removed"
else
warning "Keeping existing files. Installation may fail due to conflicts."
fi
fi
# Install Docker Desktop with force flag to overwrite existing files
info "📦 Installing Docker Desktop via Homebrew..."
sudo -u "$actual_user" brew install --cask docker --force
# Start Docker Desktop with user interaction guidance
info "🚀 Starting Docker Desktop..."
open -a "Docker Desktop"
echo
warning "📋 IMPORTANT: Docker Desktop First-Time Setup Required"
warning "=============================================="
info "Docker Desktop will now open and may require user interaction:"
info " 1. ✅ Click 'Open' if macOS asks to confirm opening Docker Desktop"
info " 2. ✅ Accept the Docker Desktop license agreement"
info " 3. ✅ Complete the Docker Desktop onboarding/tutorial"
info " 4. ✅ Sign in to Docker Hub (optional, can skip)"
info " 5. ✅ Configure Docker settings if prompted"
info " 6. ⚠️ Do NOT close Docker Desktop during setup"
echo
warning "The installation will wait for you to complete these steps..."
echo
# Wait for user to complete setup
read -p "❓ Press ENTER after you've completed the Docker Desktop setup and it's running..."
echo
# Wait for Docker to start
info "⏳ Verifying Docker Desktop is running..."
local max_wait=60
local wait_time=0
local docker_started=false
while [[ $wait_time -lt $max_wait ]]; do
# Try to connect to Docker daemon
if docker info >/dev/null 2>&1; then
docker_started=true
break
fi
sleep 2
((wait_time += 2))
echo -n "."
done
echo
if [[ "$docker_started" == true ]]; then
success "✅ Docker Desktop is running and ready!"
else
warning "❌ Docker Desktop doesn't appear to be running."
warning "Please ensure Docker Desktop is:"
info " 1. Completely started (not just the app, but the Docker engine)"
info " 2. Shows 'Docker Desktop is running' in the menu bar"
info " 3. The whale icon in the menu bar is solid (not animated)"
echo
read -p "❓ Try verification again? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
if docker info >/dev/null 2>&1; then
success "✅ Docker Desktop is now running!"
else
error_exit "Docker Desktop still not responding. Please resolve the issue and re-run HOPS installation."
fi
else
error_exit "Installation cancelled. Please ensure Docker Desktop is running and try again."
fi
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Check for existing Docker installation
if command_exists docker; then
local docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker version: $docker_version"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_linux
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker service manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Check for and handle conflicting files
local conflicting_files=()
local potential_conflicts=(
"/usr/bin/docker"
"/usr/bin/docker-compose"
"/usr/local/bin/docker"
"/usr/local/bin/docker-compose"
"/etc/docker"
"/var/lib/docker"
)
for file in "${potential_conflicts[@]}"; do
if [[ -e "$file" ]]; then
conflicting_files+=("$file")
fi
done
if [[ ${#conflicting_files[@]} -gt 0 ]]; then
warning "⚠️ Conflicting Docker files detected:"
for file in "${conflicting_files[@]}"; do
info " - $file"
done
echo
read -p "❓ Do you want to remove these conflicting files before installing Docker? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
for file in "${conflicting_files[@]}"; do
info "🗑️ Removing conflicting file: $file"
if [[ -d "$file" ]]; then
rm -rf "$file" 2>/dev/null || {
warning "Failed to remove $file - you may need to remove it manually"
}
else
rm -f "$file" 2>/dev/null || {
warning "Failed to remove $file - you may need to remove it manually"
}
fi
done
success "Conflicting files removed"
else
warning "Keeping existing files. Installation may fail due to conflicts."
fi
fi
# Install fresh Docker using the official script
info "📦 Installing Docker Engine..."
curl -fsSL https://get.docker.com | sh
# Add user to docker group if we're running with sudo
if [[ -n "$SUDO_USER" ]]; then
usermod -aG docker "$SUDO_USER"
fi
# Start and enable Docker service
start_service docker
enable_service docker
success "Docker installed and configured"
;;
*)
error_exit "Unsupported OS for Docker installation: $OS_NAME"
;;
esac
}
# Check if Docker is properly installed and running
check_docker_installation() {
info "🐳 Checking Docker installation..."
# Check if Docker command exists
if ! command_exists docker; then
return 1
fi
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
return 1
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
return 1
fi
success "Docker is properly installed and running"
return 0
}