#!/bin/bash # Combined update script for docker-compose-nas # - Updates .env file from .env.example while preserving values # - Updates Authelia configuration from example file # - Configures services with correct paths and API keys # - Manages Authelia accounts # Created: April 26, 2025 set -e # Color definitions RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # No Color # Files ENV_FILE=".env" ENV_EXAMPLE=".env.example" TIMESTAMP=$(date +"%Y%m%d-%H%M%S") ENV_BACKUP=".env.${TIMESTAMP}.bak" AUTHELIA_CONFIG="authelia/configuration.yml" AUTHELIA_CONFIG_EXAMPLE="authelia/configuration.example.yml" AUTHELIA_CONFIG_BACKUP="authelia/configuration.${TIMESTAMP}.bak" # Print section header print_header() { echo -e "\n${CYAN}${BOLD}$1${NC}" echo -e "${CYAN}$(printf '=%.0s' $(seq 1 ${#1}))${NC}" } # Check if a file exists check_file() { if [ ! -f "$1" ]; then echo -e "${RED}Error: $1 doesn't exist${NC}" return 1 fi return 0 } # Function to create a backup create_backup() { echo -e "${BLUE}Creating backup of $1 as $2...${NC}" cp "$1" "$2" } ################################################## # PART 1: Update .env file from .env.example ################################################## update_env_file() { print_header "Environment File Update Tool" # Check if files exist if [ ! -f "$ENV_FILE" ]; then echo -e "${RED}Error: $ENV_FILE doesn't exist${NC}" echo -e "Creating a new $ENV_FILE from $ENV_EXAMPLE" cp "$ENV_EXAMPLE" "$ENV_FILE" echo -e "${GREEN}Done! Please review and fill in required values in $ENV_FILE${NC}" return 0 fi if ! check_file "$ENV_EXAMPLE"; then return 1 fi echo -e "${BLUE}This will update your $ENV_FILE based on the structure in $ENV_EXAMPLE${NC}" echo -e "${BLUE}Your existing values will be preserved where possible${NC}" echo -e "${BLUE}Backup will be created as: $ENV_BACKUP${NC}" echo -e "${YELLOW}Continue? [y/N]:${NC}" read -r answer if [[ ! "$answer" =~ ^[Yy]$ ]]; then echo -e "${RED}Environment update cancelled.${NC}" return 0 fi # Create backup of current .env create_backup "$ENV_FILE" "$ENV_BACKUP" # Store current env values echo -e "${BLUE}Reading current environment values...${NC}" declare -A current_values declare -A current_keys_present while IFS='=' read -r key value; do # Skip comments and empty lines if [[ ! "$key" =~ ^#.*$ ]] && [[ ! -z "$key" ]]; then # Clean up any comments after the value value=$(echo "$value" | sed 's/[[:space:]]*#.*$//') # Trim leading/trailing whitespace key=$(echo "$key" | xargs) value=$(echo "$value" | xargs) # Store in associative array if key is not empty if [[ ! -z "$key" ]]; then current_values["$key"]="$value" # Track that this key existed in original file, regardless of value current_keys_present["$key"]=1 fi fi done < "$ENV_FILE" # Create new env file from example echo -e "${BLUE}Creating new $ENV_FILE from $ENV_EXAMPLE...${NC}" cp "$ENV_EXAMPLE" "$ENV_FILE.new" # Track which keys from the current env have been used declare -A used_keys # Track new keys that need attention new_keys=() # Track keys with special warnings special_keys=() # Process the template and fill in values from current env while IFS= read -r line; do if [[ "$line" =~ ^([A-Za-z0-9_]+)=(.*)$ ]]; then key="${BASH_REMATCH[1]}" default_value="${BASH_REMATCH[2]}" # Mark the key as used if it exists in the original file if [[ -n "${current_keys_present[$key]}" ]]; then used_keys["$key"]=1 # Replace the line with the current value if one exists if [[ -n "${current_values[$key]}" ]]; then sed -i "s|^$key=.*$|$key=${current_values[$key]}|" "$ENV_FILE.new" fi # If key doesn't exist in original file and has empty/placeholder value elif [[ -z "$default_value" ]] || [[ "$default_value" == '""' ]] || [[ "$default_value" == "''" ]]; then new_keys+=("$key") # Special attention for Authelia keys if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then special_keys+=("$key") fi fi fi done < "$ENV_FILE.new" # Create section for unused/deprecated keys at the bottom of the file echo -e "\n\n# --- DEPRECATED OR UNUSED KEYS (Kept for Reference) ---" >> "$ENV_FILE.new" echo -e "# Keys below were in your original .env but aren't in the current .env.example" >> "$ENV_FILE.new" echo -e "# They may be deprecated or renamed. Review and remove if no longer needed\n" >> "$ENV_FILE.new" unused_keys_count=0 for key in "${!current_values[@]}"; do if [[ -z "${used_keys[$key]}" ]]; then echo "$key=${current_values[$key]} # DEPRECATED/UNUSED - Review" >> "$ENV_FILE.new" unused_keys_count=$((unused_keys_count + 1)) fi done # Replace the old file with the new one mv "$ENV_FILE.new" "$ENV_FILE" # Generate summary echo -e "\n${GREEN}${BOLD}Environment Update Complete!${NC}" echo -e "${BLUE}Summary:${NC}" echo -e " - ${CYAN}Original config backed up to: $ENV_BACKUP${NC}" echo -e " - ${CYAN}Updated .env structure to match .env.example${NC}" echo -e " - ${CYAN}Preserved ${#used_keys[@]} existing values${NC}" if [[ $unused_keys_count -gt 0 ]]; then echo -e " - ${YELLOW}Found $unused_keys_count deprecated/unused keys${NC}" echo -e " ${YELLOW}These have been moved to the bottom of the file with warnings${NC}" fi if [[ ${#new_keys[@]} -gt 0 ]]; then echo -e "\n${YELLOW}${BOLD}NEW KEYS NEEDING ATTENTION:${NC}" echo -e "${YELLOW}The following keys are new and may need values set:${NC}" for key in "${new_keys[@]}"; do echo -e " - ${MAGENTA}$key${NC}" done fi if [[ ${#special_keys[@]} -gt 0 ]]; then echo -e "\n${RED}${BOLD}IMPORTANT SECURITY KEYS:${NC}" echo -e "${RED}The following keys require secure values:${NC}" for key in "${special_keys[@]}"; do echo -e " - ${MAGENTA}$key${NC}" # Specific advice for Authelia keys if [[ "$key" == AUTHELIA_*_SECRET* ]] || [[ "$key" == AUTHELIA_*_KEY* ]]; then echo -e " ${CYAN}Generate with: ${GREEN}openssl rand -hex 32${NC}" fi done fi echo -e "\n${BLUE}Review your updated $ENV_FILE file and adjust any values as needed.${NC}" } ################################################## # PART 2: Update Authelia configuration ################################################## update_authelia_config() { print_header "Authelia Configuration Update Tool" # Check if files exist if ! check_file "$AUTHELIA_CONFIG_EXAMPLE"; then echo -e "${RED}Error: Example configuration file doesn't exist${NC}" return 1 fi # If config file doesn't exist, create it from example if [ ! -f "$AUTHELIA_CONFIG" ]; then echo -e "${YELLOW}Authelia configuration file doesn't exist, creating from example...${NC}" cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG" echo -e "${GREEN}Created new Authelia configuration file.${NC}" return 0 fi echo -e "${BLUE}This will update your Authelia configuration based on the example file${NC}" echo -e "${BLUE}Backup will be created as: $AUTHELIA_CONFIG_BACKUP${NC}" echo -e "${YELLOW}Continue? [y/N]:${NC}" read -r answer if [[ ! "$answer" =~ ^[Yy]$ ]]; then echo -e "${RED}Authelia config update cancelled.${NC}" return 0 fi # Create backup of current config create_backup "$AUTHELIA_CONFIG" "$AUTHELIA_CONFIG_BACKUP" # Copy the example file over the current one cp "$AUTHELIA_CONFIG_EXAMPLE" "$AUTHELIA_CONFIG" # Get the tailnet domain from .env for proper configuration if [ -f "$ENV_FILE" ]; then TAILNET_DOMAIN=$(grep -o "TAILSCALE_TAILNET_DOMAIN=.*" "$ENV_FILE" | cut -d'=' -f2 | tr -d '"' | tr -d "'") TAILSCALE_HOSTNAME=$(grep -o "TAILSCALE_HOSTNAME=.*" "$ENV_FILE" | cut -d'=' -f2 | tr -d '"' | tr -d "'") if [ -n "$TAILNET_DOMAIN" ] && [ -n "$TAILSCALE_HOSTNAME" ]; then # Replace placeholders with actual values sed -i "s/\*.ts.net/\*.$TAILNET_DOMAIN/g" "$AUTHELIA_CONFIG" sed -i "s/tailscale-nas.ts.net/$TAILSCALE_HOSTNAME.$TAILNET_DOMAIN/g" "$AUTHELIA_CONFIG" echo -e "${GREEN}Configured Authelia with your Tailscale domain: $TAILSCALE_HOSTNAME.$TAILNET_DOMAIN${NC}" fi fi echo -e "${GREEN}${BOLD}Authelia Configuration Update Complete!${NC}" echo -e "${BLUE}${BOLD}Note:${NC} Original config backed up to: $AUTHELIA_CONFIG_BACKUP" } ################################################## # PART 3: Update Service Configurations ################################################## update_arr_config() { local container=$1 local path=$2 echo -e "${BLUE}Updating ${container} configuration...${NC}" until [ -f "${CONFIG_ROOT:-.}"/"$container"/config.xml ]; do sleep 1; done sed -i.bak "s/<\/UrlBase>/\/$path<\/UrlBase>/" "${CONFIG_ROOT:-.}"/"$container"/config.xml && rm "${CONFIG_ROOT:-.}"/"$container"/config.xml.bak CONTAINER_NAME_UPPER=$(echo "$container" | tr '[:lower:]' '[:upper:]') sed -i.bak 's/^'"${CONTAINER_NAME_UPPER}"'_API_KEY=.*/'"${CONTAINER_NAME_UPPER}"'_API_KEY='"$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config.xml)"'/' .env && rm .env.bak echo -e "${GREEN}Update of ${container} configuration complete, restarting...${NC}" docker compose restart "$container" } update_qbittorrent_config() { local container=$1 echo -e "${BLUE}Updating ${container} configuration...${NC}" docker compose stop "$container" until [ -f "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf ]; do sleep 1; done sed -i.bak '/WebUI\\ServerDomains=*/a WebUI\\Password_PBKDF2="@ByteArray(ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ==)"' "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf && rm "${CONFIG_ROOT:-.}"/"$container"/qBittorrent/qBittorrent.conf.bak echo -e "${GREEN}Update of ${container} configuration complete, restarting...${NC}" docker compose start "$container" } update_bazarr_config() { local container=$1 echo -e "${BLUE}Updating ${container} configuration...${NC}" until [ -f "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml ]; do sleep 1; done sed -i.bak "s/base_url: ''/base_url: '\/$container'/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak sed -i.bak "s/use_radarr: false/use_radarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak sed -i.bak "s/use_sonarr: false/use_sonarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak until [ -f "${CONFIG_ROOT:-.}"/sonarr/config.xml ]; do sleep 1; done SONARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/sonarr/config.xml) sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $SONARR_API_KEY/; s/base_url: .*/base_url: \/sonarr/; s/ip: .*/ip: sonarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak until [ -f "${CONFIG_ROOT:-.}"/radarr/config.xml ]; do sleep 1; done RADARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/radarr/config.xml) sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $RADARR_API_KEY/; s/base_url: .*/base_url: \/radarr/; s/ip: .*/ip: radarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak sed -i.bak 's/^BAZARR_API_KEY=.*/BAZARR_API_KEY='"$(sed -n 's/.*apikey: \(.*\)*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml | head -n 1)"'/' .env && rm .env.bak echo -e "${GREEN}Update of ${container} configuration complete, restarting...${NC}" docker compose restart "$container" } update_service_configs() { print_header "Service Configuration Update Tool" echo -e "${BLUE}This will update service configurations for running containers${NC}" echo -e "${BLUE}It will set proper URL bases and extract API keys${NC}" echo -e "${YELLOW}Continue? [y/N]:${NC}" read -r answer if [[ ! "$answer" =~ ^[Yy]$ ]]; then echo -e "${RED}Service configuration update cancelled.${NC}" return 0 fi # Check if Docker is running if ! docker ps > /dev/null 2>&1; then echo -e "${RED}Error: Docker is not running or you don't have permission to use it.${NC}" echo -e "${YELLOW}Make sure Docker is running and you have proper permissions.${NC}" return 1 fi echo -e "${BLUE}Checking for running containers to update...${NC}" for container in $(docker ps --format '{{.Names}}'); do if [[ "$container" =~ ^(radarr|sonarr|lidarr|prowlarr)$ ]]; then update_arr_config "$container" "$container" elif [[ "$container" =~ ^(bazarr)$ ]]; then update_bazarr_config "$container" elif [[ "$container" =~ ^(qbittorrent)$ ]]; then update_qbittorrent_config "$container" fi done echo -e "\n${GREEN}${BOLD}Service Configuration Update Complete!${NC}" } ################################################## # Additional utility functions ################################################## # Function to generate a random passphrase with random separators and numbers generate_passphrase() { local words=( "apple" "banana" "cherry" "dragon" "eagle" "forest" "guitar" "harbor" "island" "jungle" "kitchen" "lemon" "mountain" "notebook" "ocean" "planet" "quiet" "river" "summer" "tiger" "umbrella" "village" "winter" "xylophone" "yellow" "zebra" "anchor" "beaver" "candle" "dolphin" "elephant" "falcon" "giraffe" "hamster" "iguana" "jaguar" ) local separators=( "-" "_" "." "+" "=" "*" "~" "^" "@" "#" "%" "&" "!" "?" ) # Generate a random number between 100 and 999 local random_num=$((RANDOM % 900 + 100)) # Select 3 random words local selected_words=() for i in {1..3}; do local index=$((RANDOM % ${#words[@]})) selected_words+=(${words[$index]}) done # Select 2 random separators local separator1=${separators[$((RANDOM % ${#separators[@]}))]} local separator2=${separators[$((RANDOM % ${#separators[@]}))]} # Combine to form passphrase: word1-word2_word3123 echo "${selected_words[0]}${separator1}${selected_words[1]}${separator2}${selected_words[2]}${random_num}" } ################################################## # PART 6: Authelia Account Management ################################################## manage_authelia_accounts() { print_header "Authelia Account Management" # Check if the users_database.yml file exists local users_file="${CONFIG_ROOT:-.}/authelia/users_database.yml" if [ ! -f "$users_file" ]; then echo -e "${RED}Error: users_database.yml not found at $users_file${NC}" echo -e "${YELLOW}Would you like to create a new users database file? [y/N]:${NC}" read -r answer if [[ "$answer" =~ ^[Yy]$ ]]; then # Create minimal users database file cat > "$users_file" </dev/null) if [ -z "$password_hash" ]; then echo -e "${RED}Error: Failed to generate password hash. Is the authelia container available?${NC}" echo -e "${YELLOW}Trying direct docker run method...${NC}" password_hash=$(docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password "$password" 2>/dev/null) if [ -z "$password_hash" ]; then echo -e "${RED}Error: Both methods failed to generate a password hash. Skipping user.${NC}" continue fi fi # Strip the "Digest: " prefix if present password_hash=$(echo "$password_hash" | sed 's/^Digest: //') # Verify hash format if [[ ! "$password_hash" =~ ^\$argon2id.*$ ]]; then echo -e "${RED}Error: Generated hash does not have the expected format. Actual value:${NC}" echo -e "${YELLOW}$password_hash${NC}" echo -e "${RED}Skipping user creation.${NC}" continue fi echo -e "${GREEN}Password hash generated successfully.${NC}" # Check if user already exists in the file and update if grep -q "^[[:space:]]*${username}:" "$users_file"; then # User exists, update entry by commenting out old lines and adding new ones sed -i "/^[[:space:]]*${username}:/,/^[[:space:]]*[a-zA-Z0-9_-]\+:/ s/^/# UPDATED: /" "$users_file" # Remove the last comment marker if it matched a different user sed -i "0,/^# UPDATED: [[:space:]]*[a-zA-Z0-9_-]\+:/ s/^# UPDATED: //" "$users_file" fi # Add user to the file cat >> "$users_file" <