Files
hops/lib/system.sh
T
Stephen Klein d2e9a69313 Fix Docker repository issues with debug tracing and cleanup order
- Add comprehensive debug output to trace Docker installation execution paths
- Fix early return issue that was skipping Linux Mint repository fix
- Reorder Docker removal to clean repositories before running apt commands
- Add specific cleanup for Linux Mint codenames (xia, vera, vanessa)
- Prevent apt errors during existing Docker removal process

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-20 07:24:23 -04:00

1227 lines
42 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() {
echo "DEBUG: remove_docker_linux() function called"
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 repository and GPG keys FIRST to prevent apt errors
info "🗑️ Removing Docker repository and keys..."
rm -f /etc/apt/sources.list.d/docker.list
rm -f /etc/apt/sources.list.d/*docker*
rm -f /etc/apt/keyrings/docker.gpg
rm -f /usr/share/keyrings/docker*
rm -f /etc/apt/keyrings/docker*
# Also check for and remove any repositories that might contain Linux Mint codenames
if grep -r "download.docker.com.*xia\|download.docker.com.*vera\|download.docker.com.*vanessa" /etc/apt/sources.list.d/ 2>/dev/null; then
info "🗑️ Found Docker repositories with Linux Mint codenames, removing..."
grep -l "download.docker.com.*xia\|download.docker.com.*vera\|download.docker.com.*vanessa" /etc/apt/sources.list.d/* 2>/dev/null | xargs rm -f
fi
# Clean apt cache to remove any cached repository data
apt-get clean
# 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
# Repository and GPG keys already removed above
# 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() {
echo "DEBUG: install_docker() function called from lib/system.sh"
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")
echo "DEBUG: Linux/Ubuntu/Mint Docker installation path"
# Check for existing Docker installation
if command_exists docker; then
echo "DEBUG: Existing Docker installation detected"
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"
echo "DEBUG: Returning early - skipping Linux Mint repository fix"
return 0
fi
fi
echo "DEBUG: No existing Docker found, proceeding with fresh installation"
# 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 manual repository setup
info "📦 Installing Docker Engine..."
# Install required packages
apt-get update
apt-get install -y ca-certificates curl gnupg lsb-release
# Add Docker GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repository with proper Ubuntu codename detection for Linux Mint
local ubuntu_codename
echo "DEBUG: Configuring Docker repository"
echo "DEBUG: Detected OS: $(lsb_release -is)"
if [[ "$(lsb_release -is)" == "LinuxMint" ]]; then
echo "DEBUG: Linux Mint detected, checking for UBUNTU_CODENAME"
# Linux Mint provides UBUNTU_CODENAME in /etc/os-release
if [[ -f /etc/os-release ]]; then
ubuntu_codename=$(grep '^UBUNTU_CODENAME=' /etc/os-release | cut -d= -f2)
echo "DEBUG: Found UBUNTU_CODENAME=$ubuntu_codename in /etc/os-release"
fi
# Fallback to version mapping if UBUNTU_CODENAME not found
if [[ -z "$ubuntu_codename" ]]; then
case "$(lsb_release -rs)" in
"22"|"22.1"|"22.2"|"22.3")
ubuntu_codename="noble" # Ubuntu 24.04
;;
"21"|"21.1"|"21.2"|"21.3")
ubuntu_codename="jammy" # Ubuntu 22.04
;;
"20"|"20.1"|"20.2"|"20.3")
ubuntu_codename="focal" # Ubuntu 20.04
;;
*)
ubuntu_codename="noble" # Default to latest LTS
;;
esac
fi
else
ubuntu_codename=$(lsb_release -cs)
fi
info "Using Ubuntu codename: $ubuntu_codename for Docker repository"
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $ubuntu_codename stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update package index with Docker packages
apt-get update
# Install Docker
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 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
}