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:
parent
f0dba7cc0a
commit
70486907aa
18 changed files with 3788 additions and 1767 deletions
150
update-agent.sh
150
update-agent.sh
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue