Consolidate duplicate functions, bump to v1.0.1

- Remove duplicate log/error_exit/warning/success/info from hops and
  uninstall; remove validate_password, generate_secure_password,
  create_docker_networks, validate_timezone from install. Single
  canonical copies now live in lib/common.sh, lib/security.sh,
  lib/validation.sh, and lib/docker.sh (A5, Q1)
- Fix lib/docker.sh, lib/validation.sh, lib/security.sh to use LIB_DIR
  instead of SCRIPT_DIR so sourcing them inside a function does not
  clobber the caller's SCRIPT_DIR
- Move SCRIPT_VERSION to lib/common.sh as the single source of truth;
  remove local declarations from hops, install, and uninstall
- uninstall now sources lib/common.sh directly for standalone safety
- validate_timezone updated to warn-and-default instead of error_exit
- validate_password updated to handle empty input (return 3)
- Update CHANGELOG and TODO to reflect resolved items (B1-B6, A1, A3,
  A5, Q1) and bump version to 1.0.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Stephen Klein
2026-06-10 22:11:34 -04:00
parent a7c38cd58d
commit 3cba0998a7
9 changed files with 87 additions and 213 deletions
+29 -22
View File
@@ -7,32 +7,39 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Removed ---
- Path B install pipeline (setup, privileged-setup, user-operations,
services-improved, lib/privileges.sh) -- dead code, never wired in ## [1.0.1] - 2026-06-10
Stabilization pass: bug fixes, security hardening, and codebase consolidation
on the Path A install pipeline.
### Fixed
- Infinite recursion in get_timezone_mount() and get_gpu_devices() on Linux (B1)
- ((x++)) abort under set -e across hops and install (B5)
- Glob stored as string breaking multi-user directory detection (B3)
- Missing hops_service_definitions.sh reference in firewall setup (B4)
- Linux-only guard missing from hops entry point (B6)
- eval echo "~$SUDO_USER" replaced with getent passwd in uninstall (B3/S5)
### Changed ### Changed
- Version reset to 1.0.0 - Duplicate log, error_exit, warning, success, info removed from hops,
uninstall, and install; single canonical copies in lib/common.sh (A5, Q1)
- Duplicate validate_password, generate_secure_password, create_docker_networks,
validate_timezone removed from install; lib/security.sh, lib/docker.sh,
lib/validation.sh are now the sole definitions (A5)
- lib/docker.sh, lib/validation.sh, lib/security.sh use LIB_DIR instead of
SCRIPT_DIR so sourcing them inside a function does not clobber the caller
- validate_timezone updated to warn-and-default instead of error_exit
- validate_password updated to handle empty input (return 3)
- uninstall sources lib/common.sh directly, allowing standalone execution
### Removed
- Path B install pipeline (setup, privileged-setup, user-operations,
services-improved, lib/privileges.sh) -- dead code, never wired in (A1, A3)
--- ---
## [1.0.0] - TBD ## [1.0.0] - TBD
Full rewrite and stabilization of the Path A install pipeline. Full rewrite establishing the Path A install pipeline.
### Fixed
- Infinite recursion in get_timezone_mount() and get_gpu_devices() on Linux
- ((x++)) abort under set -e across hops and install
- Glob stored as string breaking multi-user directory detection
- Missing hops_service_definitions.sh reference in firewall setup
### Security
- Replace broken AES-GCM encryption with supported cipher
- Move passphrases off command line (use fd-based passphrase input)
- Remove committed default Authelia credential
- Use mktemp for temp files instead of predictable /tmp paths
### Changed
- Single canonical service catalog (services)
- Latest image tags throughout
- lib/secrets.sh wired into install flow for .env encryption at rest
+16 -14
View File
@@ -160,12 +160,16 @@ Generated by codebase audit (2026-06-10). Ranked by severity.
- Fix passphrase-on-command-line exposure (S1, S2). - Fix passphrase-on-command-line exposure (S1, S2).
- Wire encrypt/decrypt calls into `install` flow. - Wire encrypt/decrypt calls into `install` flow.
### A5 -- `hops` duplicates functions from `lib/common.sh` [HIGH] -- DO FIRST ### A5 -- `hops` duplicates functions from `lib/common.sh` [HIGH] -- RESOLVED
- `log`, `error_exit`, `warning`, `success`, `info`, `validate_timezone`, - `log`, `error_exit`, `warning`, `success`, `info`, `validate_timezone`,
`validate_password`, `generate_secure_password`, `create_docker_networks`, `validate_password`, `generate_secure_password`, `create_docker_networks`
`get_service_port/image` are all defined twice (or three times). removed from `hops`, `uninstall`, and `install`. Canonical copies kept in
- Fix: source `lib/common.sh` from `hops` and remove local duplicates. `lib/common.sh`, `lib/security.sh`, `lib/validation.sh`, `lib/docker.sh`.
- Must be done before bug fixes to avoid patching the same logic in multiple places. - Fixed `lib/docker.sh`, `lib/validation.sh`, `lib/security.sh` to use `LIB_DIR`
instead of `SCRIPT_DIR` so sourcing them inside a function doesn't clobber
the caller's `SCRIPT_DIR`.
- `validate_timezone` updated to warn-and-default instead of error_exit.
- `validate_password` updated to handle empty input (return 3).
### A6 -- Caddy is unreachable via the menu [LOW] ### A6 -- Caddy is unreachable via the menu [LOW]
- `services` defines `generate_caddy` but the `select_services` menu in - `services` defines `generate_caddy` but the `select_services` menu in
@@ -236,10 +240,8 @@ Generated by codebase audit (2026-06-10). Ranked by severity.
## CODE QUALITY ## CODE QUALITY
### Q1 -- Three separate error-handling implementations [HIGH] -- DO FIRST ### Q1 -- Three separate error-handling implementations [HIGH] -- RESOLVED
- `hops`, `uninstall`, and `lib/common.sh` each define their own `error_exit` - Covered by A5; all resolved.
and `log` with different formats. Consolidate in `lib/common.sh`.
- Covered by A5; tracked here for completeness.
### Q2 -- `set -e` + intentional non-zero returns is a minefield [MED] ### Q2 -- `set -e` + intentional non-zero returns is a minefield [MED]
- `validate_password` returns 1/2/3, `check_port` returns 1 -- these work only - `validate_password` returns 1/2/3, `check_port` returns 1 -- these work only
@@ -262,15 +264,15 @@ Generated by codebase audit (2026-06-10). Ranked by severity.
### Cleanup first (do before any bug fixes) ### Cleanup first (do before any bug fixes)
1. [DONE] Delete Path B files (A1/A3) 1. [DONE] Delete Path B files (A1/A3)
2. Consolidate duplicate functions into `lib/common.sh` (A5, Q1) -- one copy to fix 2. [DONE] Consolidate duplicate functions into `lib/common.sh` (A5, Q1)
3. Reconcile `lib/docker.sh` service maps with `services` (A2) -- one catalog to fix 3. Reconcile `lib/docker.sh` service maps with `services` (A2) -- one catalog to fix
4. Remove debug echo statements from `lib/system.sh` (Q3) -- reduce noise 4. Remove debug echo statements from `lib/system.sh` (Q3) -- reduce noise
### Bug fixes ### Bug fixes
5. Fix B1 (infinite recursion in `services`) -- unblocks all Linux installs 5. [DONE] Fix B1 (infinite recursion in `services`)
6. Fix B5 (`((x++))` under `set -e`) -- prevents silent aborts 6. [DONE] Fix B5 (`((x++))` under `set -e`)
7. Fix B3 (glob directory detection) -- fixes multi-user and uninstall 7. [DONE] Fix B3 (glob directory detection)
8. Fix B4 (wrong filename in firewall setup) 8. [DONE] Fix B4 (wrong filename in firewall setup)
9. Fix B7 (intra-selection port collision detection) 9. Fix B7 (intra-selection port collision detection)
### Security pass ### Security pass
+4 -43
View File
@@ -12,8 +12,7 @@ if [[ "$(uname -s)" != "Linux" ]]; then
exit 1 exit 1
fi fi
# Script version and metadata # Script metadata (SCRIPT_VERSION defined in lib/common.sh)
readonly SCRIPT_VERSION="1.0.0"
readonly SCRIPT_NAME="HOPS" readonly SCRIPT_NAME="HOPS"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -22,53 +21,15 @@ readonly INSTALLER_SCRIPT="$SCRIPT_DIR/install"
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/uninstall" readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/uninstall"
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/services" readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/services"
# Load system utilities # Load shared utilities
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/system.sh" source "$SCRIPT_DIR/lib/system.sh"
# Color codes are defined in lib/common.sh
# Logging setup (will be set by setup_logging)
LOG_DIR=""
LOG_FILE=""
# Initialize logging # Initialize logging
init_logging() { init_logging() {
setup_logging "hops-main" setup_logging "hops-main"
} }
# Logging function
log() {
local message="$1"
local timestamp="$(date '+%Y-%m-%d %T')"
if [[ -w "$LOG_FILE" ]]; then
echo "$timestamp - $message" >> "$LOG_FILE"
fi
echo -e "$message"
}
# Error handling
error_exit() {
log "${RED}❌ ERROR: $1${NC}"
exit 1
}
# Warning function
warning() {
log "${YELLOW}⚠️ WARNING: $1${NC}"
}
# Success function
success() {
log "${GREEN}✅ $1${NC}"
}
# Info function
info() {
log "${BLUE}️ $1${NC}"
}
# Clear screen and show header # Clear screen and show header
show_header() { show_header() {
clear clear
@@ -632,7 +593,7 @@ update_hops() {
# Source the updated script to get new version # Source the updated script to get new version
if [[ -f "$SCRIPT_DIR/hops" ]]; then if [[ -f "$SCRIPT_DIR/hops" ]]; then
local new_version=$(grep '^readonly SCRIPT_VERSION=' "$SCRIPT_DIR/hops" | cut -d'"' -f2) local new_version=$(grep '^readonly SCRIPT_VERSION=' "$SCRIPT_DIR/lib/common.sh" | cut -d'"' -f2)
success "Updated to version $new_version" success "Updated to version $new_version"
fi fi
+5 -92
View File
@@ -8,12 +8,14 @@ install_hops() {
set -e set -e
# Script version for update tracking # Script version for update tracking
local SCRIPT_VERSION="1.0.0"
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Load system utilities # Load shared utilities
source "$SCRIPT_DIR/lib/common.sh" source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/system.sh" source "$SCRIPT_DIR/lib/system.sh"
source "$SCRIPT_DIR/lib/validation.sh"
source "$SCRIPT_DIR/lib/security.sh"
source "$SCRIPT_DIR/lib/docker.sh"
# -------------------------------------------- # --------------------------------------------
# LOGGING SETUP # LOGGING SETUP
@@ -174,8 +176,7 @@ EOF
if [[ "$keep_tz" =~ ^[Nn]$ ]]; then if [[ "$keep_tz" =~ ^[Nn]$ ]]; then
echo -e "Enter timezone (e.g., America/New_York, Europe/London): " echo -e "Enter timezone (e.g., America/New_York, Europe/London): "
read -r user_timezone read -r user_timezone
validate_timezone "$user_timezone" TIMEZONE=$(validate_timezone "$user_timezone")
TIMEZONE="$user_timezone"
else else
TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "America/New_York") TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "America/New_York")
fi fi
@@ -210,42 +211,6 @@ EOF
log " AppData: $APPDATA_DIR" log " AppData: $APPDATA_DIR"
} }
# --------------------------------------------
# VALIDATION FUNCTIONS
# --------------------------------------------
validate_timezone() {
if ! timedatectl list-timezones | grep -qx "$1" 2>/dev/null; then
log "⚠️ Timezone '$1' invalid, defaulting to 'America/New_York'"
TIMEZONE="America/New_York"
fi
}
validate_password() {
local password="$1"
local min_length="${2:-12}"
if [[ -z "$password" ]]; then
echo -e "\n🔐 Password must meet these requirements:"
echo " • Minimum $min_length characters"
echo " • At least one uppercase letter"
echo " • At least one lowercase letter"
echo " • At least one number"
echo " • At least one special character"
return 3
fi
if [[ ${#password} -lt $min_length ]]; then
return 1
fi
if [[ ! "$password" =~ [A-Z] ]] || [[ ! "$password" =~ [a-z] ]] || \
[[ ! "$password" =~ [0-9] ]] || [[ ! "$password" =~ [^A-Za-z0-9] ]]; then
return 2
fi
return 0
}
check_port() { check_port() {
local PORT=$1 local PORT=$1
local SERVICE=$2 local SERVICE=$2
@@ -315,42 +280,6 @@ EOF
error_exit "No Docker Compose detected. Please install Docker first." error_exit "No Docker Compose detected. Please install Docker first."
} }
# --------------------------------------------
# IMPROVED PASSWORD GENERATION
# --------------------------------------------
generate_secure_password() {
local length="${1:-16}"
local max_attempts=5
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
# Generate password with mixed case, numbers, and symbols
local password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-${length})
# Ensure it meets complexity requirements
if validate_password "$password" "$length"; then
echo "$password"
return 0
fi
attempt=$((attempt + 1))
done
# Fallback: construct a guaranteed compliant password
local upper=$(generate_chars 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 2)
local lower=$(generate_chars 'abcdefghijklmnopqrstuvwxyz' 4)
local digits=$(generate_chars '0123456789' 2)
local symbols=$(generate_chars '!@#$%^&*' 2)
local remaining_length=$((length - 10))
if [[ $remaining_length -gt 0 ]]; then
local remaining=$(generate_chars 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' $remaining_length)
shuffle_string "${upper}${lower}${digits}${symbols}${remaining}"
else
shuffle_string "${upper}${lower}${digits}${symbols}"
fi
}
# -------------------------------------------- # --------------------------------------------
# ENVIRONMENT FILE GENERATION # ENVIRONMENT FILE GENERATION
# -------------------------------------------- # --------------------------------------------
@@ -642,22 +571,6 @@ EOF
create_docker_networks create_docker_networks
} }
# --------------------------------------------
# NETWORK CREATION
# --------------------------------------------
create_docker_networks() {
log "🌐 Creating Docker networks..."
# Create traefik network if it doesn't exist
if ! docker network ls --format "{{.Name}}" | grep -q "^traefik$"; then
if docker network create traefik 2>/dev/null; then
log "✅ Created traefik network"
else
log "⚠️ Could not create traefik network (may already exist)"
fi
fi
}
# -------------------------------------------- # --------------------------------------------
# ENHANCED DEPLOYMENT WITH ROLLBACK # ENHANCED DEPLOYMENT WITH ROLLBACK
# -------------------------------------------- # --------------------------------------------
+2 -1
View File
@@ -2,7 +2,6 @@
# HOPS - Common Utility Functions # HOPS - Common Utility Functions
# Shared functions for logging, error handling, and UI # Shared functions for logging, error handling, and UI
# Version: 1.0.0
# Prevent multiple sourcing # Prevent multiple sourcing
if [[ -n "${HOPS_COMMON_LOADED:-}" ]]; then if [[ -n "${HOPS_COMMON_LOADED:-}" ]]; then
@@ -10,6 +9,8 @@ if [[ -n "${HOPS_COMMON_LOADED:-}" ]]; then
fi fi
readonly HOPS_COMMON_LOADED=1 readonly HOPS_COMMON_LOADED=1
readonly SCRIPT_VERSION="1.0.1"
# Color codes for output # Color codes for output
readonly RED='\033[0;31m' readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m' readonly GREEN='\033[0;32m'
+2 -2
View File
@@ -5,8 +5,8 @@
# Version: 1.0.0 # Version: 1.0.0
# Source common functions # Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh" source "$LIB_DIR/common.sh"
# Service definitions with pinned versions # Service definitions with pinned versions
declare -A HOPS_SERVICES=( declare -A HOPS_SERVICES=(
+12 -2
View File
@@ -5,14 +5,24 @@
# Version: 1.0.0 # Version: 1.0.0
# Source common functions # Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh" source "$LIB_DIR/common.sh"
# Password validation # Password validation
validate_password() { validate_password() {
local password="$1" local password="$1"
local min_length="${2:-12}" local min_length="${2:-12}"
if [[ -z "$password" ]]; then
echo -e "\n Password must meet these requirements:"
echo " - Minimum $min_length characters"
echo " - At least one uppercase letter"
echo " - At least one lowercase letter"
echo " - At least one number"
echo " - At least one special character"
return 3
fi
# Check minimum length # Check minimum length
if [[ ${#password} -lt $min_length ]]; then if [[ ${#password} -lt $min_length ]]; then
debug "Password too short: ${#password} < $min_length" debug "Password too short: ${#password} < $min_length"
+9 -15
View File
@@ -5,8 +5,8 @@
# Version: 1.0.0 # Version: 1.0.0
# Source common functions # Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh" source "$LIB_DIR/common.sh"
# Validate and sanitize directory path # Validate and sanitize directory path
validate_directory_path() { validate_directory_path() {
@@ -60,22 +60,16 @@ validate_directory_path() {
echo "$path" echo "$path"
} }
# Validate timezone # Validate timezone -- returns the timezone string, defaulting to America/New_York if invalid
validate_timezone() { validate_timezone() {
local timezone="$1" local timezone="$1"
local default="America/New_York"
if [[ -z "$timezone" ]]; then if [[ -z "$timezone" ]] || \
error_exit "Timezone cannot be empty" [[ ! "$timezone" =~ ^[A-Za-z_]+(/[A-Za-z_]+)*$ ]] || \
fi ! timedatectl list-timezones 2>/dev/null | grep -qx "$timezone"; then
warning "Timezone '${timezone}' invalid, defaulting to '${default}'"
# Basic format validation timezone="$default"
if [[ ! "$timezone" =~ ^[A-Za-z_]+(/[A-Za-z_]+)*$ ]]; then
error_exit "Invalid timezone format: $timezone"
fi
# Check if timezone file exists
if [[ ! -f "/usr/share/zoneinfo/$timezone" ]]; then
error_exit "Unknown timezone: $timezone"
fi fi
echo "$timezone" echo "$timezone"
+5 -19
View File
@@ -8,29 +8,15 @@ uninstall_hops() {
set +e set +e
# Script version for consistency # Script version for consistency
local SCRIPT_VERSION="1.0.0" local _UNINSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Load shared utilities if not already loaded (allows standalone execution)
source "$_UNINSTALL_DIR/lib/common.sh"
# -------------------------------------------- # --------------------------------------------
# LOGGING SETUP # LOGGING SETUP
# -------------------------------------------- # --------------------------------------------
local LOG_DIR="/var/log/hops" setup_logging "hops-uninstall"
local LOG_FILE="$LOG_DIR/hops-uninstall-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
log() {
echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE"
}
error_exit() {
log "❌ ERROR: $1"
log "❌ Uninstallation failed. Check logs at: $LOG_FILE"
exit 1
}
warning() {
log "⚠️ WARNING: $1"
}
# -------------------------------------------- # --------------------------------------------
# HEADER # HEADER