diff --git a/TODO.md b/TODO.md index f7a5eaa..d627056 100644 --- a/TODO.md +++ b/TODO.md @@ -17,12 +17,10 @@ Generated by codebase audit (2026-06-10). Ranked by severity. ## CRITICAL BUGS (breaks primary use cases) -### B1 -- Infinite recursion in `services` on Linux [CRITICAL] +### B1 -- Infinite recursion in `services` on Linux [CRITICAL] -- RESOLVED - File: `services:25-46` -- `get_timezone_mount()` and `get_gpu_devices()` call themselves on the non-Darwin - branch via `echo "$(get_timezone_mount)"`. Hits bash FUNCNEST limit on every - Linux compose generation. Main `./hops` install is broken on Linux. -- Fix: replace the recursive calls with the literal YAML strings they should emit. +- `get_timezone_mount()` and `get_gpu_devices()` called themselves on the non-Darwin + branch. Fixed: both functions now return literal YAML strings directly. ### B2 -- Brace mismatch in `lib/privileges.sh` [CRITICAL] -- RESOLVED: delete file - File: `lib/privileges.sh:429,612` @@ -32,32 +30,22 @@ Generated by codebase audit (2026-06-10). Ranked by severity. ## HIGH BUGS -### B3 -- Glob stored as string, directory detection always fails [HIGH] +### B3 -- Glob stored as string, directory detection always fails [HIGH] -- RESOLVED - Files: `hops:154-166`, `uninstall:127-147` -- `homelab_dirs=( "/home/*/hops" )` stores a literal glob; the quoted for-loop - never expands it. Multi-user detection is broken, `cd "$HOMELAB_DIR"` fails - under `set -e`. -- Fix: iterate unquoted or use `compgen -G "/home/*/hops"`. +- Glob removed from array; expanded separately via `compgen -G "/home/*/hops"`. + Also fixed `eval echo "~$SUDO_USER"` -> `getent passwd` in `uninstall`. -### B4 -- Missing service definitions file reference [HIGH] +### B4 -- Missing service definitions file reference [HIGH] -- RESOLVED - File: `install:916` -- `setup_firewall()` sources `"$SCRIPT_DIR/hops_service_definitions.sh"` which - does not exist (the file is named `services`). Per-service firewall rules are - silently never applied. -- Fix: correct the filename to `services`. +- Corrected source path from `hops_service_definitions.sh` to `services`. -### B5 -- `((x++))` aborts script under `set -e` [HIGH] -- Files: `hops:299,317`, `install:784`, and others -- `((running_count++))` returns exit code 1 when the pre-increment value is 0, - which kills the script under `set -e`. -- Fix: use `running_count=$((running_count + 1))` or append `|| true`. +### B5 -- `((x++))` aborts script under `set -e` [HIGH] -- RESOLVED +- Files: `hops`, `install` +- All `((x++))` occurrences replaced with `x=$((x + 1))`. -### B6 -- `hops` entry point is Linux-only despite macOS library support [HIGH] -- File: `hops:108-136,263` -- `check_dependencies` requires `systemctl`, `check_system_requirements` calls - `free` and `df -BG`, `show_service_status` calls `systemctl`. All Linux-only. - The documented entry point fails immediately on macOS. -- Fix: add OS guards or document `hops` as Linux-only. +### B6 -- `hops` entry point is Linux-only despite macOS library support [HIGH] -- RESOLVED +- File: `hops` +- Added Linux-only guard at top of script; exits immediately with a clear error on non-Linux. ### B7 -- Port collisions not detected within a selection [HIGH] - File: `services` (port map) diff --git a/hops b/hops index df8e42d..85f9dee 100755 --- a/hops +++ b/hops @@ -7,6 +7,11 @@ # Exit on any error set -e +if [[ "$(uname -s)" != "Linux" ]]; then + echo "ERROR: HOPS requires Linux (Ubuntu 20.04+, Debian 11+, or Linux Mint 20+)." >&2 + exit 1 +fi + # Script version and metadata readonly SCRIPT_VERSION="1.0.0" readonly SCRIPT_NAME="HOPS" @@ -151,11 +156,11 @@ get_installation_status() { local status="not_installed" local homelab_dirs=( "$HOME/hops" - "/home/*/hops" "/opt/hops" "/srv/hops" ) - + while IFS= read -r d; do homelab_dirs+=("$d"); done < <(compgen -G "/home/*/hops" 2>/dev/null) + # Check for existing installation for dir in "${homelab_dirs[@]}"; do if [[ -f "$dir/docker-compose.yml" ]]; then @@ -291,14 +296,14 @@ show_service_status() { local service_port="${service_info#*:}" if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then - ((total_count++)) + total_count=$((total_count + 1)) local status_symbol="${GREEN}●${NC}" local status_text="Running" # Check if port is accessible if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then status_text="Running & Accessible" - ((running_count++)) + running_count=$((running_count + 1)) else status_text="Running (starting up)" status_symbol="${YELLOW}●${NC}" @@ -306,7 +311,7 @@ show_service_status() { printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port" elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then - ((total_count++)) + total_count=$((total_count + 1)) printf " %s %-20s %s\n" "${RED}●${NC}" "$service_name" "Stopped" fi done @@ -471,7 +476,7 @@ show_access_info() { if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then local url="http://${local_ip}:${service_port}${service_path}" printf " ${GREEN}●${NC} %-15s %s\n" "$service_name" "$url" - ((active_services++)) + active_services=$((active_services + 1)) fi done @@ -518,7 +523,7 @@ show_logs() { local date=$(stat -c %y "$log_file" | cut -d' ' -f1) printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date" - ((count++)) + count=$((count + 1)) done echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}" diff --git a/install b/install index b794389..552f422 100755 --- a/install +++ b/install @@ -333,7 +333,7 @@ EOF return 0 fi - ((attempt++)) + attempt=$((attempt + 1)) done # Fallback: construct a guaranteed compliant password @@ -913,8 +913,8 @@ EOF fi # Allow service ports based on selection - if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then - source "$SCRIPT_DIR/hops_service_definitions.sh" + if [[ -f "$SCRIPT_DIR/services" ]]; then + source "$SCRIPT_DIR/services" for svc in "${SERVICES[@]}"; do local ports=$(get_service_ports "$svc") @@ -1015,7 +1015,7 @@ EOF local main_port=$(echo $ports | cut -d' ' -f1) if [[ -n "$main_port" ]]; then echo " • $svc: http://$(get_primary_ip):$main_port" - ((service_count++)) + service_count=$((service_count + 1)) fi fi done diff --git a/services b/services index 6604628..275c910 100755 --- a/services +++ b/services @@ -24,24 +24,18 @@ EOF # Get timezone mount path for current platform get_timezone_mount() { if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS doesn't need timezone mount, use TZ environment variable echo "" else - # Linux timezone mount - echo "$(get_timezone_mount)" + echo " - /etc/localtime:/etc/localtime:ro" fi } # Get GPU device access for current platform get_gpu_devices() { if [[ "$(uname -s)" == "Darwin" ]]; then - # macOS doesn't support GPU passthrough to Docker containers echo "" else - # Linux GPU device access - cat </dev/null) + if [[ -n "$SUDO_USER" ]]; then - local user_home=$(eval echo "~$SUDO_USER") + local user_home + user_home=$(getent passwd "$SUDO_USER" | cut -d: -f6) POSSIBLE_DIRS=("$user_home/hops" "${POSSIBLE_DIRS[@]}") fi - + HOMELAB_DIR="" for dir in "${POSSIBLE_DIRS[@]}"; do if [[ -f "$dir/docker-compose.yml" ]]; then