From 81d5ef65f6c518f786631ba7082e7c50c25e3ddc Mon Sep 17 00:00:00 2001 From: Vorezal <37914382+vorezal@users.noreply.github.com> Date: Tue, 3 Feb 2026 06:11:09 +0000 Subject: [PATCH 1/3] Start/restart stacks once. Skip failed pulls. Final summary notification. --- .gitignore | 7 ++++- default.config | 2 ++ dockcheck.sh | 36 ++++++++++++++++++------- notify_templates/notify_v2.sh | 51 ++++++++++++++++++++++++++++++++--- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 182c4aa..f490fe8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,9 @@ regctl # ignore snooze file snooze.list # ignore updates file -updates_available.txt \ No newline at end of file +updates_available.txt +# ignore user compose files +compose.yaml +compose.yml +docker-compose.yaml +docker-compose.yml diff --git a/default.config b/default.config index f9076a2..3c68605 100644 --- a/default.config +++ b/default.config @@ -44,6 +44,8 @@ # DISABLE_DOCKCHECK_NOTIFICATION=false ## Uncomment and set to true to disable notifications when notify scripts themselves have updates. # DISABLE_NOTIFY_NOTIFICATION=false +## Uncomment and set to true to enable a final action summary notification. +# ENABLE_SUMMARY_NOTIFICATION=false # ## Apprise configuration variables. Set APPRISE_PAYLOAD to make a CLI call or set APPRISE_URL to make an API request instead. # APPRISE_PAYLOAD='mailto://myemail:mypass@gmail.com diff --git a/dockcheck.sh b/dockcheck.sh index 24e7c62..6968a94 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.7.6" -# ChangeNotes: Bugfixes and sanitation. Cleanup of default.config - migrate settings manually (optional). +VERSION="v0.7.7" +# ChangeNotes: Start/restart stacks once. Skip failed pulls. Final summary notification. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -94,6 +94,7 @@ GotUpdates=() NoUpdates=() GotErrors=() SelectedUpdates=() +Actions=() CurlArgs="--retry ${CurlRetryCount:=3} --retry-delay ${CurlRetryDelay:=1} --connect-timeout ${CurlConnectTimeout:=5} -sf" regbin="" jqbin="" @@ -159,6 +160,7 @@ fi if [[ "$DontUpdate" == true ]]; then AutoMode=true; fi if [[ "$MonoMode" == true ]]; then declare c_{red,green,yellow,blue,teal,reset}=""; fi if [[ "$Notify" == true ]]; then + source "${ScriptWorkDir}/notify_templates/notify_v2.sh" source_if_exists_or_fail "${ScriptWorkDir}/notify.sh" || source_if_exists_or_fail "${ScriptWorkDir}/notify_templates/notify_v2.sh" || Notify=false fi if [[ -n "$Exclude" ]]; then @@ -610,8 +612,10 @@ if [[ -n "${GotUpdates:-}" ]]; then if [[ "$DRunUp" == true ]]; then docker pull "$ContImage" printf "%s\n" "$i got a new image downloaded, rebuild manually with preferred 'docker run'-parameters" + Actions+=("Pull $ContImage;Success;Manual rebuild with 'docker run'-parameters required") else printf "\n%b%s%b has no compose labels, probably started with docker run - %bskipping%b\n\n" "$c_yellow" "$i" "$c_reset" "$c_yellow" "$c_reset" + Actions+=("Pull $ContImage;Skipped;Started with docker run") fi continue fi @@ -619,8 +623,12 @@ if [[ -n "${GotUpdates:-}" ]]; then if docker pull "$ContImage"; then # Removal of the -tag image left behind from backup if [[ ! -z "${ContRepoDigests:-}" ]] && [[ -n "${BackupForDays:-}" ]]; then docker rmi "$ContRepoDigests"; fi + SuccessfulUpdates+=("$i") + Actions+=("Pull $ContImage;Success") else - printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1 + printf "\n%bError pulling update for %S. Skipping. %b\n" "$c_red" "$i" "$c_reset" + FailedUpdates+=("$i") + Actions+=("Pull $ContImage;Error") fi done @@ -630,8 +638,9 @@ if [[ -n "${GotUpdates:-}" ]]; then printf "%bSkipping container recreation due to -R.%b\n" "$c_yellow" "$c_reset" else printf "%bRecreating updated containers.%b\n" "$c_blue" "$c_reset" + RestartedStacks=() CurrentQue=0 - for i in "${SelectedUpdates[@]}"; do + for i in "${SuccessfulUpdates[@]}"; do ((CurrentQue+=1)) unset CompleteConfs # Extract labels and metadata @@ -654,16 +663,18 @@ if [[ -n "${GotUpdates:-}" ]]; then if [[ "$ContStateRunning" == "true" ]]; then printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" + { [[ "${RestartedStacks[@]}" == *"$ContPath"* ]] && { [[ $OnlySpecific != true ]] || [[ $ContOnlySpecific != true ]] } } && printf "%bStack already restarted. Skipping.%b\n" "$c_yellow" "$c_reset" else printf "\n%bSkipping recreation of %b%s%b as it's not running.%b\n" "$c_yellow" "$c_blue" "$i" "$c_yellow" "$c_reset" + Actions+=("Recreate $i;Skipped;Not Running") continue fi # Checking if compose-values are empty - hence started with docker run - [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; continue; } + [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; Actions+=("Recreate $i;Skipped;Not compose") ; continue; } # cd to the compose-file directory to account for people who use relative volumes - cd "$ContPath" || { printf "\n%bPath error - skipping%b %s" "$c_red" "$c_reset" "$i"; continue; } + cd "$ContPath" || { printf "\n%bPath error - skipping%b %s" "$c_red" "$c_reset" "$i"; Actions+=("Recreate $i;Skipped;Path error") ; continue; } # Reformatting path + multi compose if [[ $ContConfigFile == '/'* ]]; then CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s " "$conf"; done) @@ -678,9 +689,13 @@ if [[ -n "${GotUpdates:-}" ]]; then # Check if the whole stack should be restarted if [[ "$ContRestartStack" == true ]] || [[ "$ForceRestartStacks" == true ]]; then - ${DockerBin} ${CompleteConfs} stop; ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } + [[ "${RestartedStacks[@]}" != *"$ContPath"* ]] && { ${DockerBin} ${CompleteConfs} stop; ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d && Actions+=("Recreate $i;Success;Full stack restart") || { printf "\n%bFailed to recreate $i, skipping.%b\n" "$c_red" "$c_reset" ; Actions+=("Recreate $i;Error;Failed to start stack") ; } } + RestartedStacks+=("$ContPath") else - ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } + { [[ "${RestartedStacks[@]}" != *"$ContPath"* ]] || [[ -n "${SpecificContainer:-}" ]] } && { ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} && Actions+=("Recreate $i;Success;${SpecificContainer:-Stack}") || { printf "\n%bFailed to recreate $i, skipping.%b\n" "$c_red" "$c_reset" ; Actions+=("Recreate $i;Error;Failed to start ${SpecificContainer:-Stack}") ; } } + if [[ -z "${SpecificContainer:-}" ]]; then + RestartedStacks+=("$ContPath") + fi fi done fi @@ -689,7 +704,7 @@ if [[ -n "${GotUpdates:-}" ]]; then # Trigger pruning only when backup-function is not used if [[ -z "${BackupForDays:-}" ]]; then if [[ "$AutoPrune" == false ]] && [[ "$AutoMode" == false ]]; then printf "\n"; read -rep "Would you like to prune all dangling images? y/[n]: " AutoPrune; fi - if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then printf "\nAuto pruning.."; docker image prune -f; fi + if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then printf "\nAuto pruning.."; docker image prune -f && Actions+=("Prune images;Success"); fi fi else @@ -702,4 +717,7 @@ fi # Clean up old backup image tags if -b is used [[ -n "${BackupForDays:-}" ]] && remove_backups +# Send final summary notification if enabled +[[ "${Notify}" == true ]] && [[ "${ENABLE_SUMMARY_NOTIFICATION:-}" == true ]] && [[ "${#Actions[@]} -gt 0" ]] && { exec_if_exists_or_fail send_summary_notification || printf "Could not source summary notification function.\n"; } + exit 0 diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index a44870a..2ed7246 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -1,4 +1,4 @@ -NOTIFY_V2_VERSION="v0.7" +NOTIFY_V2_VERSION="v0.8" # # If migrating from an older notify template, remove your existing notify.sh file. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -180,7 +180,9 @@ format_output() { local FormattedTextTemplate="$3" local tempcsv="" - if [[ ! "${UpdateType}" == "dockcheck_update" ]]; then + if [[ "${UpdateType}" == "summary" ]]; then + tempcsv="${UpdToString//;/,}" + elif [[ ! "${UpdateType}" == "dockcheck_update" ]]; then tempcsv="${UpdToString// -> /,}" tempcsv="${tempcsv//.sh /.sh,}" else @@ -194,12 +196,17 @@ format_output() { FormattedOutput="${tempcsv}" fi elif [[ "${OutputFormat}" == "json" ]]; then - if [[ -z "${UpdToString}" ]]; then + if [[ "${UpdateType}" == "summary" ]] && [[ -z "${UpdToString}" ]]; then + FormattedOutput='{"actions": []}' + elif [[ -z "${UpdToString}" ]]; then FormattedOutput='{"updates": []}' else if [[ "${UpdateType}" == "container_update" ]]; then # container updates case FormattedOutput=$(jq --compact-output --null-input --arg updates "${tempcsv}" '($updates | split("\\n")) | map(split(",")) | {"updates": map({"container_name": .[0], "release_notes": .[1]})} | del(..|nulls)') + elif [[ "${UpdateType}" == "summary" ]]; then + # final summary notification case + FormattedOutput=$(jq --compact-output --null-input --arg actions "${tempcsv}" '($actions | split("\\n")) | map(split(",")) | {"actions": map({"action": .[0], "result": .[1], "description": .[2]})} | del(..|nulls)') elif [[ "${UpdateType}" == "notify_update" ]]; then # script updates case FormattedOutput=$(jq --compact-output --null-input --arg updates "${tempcsv}" '($updates | split("\\n")) | map(split(",")) | {"updates": map({"script_name": .[0], "installed_version": .[1], "latest_version": .[2]})}') @@ -216,6 +223,8 @@ format_output() { else if [[ "${UpdateType}" == "container_update" ]]; then FormattedOutput="${FormattedTextTemplate//${UpdToString}}" + elif [[ "${UpdateType}" == "summary" ]]; then + FormattedOutput="${FormattedTextTemplate//${UpdToString}}" elif [[ "${UpdateType}" == "notify_update" ]]; then FormattedOutput="${FormattedTextTemplate//${UpdToString}}" elif [[ "${UpdateType}" == "dockcheck_update" ]]; then @@ -301,6 +310,42 @@ send_notification() { return 0 } +### Set ENABLE_SUMMARY_NOTIFICATION=true in dockcheck.config +### to send a final action summary notification +send_summary_notification() { + Notified="false" + + MessageTitle="$FromHost - Action summary" + + UpdToString=$( printf '%s\\n' "${Actions[@]}" ) + UpdToString="${UpdToString%, }" + UpdToString=${UpdToString%\\n} + + for channel in "${enabled_notify_channels[@]}"; do + local SkipNotification=$(skip_notification "${channel}" "1" "summary") + if [[ "${SkipNotification}" == "false" ]]; then + local template=$(get_channel_template "${channel}") + + # Formats UpdToString variable per channel settings + format_output "summary" "$(output_format "${channel}")" "🐋 Actions taken by $FromHost:\n\n" + + # Setting the MessageBody variable here. + printf -v MessageBody "${FormattedOutput}" + + printf "\nSending ${channel} summary notification" + exec_if_exists_or_fail trigger_${template}_notification "${channel}" || \ + printf "\nAttempted to send summary notification to channel ${channel}, but the function was not found. Make sure notify_${template}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory." + Notified="true" + fi + done + + if [[ "${Notified}" == "true" ]]; then + printf "\n" + fi + + return 0 +} + ### Set DISABLE_DOCKCHECK_NOTIFICATION=false in dockcheck.config ### to not send notifications when dockcheck itself has updates. dockcheck_notification() { From bae266a58df172e43a2d32e95c70a26aa9861189 Mon Sep 17 00:00:00 2001 From: Vorezal <37914382+vorezal@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:04:17 +0000 Subject: [PATCH 2/3] Command execution and log function --- .gitignore | 2 + dockcheck.sh | 287 ++++++++++++++++++++++++++-------- notify_templates/notify_v2.sh | 2 +- 3 files changed, 222 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index f490fe8..a498ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ compose.yaml compose.yml docker-compose.yaml docker-compose.yml +# ignore log directory +/log/ diff --git a/dockcheck.sh b/dockcheck.sh index 6968a94..7b840ed 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -13,20 +13,6 @@ ScriptArgs=( "$@" ) ScriptPath="$(readlink -f "$0")" ScriptWorkDir="$(dirname "$ScriptPath")" -# Source helper functions -source_if_exists_or_fail() { - if [[ -s "$1" ]]; then - source "$1" - [[ "${DisplaySourcedFiles:-false}" == true ]] && echo " * sourced config: ${1}" - return 0 - else - return 1 - fi -} - -# User customizable defaults -source_if_exists_or_fail "${HOME}/.config/dockcheck.config" || source_if_exists_or_fail "${ScriptWorkDir}/dockcheck.config" - # Help Function Help() { echo "Syntax: dockcheck.sh [OPTION] [comma separated names to include]" @@ -49,6 +35,7 @@ Help() { echo "-M Prints custom releasenote urls as markdown (requires template support)." echo "-n No updates; only checking availability without interaction." echo "-p Auto-prune dangling images after update." + echo "-q Quiet mode. Minmal text output. Does not effect file based logging, if enabled." echo "-r Allow checking/updating images created by 'docker run', containers need to be recreated manually." echo "-R Skip container recreation after pulling images." echo "-s Include stopped containers in the check. (Logic: docker ps -a)." @@ -60,12 +47,51 @@ Help() { echo "Project source: $Github" } -# Print current backups function -print_backups() { - printf "\n%b---%b Currently backed up images %b---%b\n\n" "$c_teal" "$c_blue" "$c_teal" "$c_reset" - docker images | sed -ne '/^REPOSITORY/p' -ne '/^dockcheck/p' +while getopts "ayb:BfFhiIlmMnpqrsuvc:e:d:t:x:R" options; do + case "${options}" in + a|y) AutoMode=true ;; + b) BackupForDays="${OPTARG}" ;; + B) PrintBackups=true ;; + c) CollectorTextFileDirectory="${OPTARG}" ;; + d) DaysOld=${OPTARG} ;; + e) Exclude=${OPTARG} ;; + f) ForceRestartStacks=true ;; + F) OnlySpecific=true ;; + i) Notify=true ;; + I) PrintReleaseURL=true ;; + l) OnlyLabel=true ;; + m) MonoMode=true ;; + M) PrintMarkdownURL=true ;; + n) DontUpdate=true; AutoMode=true;; + p) AutoPrune=true ;; + q) Quiet=true ;; + R) SkipRecreate=true ;; + r) DRunUp=true ;; + s) Stopped="-a" ;; + t) Timeout="${OPTARG}" ;; + u) AutoSelfUpdate=true ;; + v) printf "%s\n" "$VERSION"; exit 0 ;; + x) MaxAsync=${OPTARG} ;; + h|*) Help; exit 2 ;; + esac +done +shift "$((OPTIND-1))" + +# Source helper function +source_if_exists_or_fail() { + if [[ -s "$1" ]]; then + source "$1" + # DisplaySourcedFiles used for debugging purposes only + [[ "${DisplaySourcedFiles:-false}" == true ]] && echo " * sourced config: ${1}" + return 0 + else + return 1 + fi } +# User customizable defaults +source_if_exists_or_fail "${HOME}/.config/dockcheck.config" || source_if_exists_or_fail "${ScriptWorkDir}/dockcheck.config" + # Initialise variables Timeout=${Timeout:-10} MaxAsync=${MaxAsync:-1} @@ -89,13 +115,17 @@ BackupForDays=${BackupForDays:-} OnlySpecific=${OnlySpecific:-false} SpecificContainer=${SpecificContainer:-""} SkipRecreate=${SkipRecreate:-false} +LogToFile=${LogToFile:-false} +LogDaysToKeep=${LogDaysToKeep:-14} +LOG_LEVEL=${LOG_LEVEL:-INFO} +Quiet=${Quiet:-false} Excludes=() GotUpdates=() NoUpdates=() GotErrors=() SelectedUpdates=() -Actions=() CurlArgs="--retry ${CurlRetryCount:=3} --retry-delay ${CurlRetryDelay:=1} --connect-timeout ${CurlConnectTimeout:=5} -sf" +LatestOutput="" regbin="" jqbin="" @@ -111,34 +141,132 @@ c_reset="\033[0m" RunTimestamp=$(date +'%Y-%m-%d_%H%M') RunEpoch=$(date +'%s') -while getopts "ayb:BfFhiIlmMnprsuvc:e:d:t:x:R" options; do - case "${options}" in - a|y) AutoMode=true ;; - b) BackupForDays="${OPTARG}" ;; - B) print_backups; exit 0 ;; - c) CollectorTextFileDirectory="${OPTARG}" ;; - d) DaysOld=${OPTARG} ;; - e) Exclude=${OPTARG} ;; - f) ForceRestartStacks=true ;; - F) OnlySpecific=true ;; - i) Notify=true ;; - I) PrintReleaseURL=true ;; - l) OnlyLabel=true ;; - m) MonoMode=true ;; - M) PrintMarkdownURL=true ;; - n) DontUpdate=true; AutoMode=true;; - p) AutoPrune=true ;; - R) SkipRecreate=true ;; - r) DRunUp=true ;; - s) Stopped="-a" ;; - t) Timeout="${OPTARG}" ;; - u) AutoSelfUpdate=true ;; - v) printf "%s\n" "$VERSION"; exit 0 ;; - x) MaxAsync=${OPTARG} ;; - h|*) Help; exit 2 ;; +# Duplicate stdout file descriptor for command output purposes +exec 5>&1 + +# Exec helper function +# Executes the command and arguments passed while both outputting and capturing output to the LatestOutput variable while maintaining return status code +exec_command() { + LatestOutput="" + if [[ "${Quiet}" == "false" ]]; then + LatestOutput=$("$@" | tee >(cat - >&5)) + else + LatestOutput=$("$@") + fi + + return $? +} + +# File Log Path +LogFile="${ScriptWorkDir}/log/${RunTimestamp}.log" +[[ "${LogToFile}" == "true" ]] && mkdir -p $(dirname "${LogFile}") + +# Assign log level to number (based on OpenTelemetry log severity values) +# Acceptable values are recognized strings or an integer between 0 and 24 +log_severity() { + local loglevel="$1" + local levelnum=0 + + case "${loglevel}" in + TRACE) + levelnum=1 + ;; + DEBUG) + levelnum=5 + ;; + INFO) + levelnum=9 + ;; + WARN) + levelnum=13 + ;; + ERROR) + levelnum=17 + ;; + FATAL) + levelnum=21 + ;; + *) + if [[ "${loglevel}" =~ ^[0-9]+$ ]]; then + if [[ $loglevel -ge 0 ]] && [[ $loglevel -le 24 ]]; then + levelnum=${loglevel} + else + levelnum=0 + fi + else + levelnum=0 + fi + ;; esac -done -shift "$((OPTIND-1))" + + echo ${levelnum} +} + +LOG_LEVEL_NUM=$(log_severity $LOG_LEVEL) + +# Logger helper functions +# Log and print out. For script managed output. +log_print() { + local logvar="$1" + local loglevel="$2" + local format="$3" + shift 3 + + levelnum=$(log_severity ${loglevel}) + + local buffer="" + + if [[ $levelnum -ge $LOG_LEVEL_NUM ]]; then + # Don't print if in quiet mode + [[ "$Quiet" == "false" ]] && printf "${format}\n" "$@" + # Also log to buffer and file if configured + log "$logvar" "$loglevel" "$format" "$@" + fi +} + +# Log only. For use with command executions that already send output or when no stdout is desired. +log() { + local logvar="$1" + local loglevel="$2" + local format="$3" + shift 3 + + levelnum=$(log_severity ${loglevel}) + + local buffer="" + + if [[ $levelnum -ge $LOG_LEVEL_NUM ]]; then + printf -v buffer "${format}" "$@" # store in buffer + + # optionally output to file if enabled + # Replace all newlines with literal \n and remove all color codes + [[ "${LogToFile}" == "true" ]] && { printf "[%(%Y-%m-%d %H:%M:%S)T] [%-5s] ${format}" -1 "$loglevel" "$@" | awk '{printf "%s\\n", $0}' | sed -r 's/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]//g'; echo; } >> "${LogFile}" # write to file + + declare -n out_array="${logvar}" + out_array+=("${buffer}") + fi +} + +log_cleanup() { + find "$(dirname "${LogFile}")" -type f -name '*.log' -mtime +${LogDaysToKeep} -exec rm {} \; +} + +print_buffer() { + declare -n buffer="$1" + local BufToString + + if [[ "${#buffer[@]}" -gt 0 ]]; then + BufToString=$( printf '%s' "${buffer[@]}" ) + echo "${BufToString}" + fi +} + +# Print current backups function +print_backups() { + printf "\n%b---%b Currently backed up images %b---%b\n\n" "$c_teal" "$c_blue" "$c_teal" "$c_reset" + docker images --format table | sed -ne '/^REPOSITORY/p' -ne '/^dockcheck/p' +} +[[ "${PrintBackups:-false}" == "true" ]] && { exec_command print_backups; log backupbuf INFO "$LatestOutput"; exit 0; } # Set $1 to a variable for name filtering later, rewriting if multiple SearchName="${1:-}" @@ -160,7 +288,6 @@ fi if [[ "$DontUpdate" == true ]]; then AutoMode=true; fi if [[ "$MonoMode" == true ]]; then declare c_{red,green,yellow,blue,teal,reset}=""; fi if [[ "$Notify" == true ]]; then - source "${ScriptWorkDir}/notify_templates/notify_v2.sh" source_if_exists_or_fail "${ScriptWorkDir}/notify.sh" || source_if_exists_or_fail "${ScriptWorkDir}/notify_templates/notify_v2.sh" || Notify=false fi if [[ -n "$Exclude" ]]; then @@ -408,7 +535,7 @@ list_options() { # Version check & initiate self update if [[ "$LatestSnippet" != "undefined" ]]; then if [[ "$VERSION" != "$LatestRelease" ]]; then - printf "New version available! %b%s%b ⇒ %b%s%b \n Change Notes: %s \n" "$c_yellow" "$VERSION" "$c_reset" "$c_green" "$LatestRelease" "$c_reset" "$LatestChanges" + log_print mainbuf INFO "New version available! %b%s%b ⇒ %b%s%b \n Change Notes: %s \n" "$c_yellow" "$VERSION" "$c_reset" "$c_green" "$LatestRelease" "$c_reset" "$LatestChanges" if [[ "$AutoMode" == false ]]; then read -r -p "Would you like to update? y/[n]: " SelfUpdate [[ "$SelfUpdate" =~ [yY] ]] && self_update @@ -518,7 +645,7 @@ fi # Asynchronously check the image-hash of every running container VS the registry while read -r line; do ((RegCheckQue+=1)) - if [[ "$MonoMode" == false ]]; then progress_bar "$RegCheckQue" "$ContCount"; fi + if [[ "$MonoMode" == false ]]; then exec_command progress_bar "$RegCheckQue" "$ContCount"; fi Got=${line%% *} # Extracts the first word (NoUpdates, GotUpdates, GotErrors) item=${line#* } @@ -612,23 +739,23 @@ if [[ -n "${GotUpdates:-}" ]]; then if [[ "$DRunUp" == true ]]; then docker pull "$ContImage" printf "%s\n" "$i got a new image downloaded, rebuild manually with preferred 'docker run'-parameters" - Actions+=("Pull $ContImage;Success;Manual rebuild with 'docker run'-parameters required") + log actionbuf INFO "Pull $ContImage;Success;Manual rebuild with 'docker run'-parameters required" else printf "\n%b%s%b has no compose labels, probably started with docker run - %bskipping%b\n\n" "$c_yellow" "$i" "$c_reset" "$c_yellow" "$c_reset" - Actions+=("Pull $ContImage;Skipped;Started with docker run") + log actionbuf INFO "Pull $ContImage;Skipped;Started with docker run" fi continue fi - if docker pull "$ContImage"; then + if exec_command docker pull "$ContImage"; then # Removal of the -tag image left behind from backup if [[ ! -z "${ContRepoDigests:-}" ]] && [[ -n "${BackupForDays:-}" ]]; then docker rmi "$ContRepoDigests"; fi - SuccessfulUpdates+=("$i") - Actions+=("Pull $ContImage;Success") + SuccessfulUpdates+=("$i") + log actionbuf INFO "Pull $ContImage;Success" else - printf "\n%bError pulling update for %S. Skipping. %b\n" "$c_red" "$i" "$c_reset" + printf "\n%bError pulling update for %S. Skipping. %b\n" "$c_red" "$i" "$c_reset" FailedUpdates+=("$i") - Actions+=("Pull $ContImage;Error") + log actionbuf ERROR "Pull $ContImage;Error" fi done @@ -663,18 +790,17 @@ if [[ -n "${GotUpdates:-}" ]]; then if [[ "$ContStateRunning" == "true" ]]; then printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" - { [[ "${RestartedStacks[@]}" == *"$ContPath"* ]] && { [[ $OnlySpecific != true ]] || [[ $ContOnlySpecific != true ]] } } && printf "%bStack already restarted. Skipping.%b\n" "$c_yellow" "$c_reset" else printf "\n%bSkipping recreation of %b%s%b as it's not running.%b\n" "$c_yellow" "$c_blue" "$i" "$c_yellow" "$c_reset" - Actions+=("Recreate $i;Skipped;Not Running") + log actionbuf INFO "Recreate $i;Skipped;Not Running" continue fi # Checking if compose-values are empty - hence started with docker run - [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; Actions+=("Recreate $i;Skipped;Not compose") ; continue; } + [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; log actionbuf INFO "Recreate $i;Skipped;Not compose" ; continue; } # cd to the compose-file directory to account for people who use relative volumes - cd "$ContPath" || { printf "\n%bPath error - skipping%b %s" "$c_red" "$c_reset" "$i"; Actions+=("Recreate $i;Skipped;Path error") ; continue; } + cd "$ContPath" || { printf "\n%bPath error - skipping%b %s" "$c_red" "$c_reset" "$i"; log actionbuf INFO "Recreate $i;Skipped;Path error" ; continue; } # Reformatting path + multi compose if [[ $ContConfigFile == '/'* ]]; then CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s " "$conf"; done) @@ -689,13 +815,34 @@ if [[ -n "${GotUpdates:-}" ]]; then # Check if the whole stack should be restarted if [[ "$ContRestartStack" == true ]] || [[ "$ForceRestartStacks" == true ]]; then - [[ "${RestartedStacks[@]}" != *"$ContPath"* ]] && { ${DockerBin} ${CompleteConfs} stop; ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d && Actions+=("Recreate $i;Success;Full stack restart") || { printf "\n%bFailed to recreate $i, skipping.%b\n" "$c_red" "$c_reset" ; Actions+=("Recreate $i;Error;Failed to start stack") ; } } - RestartedStacks+=("$ContPath") + # Restart if compose path has not already been restarted + if [[ "${RestartedStacks[@]}" != *"$ContPath"* ]]; then # Restart if stack has not already been restarted + if ${DockerBin} ${CompleteConfs} stop; ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d; then + log actionbuf INFO "Recreate $i;Success;Full stack restart" + RestartedStacks+=("$ContPath") + else + printf "\n%bFailed to recreate $i, skipping.%b\n" "$c_red" "$c_reset" + log actionbuf INFO "Recreate $i;Error;Failed to start stack" + fi + else + printf "%bStack already restarted. Skipping.%b\n" "$c_yellow" "$c_reset" + fi else - { [[ "${RestartedStacks[@]}" != *"$ContPath"* ]] || [[ -n "${SpecificContainer:-}" ]] } && { ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} && Actions+=("Recreate $i;Success;${SpecificContainer:-Stack}") || { printf "\n%bFailed to recreate $i, skipping.%b\n" "$c_red" "$c_reset" ; Actions+=("Recreate $i;Error;Failed to start ${SpecificContainer:-Stack}") ; } } - if [[ -z "${SpecificContainer:-}" ]]; then - RestartedStacks+=("$ContPath") - fi + # Restart if compose path has not already been restarted or specific container(s) are configured to be restarted individually + if [[ "${RestartedStacks[@]}" != *"$ContPath"* ]] || [[ -n "${SpecificContainer:-}" ]]; then + if ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer}; then + log actionbuf INFO "Recreate $i;Success;${SpecificContainer:-Stack}" + # Consider stack restarted only if specific container is not set + if [[ -z "${SpecificContainer:-}" ]]; then + RestartedStacks+=("$ContPath") + fi + else + printf "\n%bFailed to recreate $i, skipping.%b\n" "$c_red" "$c_reset" + log actionbuf INFO "Recreate $i;Error;Failed to start ${SpecificContainer:-Stack}" + fi + else + printf "%bStack already restarted. Skipping.%b\n" "$c_yellow" "$c_reset" + fi fi done fi @@ -704,7 +851,7 @@ if [[ -n "${GotUpdates:-}" ]]; then # Trigger pruning only when backup-function is not used if [[ -z "${BackupForDays:-}" ]]; then if [[ "$AutoPrune" == false ]] && [[ "$AutoMode" == false ]]; then printf "\n"; read -rep "Would you like to prune all dangling images? y/[n]: " AutoPrune; fi - if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then printf "\nAuto pruning.."; docker image prune -f && Actions+=("Prune images;Success"); fi + if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then printf "\nAuto pruning.."; docker image prune -f && log actionbuf INFO "Prune images;Success"; fi fi else @@ -718,6 +865,10 @@ fi [[ -n "${BackupForDays:-}" ]] && remove_backups # Send final summary notification if enabled -[[ "${Notify}" == true ]] && [[ "${ENABLE_SUMMARY_NOTIFICATION:-}" == true ]] && [[ "${#Actions[@]} -gt 0" ]] && { exec_if_exists_or_fail send_summary_notification || printf "Could not source summary notification function.\n"; } +[[ "${Notify}" == true ]] && [[ "${ENABLE_SUMMARY_NOTIFICATION:-}" == true ]] && [[ -n "${actionbuf:-}" ]] && [[ "${#actionbuf[@]} -gt 0" ]] && { exec_if_exists_or_fail send_summary_notification || printf "Could not source summary notification function.\n"; } + +if [[ -d $(dirname "${LogFile}") ]]; then + log_cleanup +fi exit 0 diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index 2ed7246..a722bfc 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -317,7 +317,7 @@ send_summary_notification() { MessageTitle="$FromHost - Action summary" - UpdToString=$( printf '%s\\n' "${Actions[@]}" ) + UpdToString=$( printf '%s\\n' "${actionbuf[@]}" ) UpdToString="${UpdToString%, }" UpdToString=${UpdToString%\\n} From 37f81de55665b2ae3c9df27195cedf324ecd6d75 Mon Sep 17 00:00:00 2001 From: Vorezal <37914382+vorezal@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:35:36 +0000 Subject: [PATCH 3/3] Make buffer print and notification generic. Add print_buffer notification arguments. --- dockcheck.sh | 21 ++++++++++++++------ notify_templates/notify_v2.sh | 36 +++++++++++++++++------------------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/dockcheck.sh b/dockcheck.sh index 7b840ed..9b29da5 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -218,7 +218,7 @@ log_print() { if [[ $levelnum -ge $LOG_LEVEL_NUM ]]; then # Don't print if in quiet mode - [[ "$Quiet" == "false" ]] && printf "${format}\n" "$@" + [[ "$Quiet" == "false" ]] && printf "${format}" "$@" # Also log to buffer and file if configured log "$logvar" "$loglevel" "$format" "$@" fi @@ -240,7 +240,10 @@ log() { # optionally output to file if enabled # Replace all newlines with literal \n and remove all color codes - [[ "${LogToFile}" == "true" ]] && { printf "[%(%Y-%m-%d %H:%M:%S)T] [%-5s] ${format}" -1 "$loglevel" "$@" | awk '{printf "%s\\n", $0}' | sed -r 's/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]//g'; echo; } >> "${LogFile}" # write to file + [[ "${LogToFile}" == "true" ]] && \ + { printf "[%(%Y-%m-%d %H:%M:%S)T] [%-5s] ${format}" -1 "$loglevel" "$@" | \ + awk 'NR > 1 {printf "%s\\n", prev} {prev = $0} END {printf "%s\n", prev}' | \ + sed -r 's/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]//g'; } >> "${LogFile}" declare -n out_array="${logvar}" out_array+=("${buffer}") @@ -255,9 +258,15 @@ print_buffer() { declare -n buffer="$1" local BufToString - if [[ "${#buffer[@]}" -gt 0 ]]; then - BufToString=$( printf '%s' "${buffer[@]}" ) - echo "${BufToString}" + if [[ -n "${buffer:-}" ]] && [[ "${#buffer[@]}" -gt 0 ]]; then + if [[ "${2:-}" != "notifyonly" ]]; then + BufToString=$( printf '%s' "${buffer[@]}" ) + echo "${BufToString}" + fi + + if [[ "${2:-}" == "notify" ]] || [[ "${2:-}" == "notifyonly" ]]; then + exec_if_exists_or_fail send_buffer_notification $1 ${3:-} || printf "Could not source buffer notification function.\n"; + fi fi } @@ -865,7 +874,7 @@ fi [[ -n "${BackupForDays:-}" ]] && remove_backups # Send final summary notification if enabled -[[ "${Notify}" == true ]] && [[ "${ENABLE_SUMMARY_NOTIFICATION:-}" == true ]] && [[ -n "${actionbuf:-}" ]] && [[ "${#actionbuf[@]} -gt 0" ]] && { exec_if_exists_or_fail send_summary_notification || printf "Could not source summary notification function.\n"; } +[[ "${Notify}" == true ]] && [[ "${ENABLE_SUMMARY_NOTIFICATION:-}" == true ]] && print_buffer actionbuf notifyonly Actions; if [[ -d $(dirname "${LogFile}") ]]; then log_cleanup diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index a722bfc..9643084 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -180,7 +180,7 @@ format_output() { local FormattedTextTemplate="$3" local tempcsv="" - if [[ "${UpdateType}" == "summary" ]]; then + if [[ "${UpdateType}" == "buffer" ]]; then tempcsv="${UpdToString//;/,}" elif [[ ! "${UpdateType}" == "dockcheck_update" ]]; then tempcsv="${UpdToString// -> /,}" @@ -196,17 +196,17 @@ format_output() { FormattedOutput="${tempcsv}" fi elif [[ "${OutputFormat}" == "json" ]]; then - if [[ "${UpdateType}" == "summary" ]] && [[ -z "${UpdToString}" ]]; then - FormattedOutput='{"actions": []}' + if [[ "${UpdateType}" == "buffer" ]] && [[ -z "${UpdToString}" ]]; then + FormattedOutput='{"buffer": []}' elif [[ -z "${UpdToString}" ]]; then FormattedOutput='{"updates": []}' else if [[ "${UpdateType}" == "container_update" ]]; then # container updates case FormattedOutput=$(jq --compact-output --null-input --arg updates "${tempcsv}" '($updates | split("\\n")) | map(split(",")) | {"updates": map({"container_name": .[0], "release_notes": .[1]})} | del(..|nulls)') - elif [[ "${UpdateType}" == "summary" ]]; then - # final summary notification case - FormattedOutput=$(jq --compact-output --null-input --arg actions "${tempcsv}" '($actions | split("\\n")) | map(split(",")) | {"actions": map({"action": .[0], "result": .[1], "description": .[2]})} | del(..|nulls)') + elif [[ "${UpdateType}" == "buffer" ]]; then + # buffer notification case + FormattedOutput=$(jq --compact-output --null-input --arg buffer "${tempcsv}" '($buffer | split("\\n")) | map(split(",")) | {"events": map({"event": .[0], "result": .[1], "description": .[2]})} | del(..|nulls)') elif [[ "${UpdateType}" == "notify_update" ]]; then # script updates case FormattedOutput=$(jq --compact-output --null-input --arg updates "${tempcsv}" '($updates | split("\\n")) | map(split(",")) | {"updates": map({"script_name": .[0], "installed_version": .[1], "latest_version": .[2]})}') @@ -223,7 +223,7 @@ format_output() { else if [[ "${UpdateType}" == "container_update" ]]; then FormattedOutput="${FormattedTextTemplate//${UpdToString}}" - elif [[ "${UpdateType}" == "summary" ]]; then + elif [[ "${UpdateType}" == "buffer" ]]; then FormattedOutput="${FormattedTextTemplate//${UpdToString}}" elif [[ "${UpdateType}" == "notify_update" ]]; then FormattedOutput="${FormattedTextTemplate//${UpdToString}}" @@ -310,31 +310,31 @@ send_notification() { return 0 } -### Set ENABLE_SUMMARY_NOTIFICATION=true in dockcheck.config -### to send a final action summary notification -send_summary_notification() { +### Set notification settings in dockcheck.config +### to send buffer notifications +send_buffer_notification() { Notified="false" - MessageTitle="$FromHost - Action summary" + declare -n buffer="$1" - UpdToString=$( printf '%s\\n' "${actionbuf[@]}" ) - UpdToString="${UpdToString%, }" - UpdToString=${UpdToString%\\n} + MessageTitle="$FromHost - ${2:-$1}" + + UpdToString=$( printf '%s' "${buffer[@]}" ) for channel in "${enabled_notify_channels[@]}"; do - local SkipNotification=$(skip_notification "${channel}" "1" "summary") + local SkipNotification=$(skip_notification "${channel}" "1" "buffer") if [[ "${SkipNotification}" == "false" ]]; then local template=$(get_channel_template "${channel}") # Formats UpdToString variable per channel settings - format_output "summary" "$(output_format "${channel}")" "🐋 Actions taken by $FromHost:\n\n" + format_output "buffer" "$(output_format "${channel}")" "🐋 ${2:-Events} on $FromHost:\n\n" # Setting the MessageBody variable here. printf -v MessageBody "${FormattedOutput}" - printf "\nSending ${channel} summary notification" + printf "\nSending ${channel} buffer notification" exec_if_exists_or_fail trigger_${template}_notification "${channel}" || \ - printf "\nAttempted to send summary notification to channel ${channel}, but the function was not found. Make sure notify_${template}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory." + printf "\nAttempted to send buffer notification to channel ${channel}, but the function was not found. Make sure notify_${template}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory." Notified="true" fi done