Files
Christopher d241a437f3 Improve Docker downgrade robustness and version reporting (#72)
* Improve Docker downgrade robustness and version reporting

Enhances the downgrade_docker function to retry apt-get installs, verify package installation after errors, and provide more detailed status and version reporting for containerd. Also updates package hold/unhold logic to include docker-ce-rootless-extras and improves logging in both run.sh and test-script.sh for better diagnostics and validation.

* Refactor variable assignment and improve containerd version check

Refactored variable assignments in run.sh and test-script.sh to use separate declaration and assignment for better readability and shell compatibility. Updated the containerd version check in test-script.sh to accept both '1.7.28' and 'v1.7.28' formats.
2025-12-05 17:37:14 -08:00

2224 lines
63 KiB
Bash
Executable File

#!/usr/bin/env bash
# Test script for casaos-fix-docker-api-version
# This script simulates upgrading Docker to a newer version and then tests
# that the fix script can successfully downgrade it back to 24.0.7
#
# Usage:
# ./test-script.sh upgrade - Upgrade Docker to latest version (simulates the problem)
# ./test-script.sh test - Run the fix script and verify it works
# ./test-script.sh full - Do both: upgrade then test the fix
# ./test-script.sh status - Show current Docker version and API
#
set -o pipefail
# Initialize SUDO variable
if [ "$EUID" -eq 0 ]; then
SUDO=""
else
SUDO="sudo"
fi
# Check for non-interactive mode
if [ "$NON_INTERACTIVE" = "true" ]; then
echo "Running in NON-INTERACTIVE mode"
fi
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_header() {
echo ""
echo "=========================================="
echo "$1"
echo "=========================================="
echo ""
}
# Function to get Docker version
get_docker_version() {
if command -v docker &>/dev/null; then
local version=$(timeout 10 $SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo "")
echo "$version"
else
echo "not_installed"
fi
}
# Function to get Docker API version
get_docker_api_version() {
if command -v docker &>/dev/null; then
local api_version=$(timeout 10 $SUDO docker version --format '{{.Server.APIVersion}}' 2>/dev/null || echo "")
if [ -z "$api_version" ]; then
# Fallback method
api_version=$(timeout 10 $SUDO docker version 2>/dev/null | grep -A 5 "Server:" | grep "API version:" | awk '{print $3}' | head -n1 || echo "")
fi
echo "$api_version"
else
echo "not_installed"
fi
}
# Function to get dockerd binary version
get_dockerd_binary_version() {
if command -v dockerd &>/dev/null; then
dockerd --version 2>/dev/null | head -n1 | awk '{print $3}' | sed 's/,//'
else
echo "not_installed"
fi
}
# Function to show current status
show_status() {
print_header "Current Docker Status"
local docker_version=$(get_docker_version)
local api_version=$(get_docker_api_version)
local dockerd_version=$(get_dockerd_binary_version)
local containerd_pkg
containerd_pkg=$(dpkg -l 2>/dev/null | grep containerd.io | awk 'NR==1 {print $3}')
local containerd_bin
containerd_bin=$(containerd --version 2>/dev/null | awk '{print $3}')
echo "Docker daemon version: $docker_version"
echo "Docker API version: $api_version"
echo "dockerd binary version: $dockerd_version"
echo "containerd package: ${containerd_pkg:-unknown}"
echo "containerd binary: ${containerd_bin:-unknown}"
echo ""
# Show package versions
echo "Installed packages:"
dpkg -l | grep -E "docker-ce|containerd.io" | awk '{print " " $2 " = " $3}' 2>/dev/null || echo " No Docker packages found"
echo ""
# Show which docker binary
if command -v docker &>/dev/null; then
echo "Docker binary location: $(which docker)"
fi
if command -v dockerd &>/dev/null; then
echo "dockerd binary location: $(which dockerd)"
fi
echo ""
# Check if Docker is running
if $SUDO systemctl is-active --quiet docker 2>/dev/null; then
print_success "Docker service is running"
else
print_warning "Docker service is NOT running"
fi
# Check running processes
if pgrep -x dockerd >/dev/null 2>&1; then
echo ""
echo "Running dockerd process(es):"
pgrep -a dockerd
fi
echo ""
}
# Function to detect OS
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
VERSION_CODENAME=${VERSION_CODENAME:-$(lsb_release -cs 2>/dev/null)}
else
print_error "Cannot detect OS. This script supports Debian/Ubuntu-based systems."
exit 1
fi
}
# Function to check if CasaOS is installed
check_casaos_installed() {
if command -v casaos &>/dev/null; then
return 0
else
return 1
fi
}
# Function to restart CasaOS services
restart_casaos() {
print_info "Restarting CasaOS services..."
local CASA_SERVICES=(
"casaos-gateway.service"
"casaos-message-bus.service"
"casaos-user-service.service"
"casaos-local-storage.service"
"casaos-app-management.service"
"casaos.service"
)
# Stop services
for SERVICE in "${CASA_SERVICES[@]}"; do
if $SUDO systemctl is-active --quiet "$SERVICE" 2>/dev/null; then
$SUDO systemctl stop "$SERVICE" 2>/dev/null || true
fi
done
sleep 2
# Start services
for SERVICE in "${CASA_SERVICES[@]}"; do
if $SUDO systemctl is-enabled --quiet "$SERVICE" 2>/dev/null; then
$SUDO systemctl start "$SERVICE" 2>/dev/null || true
fi
done
sleep 2
print_success "CasaOS services restarted"
echo ""
}
# Function to apply Docker API override (for newer distros without Docker 24.0.x)
apply_docker_api_override() {
print_header "Applying Docker API Override Configuration"
print_info "This sets DOCKER_MIN_API_VERSION=1.24 to allow older CasaOS versions"
print_info "to work with newer Docker versions (27.x, 28.x, 29.x)"
echo ""
local override_dir="/etc/systemd/system/docker.service.d"
local override_file="$override_dir/override.conf"
# Create directory if it doesn't exist
if [ ! -d "$override_dir" ]; then
print_info "Creating directory: $override_dir"
$SUDO mkdir -p "$override_dir"
fi
# Create or update override.conf
print_info "Creating Docker service override configuration..."
$SUDO tee "$override_file" > /dev/null <<'EOF'
[Service]
Environment=DOCKER_MIN_API_VERSION=1.24
EOF
if [ -f "$override_file" ]; then
print_success "Created: $override_file"
echo ""
echo "Contents:"
$SUDO cat "$override_file" | sed 's/^/ /'
echo ""
else
print_error "Failed to create override file"
return 1
fi
# Reload systemd daemon
print_info "Reloading systemd daemon..."
$SUDO systemctl daemon-reload
echo ""
# Restart Docker
print_info "Restarting Docker service..."
$SUDO systemctl restart docker
sleep 3
echo ""
# Verify Docker is running
if $SUDO systemctl is-active --quiet docker; then
print_success "Docker service restarted successfully"
# Show current Docker info
local docker_version=$(get_docker_version)
local api_version=$(get_docker_api_version)
echo ""
echo "Current configuration:"
echo " Docker version: $docker_version"
echo " API version: $api_version"
echo ""
print_success "Docker API override applied successfully!"
print_info "CasaOS should now work with this Docker version"
else
print_error "Docker service failed to start after applying override"
return 1
fi
echo ""
return 0
}
# Function to remove Docker API override
remove_docker_api_override() {
print_header "Removing Docker API Override Configuration"
local override_file="/etc/systemd/system/docker.service.d/override.conf"
if [ ! -f "$override_file" ]; then
print_info "No override configuration found"
return 0
fi
print_info "Removing: $override_file"
$SUDO rm -f "$override_file"
# Reload systemd daemon
print_info "Reloading systemd daemon..."
$SUDO systemctl daemon-reload
# Restart Docker
print_info "Restarting Docker service..."
$SUDO systemctl restart docker
sleep 3
if $SUDO systemctl is-active --quiet docker; then
print_success "Docker API override removed successfully"
else
print_warning "Docker service may have issues after removing override"
fi
echo ""
return 0
}
# Function to upgrade Docker to latest version (simulate the problem)
upgrade_docker_to_latest() {
print_header "Upgrading Docker to Latest Version"
print_info "This simulates the problem where Docker gets upgraded to a newer version"
print_info "that is incompatible with CasaOS (API 1.45+)"
echo ""
detect_os
# Unhold packages if they're held
print_info "Unholding Docker packages..."
$SUDO apt-mark unhold docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras 2>/dev/null || true
echo ""
# Setup Docker repository if not already present
print_info "Setting up Docker repository..."
$SUDO apt-get update
if [ ! -f /etc/apt/keyrings/docker.asc ]; then
print_info "Adding Docker GPG key..."
$SUDO install -m 0755 -d /etc/apt/keyrings
$SUDO curl -fsSL https://download.docker.com/linux/${OS}/gpg -o /etc/apt/keyrings/docker.asc
$SUDO chmod a+r /etc/apt/keyrings/docker.asc
fi
if [ ! -f /etc/apt/sources.list.d/docker.list ]; then
print_info "Adding Docker repository..."
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${OS} \
${VERSION_CODENAME} stable" | \
$SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null
$SUDO apt-get update
fi
echo ""
# Install latest Docker
print_info "Installing latest Docker version..."
print_warning "This will upgrade Docker to the latest version (API 1.45+)"
echo ""
if $SUDO apt-get install -y --allow-downgrades \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin \
docker-ce-rootless-extras; then
print_success "Docker upgraded to latest version"
else
print_error "Failed to upgrade Docker"
return 1
fi
echo ""
# Restart Docker
print_info "Restarting Docker to ensure new version loads..."
$SUDO systemctl daemon-reload
$SUDO systemctl stop docker 2>/dev/null || true
$SUDO pkill -9 dockerd 2>/dev/null || true
sleep 2
$SUDO systemctl start docker
sleep 3
echo ""
# Verify upgrade
local new_version=$(get_docker_version)
local new_api=$(get_docker_api_version)
print_header "Upgrade Complete"
echo "New Docker version: $new_version"
echo "New API version: $new_api"
echo ""
if awk -v ver="$new_api" 'BEGIN {exit !(ver >= 1.45)}'; then
print_success "Successfully upgraded to newer Docker (API $new_api)"
print_warning "This version is incompatible with older CasaOS versions"
return 0
else
print_warning "Upgrade may not have worked as expected (API: $new_api)"
print_warning "Expected API 1.45 or higher"
return 1
fi
}
# Function to test the fix script
test_fix_script() {
print_header "Testing the Fix Script"
# Get current version before fix
local before_version=$(get_docker_version)
local before_api=$(get_docker_api_version)
print_info "Docker version before fix: $before_version"
print_info "API version before fix: $before_api"
echo ""
# Check if the fix script exists
if [ ! -f "./run.sh" ]; then
print_error "Fix script (run.sh) not found in current directory"
print_info "Please run this test script from the casaos-fix-docker-api-version directory"
return 1
fi
print_info "Running the fix script..."
echo ""
echo "=========================================="
# Run the fix script
if bash ./run.sh; then
echo "=========================================="
echo ""
print_success "Fix script completed"
else
local exit_code=$?
echo "=========================================="
echo ""
print_error "Fix script failed with exit code $exit_code"
return $exit_code
fi
# Wait a moment for Docker to stabilize
sleep 3
# Verify the fix
print_header "Verifying the Fix"
local after_version=$(get_docker_version)
local after_api=$(get_docker_api_version)
local dockerd_binary=$(get_dockerd_binary_version)
local containerd_pkg
containerd_pkg=$(dpkg -l 2>/dev/null | grep containerd.io | awk 'NR==1 {print $3}')
local containerd_bin
containerd_bin=$(containerd --version 2>/dev/null | awk '{print $3}')
echo "Docker version after fix: $after_version"
echo "API version after fix: $after_api"
echo "dockerd binary version: $dockerd_binary"
echo "containerd package: ${containerd_pkg:-unknown}"
echo "containerd binary: ${containerd_bin:-unknown}"
echo ""
# Check package versions
echo "Package versions:"
dpkg -l | grep -E "docker-ce|containerd.io" | awk '{print " " $2 " = " $3}'
echo ""
# Verify results
local success=true
# Check if version contains 28.x (28.0, 28.1, 28.5, etc.)
if echo "$after_version" | grep -q "^28\."; then
print_success "✓ Docker version is 28.x ($after_version)"
else
print_error "✗ Docker version is NOT 28.x (got: $after_version)"
success=false
fi
# Check if API is below 1.52 (Docker 28.x series, before breaking change)
# Use awk for decimal comparison (doesn't require bc)
if awk -v ver="$after_api" 'BEGIN {exit !(ver >= 1.47 && ver < 1.52)}'; then
print_success "✓ API version is compatible ($after_api - Docker 28.x, below 1.52 breaking change)"
else
print_error "✗ API version is NOT compatible (got: $after_api, expected: 1.47-1.51, below 1.52 breaking change)"
success=false
fi
# Check if dockerd binary is correct
if echo "$dockerd_binary" | grep -q "^28\."; then
print_success "✓ dockerd binary is version 28.x ($dockerd_binary)"
else
print_error "✗ dockerd binary is NOT 28.x (got: $dockerd_binary)"
success=false
fi
# Check containerd package/bin match expected 1.7.28
if echo "$containerd_pkg" | grep -q "1.7.28"; then
print_success "✓ containerd package is 1.7.28 (${containerd_pkg})"
else
print_error "✗ containerd package is not 1.7.28 (got: ${containerd_pkg:-unknown})"
success=false
fi
if echo "$containerd_bin" | grep -q "^v\?1\.7\.28"; then
print_success "✓ containerd binary is 1.7.28 (${containerd_bin})"
else
print_error "✗ containerd binary is not 1.7.28 (got: ${containerd_bin:-unknown})"
success=false
fi
# Check if Docker is running
if $SUDO systemctl is-active --quiet docker; then
print_success "✓ Docker service is running"
else
print_error "✗ Docker service is NOT running"
success=false
fi
# Check if packages are held
echo ""
echo "Checking if packages are held:"
local held_packages=$($SUDO apt-mark showhold | grep -E "docker-ce|containerd.io")
if [ -n "$held_packages" ]; then
print_success "✓ Docker packages are held:"
echo "$held_packages" | sed 's/^/ /'
else
print_warning "⚠ Docker packages are NOT held (they may auto-upgrade)"
fi
echo ""
if [ "$success" = true ]; then
print_header "Test Result: PASSED"
print_success "The fix script successfully installed Docker 28.x"
print_success "API version is compatible with CasaOS"
return 0
else
print_header "Test Result: FAILED"
print_error "The fix script did not successfully install Docker 28.x"
print_error "Please review the errors above"
return 1
fi
}
# Function to run full test
run_full_test() {
print_header "Full Test: Upgrade then Fix"
print_info "This will:"
print_info " 1. Upgrade Docker to latest version (simulate the problem)"
print_info " 2. Run the fix script to install Docker 28.x"
print_info " 3. Verify everything works correctly"
echo ""
if [ "$NON_INTERACTIVE" = "true" ]; then
REPLY="yes"
else
read -p "Do you want to continue? (yes/no): " -r REPLY
echo ""
fi
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]] && [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Test cancelled"
return 0
fi
# Step 1: Upgrade
if ! upgrade_docker_to_latest; then
print_error "Upgrade step failed"
return 1
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue with the fix script test..." -r
echo ""
fi
# Step 2: Test fix
if ! test_fix_script; then
print_error "Fix script test failed"
return 1
fi
return 0
}
# Function to install specific Docker version
install_docker_version() {
local version=$1
local description=$2
print_header "Installing Docker $version"
print_info "$description"
echo ""
detect_os
# Unhold packages if they're held
print_info "Unholding Docker packages..."
$SUDO apt-mark unhold docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras 2>/dev/null || true
echo ""
# Setup Docker repository if not already present
print_info "Setting up Docker repository..."
$SUDO apt-get update -qq
if [ ! -f /etc/apt/keyrings/docker.asc ]; then
print_info "Adding Docker GPG key..."
$SUDO install -m 0755 -d /etc/apt/keyrings
$SUDO curl -fsSL https://download.docker.com/linux/${OS}/gpg -o /etc/apt/keyrings/docker.asc
$SUDO chmod a+r /etc/apt/keyrings/docker.asc
fi
if [ ! -f /etc/apt/sources.list.d/docker.list ]; then
print_info "Adding Docker repository..."
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${OS} \
${VERSION_CODENAME} stable" | \
$SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null
$SUDO apt-get update -qq
fi
echo ""
# Query available versions
print_info "Looking for Docker $version in repository..."
local full_version=$(apt-cache madison docker-ce 2>/dev/null | grep "$version" | head -n1 | awk '{print $3}')
if [ -z "$full_version" ]; then
print_error "Could not find Docker version $version in repository"
print_info "Available versions:"
apt-cache madison docker-ce 2>/dev/null | head -n 10
return 1
fi
print_info "Found: $full_version"
echo ""
# Install specific version
print_info "Installing Docker $full_version..."
if $SUDO apt-get install -y --allow-downgrades \
docker-ce=$full_version \
docker-ce-cli=$full_version \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin \
docker-ce-rootless-extras; then
print_success "Docker $version installed successfully"
else
print_error "Failed to install Docker $version"
return 1
fi
echo ""
# Restart Docker
print_info "Restarting Docker to ensure new version loads..."
$SUDO systemctl daemon-reload
$SUDO systemctl stop docker 2>/dev/null || true
$SUDO pkill -9 dockerd 2>/dev/null || true
sleep 2
$SUDO systemctl start docker
sleep 3
echo ""
# Verify installation
local installed_version=$(get_docker_version)
local installed_api=$(get_docker_api_version)
print_header "Installation Complete"
echo "Installed Docker version: $installed_version"
echo "Installed API version: $installed_api"
echo ""
if echo "$installed_version" | grep -q "$version"; then
print_success "Successfully installed Docker $version (API $installed_api)"
else
print_warning "Version mismatch: expected $version, got $installed_version"
fi
# Check if CasaOS is installed and offer to restart it
if check_casaos_installed; then
echo ""
print_info "CasaOS is installed on this system"
if [ "$NON_INTERACTIVE" = "true" ]; then
RESTART_REPLY="yes"
else
read -p "Do you want to restart CasaOS services? (yes/no): " -r RESTART_REPLY
echo ""
fi
if [[ $RESTART_REPLY =~ ^[Yy][Ee][Ss]$ ]] || [[ $RESTART_REPLY =~ ^[Yy]$ ]]; then
restart_casaos
else
print_info "Skipping CasaOS restart"
echo ""
fi
fi
if echo "$installed_version" | grep -q "$version"; then
return 0
else
return 1
fi
}
# Function to test with API 1.45 (Docker 27.x)
test_api_1_45() {
print_header "Testing with API 1.45 (Docker 27.x)"
if ! install_docker_version "27." "API 1.45 - Docker 27.x series"; then
return 1
fi
echo ""
print_info "Docker 27.x installed. You can now test CasaOS behavior with this version."
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to run the fix script..." -r
echo ""
fi
test_fix_script
}
# Function to test with API 1.46 (Docker 28.x)
test_api_1_46() {
print_header "Testing with API 1.46 (Docker 28.x)"
if ! install_docker_version "28." "API 1.46 - Docker 28.x series"; then
return 1
fi
echo ""
print_info "Docker 28.x installed. You can now test CasaOS behavior with this version."
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to run the fix script..." -r
echo ""
fi
test_fix_script
}
# Function to test with API 1.47 (Docker 29.x)
test_api_1_47() {
print_header "Testing with API 1.47 (Docker 29.x)"
if ! install_docker_version "29." "API 1.47 - Docker 29.x series"; then
return 1
fi
echo ""
print_info "Docker 29.x installed. You can now test CasaOS behavior with this version."
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to run the fix script..." -r
echo ""
fi
test_fix_script
}
# Function to test GPG key conflict handling
test_gpg_key_conflicts() {
print_header "Testing GPG Key Conflict Handling"
print_info "This test simulates the GPG key conflict issue reported in Debian/OMV"
print_info "where multiple Docker GPG keys exist and cause repository conflicts"
echo ""
detect_os
# Create conflicting GPG keys
print_info "Creating conflicting Docker GPG keys..."
# Backup existing keys if present
local backup_dir="/tmp/docker-gpg-backup-$(date +%s)"
mkdir -p "$backup_dir"
if [ -f /etc/apt/keyrings/docker.asc ]; then
print_info "Backing up /etc/apt/keyrings/docker.asc"
$SUDO cp /etc/apt/keyrings/docker.asc "$backup_dir/"
fi
if [ -f /usr/share/keyrings/docker.gpg ]; then
print_info "Backing up /usr/share/keyrings/docker.gpg"
$SUDO cp /usr/share/keyrings/docker.gpg "$backup_dir/"
fi
# Download Docker GPG key
print_info "Downloading Docker GPG key..."
$SUDO install -m 0755 -d /etc/apt/keyrings
$SUDO install -m 0755 -d /usr/share/keyrings
$SUDO curl -fsSL https://download.docker.com/linux/${OS}/gpg -o /tmp/docker.gpg
# Create the conflict: place the same key in two locations
print_info "Creating conflicting GPG key locations..."
$SUDO cp /tmp/docker.gpg /etc/apt/keyrings/docker.asc
$SUDO cp /tmp/docker.gpg /usr/share/keyrings/docker.gpg
$SUDO chmod a+r /etc/apt/keyrings/docker.asc
$SUDO chmod a+r /usr/share/keyrings/docker.gpg
# Create a conflicting docker.list that references the old location
print_info "Creating conflicting Docker repository configuration..."
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/${OS} \
${VERSION_CODENAME} stable" | \
$SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null
# Try to update - this should fail with the conflict error
print_info "Testing that apt update fails with conflict..."
if $SUDO apt-get update 2>&1 | grep -q "Conflicting values set for option Signed-By"; then
print_success "✓ Confirmed: GPG key conflict error reproduced"
else
print_warning "⚠ Could not reproduce GPG key conflict (might already be fixed)"
fi
echo ""
print_info "Running fix script to resolve GPG key conflicts..."
echo ""
echo "=========================================="
# Run the fix script - it should handle the conflict
if bash ./run.sh; then
echo "=========================================="
echo ""
print_success "Fix script completed"
else
echo "=========================================="
echo ""
print_error "Fix script failed"
# Restore backups
print_info "Restoring GPG key backups..."
if [ -f "$backup_dir/docker.asc" ]; then
$SUDO cp "$backup_dir/docker.asc" /etc/apt/keyrings/
fi
if [ -f "$backup_dir/docker.gpg" ]; then
$SUDO cp "$backup_dir/docker.gpg" /usr/share/keyrings/
fi
return 1
fi
# Verify the fix
print_header "Verifying GPG Key Conflict Resolution"
# Check that only docker.asc exists in the correct location
local gpg_success=true
if [ -f /etc/apt/keyrings/docker.asc ]; then
print_success "✓ /etc/apt/keyrings/docker.asc exists"
else
print_error "✗ /etc/apt/keyrings/docker.asc missing"
gpg_success=false
fi
if [ ! -f /usr/share/keyrings/docker.gpg ]; then
print_success "✓ /usr/share/keyrings/docker.gpg removed (conflict resolved)"
else
print_error "✗ /usr/share/keyrings/docker.gpg still exists (conflict not resolved)"
gpg_success=false
fi
# Check that apt update works now
print_info "Testing apt update after fix..."
if $SUDO apt-get update >/dev/null 2>&1; then
print_success "✓ apt update works (no conflicts)"
else
print_error "✗ apt update still fails"
$SUDO apt-get update 2>&1 | tail -n 5
gpg_success=false
fi
# Check Docker installation
local docker_version=$(get_docker_version)
local api_version=$(get_docker_api_version)
echo ""
echo "Docker status after fix:"
echo " Version: $docker_version"
echo " API: $api_version"
echo ""
if echo "$docker_version" | grep -q "^28\."; then
print_success "✓ Docker 28.x installed successfully ($docker_version)"
else
print_error "✗ Docker version incorrect: $docker_version (expected 28.x)"
gpg_success=false
fi
if awk -v ver="$api_version" 'BEGIN {exit !(ver >= 1.47 && ver < 1.52)}'; then
print_success "✓ Docker API $api_version confirmed (Docker 28.x series, below 1.52 breaking change)"
else
print_error "✗ Docker API incorrect: $api_version (expected 1.47-1.51, below 1.52 breaking change)"
gpg_success=false
fi
# Cleanup
rm -rf "$backup_dir"
$SUDO rm -f /tmp/docker.gpg
echo ""
if [ "$gpg_success" = true ]; then
print_header "Test Result: PASSED"
print_success "GPG key conflict handling works correctly"
return 0
else
print_header "Test Result: FAILED"
print_error "GPG key conflict handling has issues"
return 1
fi
}
# Function to test GPG download resilience
test_gpg_download_resilience() {
print_header "Testing GPG Download Resilience"
print_info "This test verifies the GPG key download logic (retries + HTTP/1.1 fallback)"
echo ""
# Backup existing key
if [ -f /etc/apt/keyrings/docker.asc ]; then
print_info "Backing up existing GPG key..."
$SUDO cp /etc/apt/keyrings/docker.asc /tmp/docker.asc.backup
fi
# Remove key to force download
print_info "Removing GPG key to force download..."
$SUDO rm -f /etc/apt/keyrings/docker.asc
# Run the script
print_info "Running fix script..."
if bash ./run.sh; then
print_success "Script finished successfully"
else
print_error "Script failed"
# Restore backup if script failed
if [ -f /tmp/docker.asc.backup ]; then
$SUDO cp /tmp/docker.asc.backup /etc/apt/keyrings/docker.asc
fi
return 1
fi
# Verify key exists
if [ -f /etc/apt/keyrings/docker.asc ]; then
print_success "✓ GPG key downloaded successfully"
# Verify it's a valid key
if file /etc/apt/keyrings/docker.asc | grep -q "PGP public key"; then
print_success "✓ File is a valid PGP public key"
else
print_error "✗ File is not a valid PGP key"
return 1
fi
else
print_error "✗ GPG key failed to download"
# Restore backup
if [ -f /tmp/docker.asc.backup ]; then
$SUDO cp /tmp/docker.asc.backup /etc/apt/keyrings/docker.asc
fi
return 1
fi
# Clean up backup
rm -f /tmp/docker.asc.backup
return 0
}
# Function to test Snap daemon hang handling
test_snap_hang() {
print_header "Testing Snap Daemon Hang Handling"
print_info "This test directly tests the check_and_remove_snap_docker function"
print_info "It should timeout in 3 seconds and not hang forever"
echo ""
# Create mock snap command
local mock_dir="/tmp/mock-snap-$$"
mkdir -p "$mock_dir"
cat > "$mock_dir/snap" << 'EOF'
#!/bin/sh
# Simulate hang
sleep 10
exit 0
EOF
chmod +x "$mock_dir/snap"
# Save PATH
local old_path="$PATH"
export PATH="$mock_dir:$PATH"
print_info "Testing: timeout 3 snap list docker"
# Test the timeout command directly
local start_time=$(date +%s)
timeout 3 snap list docker >/dev/null 2>&1
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
# Restore PATH
export PATH="$old_path"
print_info "Command completed in $duration seconds with exit code $exit_code"
# Analyze results
if [ $exit_code -eq 124 ]; then
print_success "✓ Timeout occurred (exit code 124)"
else
print_error "✗ Expected exit code 124 (timeout), got $exit_code"
rm -rf "$mock_dir"
return 1
fi
if [ $duration -le 5 ]; then
print_success "✓ Timeout triggered in $duration seconds (expected ~3s)"
else
print_error "✗ Timeout took $duration seconds (expected ~3s)"
rm -rf "$mock_dir"
return 1
fi
# Now test the actual function behavior by sourcing and calling it
print_info "Testing actual check_and_remove_snap_docker function..."
# Extract just the function from run.sh
local test_script="/tmp/test-snap-func-$$.sh"
cat > "$test_script" << 'TESTEOF'
#!/bin/bash
set -o pipefail
SUDO=""
if [ "$EUID" -ne 0 ]; then
SUDO="sudo"
fi
check_and_remove_snap_docker() {
echo "Checking for Docker installed via Snap..."
if ! command -v snap &>/dev/null; then
echo "Snap is not installed on this system"
echo ""
return 0
fi
# Check if Docker is installed via snap
# Use timeout to prevent hanging if snapd is unresponsive
if timeout 3 snap list docker >/dev/null 2>&1; then
echo ""
echo "=========================================="
echo "WARNING: Docker installed via Snap detected!"
echo "=========================================="
echo ""
return 0
else
# Check if it was a timeout or just not found
local exit_code=$?
if [ $exit_code -eq 124 ]; then
echo "WARNING: 'snap list' timed out. Snap daemon might be unresponsive."
echo "Skipping Snap Docker check to prevent hanging."
echo ""
return 0
fi
echo "No Docker Snap package found"
echo ""
return 0
fi
}
check_and_remove_snap_docker
TESTEOF
chmod +x "$test_script"
# Run with mocked snap
export PATH="$mock_dir:$PATH"
start_time=$(date +%s)
local output
output=$($test_script 2>&1)
end_time=$(date +%s)
duration=$((end_time - start_time))
export PATH="$old_path"
print_info "Function test completed in $duration seconds"
if [ $duration -le 5 ]; then
print_success "✓ Function completed in $duration seconds (expected ~3s)"
else
print_error "✗ Function took $duration seconds (expected ~3s)"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
if echo "$output" | grep -q "WARNING: 'snap list' timed out"; then
print_success "✓ Function detected timeout and printed correct warning"
else
print_error "✗ Function did not detect timeout correctly"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
# Clean up
rm -rf "$mock_dir" "$test_script"
print_header "Test Result: PASSED"
print_success "Snap timeout handling works correctly"
return 0
}
# Function to test network namespace cleanup
test_netns_cleanup() {
print_header "Testing Network Namespace Cleanup"
print_info "This test verifies that the script handles 'device busy' errors"
print_info "when cleaning up Docker network namespaces"
echo ""
# Check if Docker is running
if ! $SUDO systemctl is-active --quiet docker; then
print_warning "Docker is not running, starting it..."
$SUDO systemctl start docker
sleep 3
fi
# Create some network namespaces by running containers
print_info "Creating Docker containers with network namespaces..."
local containers=()
for i in 1 2 3; do
local container_id=$(timeout 10 $SUDO docker run -d --rm --name test-netns-$i busybox sleep 300 2>/dev/null || echo "")
if [ -n "$container_id" ]; then
containers+=("test-netns-$i")
print_success "Created container: test-netns-$i"
else
print_warning "Could not create test container $i (busybox image may need to be pulled)"
fi
done
if [ ${#containers[@]} -eq 0 ]; then
print_warning "No containers created, trying to pull busybox..."
if timeout 60 $SUDO docker pull busybox >/dev/null 2>&1; then
print_success "Pulled busybox image"
# Try again
for i in 1 2 3; do
local container_id=$(timeout 10 $SUDO docker run -d --rm --name test-netns-$i busybox sleep 300 2>/dev/null || echo "")
if [ -n "$container_id" ]; then
containers+=("test-netns-$i")
print_success "Created container: test-netns-$i"
fi
done
fi
fi
echo ""
print_info "Containers created: ${#containers[@]}"
if [ ${#containers[@]} -eq 0 ]; then
print_warning "Could not create test containers, skipping netns cleanup test"
return 0
fi
# Check that network namespaces exist
if [ -d /var/run/docker/netns ]; then
local netns_count=$(find /var/run/docker/netns -type f 2>/dev/null | wc -l)
print_info "Network namespaces found: $netns_count"
fi
# Now try to clean Docker state while containers are running
# This should trigger the "device busy" error on /var/run/docker/netns/default
print_info "Stopping Docker while containers are running (simulates cleanup scenario)..."
$SUDO systemctl stop docker 2>/dev/null || true
sleep 2
# Try to clean up - this is what the script does
print_info "Attempting to clean /var/run/docker (should handle 'device busy' gracefully)..."
if [ -d /var/run/docker ]; then
# This should NOT fail even if netns is busy
if $SUDO find /var/run/docker -mindepth 1 -maxdepth 1 ! -path '/var/run/docker/netns' -delete 2>/dev/null; then
print_success "✓ Cleaned /var/run/docker (excluding netns)"
else
print_warning "⚠ Some files could not be cleaned"
fi
# Try to clean netns, but ignore "device busy" errors
if [ -d /var/run/docker/netns ]; then
$SUDO find /var/run/docker/netns -type f -delete 2>/dev/null || true
print_success "✓ Attempted netns cleanup (errors ignored)"
fi
fi
# Start Docker again
print_info "Starting Docker..."
$SUDO systemctl start docker
sleep 3
# Clean up test containers
print_info "Cleaning up test containers..."
for container in "${containers[@]}"; do
timeout 10 $SUDO docker stop "$container" 2>/dev/null || true
timeout 10 $SUDO docker rm -f "$container" 2>/dev/null || true
done
# Verify Docker is working
if $SUDO systemctl is-active --quiet docker; then
print_success "✓ Docker service is running after cleanup test"
else
print_error "✗ Docker service failed to start after cleanup test"
return 1
fi
# Try to run a simple container to verify functionality
print_info "Testing Docker functionality with hello-world..."
if timeout 60 $SUDO docker run --rm hello-world >/dev/null 2>&1; then
print_success "✓ Docker is fully functional after netns cleanup test"
else
print_warning "⚠ Docker may have issues after netns cleanup test"
fi
print_header "Test Result: PASSED"
print_success "Network namespace cleanup handled correctly"
print_info "The script properly handles 'device busy' errors during cleanup"
return 0
}
# Function to test all API versions
test_all_apis() {
print_header "Testing All API Versions Above 1.44"
print_info "This will test the fix script against multiple Docker versions:"
print_info " • API 1.45 (Docker 27.x)"
print_info " • API 1.46 (Docker 28.x)"
print_info " • API 1.47 (Docker 29.x)"
echo ""
if [ "$NON_INTERACTIVE" = "true" ]; then
REPLY="yes"
else
read -p "Do you want to continue? (yes/no): " -r REPLY
echo ""
fi
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]] && [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Test cancelled"
return 0
fi
local results=()
local failed=0
# Test API 1.45
print_info "=== Test 1 of 3: API 1.45 ==="
echo ""
if install_docker_version "27." "API 1.45 - Docker 27.x series"; then
if test_fix_script; then
results+=("✓ API 1.45 (Docker 27.x): PASSED")
else
results+=("✗ API 1.45 (Docker 27.x): FAILED")
failed=$((failed + 1))
fi
else
results+=("✗ API 1.45 (Docker 27.x): Installation FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test API 1.46
print_info "=== Test 2 of 3: API 1.46 ==="
echo ""
if install_docker_version "28." "API 1.46 - Docker 28.x series"; then
if test_fix_script; then
results+=("✓ API 1.46 (Docker 28.x): PASSED")
else
results+=("✗ API 1.46 (Docker 28.x): FAILED")
failed=$((failed + 1))
fi
else
results+=("✗ API 1.46 (Docker 28.x): Installation FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test API 1.47
print_info "=== Test 3 of 3: API 1.47 ==="
echo ""
if install_docker_version "29." "API 1.47 - Docker 29.x series"; then
if test_fix_script; then
results+=("✓ API 1.47 (Docker 29.x): PASSED")
else
results+=("✗ API 1.47 (Docker 29.x): FAILED")
failed=$((failed + 1))
fi
else
results+=("✗ API 1.47 (Docker 29.x): Installation FAILED")
failed=$((failed + 1))
fi
# Print summary
print_header "Test Summary: All API Versions"
echo "Results:"
for result in "${results[@]}"; do
echo " $result"
done
echo ""
if [ $failed -eq 0 ]; then
print_success "All tests PASSED!"
return 0
else
print_error "$failed test(s) FAILED"
return 1
fi
}
# Function to show usage
show_usage() {
echo "Usage: $0 [command]"
echo ""
echo "Commands:"
echo " status - Show current Docker version and API"
echo " upgrade - Upgrade Docker to latest (simulates the problem)"
echo " test - Run the fix script and verify it works"
echo " full - Run full test (upgrade + test fix)"
echo ""
echo "Test specific API versions:"
echo " api-1.45 - Test with API 1.45 (Docker 27.x)"
echo " api-1.46 - Test with API 1.46 (Docker 28.x)"
echo " api-1.47 - Test with API 1.47 (Docker 29.x)"
echo " test-all - Test all API versions above 1.44"
echo ""
echo "Bug fix tests (for reported issues):"
echo " test-gpg - Test GPG key conflict handling (Debian/OMV issue)"
echo " test-download - Test GPG download resilience (retries + HTTP/1.1)"
echo " test-snap - Test Snap daemon hang handling (timeout check)"
echo " test-netns - Test network namespace cleanup (device busy error)"
echo " test-casaos - Test CasaOS version check hang handling (timeout check)"
echo " test-dpkg - Test dpkg database hang handling (timeout check)"
echo " test-polling - Test Docker startup polling (two runs fix)"
echo " test-bugfixes - Run all bug fix tests"
echo ""
echo "Alternative fix for newer distros (Ubuntu 24.04+, Debian trixie):"
echo " apply-override - Apply Docker API override (DOCKER_MIN_API_VERSION=1.24)"
echo " remove-override - Remove Docker API override configuration"
echo ""
echo " help - Show this help message"
echo ""
echo "Example workflow:"
echo " 1. $0 status # Check current state"
echo " 2. $0 upgrade # Simulate the problem"
echo " 3. $0 status # Verify Docker was upgraded"
echo " 4. $0 test # Test the fix script"
echo ""
echo "Test specific API versions:"
echo " $0 api-1.45 # Install Docker 27.x and test"
echo " $0 api-1.46 # Install Docker 28.x and test"
echo " $0 api-1.47 # Install Docker 29.x and test"
echo ""
echo "Test bug fixes:"
echo " $0 test-gpg # Test GPG key conflict resolution (Debian/OMV)"
echo " $0 test-download # Test GPG download resilience"
echo " $0 test-snap # Test Snap hang handling"
echo " $0 test-netns # Test network namespace cleanup"
echo " $0 test-casaos # Test CasaOS version check hang handling"
echo " $0 test-dpkg # Test dpkg database hang handling"
echo " $0 test-polling # Test Docker startup polling (two runs fix)"
echo " $0 test-bugfixes # Run all bug fix tests"
echo ""
echo "Alternative fix (for distros without Docker 28.x):"
echo " $0 apply-override # Apply API override to work with newer Docker"
echo " $0 remove-override # Remove API override"
echo ""
echo "Or run all tests:"
echo " $0 test-all # Test against all API versions"
echo " $0 full # Standard full test (upgrade + fix)"
echo ""
}
# Function to test CasaOS version check hang
test_casaos_hang() {
print_header "Testing CasaOS Version Check Hang Handling"
print_info "This test verifies that the script handles a hanging 'casaos -v' command"
print_info "It should timeout in 5 seconds and not hang forever"
echo ""
# Create mock casaos command
local mock_dir="/tmp/mock-casaos-$$"
mkdir -p "$mock_dir"
cat > "$mock_dir/casaos" << 'EOF'
#!/bin/sh
# Simulate hang (like when CasaOS is in a broken state)
sleep 15
echo "v0.4.4"
exit 0
EOF
chmod +x "$mock_dir/casaos"
# Save PATH
local old_path="$PATH"
export PATH="$mock_dir:$PATH"
print_info "Testing: timeout 5 casaos -v"
# Test the timeout command directly
local start_time=$(date +%s)
timeout 5 casaos -v >/dev/null 2>&1
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
# Restore PATH
export PATH="$old_path"
print_info "Command completed in $duration seconds with exit code $exit_code"
# Analyze results
if [ $exit_code -eq 124 ]; then
print_success "✓ Timeout occurred (exit code 124)"
else
print_error "✗ Expected exit code 124 (timeout), got $exit_code"
rm -rf "$mock_dir"
return 1
fi
if [ $duration -le 7 ]; then
print_success "✓ Timeout triggered in $duration seconds (expected ~5s)"
else
print_error "✗ Timeout took $duration seconds (expected ~5s)"
rm -rf "$mock_dir"
return 1
fi
# Now test the actual function behavior by sourcing and calling it
print_info "Testing actual check_casaos function..."
# Extract just the function from run.sh
local test_script="/tmp/test-casaos-func-$$.sh"
cat > "$test_script" << 'TESTEOF'
#!/bin/bash
set -o pipefail
# Function to check if CasaOS is installed
check_casaos() {
if command -v casaos &>/dev/null; then
# Use timeout to prevent hanging if CasaOS is in a broken state
local version
version=$(timeout 5 casaos -v 2>/dev/null) || version="version unknown"
echo "CasaOS is installed: $version"
return 0
else
echo "CasaOS not detected."
return 1
fi
}
echo "Calling check_casaos..."
check_casaos
TESTEOF
chmod +x "$test_script"
# Run with mocked casaos
export PATH="$mock_dir:$PATH"
start_time=$(date +%s)
local output
output=$($test_script 2>&1)
end_time=$(date +%s)
duration=$((end_time - start_time))
export PATH="$old_path"
print_info "Function test completed in $duration seconds"
if [ $duration -le 7 ]; then
print_success "✓ Function completed in $duration seconds (expected ~5s)"
else
print_error "✗ Function took $duration seconds (expected ~5s)"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
if echo "$output" | grep -q "version unknown"; then
print_success "✓ Function detected timeout and set version to 'version unknown'"
else
print_error "✗ Function did not handle timeout correctly"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
if echo "$output" | grep -q "CasaOS is installed:"; then
print_success "✓ Function correctly identified CasaOS as installed"
else
print_error "✗ Function did not identify CasaOS as installed"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
# Clean up
rm -rf "$mock_dir" "$test_script"
print_header "Test Result: PASSED"
print_success "CasaOS version check timeout handling works correctly"
return 0
}
# Function to test unresponsive Docker daemon
test_unresponsive_daemon() {
print_header "Testing Unresponsive Docker Daemon Handling"
print_info "This test verifies that the script handles an unresponsive Docker daemon"
print_info "It should timeout in 10 seconds and not hang forever"
echo ""
# Create mock docker command
local mock_dir="/tmp/mock-docker-$$"
mkdir -p "$mock_dir"
cat > "$mock_dir/docker" << 'EOF'
#!/bin/sh
# Simulate hang
sleep 20
exit 0
EOF
chmod +x "$mock_dir/docker"
# Save PATH
local old_path="$PATH"
export PATH="$mock_dir:$PATH"
print_info "Testing: timeout 10 docker version"
# Test the timeout command directly
local start_time=$(date +%s)
timeout 10 docker version >/dev/null 2>&1
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
# Restore PATH
export PATH="$old_path"
print_info "Command completed in $duration seconds with exit code $exit_code"
# Analyze results
if [ $exit_code -eq 124 ]; then
print_success "✓ Timeout occurred (exit code 124)"
else
print_error "✗ Expected exit code 124 (timeout), got $exit_code"
rm -rf "$mock_dir"
return 1
fi
if [ $duration -le 12 ]; then
print_success "✓ Timeout triggered in $duration seconds (expected ~10s)"
else
print_error "✗ Timeout took $duration seconds (expected ~10s)"
rm -rf "$mock_dir"
return 1
fi
# Now test the actual function behavior by sourcing and calling it
print_info "Testing actual get_docker_api_version function..."
# Extract just the function from run.sh
local test_script="/tmp/test-docker-func-$$.sh"
cat > "$test_script" << 'TESTEOF'
#!/bin/bash
set -o pipefail
SUDO=""
if [ "$EUID" -ne 0 ]; then
SUDO="sudo"
fi
# Function to get Docker API version
get_docker_api_version() {
local api_version=""
# Try to get the API version from docker version command
if command -v docker &>/dev/null; then
# Get server API version
# Use timeout to prevent hanging if Docker daemon is unresponsive
local output
output=$(timeout 10 $SUDO docker version --format '{{.Server.APIVersion}}' 2>/dev/null)
local exit_code=$?
if [ $exit_code -eq 0 ]; then
api_version="$output"
elif [ $exit_code -eq 124 ]; then
# Timed out - do not try fallback as it will likely hang too
api_version=""
else
# Other error (e.g. format not supported), try fallback
api_version=""
fi
if [ -z "$api_version" ] && [ $exit_code -ne 124 ]; then
# Fallback: try to parse from docker version output
api_version=$(timeout 10 $SUDO docker version 2>/dev/null | grep -A 5 "Server:" | grep "API version:" | awk '{print $3}' | head -n1 || echo "")
fi
fi
echo "$api_version"
return 0
}
echo "Calling get_docker_api_version..."
get_docker_api_version
TESTEOF
chmod +x "$test_script"
# Run with mocked docker
export PATH="$mock_dir:$PATH"
start_time=$(date +%s)
local output
output=$($test_script 2>&1)
end_time=$(date +%s)
duration=$((end_time - start_time))
export PATH="$old_path"
print_info "Function test completed in $duration seconds"
if [ $duration -le 12 ]; then
print_success "✓ Function completed in $duration seconds (expected ~10s)"
else
print_error "✗ Function took $duration seconds (expected ~10s)"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
# Clean up
rm -rf "$mock_dir" "$test_script"
print_header "Test Result: PASSED"
print_success "Docker daemon timeout handling works correctly"
return 0
}
# Function to test dpkg database hang handling
test_dpkg_hang() {
print_header "Testing dpkg Database Hang Handling"
print_info "This test verifies that the script handles a hanging 'dpkg -l' command"
print_info "It should timeout in 10 seconds and not hang forever"
echo ""
# Declare local variables upfront to avoid SC2155 issues with $?
local mock_dir old_path which_dpkg start_time exit_code end_time duration output test_script
# Create mock dpkg command
mock_dir="/tmp/mock-dpkg-$$"
mkdir -p "$mock_dir"
cat > "$mock_dir/dpkg" << 'EOF'
#!/bin/sh
# Simulate dpkg database lock (hangs waiting for lock)
sleep 20
echo "docker-ce 28.0.0 amd64 Docker Engine"
exit 0
EOF
chmod +x "$mock_dir/dpkg"
# Save PATH and update it
old_path="$PATH"
export PATH="$mock_dir:$PATH"
# Clear bash's command hash table so it finds our mock
hash -r
print_info "Testing: timeout 10 dpkg -l (using mock at $mock_dir/dpkg)"
# Verify mock is being used
which_dpkg=$(which dpkg 2>/dev/null)
print_info "Using dpkg at: $which_dpkg"
# Test the timeout command directly using explicit path to mock
start_time=$(date +%s)
timeout 10 "$mock_dir/dpkg" -l >/dev/null 2>&1
exit_code=$?
end_time=$(date +%s)
duration=$((end_time - start_time))
# Restore PATH
export PATH="$old_path"
hash -r
print_info "Command completed in $duration seconds with exit code $exit_code"
# Analyze results
if [ $exit_code -eq 124 ]; then
print_success "✓ Timeout occurred (exit code 124)"
else
print_error "✗ Expected exit code 124 (timeout), got $exit_code"
rm -rf "$mock_dir"
return 1
fi
if [ $duration -le 12 ]; then
print_success "✓ Timeout triggered in $duration seconds (expected ~10s)"
else
print_error "✗ Timeout took $duration seconds (expected ~10s)"
rm -rf "$mock_dir"
return 1
fi
# Now test the actual function behavior by sourcing and calling it
print_info "Testing actual display_versions function..."
# Extract just the function from run.sh - but use explicit mock path
test_script="/tmp/test-dpkg-func-$$.sh"
cat > "$test_script" << TESTEOF
#!/bin/bash
set -o pipefail
SUDO=""
if [ "\$EUID" -ne 0 ]; then
SUDO="sudo"
fi
# Override dpkg to use our mock
dpkg() {
"$mock_dir/dpkg" "\$@"
}
# Function to display current versions (simplified for testing)
display_versions() {
echo "Current Docker versions:"
if command -v docker &>/dev/null; then
echo "(docker version output skipped for test)"
echo ""
# Also show installed package versions for clarity
echo "Installed Docker packages:"
timeout 10 dpkg -l 2>/dev/null | grep -E "docker-ce|containerd.io" | awk '{print \$2, \$3}' || echo "Unable to query package versions"
echo ""
else
echo "Docker command not found"
echo ""
fi
}
echo "Calling display_versions..."
display_versions
TESTEOF
chmod +x "$test_script"
# Create a mock docker command too (just to pass the command -v check)
cat > "$mock_dir/docker" << 'EOF'
#!/bin/sh
echo "Docker version 28.0.0"
exit 0
EOF
chmod +x "$mock_dir/docker"
# Run with mocked dpkg (and docker for the command -v check)
export PATH="$mock_dir:$PATH"
hash -r
start_time=$(date +%s)
output=$($test_script 2>&1)
end_time=$(date +%s)
duration=$((end_time - start_time))
export PATH="$old_path"
hash -r
print_info "Function test completed in $duration seconds"
if [ $duration -le 12 ]; then
print_success "✓ Function completed in $duration seconds (expected ~10s)"
else
print_error "✗ Function took $duration seconds (expected ~10s)"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
if echo "$output" | grep -q "Unable to query package versions"; then
print_success "✓ Function detected timeout and printed fallback message"
else
print_error "✗ Function did not handle timeout correctly"
echo "Output: $output"
rm -rf "$mock_dir" "$test_script"
return 1
fi
# Clean up
rm -rf "$mock_dir" "$test_script"
print_header "Test Result: PASSED"
print_success "dpkg database hang handling works correctly"
return 0
}
# Function to test Docker startup polling (fixes the "two runs needed" issue)
test_docker_startup_polling() {
print_header "Testing Docker Startup Polling"
print_info "This test verifies the fix for the 'two runs needed' bug"
print_info "The script now uses active polling to wait for Docker to be ready"
print_info "instead of fixed sleep times that were often too short"
echo ""
# Test 1: Verify the polling logic exists in run.sh
print_info "Test 1: Checking for active polling loops in run.sh..."
local run_script="./run.sh"
if [ ! -f "$run_script" ]; then
print_error "run.sh not found in current directory"
return 1
fi
# Check for the containerd polling loop
if grep -q "Waiting for containerd to be fully ready" "$run_script"; then
print_success "✓ Containerd polling loop found"
else
print_error "✗ Containerd polling loop NOT found"
print_info "Expected: 'Waiting for containerd to be fully ready'"
return 1
fi
# Check for the Docker polling loop with docker info verification
if grep -q "Waiting for Docker to be fully ready" "$run_script" && grep -q 'docker info' "$run_script"; then
print_success "✓ Docker polling loop with 'docker info' verification found"
else
print_error "✗ Docker polling loop with 'docker info' verification NOT found"
print_info "Expected: 'Waiting for Docker to be fully ready' and 'docker info' check"
return 1
fi
# Check for sleep after daemon-reload
if grep -A1 'systemctl daemon-reload' "$run_script" | grep -q 'sleep 2'; then
print_success "✓ Sleep after daemon-reload found (prevents race condition)"
else
print_warning "⚠ Sleep after daemon-reload may be missing or different"
fi
echo ""
# Test 2: Simulate slow Docker startup and verify polling works
print_info "Test 2: Testing polling behavior with simulated slow startup..."
local mock_dir="/tmp/mock-docker-polling-$$"
mkdir -p "$mock_dir"
# Create a state file to track docker info calls
local state_file="$mock_dir/docker_state"
echo "0" > "$state_file"
# Create mock docker that fails first few times, then succeeds
# Note: We use double quotes for the heredoc delimiter to allow variable expansion
cat > "$mock_dir/docker" << EOF
#!/bin/bash
STATE_FILE="$state_file"
if [ ! -f "\$STATE_FILE" ]; then
echo "0" > "\$STATE_FILE"
fi
# Read current call count
count=\$(cat "\$STATE_FILE")
count=\$((count + 1))
echo "\$count" > "\$STATE_FILE"
# For 'info' command: fail first 2 times, succeed after
if [ "\$1" = "info" ]; then
if [ \$count -le 2 ]; then
# Simulate Docker not ready yet
exit 1
else
# Docker is ready now
echo "Docker is ready"
exit 0
fi
fi
# For 'version' command: always succeed with mock version
if [ "\$1" = "version" ]; then
echo "Docker version 28.0.4, build 2a55bb0"
exit 0
fi
exit 0
EOF
chmod +x "$mock_dir/docker"
# Create mock systemctl
cat > "$mock_dir/systemctl" << 'EOF'
#!/bin/bash
# Always report docker as active
if [ "$1" = "is-active" ] && [ "$2" = "--quiet" ] && [ "$3" = "docker" ]; then
exit 0
fi
if [ "$1" = "is-active" ] && [ "$2" = "--quiet" ] && [ "$3" = "containerd" ]; then
exit 0
fi
# Pass through other commands
exit 0
EOF
chmod +x "$mock_dir/systemctl"
# Create test script that simulates the polling logic from run.sh
local test_script="$mock_dir/test_polling.sh"
cat > "$test_script" << 'TESTEOF'
#!/bin/bash
set -o pipefail
SUDO=""
echo "Testing Docker startup polling..."
echo ""
# Simulate the polling logic from run.sh
echo "Waiting for Docker to be fully ready..."
docker_ready=false
for i in {1..15}; do
if $SUDO systemctl is-active --quiet docker 2>/dev/null; then
if timeout 5 $SUDO docker info >/dev/null 2>&1; then
docker_ready=true
echo "✓ Docker is ready and responding (attempt $i)"
break
fi
fi
echo " Waiting for Docker... ($i/15)"
sleep 0.1 # Use short sleep for test speed
done
if [ "$docker_ready" = true ]; then
echo ""
echo "SUCCESS: Docker became ready after polling"
exit 0
else
echo ""
echo "FAILURE: Docker did not become ready"
exit 1
fi
TESTEOF
chmod +x "$test_script"
# Save PATH and run with mocks
local old_path="$PATH"
export PATH="$mock_dir:$PATH"
hash -r
local start_time=$(date +%s)
local output
output=$("$test_script" 2>&1)
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
# Restore PATH
export PATH="$old_path"
hash -r
echo "$output"
echo ""
if [ $exit_code -eq 0 ]; then
print_success "✓ Polling detected Docker became ready after retries"
else
print_error "✗ Polling failed to detect Docker readiness"
rm -rf "$mock_dir"
return 1
fi
# Verify multiple attempts were made
local final_count=$(cat "$state_file" 2>/dev/null || echo "0")
if [ "$final_count" -ge 3 ]; then
print_success "✓ Multiple polling attempts were made ($final_count calls to docker)"
else
print_warning "⚠ Expected at least 3 docker calls, got $final_count"
fi
echo ""
# Test 3: Verify that old fixed sleep times are replaced with polling
print_info "Test 3: Verifying fixed 'sleep 5' for Docker startup is removed..."
# The problematic pattern was "sleep 5 # Give Docker more time"
# This should be replaced with the polling loop
if grep -n 'sleep 5.*Give Docker more time' "$run_script" 2>/dev/null; then
print_error "✗ Old fixed 'sleep 5' pattern still exists"
print_info "This should be replaced with active polling"
rm -rf "$mock_dir"
return 1
else
print_success "✓ Old fixed 'sleep 5' pattern has been removed"
fi
# Check that polling loop exists in docker startup section
if grep -A30 "Enabling and starting Docker service" "$run_script" | grep -q "Waiting for Docker to be fully ready"; then
print_success "✓ Polling loop is in the correct location (after Docker service start)"
else
print_error "✗ Polling loop not found after Docker service start"
rm -rf "$mock_dir"
return 1
fi
# Cleanup
rm -rf "$mock_dir"
print_header "Test Result: PASSED"
print_success "Docker startup polling fix is correctly implemented"
print_info "The 'two runs needed' bug should now be fixed"
return 0
}
# Function to run all bug fix tests
test_all_bugfixes() {
print_header "Running All Bug Fix Tests"
print_info "This will test all bug fixes:"
print_info " 1. GPG key conflict handling"
print_info " 2. GPG download resilience"
print_info " 3. Snap daemon hang handling"
print_info " 4. Network namespace cleanup"
print_info " 5. Unresponsive Docker daemon handling"
print_info " 6. CasaOS version check hang handling"
print_info " 7. dpkg database hang handling"
print_info " 8. Docker startup polling (two runs fix)"
echo ""
local results=()
local failed=0
# Test GPG conflicts
print_info "=== Test 1 of 8: GPG Key Conflicts ==="
echo ""
if test_gpg_key_conflicts; then
results+=("✓ GPG key conflict handling: PASSED")
else
results+=("✗ GPG key conflict handling: FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test GPG download resilience
print_info "=== Test 2 of 8: GPG Download Resilience ==="
echo ""
if test_gpg_download_resilience; then
results+=("✓ GPG download resilience: PASSED")
else
results+=("✗ GPG download resilience: FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test Snap hang
print_info "=== Test 3 of 8: Snap Daemon Hang ==="
echo ""
if test_snap_hang; then
results+=("✓ Snap daemon hang handling: PASSED")
else
results+=("✗ Snap daemon hang handling: FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test netns cleanup
print_info "=== Test 4 of 8: Network Namespace Cleanup ==="
echo ""
if test_netns_cleanup; then
results+=("✓ Network namespace cleanup: PASSED")
else
results+=("✗ Network namespace cleanup: FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test unresponsive daemon
print_info "=== Test 5 of 8: Unresponsive Docker Daemon ==="
echo ""
if test_unresponsive_daemon; then
results+=("✓ Unresponsive daemon handling: PASSED")
else
results+=("✗ Unresponsive daemon handling: FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test CasaOS hang
print_info "=== Test 6 of 8: CasaOS Version Check Hang ==="
echo ""
if test_casaos_hang; then
results+=("✓ CasaOS version check hang handling: PASSED")
else
results+=("✗ CasaOS version check hang handling: FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test dpkg hang
print_info "=== Test 7 of 8: dpkg Database Hang ==="
echo ""
if test_dpkg_hang; then
results+=("✓ dpkg database hang handling: PASSED")
else
results+=("✗ dpkg database hang handling: FAILED")
failed=$((failed + 1))
fi
echo ""
if [ "$NON_INTERACTIVE" != "true" ]; then
read -p "Press Enter to continue to next test..." -r
echo ""
fi
# Test Docker startup polling (two runs fix)
print_info "=== Test 8 of 8: Docker Startup Polling (Two Runs Fix) ==="
echo ""
if test_docker_startup_polling; then
results+=("✓ Docker startup polling: PASSED")
else
results+=("✗ Docker startup polling: FAILED")
failed=$((failed + 1))
fi
# Print summary
print_header "Bug Fix Test Summary"
echo "Results:"
for result in "${results[@]}"; do
echo " $result"
done
echo ""
if [ $failed -eq 0 ]; then
print_success "All bug fix tests PASSED!"
return 0
else
print_error "$failed bug fix test(s) FAILED"
return 1
fi
}
# Main script
main() {
if [ $# -eq 0 ]; then
show_usage
exit 0
fi
case "$1" in
status)
show_status
;;
upgrade)
upgrade_docker_to_latest
;;
test)
test_fix_script
;;
full)
run_full_test
;;
api-1.45|api-1-45|api145)
test_api_1_45
;;
api-1.46|api-1-46|api146)
test_api_1_46
;;
api-1.47|api-1-47|api147)
test_api_1_47
;;
test-all|all)
test_all_apis
;;
test-gpg|gpg|test-gpg-conflicts)
test_gpg_key_conflicts
;;
test-download|download|test-gpg-download)
test_gpg_download_resilience
;;
test-snap|snap|test-snap-hang)
test_snap_hang
;;
test-netns|netns|test-network-namespace)
test_netns_cleanup
;;
test-casaos|casaos|test-casaos-hang)
test_casaos_hang
;;
test-dpkg|dpkg|test-dpkg-hang)
test_dpkg_hang
;;
test-polling|polling|test-docker-polling|two-runs)
test_docker_startup_polling
;;
test-bugfixes|bugfixes|bug-fixes)
test_all_bugfixes
;;
apply-override|override)
apply_docker_api_override
;;
remove-override|no-override)
remove_docker_api_override
;;
help|--help|-h)
show_usage
;;
*)
print_error "Unknown command: $1"
echo ""
show_usage
exit 1
;;
esac
}
# Run main function
main "$@"