cd30d45fbf
### 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>
1164 lines
39 KiB
Bash
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
|
|
} |