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() {