Major improvements: flexible install dir, configurable compose file name for git, enhanced webhook notifications, cross-platform lock, robust rollback, and updated docs.\n\n- Install dir is now user-confirmable and dynamic\n- Added COMPOSE_FILENAME for git stacks\n- Webhook payloads now include git context and rollback events\n- Lock file age check is cross-platform\n- Rollback notifications for success/failure\n- Updated TOML example and documentation\n- Many robustness and UX improvements

This commit is contained in:
robojerk 2025-06-25 15:15:40 -07:00
parent f0dba7cc0a
commit 70486907aa
18 changed files with 3788 additions and 1767 deletions

View file

@ -1,21 +1,65 @@
#!/bin/bash
# Exit on error
set -e
# ComposeSync - Automated Docker Compose Update Agent
# This script downloads and applies updates to docker-compose.yml files from remote sources
# Load environment variables
if [ -f .env ]; then
source .env
set -euo pipefail
# Source the configuration parser (if available)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SCRIPT_DIR/config-parser.sh" ]; then
source "$SCRIPT_DIR/config-parser.sh"
else
# Fallback for .env-only usage
CONFIG_DIR="$SCRIPT_DIR"
ENV_FILE="$CONFIG_DIR/.env"
# Simple load_config function for .env-only
load_config() {
# Try multiple .env locations
local env_locations=(
"$ENV_FILE" # Default location
"$(dirname "${BASH_SOURCE[0]}")/.env" # Same directory as script
".env" # Current directory
)
for env_file in "${env_locations[@]}"; do
if [ -f "$env_file" ]; then
log "Loading .env configuration from $env_file"
# Use set -a to automatically export variables
set -a
source "$env_file"
set +a
return 0
fi
done
log "ERROR: No configuration file found at any .env locations"
return 1
}
# Simple log function if not defined
if ! declare -F log >/dev/null; then
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
fi
fi
# Default values
BASE_DIR=${COMPOSESYNC_BASE_DIR:-"/opt/composesync/stacks"}
BASE_DIR=${COMPOSESYNC_BASE_DIR:-"$SCRIPT_DIR/stacks"}
KEEP_VERSIONS=${KEEP_VERSIONS:-10}
UPDATE_INTERVAL=${UPDATE_INTERVAL_SECONDS:-3600}
UPDATE_MODE=${UPDATE_MODE:-"notify_and_apply"}
DRY_RUN=${DRY_RUN:-"false"}
STACKS=${STACKS:-1}
# Load configuration (supports both .env and TOML)
if ! load_config; then
echo "ERROR: Failed to load configuration"
exit 1
fi
# Function to log messages
log() {
local prefix=""
@ -37,9 +81,26 @@ acquire_lock() {
else
# Check if lock is stale
if [ -d "$lock_file" ]; then
local lock_age=$(($(date +%s) - $(stat -c %Y "$lock_file")))
local lock_age
# Cross-platform lock age checking
if command -v stat >/dev/null 2>&1; then
# Linux: stat -c %Y
if stat -c %Y "$lock_file" >/dev/null 2>&1; then
lock_age=$(($(date +%s) - $(stat -c %Y "$lock_file")))
# BSD/macOS: stat -f %m
elif stat -f %m "$lock_file" >/dev/null 2>&1; then
lock_age=$(($(date +%s) - $(stat -f %m "$lock_file")))
else
# Fallback: use ls -ld
lock_age=$(($(date +%s) - $(ls -ld "$lock_file" | awk '{print $6, $7, $8}' | xargs -I {} date -d "{}" +%s 2>/dev/null || echo 0)))
fi
else
# Fallback: assume lock is stale if we can't determine age
lock_age=$((lock_timeout + 1))
fi
if [ $lock_age -gt $lock_timeout ]; then
log "Found stale lock, removing..."
log "Found stale lock (age: ${lock_age}s), removing..."
rm -rf "$lock_file"
if mkdir "$lock_file" 2>/dev/null; then
return 0
@ -63,6 +124,7 @@ download_file() {
local tool=$3
local subpath=$4
local git_ref=$5
local compose_filename=$6 # New parameter for configurable compose file name
log "Downloading $url to $output"
@ -106,11 +168,13 @@ download_file() {
fi
cp "${output}.git/$subpath" "$output"
else
if [ ! -f "${output}.git/docker-compose.yml" ]; then
log "ERROR: docker-compose.yml not found in repository root"
# Use configurable compose file name, default to docker-compose.yml
local compose_file_name=${compose_filename:-"docker-compose.yml"}
if [ ! -f "${output}.git/$compose_file_name" ]; then
log "ERROR: $compose_file_name not found in repository root"
return 1
fi
cp "${output}.git/docker-compose.yml" "$output"
cp "${output}.git/$compose_file_name" "$output"
fi
;;
*)
@ -155,6 +219,7 @@ send_webhook_notification() {
local message="$3"
local version_id="$4"
local diff="$5"
local git_context="$6" # New parameter for Git context
if [ -z "$NOTIFICATION_WEBHOOK_URL" ]; then
return
@ -168,18 +233,56 @@ send_webhook_notification() {
diff_json=""
fi
# Escape git context for JSON
local git_context_json=""
if [ -n "$git_context" ]; then
git_context_json=$(echo "$git_context" | sed 's/\\/\\\\/g; s/\"/\\\"/g')
fi
local payload="{\n"
payload+=" \"event\": \"$event\",\n"
payload+=" \"stack_name\": \"$stack_name\",\n"
payload+=" \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\n"
payload+=" \"message\": \"$message\",\n"
payload+=" \"version_id\": \"$version_id\",\n"
payload+=" \"diff\": \"$diff_json\"\n"
payload+=" \"diff\": \"$diff_json\",\n"
payload+=" \"git_context\": \"$git_context_json\"\n"
payload+="}"
curl -s -X POST -H "Content-Type: application/json" -d "$payload" "$NOTIFICATION_WEBHOOK_URL" >/dev/null 2>&1 || true
}
# Function to get Git context for notifications
get_git_context() {
local path=$1
local url=$2
local git_ref=$3
if [ ! -d "${path}.git" ]; then
return
fi
local context=""
# Get commit hash and message
if git -C "${path}.git" rev-parse HEAD >/dev/null 2>&1; then
local commit_hash=$(git -C "${path}.git" rev-parse --short HEAD)
local commit_msg=$(git -C "${path}.git" log -1 --pretty=format:"%s" 2>/dev/null | head -c 100)
context="Commit: $commit_hash - $commit_msg"
# Add branch/tag info if available
if [ -n "$git_ref" ]; then
context="$context (Ref: $git_ref)"
fi
# Add repository URL (sanitized)
local repo_name=$(basename "$url" .git)
context="$context | Repo: $repo_name"
fi
echo "$context"
}
# Function to process a stack
process_stack() {
local stack_num=$1
@ -191,6 +294,7 @@ process_stack() {
local keep_versions_var="STACK_${stack_num}_KEEP_VERSIONS"
local git_subpath_var="STACK_${stack_num}_GIT_SUBPATH"
local git_ref_var="STACK_${stack_num}_GIT_REF"
local compose_filename_var="STACK_${stack_num}_COMPOSE_FILENAME"
local name=${!name_var}
local url=${!url_var}
@ -200,6 +304,7 @@ process_stack() {
local keep_versions=${!keep_versions_var:-$KEEP_VERSIONS}
local git_subpath=${!git_subpath_var}
local git_ref=${!git_ref_var}
local compose_filename=${!compose_filename_var}
if [ -z "$name" ] || [ -z "$url" ] || [ -z "$path" ] || [ -z "$tool" ]; then
log "Error: Missing required configuration for stack $stack_num"
@ -226,7 +331,7 @@ process_stack() {
# Download main compose file
local compose_file="$path/docker-compose.yml"
if ! download_file "$url" "$compose_file" "$tool" "$git_subpath" "$git_ref"; then
if ! download_file "$url" "$compose_file" "$tool" "$git_subpath" "$git_ref" "$compose_filename"; then
log "ERROR: Failed to download main compose file for stack $name, skipping..."
return 1
fi
@ -235,6 +340,15 @@ process_stack() {
local version_id=$(get_version_id "$path" "$tool")
log "Version ID: $version_id"
# Get Git context for notifications
local git_context=""
if [ "$tool" = "git" ]; then
git_context=$(get_git_context "$path" "$url" "$git_ref")
if [ -n "$git_context" ]; then
log "Git context: $git_context"
fi
fi
# Create backup directory for this update
local backup_dir="$path/backups/backup-$(date +%Y%m%d%H%M%S)"
if [ "$DRY_RUN" != "true" ]; then
@ -359,7 +473,7 @@ process_stack() {
log "DRY-RUN: Would run: ${compose_cmd_array[*]}"
log "DRY-RUN: Changes that would be applied:"
echo "$diff_output"
send_webhook_notification "update_success" "$name" "[DRY-RUN] Would apply changes to $name" "$version_id" "$diff_output"
send_webhook_notification "update_success" "$name" "[DRY-RUN] Would apply changes to $name" "$version_id" "$diff_output" "$git_context"
else
log "Applying changes to $name"
@ -376,10 +490,10 @@ process_stack() {
log "Running: ${compose_cmd_array[*]}"
if "${compose_cmd_array[@]}"; then
log "Successfully updated stack $name"
send_webhook_notification "update_success" "$name" "Successfully updated stack $name" "$version_id" "$diff_output"
send_webhook_notification "update_success" "$name" "Successfully updated stack $name" "$version_id" "$diff_output" "$git_context"
else
log "ERROR: Failed to update stack $name, attempting rollback..."
send_webhook_notification "update_failure" "$name" "Failed to update stack $name, rolled back to previous version" "$version_id" "$diff_output"
send_webhook_notification "update_failure" "$name" "Failed to update stack $name, rolled back to previous version" "$version_id" "$diff_output" "$git_context"
# Rollback: restore from backup
if [ -f "$backup_dir/docker-compose.yml" ]; then
@ -406,14 +520,16 @@ process_stack() {
log "Attempting to restart stack with rolled back configuration..."
if "${compose_cmd_array[@]}"; then
log "Successfully rolled back stack $name"
send_webhook_notification "rollback_success" "$name" "Successfully rolled back stack $name to previous version" "$version_id" "" "$git_context"
else
log "CRITICAL: Failed to rollback stack $name - manual intervention required"
send_webhook_notification "rollback_failure" "$name" "CRITICAL: Failed to rollback stack $name - manual intervention required" "$version_id" "" "$git_context"
fi
fi
fi
else
log "Changes detected but not applied (notify_only mode)"
send_webhook_notification "update_success" "$name" "Changes detected in $name (notify_only mode)" "$version_id" "$diff_output"
send_webhook_notification "update_success" "$name" "Changes detected in $name (notify_only mode)" "$version_id" "$diff_output" "$git_context"
fi
else
log "No changes detected in $name"