From e4b5aaeb9d4f69293eff16404e8f9aedf9886169 Mon Sep 17 00:00:00 2001 From: op4lat <155382511+op4lat@users.noreply.github.com> Date: Wed, 7 May 2025 05:29:21 -0400 Subject: [PATCH 01/55] Enable markdown (#172) --- notify_templates/notify_ntfy-sh.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/notify_templates/notify_ntfy-sh.sh b/notify_templates/notify_ntfy-sh.sh index dc6d24f..686c7a4 100644 --- a/notify_templates/notify_ntfy-sh.sh +++ b/notify_templates/notify_ntfy-sh.sh @@ -11,10 +11,17 @@ trigger_notification() { # Modify to fit your setup: NtfyUrl="ntfy.sh/YourUniqueTopicName" + if [[ "$PrintMarkdownURL" == true ]]; then + ContentType="Markdown: yes" + else + ContentType="Markdown: no" #text/plain + fi + curl -sS -o /dev/null --show-error --fail \ -H "Title: $MessageTitle" \ + -H "$ContentType" \ -d "$MessageBody" \ - $NtfyUrl + "$NtfyUrl" } send_notification() { From ba107a424f3615278618f588a049ebdfd98efbfd Mon Sep 17 00:00:00 2001 From: mag37 Date: Wed, 7 May 2025 11:31:34 +0200 Subject: [PATCH 02/55] version bump --- notify_templates/notify_ntfy-sh.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify_templates/notify_ntfy-sh.sh b/notify_templates/notify_ntfy-sh.sh index 686c7a4..996f275 100644 --- a/notify_templates/notify_ntfy-sh.sh +++ b/notify_templates/notify_ntfy-sh.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_NTFYSH_VERSION="v0.1" +NOTIFY_NTFYSH_VERSION="v0.2" # # Copy/rename this file to notify.sh to enable the notification snippet. # Setup app and subscription at https://ntfy.sh From 8e444a688f4175086e3e1b606a507b376ef76a4c Mon Sep 17 00:00:00 2001 From: mag37 Date: Sun, 11 May 2025 20:50:09 +0200 Subject: [PATCH 03/55] Update rework (#178) * first iteration rewriting the update logic * formatting fixes * Added an option to have compose up only target the specific container. Used with either -F flag, config variable or label. * Skipping update check on non-compose containers unless option is set * Versionbump Added new info and upped the version number. --- README.md | 21 ++++++------ default.config | 5 +-- dockcheck.sh | 88 +++++++++++++++++++++++++++++++++++--------------- 3 files changed, 77 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e5ace87..8e8efae 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,15 @@ ___ ## :bell: Changelog +- **v0.6.4**: Restructured the update process - first pulls all updates, then recreates all containers. + - Added logic to skip update check on non-compose containers (unless `-r` option). + - Added option `-F` to revert to `compose up -d ` targeting specific container and not the stack. + - Also added corresponding label and config-option. + - Added markdown formatting to `notify_ntfy-sh.sh` template. - **v0.6.3**: Some fixes and changes: - Stops when a container recreation (compose up -d) fails, also `up`s the whole stack now. - `-M`, Markdown format url-releasenotes in notification (requires template rework, look at gotify!) - - Added [addons/DSM/README.md](./addons/DSM/README.md) added for more info Synology DSM info. + - Added [addons/DSM/README.md](./addons/DSM/README.md) for more info Synology DSM info. - Permission checks - graceful exit if no docker permissions + checking if root for pkg-manager. - **v0.6.2**: Style and colour changes, prometheus hotfix, new options: - `-u`, Allow auto self update of dockcheck.sh @@ -33,11 +38,6 @@ ___ - xargs/pipefail, removed `-set -e` bash option for now. - unbound variables fixed (hopefully) - dependency installer from pkgmanager rewritten -- **v0.6.0**: Refactored a lot of code, cleaner logic and syntax, safer variables. - - Safer bash options with `set -euo pipefail`, `shopt -s nullglob` and `failglob`. - - Added a `default.conf` for user settings - persistent through updates. - - Added `notify_slack.sh` template for slack curl api. - ___ @@ -54,7 +54,8 @@ Options: -c D Exports metrics as prom file for the prometheus node_exporter. Provide the collector textfile directory. -d N Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower. -e X Exclude containers, separated by comma. --f Force stack restart after update. Caution: restarts once for every updated container within stack. +-f Force stop+start stack after update. Caution: restarts once for every updated container within stack. +-F Only compose up the specific container, not the whole compose stack (useful for master-compose structure). -h Print this Help. -i Inform - send a preconfigured notification. -I Prints custom releasenote urls alongside each container with updates (requires urls.list). @@ -204,11 +205,13 @@ See [discussion here](https://github.com/mag37/dockcheck/discussions/145). Optionally add labels to compose-files. Currently these are the usable labels: ``` labels: - mag37.dockcheck.restart-stack: true mag37.dockcheck.update: true + mag37.dockcheck.only-specific-container: true + mag37.dockcheck.restart-stack: true ``` -- `mag37.dockcheck.restart-stack: true` works instead of the `-f` option, forcing stop+restart on the whole compose-stack (Caution: Will restart on every updated container within stack). - `mag37.dockcheck.update: true` will when used with the `-l` option only update containers with this label and skip the rest. Will still list updates as usual. +- `mag37.dockcheck.only-specific-container: true` works instead of the `-F` option, specifying the updated container when doing compose up, like `docker compose up -d homer`. +- `mag37.dockcheck.restart-stack: true` works instead of the `-f` option, forcing stop+restart on the whole compose-stack (Caution: Will restart on every updated container within stack). ## :roller_coaster: Workaround for non **amd64** / **arm64** `regctl` provides binaries for amd64/arm64, to use on other architecture you could try this workaround. diff --git a/default.config b/default.config index 0a69d00..406ccbf 100644 --- a/default.config +++ b/default.config @@ -17,8 +17,9 @@ #DaysOld="5" # Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower. #Stopped="-a" # Include stopped containers in the check. (Logic: docker ps -a). #OnlyLabel=true # Only update if label is set. See readme. -#ForceRestartStacks=true # Force stack restart after update. Caution. +#ForceRestartStacks=true # Force stop+start stack after update. Caution: restarts once for every updated container within stack. #DRunUp=true # Allow updating images for docker run, wont update the container. #MonoMode=true # Monochrome mode, no printf colour codes and hides progress bar. #PrintReleaseURL=true # Prints custom releasenote urls alongside each container with updates (requires urls.list)` -#PrintMarkdownURL=true # Prints custom releasenote urls as markdown +#PrintMarkdownURL=true # Prints custom releasenote urls as markdown +#OnlySpecific=true # Only compose up the specific container, not the whole compose. (useful for master-compose structure). diff --git a/dockcheck.sh b/dockcheck.sh index 1a2aa72..9e7de0b 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.6.3" -### ChangeNotes: Permission checks, now compose up on whole stack, -M markdown option added. +VERSION="v0.6.4" +### ChangeNotes: Restructured update process - first pulls all images, then recreates all containers. Added -F option. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -34,7 +34,8 @@ Help() { echo "-c Exports metrics as prom file for the prometheus node_exporter. Provide the collector textfile directory." echo "-d N Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower." echo "-e X Exclude containers, separated by comma." - echo "-f Force stack restart after update. Caution: restarts once for every updated container within stack." + echo "-f Force stop+start stack after update. Caution: restarts once for every updated container within stack." + echo "-F Only compose up the specific container, not the whole compose stack (useful for master-compose structure)." echo "-h Print this Help." echo "-i Inform - send a preconfigured notification." echo "-I Prints custom releasenote urls alongside each container with updates (requires urls.list)." @@ -72,6 +73,8 @@ Stopped=${Stopped:=""} CollectorTextFileDirectory=${CollectorTextFileDirectory:-} Exclude=${Exclude:-} DaysOld=${DaysOld:-} +OnlySpecific=false +SpecificContainer=${SpecificContainer:=""} Excludes=() GotUpdates=() NoUpdates=() @@ -88,13 +91,14 @@ c_blue="\033[0;34m" c_teal="\033[0;36m" c_reset="\033[0m" -while getopts "ayfhiIlmMnprsuvc:e:d:t:x:" options; do +while getopts "ayfFhiIlmMnprsuvc:e:d:t:x:" options; do case "${options}" in a|y) AutoMode=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 ;; @@ -195,9 +199,6 @@ choosecontainers() { done fi done - printf "\n%bUpdating container(s):%b\n" "$c_blue" "$c_reset" - printf "%s\n" "${SelectedUpdates[@]}" - printf "\n" } datecheck() { @@ -385,6 +386,15 @@ check_image() { fi done + # Skipping non-compose containers unless option is set + ContLabels=$(docker inspect "$i" --format '{{json .Config.Labels}}') + ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") + [[ "$ContPath" == "null" ]] && ContPath="" + if [[ -z "$ContPath" ]] && [[ "$DRunUp" == false ]]; then + printf "%s\n" "NoUpdates !$i - not checked, no compose file" + return + fi + local NoUpdates GotUpdates GotErrors ImageId=$(docker inspect "$i" --format='{{.Image}}') RepoUrl=$(docker inspect "$i" --format='{{.Config.Image}}') @@ -409,7 +419,7 @@ check_image() { # Make required functions and variables available to subprocesses export -f check_image datecheck export Excludes_string="${Excludes[*]:-}" # Can only export scalar variables -export t_out regbin RepoUrl DaysOld +export t_out regbin RepoUrl DaysOld DRunUp jqbin # Check for POSIX xargs with -P option, fallback without async if (echo "test" | xargs -P 2 >/dev/null 2>&1) && [[ "$MaxAsync" != 0 ]]; then @@ -483,10 +493,41 @@ if [[ -n "${GotUpdates:-}" ]]; then SelectedUpdates=( "${GotUpdates[@]}" ) fi if [[ "$DontUpdate" == false ]]; then + printf "\n%bUpdating container(s):%b\n" "$c_blue" "$c_reset" + printf "%s\n" "${SelectedUpdates[@]}" + NumberofUpdates="${#SelectedUpdates[@]}" + CurrentQue=0 - for i in "${SelectedUpdates[@]}" - do + for i in "${SelectedUpdates[@]}"; do + ((CurrentQue+=1)) + printf "\n%bNow updating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" + ContLabels=$(docker inspect "$i" --format '{{json .Config.Labels}}') + ContImage=$(docker inspect "$i" --format='{{.Config.Image}}') + ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") + [[ "$ContPath" == "null" ]] && ContPath="" + ContUpdateLabel=$($jqbin -r '."mag37.dockcheck.update"' <<< "$ContLabels") + [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" + # Checking if Label Only -option is set, and if container got the label + [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != true ]] && { echo "No update label, skipping."; continue; } } + + # Checking if compose-values are empty - hence started with docker run + if [[ -z "$ContPath" ]]; then + if [[ "$DRunUp" == true ]]; then + docker pull "$ContImage" + printf "%s\n" "$i got a new image downloaded, rebuild manually with preferred 'docker run'-parameters" + 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" + fi + continue + fi + + docker pull "$ContImage" || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } + done + printf "\n%bDone pulling updates. %bRecreating updated containers.%b\n" "$c_green" "$c_blue" "$c_reset" + + CurrentQue=0 + for i in "${SelectedUpdates[@]}"; do ((CurrentQue+=1)) unset CompleteConfs # Extract labels and metadata @@ -504,17 +545,12 @@ if [[ -n "${GotUpdates:-}" ]]; then [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" ContRestartStack=$($jqbin -r '."mag37.dockcheck.restart-stack"' <<< "$ContLabels") [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" + ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") + [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" # Checking if compose-values are empty - hence started with docker run - if [[ -z "$ContPath" ]]; then - if [[ "$DRunUp" == true ]]; then - docker pull "$ContImage" - printf "%s\n" "$i got a new image downloaded, rebuild manually with preferred 'docker run'-parameters" - 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" - fi - continue - fi + [[ -z "$ContPath" ]] && 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; } ## Reformatting path + multi compose @@ -523,22 +559,22 @@ if [[ -n "${GotUpdates:-}" ]]; then else CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s/%s " "$ContPath" "$conf"; done) fi - printf "\n%bNow updating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" - # Checking if Label Only -option is set, and if container got the label - [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != true ]] && { echo "No update label, skipping."; continue; } } - docker pull "$ContImage" || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } # Check if the container got an environment file set and reformat it ContEnvs="" if [[ -n "$ContEnv" ]]; then ContEnvs=$(for env in ${ContEnv//,/ }; do printf -- "--env-file %s " "$env"; done); fi + # Set variable when compose up should only target the specific container, not the stack + if [[ $OnlySpecific == true ]] || [[ $ContOnlySpecific == true ]]; then SpecificContainer="$ContName"; fi + + printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" # Check if the whole stack should be restarted if [[ "$ContRestartStack" == true ]] || [[ "$ForceRestartStacks" == true ]]; then - ${DockerBin} ${CompleteConfs} stop; ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d + ${DockerBin} ${CompleteConfs} stop; ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } else - ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } + ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } fi done if [[ "$AutoPrune" == false ]] && [[ "$AutoMode" == false ]]; then printf "\n"; read -rep "Would you like to prune dangling images? y/[n]: " AutoPrune; fi - if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then docker image prune -f; fi + if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then printf "\n Auto pruning.."; docker image prune -f; fi printf "\n%bAll done!%b\n" "$c_green" "$c_reset" else printf "\nNo updates installed, exiting.\n" From 7ce523c37d6b7a2ddd4fd721e08f1f32dc4398a3 Mon Sep 17 00:00:00 2001 From: mag37 Date: Mon, 12 May 2025 07:28:49 +0200 Subject: [PATCH 04/55] added sponsor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8e8efae..dab4307 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,7 @@ dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/l - [avegy](https://github.com/avegy) - [eichhorn](https://github.com/eichhorn) +- [stepdg](https://github.com/stepdg) ___ From d80fba750f55c6c88c41dcf21e73996033c6ec18 Mon Sep 17 00:00:00 2001 From: mag37 Date: Mon, 12 May 2025 15:54:34 +0200 Subject: [PATCH 05/55] quickfix printfs --- dockcheck.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dockcheck.sh b/dockcheck.sh index 9e7de0b..4ad9a89 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -257,7 +257,7 @@ binary_downloader() { GetUrl="${BinaryUrl/TEMP/"$architecture"}" if command -v curl &>/dev/null; then curl -L "$GetUrl" > "$ScriptWorkDir/$BinaryName"; elif command -v wget &>/dev/null; then wget "$GetUrl" -O "$ScriptWorkDir/$BinaryName"; - else printf "%s\n" "curl/wget not available - get $BinaryName manually from the repo link, exiting."; exit 1; + else printf "\n%bcurl/wget not available - get %s manually from the repo link, exiting.%b" "$c_red" "$BinaryName" "$c_reset"; exit 1; fi [[ -f "$ScriptWorkDir/$BinaryName" ]] && chmod +x "$ScriptWorkDir/$BinaryName" } @@ -288,7 +288,7 @@ dependency_check() { if command -v "$AppName" &>/dev/null; then export "$AppVar"="$AppName"; elif [[ -f "$ScriptWorkDir/$AppName" ]]; then export "$AppVar"="$ScriptWorkDir/$AppName"; else - printf "%s\n" "Required dependency %b'%s'%b missing, do you want to install it?\n" "$c_teal" "$AppName" "$c_reset" + printf "\nRequired dependency %b'%s'%b missing, do you want to install it?\n" "$c_teal" "$AppName" "$c_reset" read -r -p "y: With packagemanager (sudo). / s: Download static binary. y/s/[n] " GetBin GetBin=${GetBin:-no} # set default to no if nothing is given if [[ "$GetBin" =~ [yYsS] ]]; then @@ -481,7 +481,7 @@ if [[ -n ${GotUpdates[*]:-} ]]; then printf "\n%bContainers with updates available:%b\n" "$c_yellow" "$c_reset" if [[ -s "$ScriptWorkDir/urls.list" ]] && [[ "$PrintReleaseURL" == true ]]; then releasenotes; else Updates=("${GotUpdates[@]}"); fi [[ "$AutoMode" == false ]] && list_options || printf "%s\n" "${Updates[@]}" - [[ "$Notify" == true ]] && { type -t send_notification &>/dev/null && send_notification "${GotUpdates[@]}" || printf "Could not source notification function.\n"; } + [[ "$Notify" == true ]] && { type -t send_notification &>/dev/null && send_notification "${GotUpdates[@]}" || printf "\nCould not source notification function.\n"; } fi # Optionally get updates if there's any From b2d80d036a1896b6d278d40c0ea89874142c9c44 Mon Sep 17 00:00:00 2001 From: mag37 Date: Wed, 14 May 2025 20:55:09 +0200 Subject: [PATCH 06/55] info about -r option --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dab4307..4e33e3d 100644 --- a/README.md +++ b/README.md @@ -250,17 +250,18 @@ function dchk { } ``` +## :warning: `-r flag` disclaimer and warning +**Wont auto-update the containers, only their images. (compose is recommended)** +`docker run` dont support using new images just by restarting a container. +Containers need to be manually stopped, removed and created again to run on the new image. +Using the `-r` option together with eg. `-i` and `-n` to just check for updates and send notifications and not update is safe though! + ## :hammer: Known issues - No detailed error feedback (just skip + list what's skipped). - Not respecting `--profile` options when re-creating the container. - Not working well with containers created by **Portainer**. - **Watchtower** might cause issues due to retagging images when checking for updates (and thereby pulling new images). -## :warning: `-r flag` disclaimer and warning -**Wont auto-update the containers, only their images. (compose is recommended)** -`docker run` dont support using new images just by restarting a container. -Containers need to be manually stopped, removed and created again to run on the new image. - ## :wrench: Debugging If you hit issues, you could check the output of the `extras/errorCheck.sh` script for clues. Another option is to run the main script with debugging in a subshell `bash -x dockcheck.sh` - if there's a particular container/image that's causing issues you can filter for just that through `bash -x dockcheck.sh nginx`. From e4b93d113cf54d2392789abbf76923b1d2e65971 Mon Sep 17 00:00:00 2001 From: mag37 Date: Thu, 15 May 2025 15:49:49 +0200 Subject: [PATCH 07/55] fixed variable errors --- dockcheck.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dockcheck.sh b/dockcheck.sh index 4ad9a89..615fcd9 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -73,7 +73,7 @@ Stopped=${Stopped:=""} CollectorTextFileDirectory=${CollectorTextFileDirectory:-} Exclude=${Exclude:-} DaysOld=${DaysOld:-} -OnlySpecific=false +OnlySpecific=${OnlySpecific:=false} SpecificContainer=${SpecificContainer:=""} Excludes=() GotUpdates=() @@ -546,7 +546,7 @@ if [[ -n "${GotUpdates:-}" ]]; then ContRestartStack=$($jqbin -r '."mag37.dockcheck.restart-stack"' <<< "$ContLabels") [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") - [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" + [[ "$ContOnlySpecific" == "null" ]] && ContRestartStack="" # Checking if compose-values are empty - hence started with docker run [[ -z "$ContPath" ]] && continue From c63e2441faf8ed6001a44ce256ae91eab71f807f Mon Sep 17 00:00:00 2001 From: vorezal <37914382+vorezal@users.noreply.github.com> Date: Sun, 25 May 2025 12:26:13 -0400 Subject: [PATCH 08/55] Refactor notifications and add helper functions (#182) * Refactor notifications and add helper functions * Add helper functions to simplify sourcing files and executing functions if they exist * Create notify_v2.sh wrapper script * Simplify and consolidate notification logic within notify_v2.sh * Support notification management via environment variables * Move secrets to dockcheck.config * Fix NOTIFY_CHANNELS default value when not set * Feedback changes * Remove leading spaces from MessageBody * Check for valid notify v2 variables * Warn on missing configuration and bypass notifications * Update readme * Additional feedback fixes * More comments in default.config with different # depth for comments and settings * Rename NOTIFY_TOPIC_NAME variable to NTFY_TOPIC_NAME for consistency * Add TELEGRAM_TOPIC_ID * Fix AppriseURL variable * Add an ending newline to all MessageBody statements for consistency * Remove troubleshooting echo statement * Prevent attempting to trigger notifications for notification templates if versions are the same --------- Co-authored-by: Matthew Oleksowicz --- README.md | 25 ++++++-- default.config | 60 +++++++++++++++++-- dockcheck.sh | 50 ++++++++++------ notify_templates/notify_DSM.sh | 52 +++-------------- notify_templates/notify_apprise.sh | 67 +++++++-------------- notify_templates/notify_discord.sh | 47 ++++----------- notify_templates/notify_generic.sh | 41 +------------ notify_templates/notify_gotify.sh | 73 +++++++---------------- notify_templates/notify_matrix.sh | 58 +++++------------- notify_templates/notify_ntfy-sh.sh | 68 ++++++---------------- notify_templates/notify_pushbullet.sh | 55 ++++-------------- notify_templates/notify_pushover.sh | 67 ++++++--------------- notify_templates/notify_slack.sh | 62 +++++--------------- notify_templates/notify_smtp.sh | 54 ++++------------- notify_templates/notify_telegram.sh | 67 ++++++--------------- notify_templates/notify_v2.sh | 84 +++++++++++++++++++++++++++ 16 files changed, 366 insertions(+), 564 deletions(-) create mode 100644 notify_templates/notify_v2.sh diff --git a/README.md b/README.md index 4e33e3d..27ef1ad 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ ___ ## :bell: Changelog +- **v0.6.5**: Refactored notification logic. See notify_templates/notify_v2.sh for upgrade steps. + - Added helper functions to simplify sourcing files and executing functions if they exist. + - Created notify_v2.sh wrapper script. + - Simplified and consolidated notification logic within notify_v2.sh. + - Added support for notification management via environment variables. + - Moved notification secrets to dockcheck.config. - **v0.6.4**: Restructured the update process - first pulls all updates, then recreates all containers. - Added logic to skip update check on non-compose containers (unless `-r` option). - Added option `-F` to revert to `compose up -d ` targeting specific container and not the stack. @@ -126,11 +132,22 @@ Alternatively create an alias where specific flags and values are set. Example `alias dc=dockcheck.sh -p -x 10 -t 3`. ## :loudspeaker: Notifications -Trigger with the `-i` flag if `notify.sh` is present and configured. -Will send a list of containers with updates available and a notification when `dockcheck.sh` itself has an update. -Run it scheduled with `-ni` to only get notified when there's updates available! +Trigger with the `-i` flag. +If `notify.sh` is present and configured, it will be used. Otherwise, `notify_v2.sh` will be enabled. +Will send a list of containers with updates available and a notification when `dockcheck.sh` itself has an update. +Run it scheduled with `-ni` to only get notified when there's updates available! + +V2 installation and configuration (tag v0.6.5 or later): +Remove or rename `notify.sh` if previously configured using the legacy method. +Uncomment and set the NOTIFY_CHANNELS environment variable in `dockcheck.config` to a space separated string of your desired notification channels to enable. +Uncomment and set the environment variables related to the enabled notification channels. +It is recommended not to make changes directly to the `notify_X.sh` template files and to use only environment variables defined in `dockcheck.config` using this method. + +Legacy installation and configuration: +Use a previous version of a `notify_X.sh` template file (tag v0.6.4 or earlier) from the **notify_templates** directory, +copy it to `notify.sh` alongside the script, modify it to your needs! (notify.sh is added to .gitignore) + -Use a `notify_X.sh` template file from the **notify_templates** directory, copy it to `notify.sh` alongside the script, modify it to your needs! (notify.sh is added to .gitignore) **Current templates:** - Synology [DSM](https://www.synology.com/en-global/dsm) - Email with [mSMTP](https://wiki.debian.org/msmtp) (or deprecated alternative [sSMTP](https://wiki.debian.org/sSMTP)) diff --git a/default.config b/default.config index 406ccbf..9c13be0 100644 --- a/default.config +++ b/default.config @@ -1,9 +1,9 @@ ### Custom user variables -# Copy this file to "dockcheck.config" to make it active -# Can be placed in ~/.config/ or alongside dockcheck.sh -# -# Uncomment and set your preferred configuration variables here -# This will not be replaced on updates +## Copy this file to "dockcheck.config" to make it active +## Can be placed in ~/.config/ or alongside dockcheck.sh +## +## Uncomment and set your preferred configuration variables here +## This will not be replaced on updates #Timeout=10 # Set a timeout (in seconds) per container for registry checkups. #MaxAsync=10 # Set max asynchronous subprocesses, 1 default, 0 to disable. @@ -23,3 +23,53 @@ #PrintReleaseURL=true # Prints custom releasenote urls alongside each container with updates (requires urls.list)` #PrintMarkdownURL=true # Prints custom releasenote urls as markdown #OnlySpecific=true # Only compose up the specific container, not the whole compose. (useful for master-compose structure). + +### Notify settings +## All commented values are examples only. Modify as needed. +## +## Uncomment the line below and specify the notification channels you wish to enable in a space separated string +# NOTIFY_CHANNELS="apprise discord DSM generic gotify matrix ntfy-sh pushbullet pushover slack smtp telegram" +# +## Uncomment to not send notifications when dockcheck itself has updates. +# DISABLE_DOCKCHECK_NOTIFICATION=false +## Uncomment to not send notifications when notify scripts themselves have updates. +# DISABLE_NOTIFY_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 +# mastodons://{token}@{host} +# pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b +# tgram://{bot_token}/{chat_id}/' +# APPRISE_URL="http://apprise.mydomain.tld:1234/notify/apprise" +# +# DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/" +# +# DSM_SENDMAILTO="me@mydomain.com" +# DSM_SUBJECTTAG="Email Subject Prefix" +# +# GOTIFY_DOMAIN="https://gotify.domain.tld" +# GOTIFY_TOKEN="token-value" +# +# MATRIX_ACCESS_TOKEN="token-value" +# MATRIX_ROOM_ID="myroom" +# MATRIX_SERVER_URL="https://matrix.yourdomain.tld" +# +# NTFY_TOPIC_NAME="YourUniqueTopicName" +# +# PUSHBULLET_URL="https://api.pushbullet.com/v2/pushes" +# PUSHBULLET_TOKEN="token-value" +# +# PUSHOVER_URL="https://api.pushover.net/1/messages.json" +# PUSHOVER_USER_KEY="userkey" +# PUSHOVER_TOKEN="token-value" +# +# SLACK_CHANNEL_ID=mychannel +# SLACK_ACCESS_TOKEN=xoxb-token-value +# +# SMTP_MAIL_FROM="me@mydomain.tld" +# SMTP_MAIL_TO="you@yourdomain.tld" +# SMTP_SUBJECT_TAG="dockcheck" +# +# TELEGRAM_CHAT_ID="mychatid" +# TELEGRAM_TOKEN="token-value" +# TELEGRAM_TOPIC_ID="0" diff --git a/dockcheck.sh b/dockcheck.sh index 615fcd9..a0a5be2 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.6.4" -### ChangeNotes: Restructured update process - first pulls all images, then recreates all containers. Added -F option. +VERSION="v0.6.5" +### ChangeNotes: Refactored notification logic. See README.md for upgrade steps. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -17,12 +17,17 @@ ScriptWorkDir="$(dirname "$ScriptPath")" LatestRelease="$(curl -s -r 0-50 "$RawUrl" | sed -n "/VERSION/s/VERSION=//p" | tr -d '"')" LatestChanges="$(curl -s -r 0-200 "$RawUrl" | sed -n "/ChangeNotes/s/# ChangeNotes: //p")" +# Source helper functions +source_if_exists() { + if [[ -s "$1" ]]; then source "$1"; fi +} + +source_if_exists_or_fail() { + [[ -s "$1" ]] && source "$1" +} + # User customizable defaults -if [[ -s "${HOME}/.config/dockcheck.config" ]]; then - source "${HOME}/.config/dockcheck.config" -elif [[ -s "${ScriptWorkDir}/dockcheck.config" ]]; then - source "${ScriptWorkDir}/dockcheck.config" -fi +source_if_exists_or_fail "${HOME}/.config/dockcheck.config" || source_if_exists "${ScriptWorkDir}/dockcheck.config" # Help Function Help() { @@ -120,14 +125,16 @@ shift "$((OPTIND-1))" # Set $1 to a variable for name filtering later SearchName="${1:-}" +# Basic notify configuration check +if [[ "${Notify}" == true ]] && [[ ! -s "${ScriptWorkDir}/notify.sh" ]] && [[ -z "${NOTIFY_CHANNELS:-}" ]]; then + printf "Using v2 notifications with -i flag passed but no notify channels configured in dockcheck.config. This will result in no notifications being sent.\n" +fi + # Setting up options and sourcing functions if [[ "$DontUpdate" == true ]]; then AutoMode=true; fi if [[ "$MonoMode" == true ]]; then declare c_{red,green,yellow,blue,teal,reset}=""; fi if [[ "$Notify" == true ]]; then - if [[ -s "${ScriptWorkDir}/notify.sh" ]]; then - source "${ScriptWorkDir}/notify.sh" - else Notify=false - fi + 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 IFS=',' read -ra Excludes <<< "$Exclude" @@ -148,6 +155,14 @@ if [[ -n "$CollectorTextFileDirectory" ]]; then fi fi +exec_if_exists() { + if [[ $(type -t $1) == function ]]; then "$@"; fi +} + +exec_if_exists_or_fail() { + [[ $(type -t $1) == function ]] && "$@" +} + self_update_curl() { cp "$ScriptPath" "$ScriptPath".bak if command -v curl &>/dev/null; then @@ -335,10 +350,13 @@ if [[ "$VERSION" != "$LatestRelease" ]]; then [[ "$SelfUpdate" =~ [yY] ]] && self_update elif [[ "$AutoMode" == true ]] && [[ "$AutoSelfUpdate" == true ]]; then self_update; else - [[ "$Notify" == true ]] && { [[ $(type -t dockcheck_notification) == function ]] && dockcheck_notification "$VERSION" "$LatestRelease" "$LatestChanges" || printf "Could not source notification function.\n"; } + [[ "$Notify" == true ]] && { exec_if_exists_or_fail dockcheck_notification "$VERSION" "$LatestRelease" "$LatestChanges" || printf "Could not source notification function.\n"; } fi fi +# Version check for notify templates +[[ "$Notify" == true ]] && { exec_if_exists_or_fail notify_update_notification || printf "Could not source notify notification function.\n"; } + dependency_check "regctl" "regbin" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" dependency_check "jq" "jqbin" "https://github.com/jqlang/jq/releases/latest/download/jq-linux-TEMP" @@ -457,11 +475,7 @@ unset IFS # Run the prometheus exporter function if [[ -n "${CollectorTextFileDirectory:-}" ]]; then - if type -t prometheus_exporter &>/dev/null; then - prometheus_exporter ${#NoUpdates[@]} ${#GotUpdates[@]} ${#GotErrors[@]} - else - printf "%s\n" "Could not source prometheus exporter function." - fi + exec_if_exists_or_fail prometheus_exporter ${#NoUpdates[@]} ${#GotUpdates[@]} ${#GotErrors[@]} || printf "%s\n" "Could not source prometheus exporter function." fi # Define how many updates are available @@ -481,7 +495,7 @@ if [[ -n ${GotUpdates[*]:-} ]]; then printf "\n%bContainers with updates available:%b\n" "$c_yellow" "$c_reset" if [[ -s "$ScriptWorkDir/urls.list" ]] && [[ "$PrintReleaseURL" == true ]]; then releasenotes; else Updates=("${GotUpdates[@]}"); fi [[ "$AutoMode" == false ]] && list_options || printf "%s\n" "${Updates[@]}" - [[ "$Notify" == true ]] && { type -t send_notification &>/dev/null && send_notification "${GotUpdates[@]}" || printf "\nCould not source notification function.\n"; } + [[ "$Notify" == true ]] && { exec_if_exists_or_fail send_notification "${GotUpdates[@]}" || printf "\nCould not source notification function.\n"; } fi # Optionally get updates if there's any diff --git a/notify_templates/notify_DSM.sh b/notify_templates/notify_DSM.sh index ddbc2c6..17d697f 100644 --- a/notify_templates/notify_DSM.sh +++ b/notify_templates/notify_DSM.sh @@ -1,11 +1,10 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DSM_VERSION="v0.1" +NOTIFY_DSM_VERSION="v0.2" # INFO: ssmtp is deprecated - consider to use msmtp instead. # -# Copy/rename this file to notify.sh to enable the notification snipppet. # mSMTP/sSMTP has to be installed and configured manually. # The existing DSM Notification Email configuration will be used automatically. -# Modify to your liking - changing SendMailTo and Subject and content. +# Do not modify this file directly. Set DSM_SENDMAILTO and DSM_SUBJECTTAG in your dockcheck.config file. MSMTP=$(which msmtp) SSMTP=$(which ssmtp) @@ -18,18 +17,17 @@ else echo "No msmtp or ssmtp binary found in PATH: $PATH" ; exit 1 fi -FromHost=$(hostname) - -trigger_notification() { +trigger_DSM_notification() { CfgFile="/usr/syno/etc/synosmtp.conf" # User variables: # Automatically sends to your usual destination for synology DSM notification emails. -# You can also manually override by assigning something else to SendMailTo below. -SendMailTo=$(grep 'eventmail1' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p') -#SendMailTo="me@mydomain.com" +# You can also manually override by assigning something else to DSM_SENDMAILTO in dockcheck.config. +SendMailTo=${DSM_SENDMAILTO:-$(grep 'eventmail1' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p')} +# e.g. DSM_SENDMAILTO="me@mydomain.com" -SubjectTag=$(grep 'eventsubjectprefix' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p') +SubjectTag=${DSM_SUBJECTTAG:-$(grep 'eventsubjectprefix' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p')} +# e.g. DSM_SUBJECTTAG="Email Subject Prefix" SenderName=$(grep 'smtp_from_name' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p') SenderMail=$(grep 'smtp_from_mail' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p') SenderMail=${SenderMail:-$(grep 'eventmail1' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p')} @@ -38,7 +36,7 @@ $MailPkg $SendMailTo << __EOF From: "$SenderName" <$SenderMail> date:$(date -R) To: <$SendMailTo> -Subject: $SubjectTag $MessageTitle $FromHost +Subject: $SubjectTag $MessageTitle Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit @@ -48,35 +46,3 @@ __EOF # This ensures DSM's container manager will also see the update /var/packages/ContainerManager/target/tool/image_upgradable_checker } - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - printf "\nSending email notification.\n" - - MessageTitle="Updates available on" - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending email dockcheck notification.\n" - - MessageTitle="New version of dockcheck available on" - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1\nLatest version: $2\n\nChangenotes: $3\n" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_DSM.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_DSM_VERSION/s/NOTIFY_DSM_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_DSM_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_DSM.sh update avialable:\n $NOTIFY_DSM_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification -} diff --git a/notify_templates/notify_apprise.sh b/notify_templates/notify_apprise.sh index fef4303..e71a4df 100644 --- a/notify_templates/notify_apprise.sh +++ b/notify_templates/notify_apprise.sh @@ -1,55 +1,30 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_APPRISE_VERSION="v0.1" +NOTIFY_APPRISE_VERSION="v0.2" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Required receiving services must already be set up. -# Modify to fit your setup - if API, set AppriseURL to your Apprise ip/domain. +# Do not modify this file directly. Set APPRISE_PAYLOAD in your dockcheck.config file. +# If API, set APPRISE_URL instead. -FromHost=$(hostname) +if [[ -z "${APPRISE_PAYLOAD:-}" ]] && [[ -z "${APPRISE_URL:-}" ]]; then + printf "Apprise notification channel enabled, but required configuration variables are missing. Apprise notifications will not be sent.\n" -trigger_notification() { + remove_channel apprise +fi - ### Modify to fit your setup: - apprise -vv -t "$MessageTitle" -b "$MessageBody" \ - mailto://myemail:mypass@gmail.com \ - mastodons://{token}@{host} \ - pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b \ - tgram://{bot_token}/{chat_id}/ +trigger_apprise_notification() { - ### If you use the Apprise-API - Comment out the apprise command above. - ### Uncomment the AppriseURL and the curl-line below: - # AppriseURL="http://apprise.mydomain.tld:1234/notify/apprise" - # curl -X POST -F "title=$MessageTitle" -F "body=$MessageBody" -F "tags=all" $AppriseURL -} + if [[ -n "${APPRISE_PAYLOAD:-}" ]]; then + apprise -vv -t "$MessageTitle" -b "$MessageBody" \ + ${APPRISE_PAYLOAD} + fi -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) + # e.g. APPRISE_PAYLOAD='mailto://myemail:mypass@gmail.com + # mastodons://{token}@{host} + # pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b + # tgram://{bot_token}/{chat_id}/' - printf "\nSending Apprise notification\n" - - MessageTitle="$FromHost - updates available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending Apprise dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_apprise.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_APPRISE_VERSION/s/NOTIFY_APPRISE_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_APPRISE_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_apprise.sh update avialable:\n $NOTIFY_APPRISE_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification -} + if [[ -n "${APPRISE_URL:-}" ]]; then + AppriseURL="${APPRISE_URL}" + curl -X POST -F "title=$MessageTitle" -F "body=$MessageBody" -F "tags=all" $AppriseURL # e.g. APPRISE_URL=http://apprise.mydomain.tld:1234/notify/apprise + fi +} \ No newline at end of file diff --git a/notify_templates/notify_discord.sh b/notify_templates/notify_discord.sh index 5728f74..a28cda5 100644 --- a/notify_templates/notify_discord.sh +++ b/notify_templates/notify_discord.sh @@ -1,43 +1,18 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DISCORD_VERSION="v0.1" +NOTIFY_DISCORD_VERSION="v0.2" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Required receiving services must already be set up. -# Modify to fit your setup - set DiscordWebhookUrl +# Do not modify this file directly. Set DISCORD_WEBHOOK_URL in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${DISCORD_WEBHOOK_URL:-}" ]]; then + printf "Discord notification channel enabled, but required configuration variables are missing. Discord notifications will not be sent.\n" -trigger_notification() { - # Modify to fit your setup: - DiscordWebhookUrl="PasteYourFullDiscordWebhookURL" + remove_channel discord +fi - MsgBody="{\"username\":\"$FromHost\",\"content\":\"$MessageBody\"}" - curl -sS -o /dev/null --fail -X POST -H "Content-Type: application/json" -d "$MsgBody" "$DiscordWebhookUrl" -} - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - printf "\nSending Discord notification\n" - # Setting the MessageBody variable here. - MessageBody="🐋 Containers on $FromHost with updates available: \n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending Discord dockcheck notification\n" - MessageBody="$FromHost - New version of dockcheck available: \n Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_discord.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_DISCORD_VERSION/s/NOTIFY_DISCORD_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_DISCORD_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_discord.sh update avialable:\n $NOTIFY_DISCORD_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification +trigger_discord_notification() { + DiscordWebhookUrl="${DISCORD_WEBHOOK_URL}" # e.g. DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/ + + MsgBody="{\"username\":\"$FromHost\",\"content\":\"$MessageBody\"}" + curl -sS -o /dev/null --fail -X POST -H "Content-Type: application/json" -d "$MsgBody" "$DiscordWebhookUrl" } diff --git a/notify_templates/notify_generic.sh b/notify_templates/notify_generic.sh index 700e84a..cbcbf6e 100644 --- a/notify_templates/notify_generic.sh +++ b/notify_templates/notify_generic.sh @@ -1,44 +1,9 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_GENERIC_VERSION="v0.1" +NOTIFY_GENERIC_VERSION="v0.2" # -# Copy/rename this file to notify.sh to enable the notification snippet. # generic sample, the "Hello World" of notification addons -FromHost=$(hostname) - -trigger_notification() { - # Modify to fit your setup: +trigger_generic_notification() { printf "\n$MessageTitle\n" printf "\n$MessageBody\n" -} - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - # platform specific notification code would go here - printf "\n%bGeneric notification addon:%b" "$c_green" "$c_reset" - MessageTitle="$FromHost - updates available." - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nGeneric dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_generic.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_GENERIC_VERSION/s/NOTIFY_GENERIC_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_GENERIC_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_generic.sh update avialable:\n $NOTIFY_GENERIC_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification -} +} \ No newline at end of file diff --git a/notify_templates/notify_gotify.sh b/notify_templates/notify_gotify.sh index 94d1d97..f66e7e8 100644 --- a/notify_templates/notify_gotify.sh +++ b/notify_templates/notify_gotify.sh @@ -1,61 +1,30 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_GOTIFY_VERSION="v0.2" +NOTIFY_GOTIFY_VERSION="v0.3" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Required receiving services must already be set up. -# Modify to fit your setup - set GotifyUrl and GotifyToken. +# Do not modify this file directly. Set GOTIFY_TOKEN and GOTIFY_DOMAIN in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${GOTIFY_TOKEN:-}" ]] || [[ -z "${GOTIFY_DOMAIN:-}" ]]; then + printf "Gotify notification channel enabled, but required configuration variables are missing. Gotify notifications will not be sent.\n" -trigger_notification() { - # Modify to fit your setup: - GotifyToken="Your Gotify token here" - GotifyUrl="https://api.gotify/message?token=${GotifyToken}" + remove_channel gotify +fi - if [[ "$PrintMarkdownURL" == true ]]; then - ContentType="text/markdown" - else - ContentType="text/plain" - fi +trigger_gotify_notification() { + GotifyToken="${GOTIFY_TOKEN}" # e.g. GOTIFY_TOKEN=token-value + GotifyUrl="${GOTIFY_DOMAIN}/message?token=${GotifyToken}" # e.g. GOTIFY_URL=https://gotify.domain.tld - JsonData=$( jq -n \ - --arg body "$MessageBody" \ - --arg title "$MessageTitle" \ - --arg type "$ContentType" \ - '{message: $body, title: $title, priority: 5, extras: {"client::display": {"contentType": $type}}}' ) + if [[ "$PrintMarkdownURL" == true ]]; then + ContentType="text/markdown" + else + ContentType="text/plain" + fi - curl -s -S --data "${JsonData}" -H 'Content-Type: application/json' -X POST "${GotifyUrl}" 1> /dev/null -} - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - # platform specific notification code would go here - printf "\nSending Gotify notification\n" - - # Setting the MessageTitle and MessageBody variable here. - MessageTitle="${FromHost} - updates available." - printf -v MessageBody "Containers on $FromHost with updates available:\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending Gotify dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_gotify.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_GOTIFY_VERSION/s/NOTIFY_GOTIFY_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_GOTIFY_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_gotify.sh update avialable:\n $NOTIFY_GOTIFY_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification + JsonData=$( jq -n \ + --arg body "$MessageBody" \ + --arg title "$MessageTitle" \ + --arg type "$ContentType" \ + '{message: $body, title: $title, priority: 5, extras: {"client::display": {"contentType": $type}}}' ) + + curl -s -S --data "${JsonData}" -H 'Content-Type: application/json' -X POST "${GotifyUrl}" 1> /dev/null } diff --git a/notify_templates/notify_matrix.sh b/notify_templates/notify_matrix.sh index 686a4b1..87215ae 100644 --- a/notify_templates/notify_matrix.sh +++ b/notify_templates/notify_matrix.sh @@ -1,51 +1,21 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_MATRIX_VERSION="v0.1" +NOTIFY_MATRIX_VERSION="v0.2" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Required receiving services must already be set up. -# Modify to fit your setup - set MatrixServer, Room_id and AccessToken +# Do not modify this file directly. Set MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID, and MATRIX_SERVER_URL in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${MATRIX_ACCESS_TOKEN:-}" ]] || [[ -z "${MATRIX_ROOM_ID}:-" ]] || [[ -z "${MATRIX_SERVER_URL}:-" ]]; then + printf "Matrix notification channel enabled, but required configuration variables are missing. Matrix notifications will not be sent.\n" -trigger_notification() { - # Modify to fit your setup: - AccessToken="Your Matrix token here" - Room_id="Enter Room_id here" - MatrixServer="Enter Your HomeServer URL" - MsgBody="{\"msgtype\":\"m.text\",\"body\":\"$MessageBody\"}" + remove_channel matrix +fi - # URL Example: https://matrix.org/_matrix/client/r0/rooms/!xxxxxx:example.com/send/m.room.message?access_token=xxxxxxxx - curl -sS -o /dev/null --fail -X POST "$MatrixServer/_matrix/client/r0/rooms/$Room_id/send/m.room.message?access_token=$AccessToken" -H 'Content-Type: application/json' -d "$MsgBody" -} +trigger_matrix_notification() { + AccessToken="${MATRIX_ACCESS_TOKEN}" # e.g. MATRIX_ACCESS_TOKEN=token-value + Room_id="${MATRIX_ROOM_ID}" # e.g. MATRIX_ROOM_ID=myroom + MatrixServer="${MATRIX_SERVER_URL}" # e.g. MATRIX_SERVER_URL=http://matrix.yourdomain.tld + MsgBody="{\"msgtype\":\"m.text\",\"body\":\"$MessageBody\"}" -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - # platform specific notification code would go here - printf "\nSending Matrix notification\n" - - # Setting the MessageBody variable here. - MessageBody="🐋 Containers on $FromHost with updates available: \n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending Matrix dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_matrix.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_MATRIX_VERSION/s/NOTIFY_MATRIX_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_MATRIX_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_matrix.sh update avialable:\n $NOTIFY_MATRIX_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification -} + # URL Example: https://matrix.org/_matrix/client/r0/rooms/!xxxxxx:example.com/send/m.room.message?access_token=xxxxxxxx + curl -sS -o /dev/null --fail -X POST "$MatrixServer/_matrix/client/r0/rooms/$Room_id/send/m.room.message?access_token=$AccessToken" -H 'Content-Type: application/json' -d "$MsgBody" +} \ No newline at end of file diff --git a/notify_templates/notify_ntfy-sh.sh b/notify_templates/notify_ntfy-sh.sh index 996f275..0b5cc3a 100644 --- a/notify_templates/notify_ntfy-sh.sh +++ b/notify_templates/notify_ntfy-sh.sh @@ -1,57 +1,27 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_NTFYSH_VERSION="v0.2" +NOTIFY_NTFYSH_VERSION="v0.3" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Setup app and subscription at https://ntfy.sh -# Use your unique Topic Name in the URL below. +# Do not modify this file directly. Set NTFY_TOPIC_NAME in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${NTFY_TOPIC_NAME:-}" ]]; then + printf "Ntfy.sh notification channel enabled, but required configuration variables are missing. Ntfy.sh notifications will not be sent.\n" -trigger_notification() { - # Modify to fit your setup: - NtfyUrl="ntfy.sh/YourUniqueTopicName" + remove_channel ntfy-sh +fi - if [[ "$PrintMarkdownURL" == true ]]; then - ContentType="Markdown: yes" - else - ContentType="Markdown: no" #text/plain - fi +trigger_ntfy-sh_notification() { + NtfyUrl="ntfy.sh/${NTFY_TOPIC_NAME}" # e.g. NTFY_TOPIC_NAME=YourUniqueTopicName - curl -sS -o /dev/null --show-error --fail \ - -H "Title: $MessageTitle" \ - -H "$ContentType" \ - -d "$MessageBody" \ - "$NtfyUrl" -} - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - printf "\nSending ntfy.sh notification\n" - - MessageTitle="$FromHost - updates available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending ntfy.sh dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_ntfy-sh.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_NTFYSH_VERSION/s/NOTIFY_NTFYSH_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_NTFYSH_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_ntfy-sh.sh update avialable:\n $NOTIFY_NTFYSH_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification + if [[ "$PrintMarkdownURL" == true ]]; then + ContentType="Markdown: yes" + else + ContentType="Markdown: no" #text/plain + fi + + curl -sS -o /dev/null --show-error --fail \ + -H "Title: $MessageTitle" \ + -H "$ContentType" \ + -d "$MessageBody" \ + "$NtfyUrl" } diff --git a/notify_templates/notify_pushbullet.sh b/notify_templates/notify_pushbullet.sh index 0df62cb..4bad2ff 100644 --- a/notify_templates/notify_pushbullet.sh +++ b/notify_templates/notify_pushbullet.sh @@ -1,51 +1,20 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_PUSHBULLET_VERSION="v0.1" +NOTIFY_PUSHBULLET_VERSION="v0.2" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Required receiving services must already be set up. # Requires jq installed and in PATH. -# Modify to fit your setup - set Url and Token. +# Do not modify this file directly. Set PUSHBULLET_TOKEN and PUSHBULLET_URL in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${PUSHBULLET_URL:-}" ]] || [[ -z "${PUSHBULLET_TOKEN:-}" ]]; then + printf "Pushbullet notification channel enabled, but required configuration variables are missing. Pushbullet notifications will not be sent.\n" -trigger_notification() { - # Modify to fit your setup: - PushUrl="https://api.pushbullet.com/v2/pushes" - PushToken="Your Pushbullet token here" + remove_channel pushbullet +fi - # Requires jq to process json data - jq -n --arg title "$MessageTitle" --arg body "$MessageBody" '{body: $body, title: $title, type: "note"}' | curl -sS -o /dev/null --show-error --fail -X POST -H "Access-Token: $PushToken" -H "Content-type: application/json" $PushUrl -d @- -} +trigger_pushbullet_notification() { + PushUrl="${PUSHBULLET_URL}" # e.g. PUSHBULLET_URL=https://api.pushbullet.com/v2/pushes + PushToken="${PUSHBULLET_TOKEN}" # e.g. PUSHBULLET_TOKEN=token-value -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - # platform specific notification code would go here - printf "\nSending pushbullet notification\n" - - MessageTitle="$FromHost - updates available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending pushbullet dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_pushbullet.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_PUSHBULLET_VERSION/s/NOTIFY_PUSHBULLET_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_PUSHBULLET_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_pushbullet.sh update avialable:\n $NOTIFY_PUSHBULLET_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification -} + # Requires jq to process json data + jq -n --arg title "$MessageTitle" --arg body "$MessageBody" '{body: $body, title: $title, type: "note"}' | curl -sS -o /dev/null --show-error --fail -X POST -H "Access-Token: $PushToken" -H "Content-type: application/json" $PushUrl -d @- +} \ No newline at end of file diff --git a/notify_templates/notify_pushover.sh b/notify_templates/notify_pushover.sh index dc28493..16fff6a 100644 --- a/notify_templates/notify_pushover.sh +++ b/notify_templates/notify_pushover.sh @@ -1,57 +1,26 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_PUSHOVER_VERSION="v0.1" +NOTIFY_PUSHOVER_VERSION="v0.2" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Required receiving services must already be set up. # Requires jq installed and in PATH. -# Modify to fit your setup - set Url and Token. +# Do not modify this file directly. Set PUSHOVER_USER_KEY, PUSHOVER_TOKEN, and PUSHOVER_URL in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${PUSHOVER_URL:-}" ]] || [[ -z "${PUSHOVER_USER_KEY:-}" ]] || [[ -z "${PUSHOVER_TOKEN:-}" ]]; then + printf "Pushover notification channel enabled, but required configuration variables are missing. Pushover notifications will not be sent.\n" -trigger_notification() { - # Modify to fit your setup: - PushoverUrl="https://api.pushover.net/1/messages.json" - PushoverUserKey="Your Pushover User Key Here" - PushoverToken="Your Pushover API Token Here" + remove_channel pushover +fi - # Sending the notification via Pushover - curl -sS -o /dev/null --show-error --fail -X POST \ - -F "token=$PushoverToken" \ - -F "user=$PushoverUserKey" \ - -F "title=$MessageTitle" \ - -F "message=$MessageBody" \ - $PushoverUrl -} +trigger_pushover_notification() { + PushoverUrl="${PUSHOVER_URL}" # e.g. PUSHOVER_URL=https://api.pushover.net/1/messages.json + PushoverUserKey="${PUSHOVER_USER_KEY}" # e.g. PUSHOVER_USER_KEY=userkey + PushoverToken="${PUSHOVER_TOKEN}" # e.g. PUSHOVER_TOKEN=token-value -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - # platform specific notification code would go here - printf "\nSending pushover notification\n" - - MessageTitle="$FromHost - updates available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending pushover dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_pushover.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_PUSHOVER_VERSION/s/NOTIFY_PUSHOVER_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_PUSHOVER_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_pushover.sh update avialable:\n $NOTIFY_PUSHOVER_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification -} + # Sending the notification via Pushover + curl -sS -o /dev/null --show-error --fail -X POST \ + -F "token=$PushoverToken" \ + -F "user=$PushoverUserKey" \ + -F "title=$MessageTitle" \ + -F "message=$MessageBody" \ + $PushoverUrl +} \ No newline at end of file diff --git a/notify_templates/notify_slack.sh b/notify_templates/notify_slack.sh index 56ed28b..6dc3b28 100644 --- a/notify_templates/notify_slack.sh +++ b/notify_templates/notify_slack.sh @@ -1,52 +1,22 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_SLACK_VERSION="v0.1" +NOTIFY_SLACK_VERSION="v0.2" # -# Copy/rename this file to notify.sh in the same directory as dockcheck.sh to enable the notification snippet. -# Setu app and token at https://api.slack.com/tutorials/tracks/posting-messages-with-curl -# Add your AccessToken and ChannelID below +# Setup app and token at https://api.slack.com/tutorials/tracks/posting-messages-with-curl +# Do not modify this file directly. Set SLACK_ACCESS_TOKEN, and SLACK_CHANNEL_ID in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${SLACK_ACCESS_TOKEN:-}" ]] || [[ -z "${SLACK_CHANNEL_ID:-}" ]]; then + printf "Slack notification channel enabled, but required configuration variables are missing. Slack notifications will not be sent.\n" -trigger_notification() { - # Modify to fit your setup: - AccessToken="xoxb-not-a-real-token-this-will-not-work" - ChannelID="C123456" - SlackUrl="https://slack.com/api/chat.postMessage" + remove_channel slack +fi - curl -sS -o /dev/null --show-error --fail \ - -d "text=$MessageBody" -d "channel=$ChannelID" \ - -H "Authorization: Bearer $AccessToken" \ - -X POST $SlackUrl -} - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - printf "\nSending Slack notification\n" - - MessageTitle="$FromHost - updates available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending Slack dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_slack.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_SLACK_VERSION/s/NOTIFY_SLACK_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_SLACK_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_slack.sh update avialable:\n $NOTIFY_SLACK_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification +trigger_slack_notification() { + AccessToken="${SLACK_ACCESS_TOKEN}" # e.g. SLACK_ACCESS_TOKEN=some-token + ChannelID="${SLACK_CHANNEL_ID}" # e.g. CHANNEL_ID=mychannel + SlackUrl="https://slack.com/api/chat.postMessage" + + curl -sS -o /dev/null --show-error --fail \ + -d "text=$MessageBody" -d "channel=$ChannelID" \ + -H "Authorization: Bearer $AccessToken" \ + -X POST $SlackUrl } diff --git a/notify_templates/notify_smtp.sh b/notify_templates/notify_smtp.sh index 55e325b..8573640 100644 --- a/notify_templates/notify_smtp.sh +++ b/notify_templates/notify_smtp.sh @@ -1,10 +1,15 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_SMTP_VERSION="v0.1" +NOTIFY_SMTP_VERSION="v0.2" # INFO: ssmtp is depcerated - consider to use msmtp instead. # -# Copy/rename this file to notify.sh to enable the notification snipppet. # mSMTP/sSMTP has to be installed and configured manually. -# Modify to fit your setup - changing SendMailFrom, SendMailTo, SubjectTag +# Do not modify this file directly. Set SMTP_MAIL_FROM, SMTP_MAIL_TO, and SMTP_SUBJECT_TAG in your dockcheck.config file. + +if [[ -z "${SMTP_MAIL_FROM:-}" ]] || [[ -z "${SMTP_MAIL_TO:-}" ]] || [[ -z "${SMTP_SUBJECT_TAG:-}" ]]; then + printf "SMTP notification channel enabled, but required configuration variables are missing. SMTP notifications will not be sent.\n" + + remove_channel smtp +fi MSMTP=$(which msmtp) SSMTP=$(which ssmtp) @@ -17,13 +22,10 @@ else echo "No msmtp or ssmtp binary found in PATH: $PATH" ; exit 1 fi -FromHost=$(hostname) - -trigger_notification() { -# User variables: -SendMailFrom="me@mydomain.tld" -SendMailTo="me@mydomain.tld" -SubjectTag="dockcheck" +trigger_smtp_notification() { +SendMailFrom="${SMTP_MAIL_FROM}" # e.g. MAIL_FROM=me@mydomain.tld +SendMailTo="${SMTP_MAIL_TO}" # e.g. MAIL_TO=me@mydomain.tld +SubjectTag="${SMTP_SUBJECT_TAG}" # e.g. SUBJECT_TAG=dockcheck $MailPkg $SendMailTo << __EOF From: "$FromHost" <$SendMailFrom> @@ -37,35 +39,3 @@ $MessageBody __EOF } - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - printf "\nSending email notification.\n" - - MessageTitle="Updates available on" - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n\n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending email dockcheck notification.\n" - - MessageTitle="New version of dockcheck available on" - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_smtp.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_SMTP_VERSION/s/NOTIFY_SMTP_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_SMTP_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_smtp.sh update avialable:\n $NOTIFY_SMTP_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification -} diff --git a/notify_templates/notify_telegram.sh b/notify_templates/notify_telegram.sh index 0850e51..1230524 100644 --- a/notify_templates/notify_telegram.sh +++ b/notify_templates/notify_telegram.sh @@ -1,58 +1,27 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_TELEGRAM_VERSION="v0.1" +NOTIFY_TELEGRAM_VERSION="v0.2" # -# Copy/rename this file to notify.sh to enable the notification snippet. # Required receiving services must already be set up. -# Modify to fit your setup - set TelegramChatId and TelegramToken. +# Do not modify this file directly. Set TELEGRAM_CHAT_ID and TELEGRAM_TOKEN in your dockcheck.config file. -FromHost=$(hostname) +if [[ -z "${TELEGRAM_CHAT_ID:-}" ]] || [[ -z "${TELEGRAM_TOKEN:-}" ]]; then + printf "Telegram notification channel enabled, but required configuration variables are missing. Telegram notifications will not be sent.\n" -trigger_notification() { + remove_channel telegram +fi - if [[ "$PrintMarkdownURL" == true ]]; then - ParseMode="Markdown" - else - ParseMode="HTML" - fi +trigger_telegram_notification() { + if [[ "$PrintMarkdownURL" == true ]]; then + ParseMode="Markdown" + else + ParseMode="HTML" + fi - # Modify to fit your setup: - TelegramToken="Your Telegram token here" - TelegramChatId="Your Telegram ChatId here" - TelegramUrl="https://api.telegram.org/bot$TelegramToken" - TelegramTopicID=12345678 ## Set to 0 if not using specific topic within chat - TelegramData="{\"chat_id\":\"$TelegramChatId\",\"text\":\"$MessageBody\",\"message_thread_id\":\"$TelegramTopicID\",\"disable_notification\": false}" + TelegramToken="${TELEGRAM_TOKEN}" # e.g. TELEGRAM_TOKEN=token-value + TelegramChatId="${TELEGRAM_CHAT_ID}" # e.g. TELEGRAM_CHAT_ID=mychatid + TelegramUrl="https://api.telegram.org/bot$TelegramToken" + TelegramTopicID=${TELEGRAM_TOPIC_ID:="0"} + TelegramData="{\"chat_id\":\"$TelegramChatId\",\"text\":\"$MessageBody\",\"message_thread_id\":\"$TelegramTopicID\",\"disable_notification\": false}" - curl -sS -o /dev/null --fail -X POST "$TelegramUrl/sendMessage" -H 'Content-Type: application/json' -d "$TelegramData" -} - -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - - # platform specific notification code would go here - printf "\nSending Telegram notification\n" - - # Setting the MessageBody variable here. - MessageBody="🐋 Containers on $FromHost with updates available: \n$UpdToString" - - trigger_notification -} - -### Rename (eg. disabled_dockcheck_notification), remove or comment out the following function -### to not send notifications when dockcheck itself has updates. -dockcheck_notification() { - printf "\nSending Telegram dockcheck notification\n" - - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "$FromHost - New version of dockcheck available.\n\nInstalled version: $1 \nLatest version: $2 \n\nChangenotes: $3" - - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_telegram.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_TELEGRAM_VERSION/s/NOTIFY_TELEGRAM_VERSION=//p" | tr -d '"')" - if [[ "$NOTIFY_TELEGRAM_VERSION" != "$LatestNotifyRelease" ]] ; then - printf -v NotifyUpdate "\n\nnotify_telegram.sh update avialable:\n $NOTIFY_TELEGRAM_VERSION -> $LatestNotifyRelease\n" - MessageBody="${MessageBody}${NotifyUpdate}" - fi - - trigger_notification + curl -sS -o /dev/null --fail -X POST "$TelegramUrl/sendMessage" -H 'Content-Type: application/json' -d "$TelegramData" } diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh new file mode 100644 index 0000000..3beb5ef --- /dev/null +++ b/notify_templates/notify_v2.sh @@ -0,0 +1,84 @@ +NOTIFY_V2_VERSION="v0.1" +# +# If migrating from an older notify template, remove your existing notify.sh file. +# Enable and configure all required notification variables in your dockcheck.config file, e.g.: +# NOTIFY_CHANNELS=apprise gotify slack +# SLACK_TOKEN=xoxb-some-token-value +# GOTIFY_TOKEN=some.token + +enabled_notify_channels=( ${NOTIFY_CHANNELS:-} ) + +FromHost=$(hostname) + +remove_channel() { + local temp_array=() + for channel in "${enabled_notify_channels[@]}"; do + [[ "${channel}" != "$1" ]] && temp_array+=("${channel}") + done + enabled_notify_channels=( "${temp_array[@]}" ) +} + +for channel in "${enabled_notify_channels[@]}"; do + source_if_exists "${ScriptWorkDir}/notify_templates/notify_${channel}.sh" +done + +send_notification() { + [[ -s "$ScriptWorkDir"/urls.list ]] && releasenotes || Updates=("$@") + UpdToString=$( printf '%s\\n' "${Updates[@]}" ) + + for channel in "${enabled_notify_channels[@]}"; do + printf "\nSending ${channel} notification\n" + + MessageTitle="$FromHost - updates available." + # Setting the MessageBody variable here. + printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString\n" + + exec_if_exists trigger_${channel}_notification "$@" + done +} + +### Set DISABLE_DOCKCHECK_NOTIFICATION=false in dockcheck.config +### to not send notifications when dockcheck itself has updates. +dockcheck_notification() { + if [[ ! "${DISABLE_DOCKCHECK_NOTIFICATION:-}" = "true" ]]; then + MessageTitle="$FromHost - New version of dockcheck available." + # Setting the MessageBody variable here. + printf -v MessageBody "Installed version: $1\nLatest version: $2\n\nChangenotes: $3\n" + + if [[ ${#enabled_notify_channels[@]} -gt 0 ]]; then printf "\n"; fi + for channel in "${enabled_notify_channels[@]}"; do + printf "Sending dockcheck update notification - ${channel}\n" + exec_if_exists trigger_${channel}_notification + done + fi +} + +### Set DISABLE_NOTIFY_UPDATE_NOTIFICATION=false in dockcheck.config +### to not send notifications when notify scripts themselves have updates. +notify_update_notification() { + if [[ ! "${DISABLE_NOTIFY_UPDATE_NOTIFICATION:-}" = "true" ]]; then + update_channels=( "${enabled_notify_channels[@]}" "v2" ) + + for notify_script in "${update_channels[@]}"; do + upper_channel=$(tr '[:lower:]' '[:upper:]' <<< "$notify_script") + VersionVar="NOTIFY_${upper_channel}_VERSION" + if [[ -n "${!VersionVar}" ]]; then + RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_${notify_script}.sh" + LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_${upper_channel}_VERSION/s/NOTIFY_${upper_channel}_VERSION=//p" | tr -d '"')" + LatestNotifyRelease=${LatestNotifyRelease:-undefined} + if [[ ! "${LatestNotifyRelease}" = "undefined" ]]; then + if [[ "${!VersionVar}" != "$LatestNotifyRelease" ]] ; then + MessageTitle="$FromHost - New version of notify_${notify_script}.sh available." + + printf -v MessageBody "notify_${notify_script}.sh update available:\n ${!VersionVar} -> $LatestNotifyRelease\n" + + for channel in "${enabled_notify_channels[@]}"; do + printf "Sending notify_${notify_script}.sh update notification - ${channel}\n" + exec_if_exists trigger_${channel}_notification + done + fi + fi + fi + done + fi +} From 57f7580477df15a73ec5b2317ac8ce8701578cf8 Mon Sep 17 00:00:00 2001 From: mag37 Date: Sun, 25 May 2025 18:27:12 +0200 Subject: [PATCH 09/55] Update dockcheck.sh lowering version until a few more tweaks are done. --- dockcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockcheck.sh b/dockcheck.sh index a0a5be2..8b07094 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -VERSION="v0.6.5" +VERSION="v0.6.4" ### ChangeNotes: Refactored notification logic. See README.md for upgrade steps. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" From 5e7d4f0b8a4eb11c9353c308eea901e2256ffb1b Mon Sep 17 00:00:00 2001 From: mag37 Date: Sun, 25 May 2025 18:39:34 +0200 Subject: [PATCH 10/55] minor tweaks; curl/wget retries, missing variable fix in root-check --- README.md | 13 +++++-------- dockcheck.sh | 18 +++++++++--------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 27ef1ad..e66eb7d 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,10 @@ ___ - **v0.6.5**: Refactored notification logic. See notify_templates/notify_v2.sh for upgrade steps. - Added helper functions to simplify sourcing files and executing functions if they exist. - Created notify_v2.sh wrapper script. - - Simplified and consolidated notification logic within notify_v2.sh. - - Added support for notification management via environment variables. - - Moved notification secrets to dockcheck.config. + - Simplified and consolidated notification logic within notify_v2.sh. + - Added support for notification management via environment variables. + - Moved notification secrets to **dockcheck.config**. + - Added retries to wget/curl to not get empty responses when github is slow. - **v0.6.4**: Restructured the update process - first pulls all updates, then recreates all containers. - Added logic to skip update check on non-compose containers (unless `-r` option). - Added option `-F` to revert to `compose up -d ` targeting specific container and not the stack. @@ -40,10 +41,6 @@ ___ - `-u`, Allow auto self update of dockcheck.sh - `-I`, Print container release URLs in the CLI "choose update" list. (please contribute to `urls.list`) - Extras: `-m`, Monochrome mode now hides the progress bar. -- **v0.6.1**: Hotfixes: (try removing set+shopt+shopt if debugging more errors) - - xargs/pipefail, removed `-set -e` bash option for now. - - unbound variables fixed (hopefully) - - dependency installer from pkgmanager rewritten ___ @@ -64,7 +61,7 @@ Options: -F Only compose up the specific container, not the whole compose stack (useful for master-compose structure). -h Print this Help. -i Inform - send a preconfigured notification. --I Prints custom releasenote urls alongside each container with updates (requires urls.list). +-I Prints custom releasenote urls alongside each container with updates in CLI output (requires urls.list). -l Only update if label is set. See readme. -m Monochrome mode, no printf colour codes and hides progress bar. -M Prints custom releasenote urls as markdown (requires template support). diff --git a/dockcheck.sh b/dockcheck.sh index 8b07094..3144622 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash VERSION="v0.6.4" -### ChangeNotes: Refactored notification logic. See README.md for upgrade steps. +# ChangeNotes: Refactored notification logic. See README.md for upgrade steps. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -14,8 +14,8 @@ ScriptPath="$(readlink -f "$0")" ScriptWorkDir="$(dirname "$ScriptPath")" # Check if there's a new release of the script -LatestRelease="$(curl -s -r 0-50 "$RawUrl" | sed -n "/VERSION/s/VERSION=//p" | tr -d '"')" -LatestChanges="$(curl -s -r 0-200 "$RawUrl" | sed -n "/ChangeNotes/s/# ChangeNotes: //p")" +LatestRelease="$(curl --retry 3 --retry-delay 1 --retry-max-time 10 -s -r 0-50 "$RawUrl" | sed -n "/VERSION/s/VERSION=//p" | tr -d '"')" +LatestChanges="$(curl --retry 3 --retry-delay 1 --retry-max-time 10 -s -r 0-200 "$RawUrl" | sed -n "/ChangeNotes/s/# ChangeNotes: //p")" # Source helper functions source_if_exists() { @@ -43,7 +43,7 @@ Help() { echo "-F Only compose up the specific container, not the whole compose stack (useful for master-compose structure)." echo "-h Print this Help." echo "-i Inform - send a preconfigured notification." - echo "-I Prints custom releasenote urls alongside each container with updates (requires urls.list)." + echo "-I Prints custom releasenote urls alongside each container with updates in CLI output (requires urls.list)." echo "-l Only update if label is set. See readme." echo "-m Monochrome mode, no printf colour codes and hides progress bar." echo "-M Prints custom releasenote urls as markdown (requires template support)." @@ -166,12 +166,12 @@ exec_if_exists_or_fail() { self_update_curl() { cp "$ScriptPath" "$ScriptPath".bak if command -v curl &>/dev/null; then - curl -L $RawUrl > "$ScriptPath"; chmod +x "$ScriptPath" + curl --retry 3 --retry-delay 1 --retry-max-time 10 -L $RawUrl > "$ScriptPath"; chmod +x "$ScriptPath" printf "\n%b---%b starting over with the updated version %b---%b\n" "$c_yellow" "$c_teal" "$c_yellow" "$c_reset" exec "$ScriptPath" "${ScriptArgs[@]}" # run the new script with old arguments exit 1 # Exit the old instance elif command -v wget &>/dev/null; then - wget $RawUrl -O "$ScriptPath"; chmod +x "$ScriptPath" + wget --waitretry=1 --timeout=15 -t 10 $RawUrl -O "$ScriptPath"; chmod +x "$ScriptPath" printf "\n%b---%b starting over with the updated version %b---%b\n" "$c_yellow" "$c_teal" "$c_yellow" "$c_reset" exec "$ScriptPath" "${ScriptArgs[@]}" # run the new script with old arguments exit 0 # exit the old instance @@ -270,8 +270,8 @@ binary_downloader() { *) printf "\n%bArchitecture not supported, exiting.%b\n" "$c_red" "$c_reset"; exit 1;; esac GetUrl="${BinaryUrl/TEMP/"$architecture"}" - if command -v curl &>/dev/null; then curl -L "$GetUrl" > "$ScriptWorkDir/$BinaryName"; - elif command -v wget &>/dev/null; then wget "$GetUrl" -O "$ScriptWorkDir/$BinaryName"; + if command -v curl &>/dev/null; then curl --retry 3 --retry-delay 1 --retry-max-time 10 -L "$GetUrl" > "$ScriptWorkDir/$BinaryName"; + elif command -v wget &>/dev/null; then wget --waitretry=1 --timeout=15 -t 10 "$GetUrl" -O "$ScriptWorkDir/$BinaryName"; else printf "\n%bcurl/wget not available - get %s manually from the repo link, exiting.%b" "$c_red" "$BinaryName" "$c_reset"; exit 1; fi [[ -f "$ScriptWorkDir/$BinaryName" ]] && chmod +x "$ScriptWorkDir/$BinaryName" @@ -285,7 +285,7 @@ distro_checker() { elif [[ -f /etc/arch-release ]]; then [[ "$isRoot" == true ]] && PkgInstaller="pacman -S" || PkgInstaller="sudo pacman -S" elif [[ -f /etc/debian_version ]]; then - [[ "" == true ]] && PkgInstaller="apt-get install" || PkgInstaller="sudo apt-get install" + [[ "$isRoot" == true ]] && PkgInstaller="apt-get install" || PkgInstaller="sudo apt-get install" elif [[ -f /etc/redhat-release ]]; then [[ "$isRoot" == true ]] && PkgInstaller="dnf install" || PkgInstaller="sudo dnf install" elif [[ -f /etc/SuSE-release ]]; then From ec09612274d1c37529a8d691ace52f0c2203ce83 Mon Sep 17 00:00:00 2001 From: mag37 Date: Sun, 25 May 2025 18:44:20 +0200 Subject: [PATCH 11/55] version bump Version bump after minor tweaks. Should probably learn to squash properly.. soon! --- dockcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockcheck.sh b/dockcheck.sh index 3144622..f5bbad7 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -VERSION="v0.6.4" +VERSION="v0.6.5" # ChangeNotes: Refactored notification logic. See README.md for upgrade steps. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" From 22871442db509e42a5c8f0f3aecac3d7d7675bab Mon Sep 17 00:00:00 2001 From: mag37 Date: Mon, 26 May 2025 07:27:51 +0200 Subject: [PATCH 12/55] hotfix suppress noise Suppressed noise about not being able to source notification (new function) when on legacy notification template. --- dockcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockcheck.sh b/dockcheck.sh index f5bbad7..cccc6dc 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -355,7 +355,7 @@ if [[ "$VERSION" != "$LatestRelease" ]]; then fi # Version check for notify templates -[[ "$Notify" == true ]] && { exec_if_exists_or_fail notify_update_notification || printf "Could not source notify notification function.\n"; } +[[ "$Notify" == true ]] && [[ ! -s "${ScriptWorkDir}/notify.sh" ]] && { exec_if_exists_or_fail notify_update_notification || printf "Could not source notify notification function.\n"; } dependency_check "regctl" "regbin" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" dependency_check "jq" "jqbin" "https://github.com/jqlang/jq/releases/latest/download/jq-linux-TEMP" From 57650f16736359cb49217896c54f20e946727fbb Mon Sep 17 00:00:00 2001 From: vorezal <37914382+vorezal@users.noreply.github.com> Date: Thu, 29 May 2025 16:43:34 -0400 Subject: [PATCH 13/55] Notify_v2.sh bug fixes (#188) * Notify_v2.sh bug fixes * Clarify notify_v2.sh usage in README.md * Fix JSON newline handling in Discord and Telegram channels * Additional error messages when notification templates fail to be sourced * Additional variable for self-hosted ntfy.sh domain * Notify_v2.sh additional fixes * Clarify usage in README.md and notify template comments * Support sourcing template files from project root * Add days old message to notification title * Handle JSON with jq in Discord and Telegram templates * Tweak notify_v2.sh usage docs and comments * Remove extra newline from notification body * replaced jq with jqbin, reodered setting of jqbin, changed source for hostname var * moved the setting of jqbin a bit further up after further testing --------- Co-authored-by: Matthew Oleksowicz Co-authored-by: mag37 --- .gitignore | 2 +- README.md | 24 +++++++++++++++++++- default.config | 2 ++ dockcheck.sh | 6 ++--- notify_templates/notify_DSM.sh | 4 +++- notify_templates/notify_apprise.sh | 4 +++- notify_templates/notify_discord.sh | 14 ++++++++---- notify_templates/notify_gotify.sh | 6 +++-- notify_templates/notify_matrix.sh | 4 +++- notify_templates/notify_ntfy-sh.sh | 13 +++++++---- notify_templates/notify_pushbullet.sh | 8 ++++--- notify_templates/notify_pushover.sh | 4 +++- notify_templates/notify_slack.sh | 4 +++- notify_templates/notify_smtp.sh | 4 +++- notify_templates/notify_telegram.sh | 16 ++++++++++---- notify_templates/notify_v2.sh | 32 ++++++++++++++++++--------- 16 files changed, 109 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index da5921c..da6210f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # ignore users custom notify.sh -/notify.sh +/notify*.sh /urls.list # ignore user config /dockcheck.config diff --git a/README.md b/README.md index e66eb7d..73662bb 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,31 @@ Run it scheduled with `-ni` to only get notified when there's updates available! V2 installation and configuration (tag v0.6.5 or later): Remove or rename `notify.sh` if previously configured using the legacy method. +Make certain your project directory is laid out as below. You only need the notify_v2.sh file and any notification templates you wish to enable, but there is no harm in having all of them present. +``` + . +├── notify_templates/ +│ ├── notify_DSM.sh +│ ├── notify_apprise.sh +│ ├── notify_discord.sh +│ ├── notify_generic.sh +│ ├── notify_gotify.sh +│ ├── notify_matrix.sh +│ ├── notify_ntfy-sh.sh +│ ├── notify_pushbullet.sh +│ ├── notify_pushover.sh +│ ├── notify_slack.sh +│ ├── notify_smtp.sh +│ ├── notify_telegram.sh +│ └── notify_v2.sh +├── dockcheck.config +├── dockcheck.sh +└── urls.list # optional +``` +If you wish to customize `notify_v2.sh` or the notify templates yourself, you may copy them to your project root directory alongside the main dockcheck.sh script (where they will also be ignored by git). Uncomment and set the NOTIFY_CHANNELS environment variable in `dockcheck.config` to a space separated string of your desired notification channels to enable. Uncomment and set the environment variables related to the enabled notification channels. -It is recommended not to make changes directly to the `notify_X.sh` template files and to use only environment variables defined in `dockcheck.config` using this method. +It is recommended not to make changes directly to the `notify_X.sh` template files within the `notify_templates` subdirectory and instead use only environment variables defined in `dockcheck.config` using this method. Legacy installation and configuration: Use a previous version of a `notify_X.sh` template file (tag v0.6.4 or earlier) from the **notify_templates** directory, diff --git a/default.config b/default.config index 9c13be0..cd26f57 100644 --- a/default.config +++ b/default.config @@ -54,6 +54,8 @@ # MATRIX_ROOM_ID="myroom" # MATRIX_SERVER_URL="https://matrix.yourdomain.tld" # +## ntfy.sh or your custom domain with no trailing / +# NTFY_DOMAIN="ntfy.sh" # NTFY_TOPIC_NAME="YourUniqueTopicName" # # PUSHBULLET_URL="https://api.pushbullet.com/v2/pushes" diff --git a/dockcheck.sh b/dockcheck.sh index cccc6dc..3a2a04e 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -332,6 +332,9 @@ dependency_check() { ${!AppVar} "$VerFlag" &> /dev/null || { printf "%s\n" "$AppName is not working - try to remove it and re-download it, exiting."; exit 1; } } +dependency_check "regctl" "regbin" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" +dependency_check "jq" "jqbin" "https://github.com/jqlang/jq/releases/latest/download/jq-linux-TEMP" + # Numbered List function # if urls.list exists add release note url per line list_options() { @@ -357,9 +360,6 @@ fi # Version check for notify templates [[ "$Notify" == true ]] && [[ ! -s "${ScriptWorkDir}/notify.sh" ]] && { exec_if_exists_or_fail notify_update_notification || printf "Could not source notify notification function.\n"; } -dependency_check "regctl" "regbin" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" -dependency_check "jq" "jqbin" "https://github.com/jqlang/jq/releases/latest/download/jq-linux-TEMP" - # Check docker compose binary docker info &>/dev/null || { printf "\n%bYour current user does not have permissions to the docker socket - may require root / docker group. Exiting.%b\n" "$c_red" "$c_reset"; exit 1; } if docker compose version &>/dev/null; then DockerBin="docker compose" ; diff --git a/notify_templates/notify_DSM.sh b/notify_templates/notify_DSM.sh index 17d697f..edfb51f 100644 --- a/notify_templates/notify_DSM.sh +++ b/notify_templates/notify_DSM.sh @@ -4,7 +4,9 @@ NOTIFY_DSM_VERSION="v0.2" # # mSMTP/sSMTP has to be installed and configured manually. # The existing DSM Notification Email configuration will be used automatically. -# Do not modify this file directly. Set DSM_SENDMAILTO and DSM_SUBJECTTAG in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set DSM_SENDMAILTO and DSM_SUBJECTTAG in your dockcheck.config file. MSMTP=$(which msmtp) SSMTP=$(which ssmtp) diff --git a/notify_templates/notify_apprise.sh b/notify_templates/notify_apprise.sh index e71a4df..4da3d71 100644 --- a/notify_templates/notify_apprise.sh +++ b/notify_templates/notify_apprise.sh @@ -2,7 +2,9 @@ NOTIFY_APPRISE_VERSION="v0.2" # # Required receiving services must already be set up. -# Do not modify this file directly. Set APPRISE_PAYLOAD in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set APPRISE_PAYLOAD in your dockcheck.config file. # If API, set APPRISE_URL instead. if [[ -z "${APPRISE_PAYLOAD:-}" ]] && [[ -z "${APPRISE_URL:-}" ]]; then diff --git a/notify_templates/notify_discord.sh b/notify_templates/notify_discord.sh index a28cda5..97bf1fa 100644 --- a/notify_templates/notify_discord.sh +++ b/notify_templates/notify_discord.sh @@ -1,8 +1,10 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DISCORD_VERSION="v0.2" +NOTIFY_DISCORD_VERSION="v0.3" # # Required receiving services must already be set up. -# Do not modify this file directly. Set DISCORD_WEBHOOK_URL in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set DISCORD_WEBHOOK_URL in your dockcheck.config file. if [[ -z "${DISCORD_WEBHOOK_URL:-}" ]]; then printf "Discord notification channel enabled, but required configuration variables are missing. Discord notifications will not be sent.\n" @@ -13,6 +15,10 @@ fi trigger_discord_notification() { DiscordWebhookUrl="${DISCORD_WEBHOOK_URL}" # e.g. DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/ - MsgBody="{\"username\":\"$FromHost\",\"content\":\"$MessageBody\"}" - curl -sS -o /dev/null --fail -X POST -H "Content-Type: application/json" -d "$MsgBody" "$DiscordWebhookUrl" + JsonData=$( "$jqbin" -n \ + --arg username "$FromHost" \ + --arg body "$MessageBody" \ + '{"username": $username, "content": $body}' ) + + curl -sS -o /dev/null --fail -X POST -H "Content-Type: application/json" -d "$JsonData" "$DiscordWebhookUrl" } diff --git a/notify_templates/notify_gotify.sh b/notify_templates/notify_gotify.sh index f66e7e8..4e373d6 100644 --- a/notify_templates/notify_gotify.sh +++ b/notify_templates/notify_gotify.sh @@ -2,7 +2,9 @@ NOTIFY_GOTIFY_VERSION="v0.3" # # Required receiving services must already be set up. -# Do not modify this file directly. Set GOTIFY_TOKEN and GOTIFY_DOMAIN in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set GOTIFY_TOKEN and GOTIFY_DOMAIN in your dockcheck.config file. if [[ -z "${GOTIFY_TOKEN:-}" ]] || [[ -z "${GOTIFY_DOMAIN:-}" ]]; then printf "Gotify notification channel enabled, but required configuration variables are missing. Gotify notifications will not be sent.\n" @@ -20,7 +22,7 @@ trigger_gotify_notification() { ContentType="text/plain" fi - JsonData=$( jq -n \ + JsonData=$( "$jqbin" -n \ --arg body "$MessageBody" \ --arg title "$MessageTitle" \ --arg type "$ContentType" \ diff --git a/notify_templates/notify_matrix.sh b/notify_templates/notify_matrix.sh index 87215ae..8cf20b5 100644 --- a/notify_templates/notify_matrix.sh +++ b/notify_templates/notify_matrix.sh @@ -2,7 +2,9 @@ NOTIFY_MATRIX_VERSION="v0.2" # # Required receiving services must already be set up. -# Do not modify this file directly. Set MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID, and MATRIX_SERVER_URL in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID, and MATRIX_SERVER_URL in your dockcheck.config file. if [[ -z "${MATRIX_ACCESS_TOKEN:-}" ]] || [[ -z "${MATRIX_ROOM_ID}:-" ]] || [[ -z "${MATRIX_SERVER_URL}:-" ]]; then printf "Matrix notification channel enabled, but required configuration variables are missing. Matrix notifications will not be sent.\n" diff --git a/notify_templates/notify_ntfy-sh.sh b/notify_templates/notify_ntfy-sh.sh index 0b5cc3a..5a189c5 100644 --- a/notify_templates/notify_ntfy-sh.sh +++ b/notify_templates/notify_ntfy-sh.sh @@ -1,17 +1,22 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_NTFYSH_VERSION="v0.3" +NOTIFY_NTFYSH_VERSION="v0.4" # # Setup app and subscription at https://ntfy.sh -# Do not modify this file directly. Set NTFY_TOPIC_NAME in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set NTFY_DOMAIN and NTFY_TOPIC_NAME in your dockcheck.config file. -if [[ -z "${NTFY_TOPIC_NAME:-}" ]]; then +if [[ -z "${NTFY_DOMAIN:-}" ]] || [[ -z "${NTFY_TOPIC_NAME:-}" ]]; then printf "Ntfy.sh notification channel enabled, but required configuration variables are missing. Ntfy.sh notifications will not be sent.\n" remove_channel ntfy-sh fi trigger_ntfy-sh_notification() { - NtfyUrl="ntfy.sh/${NTFY_TOPIC_NAME}" # e.g. NTFY_TOPIC_NAME=YourUniqueTopicName + NtfyUrl="${NTFY_DOMAIN}/${NTFY_TOPIC_NAME}" + # e.g. + # NTFY_DOMAIN=ntfy.sh + # NTFY_TOPIC_NAME=YourUniqueTopicName if [[ "$PrintMarkdownURL" == true ]]; then ContentType="Markdown: yes" diff --git a/notify_templates/notify_pushbullet.sh b/notify_templates/notify_pushbullet.sh index 4bad2ff..182c78d 100644 --- a/notify_templates/notify_pushbullet.sh +++ b/notify_templates/notify_pushbullet.sh @@ -3,7 +3,9 @@ NOTIFY_PUSHBULLET_VERSION="v0.2" # # Required receiving services must already be set up. # Requires jq installed and in PATH. -# Do not modify this file directly. Set PUSHBULLET_TOKEN and PUSHBULLET_URL in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set PUSHBULLET_TOKEN and PUSHBULLET_URL in your dockcheck.config file. if [[ -z "${PUSHBULLET_URL:-}" ]] || [[ -z "${PUSHBULLET_TOKEN:-}" ]]; then printf "Pushbullet notification channel enabled, but required configuration variables are missing. Pushbullet notifications will not be sent.\n" @@ -16,5 +18,5 @@ trigger_pushbullet_notification() { PushToken="${PUSHBULLET_TOKEN}" # e.g. PUSHBULLET_TOKEN=token-value # Requires jq to process json data - jq -n --arg title "$MessageTitle" --arg body "$MessageBody" '{body: $body, title: $title, type: "note"}' | curl -sS -o /dev/null --show-error --fail -X POST -H "Access-Token: $PushToken" -H "Content-type: application/json" $PushUrl -d @- -} \ No newline at end of file + "$jqbin" -n --arg title "$MessageTitle" --arg body "$MessageBody" '{body: $body, title: $title, type: "note"}' | curl -sS -o /dev/null --show-error --fail -X POST -H "Access-Token: $PushToken" -H "Content-type: application/json" $PushUrl -d @- +} diff --git a/notify_templates/notify_pushover.sh b/notify_templates/notify_pushover.sh index 16fff6a..2f8bdda 100644 --- a/notify_templates/notify_pushover.sh +++ b/notify_templates/notify_pushover.sh @@ -3,7 +3,9 @@ NOTIFY_PUSHOVER_VERSION="v0.2" # # Required receiving services must already be set up. # Requires jq installed and in PATH. -# Do not modify this file directly. Set PUSHOVER_USER_KEY, PUSHOVER_TOKEN, and PUSHOVER_URL in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set PUSHOVER_USER_KEY, PUSHOVER_TOKEN, and PUSHOVER_URL in your dockcheck.config file. if [[ -z "${PUSHOVER_URL:-}" ]] || [[ -z "${PUSHOVER_USER_KEY:-}" ]] || [[ -z "${PUSHOVER_TOKEN:-}" ]]; then printf "Pushover notification channel enabled, but required configuration variables are missing. Pushover notifications will not be sent.\n" diff --git a/notify_templates/notify_slack.sh b/notify_templates/notify_slack.sh index 6dc3b28..a760f3d 100644 --- a/notify_templates/notify_slack.sh +++ b/notify_templates/notify_slack.sh @@ -2,7 +2,9 @@ NOTIFY_SLACK_VERSION="v0.2" # # Setup app and token at https://api.slack.com/tutorials/tracks/posting-messages-with-curl -# Do not modify this file directly. Set SLACK_ACCESS_TOKEN, and SLACK_CHANNEL_ID in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set SLACK_ACCESS_TOKEN, and SLACK_CHANNEL_ID in your dockcheck.config file. if [[ -z "${SLACK_ACCESS_TOKEN:-}" ]] || [[ -z "${SLACK_CHANNEL_ID:-}" ]]; then printf "Slack notification channel enabled, but required configuration variables are missing. Slack notifications will not be sent.\n" diff --git a/notify_templates/notify_smtp.sh b/notify_templates/notify_smtp.sh index 8573640..2889475 100644 --- a/notify_templates/notify_smtp.sh +++ b/notify_templates/notify_smtp.sh @@ -3,7 +3,9 @@ NOTIFY_SMTP_VERSION="v0.2" # INFO: ssmtp is depcerated - consider to use msmtp instead. # # mSMTP/sSMTP has to be installed and configured manually. -# Do not modify this file directly. Set SMTP_MAIL_FROM, SMTP_MAIL_TO, and SMTP_SUBJECT_TAG in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set SMTP_MAIL_FROM, SMTP_MAIL_TO, and SMTP_SUBJECT_TAG in your dockcheck.config file. if [[ -z "${SMTP_MAIL_FROM:-}" ]] || [[ -z "${SMTP_MAIL_TO:-}" ]] || [[ -z "${SMTP_SUBJECT_TAG:-}" ]]; then printf "SMTP notification channel enabled, but required configuration variables are missing. SMTP notifications will not be sent.\n" diff --git a/notify_templates/notify_telegram.sh b/notify_templates/notify_telegram.sh index 1230524..d254490 100644 --- a/notify_templates/notify_telegram.sh +++ b/notify_templates/notify_telegram.sh @@ -1,8 +1,10 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_TELEGRAM_VERSION="v0.2" +NOTIFY_TELEGRAM_VERSION="v0.3" # # Required receiving services must already be set up. -# Do not modify this file directly. Set TELEGRAM_CHAT_ID and TELEGRAM_TOKEN in your dockcheck.config file. +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set TELEGRAM_CHAT_ID and TELEGRAM_TOKEN in your dockcheck.config file. if [[ -z "${TELEGRAM_CHAT_ID:-}" ]] || [[ -z "${TELEGRAM_TOKEN:-}" ]]; then printf "Telegram notification channel enabled, but required configuration variables are missing. Telegram notifications will not be sent.\n" @@ -21,7 +23,13 @@ trigger_telegram_notification() { TelegramChatId="${TELEGRAM_CHAT_ID}" # e.g. TELEGRAM_CHAT_ID=mychatid TelegramUrl="https://api.telegram.org/bot$TelegramToken" TelegramTopicID=${TELEGRAM_TOPIC_ID:="0"} - TelegramData="{\"chat_id\":\"$TelegramChatId\",\"text\":\"$MessageBody\",\"message_thread_id\":\"$TelegramTopicID\",\"disable_notification\": false}" - curl -sS -o /dev/null --fail -X POST "$TelegramUrl/sendMessage" -H 'Content-Type: application/json' -d "$TelegramData" + JsonData=$( "$jqbin" -n \ + --arg chatid "$TelegramChatId" \ + --arg text "$MessageBody" \ + --arg thread "$TelegramTopicID" \ + --arg parse_mode "$ParseMode" \ + '{"chat_id": $chatid, "text": $text, "message_thread_id": $thread, "disable_notification": false, "parse_mode": $parse_mode, "disable_web_page_preview": true}' ) + + curl -sS -o /dev/null --fail -X POST "$TelegramUrl/sendMessage" -H 'Content-Type: application/json' -d "$JsonData" } diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index 3beb5ef..5990e1a 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -1,6 +1,8 @@ -NOTIFY_V2_VERSION="v0.1" +NOTIFY_V2_VERSION="v0.2" # # 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. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Enable and configure all required notification variables in your dockcheck.config file, e.g.: # NOTIFY_CHANNELS=apprise gotify slack # SLACK_TOKEN=xoxb-some-token-value @@ -8,7 +10,7 @@ NOTIFY_V2_VERSION="v0.1" enabled_notify_channels=( ${NOTIFY_CHANNELS:-} ) -FromHost=$(hostname) +FromHost=$(cat /etc/hostname) remove_channel() { local temp_array=() @@ -19,21 +21,29 @@ remove_channel() { } for channel in "${enabled_notify_channels[@]}"; do - source_if_exists "${ScriptWorkDir}/notify_templates/notify_${channel}.sh" + source_if_exists_or_fail "${ScriptWorkDir}/notify_${channel}.sh" || \ + source_if_exists_or_fail "${ScriptWorkDir}/notify_templates/notify_${channel}.sh" || \ + printf "The notification channel ${channel} is enabled, but notify_${channel}.sh was not found. Check the ${ScriptWorkDir} directory or the notify_templates subdirectory.\n" done send_notification() { [[ -s "$ScriptWorkDir"/urls.list ]] && releasenotes || Updates=("$@") UpdToString=$( printf '%s\\n' "${Updates[@]}" ) + UpdToString=${UpdToString%\\n} for channel in "${enabled_notify_channels[@]}"; do printf "\nSending ${channel} notification\n" - MessageTitle="$FromHost - updates available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString\n" + # To be added in the MessageBody if "-d X" was used + # leading space is left intentionally for clean output + [[ -n "$DaysOld" ]] && msgdaysold="with images ${DaysOld}+ days old " || msgdaysold="" - exec_if_exists trigger_${channel}_notification "$@" + MessageTitle="$FromHost - updates ${msgdaysold}available." + # Setting the MessageBody variable here. + printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n${UpdToString}\n" + + exec_if_exists_or_fail trigger_${channel}_notification || \ + printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" done } @@ -48,7 +58,8 @@ dockcheck_notification() { if [[ ${#enabled_notify_channels[@]} -gt 0 ]]; then printf "\n"; fi for channel in "${enabled_notify_channels[@]}"; do printf "Sending dockcheck update notification - ${channel}\n" - exec_if_exists trigger_${channel}_notification + exec_if_exists_or_fail trigger_${channel}_notification || \ + printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" done fi } @@ -62,7 +73,7 @@ notify_update_notification() { for notify_script in "${update_channels[@]}"; do upper_channel=$(tr '[:lower:]' '[:upper:]' <<< "$notify_script") VersionVar="NOTIFY_${upper_channel}_VERSION" - if [[ -n "${!VersionVar}" ]]; then + if [[ -n "${!VersionVar:-}" ]]; then RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_${notify_script}.sh" LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_${upper_channel}_VERSION/s/NOTIFY_${upper_channel}_VERSION=//p" | tr -d '"')" LatestNotifyRelease=${LatestNotifyRelease:-undefined} @@ -74,7 +85,8 @@ notify_update_notification() { for channel in "${enabled_notify_channels[@]}"; do printf "Sending notify_${notify_script}.sh update notification - ${channel}\n" - exec_if_exists trigger_${channel}_notification + exec_if_exists_or_fail trigger_${channel}_notification || \ + printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" done fi fi From 68c057e62dbd58a11d454668a8e1e50db2e7b48a Mon Sep 17 00:00:00 2001 From: mag37 Date: Thu, 29 May 2025 23:03:46 +0200 Subject: [PATCH 14/55] v0.6.6 bump and info --- README.md | 11 ++-- dockcheck.sh | 4 +- notify_templates/urls.list | 100 +++++++++++++++++++++++-------------- 3 files changed, 72 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 73662bb..af2f45d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ ___ ## :bell: Changelog +- **v0.6.6**: Notify_v2 bugfixes + - Clearer readme and error messages + - Sourcing templates from either project root or subdirectory + - Consistent newline handling + - Added (when using `-d`) days old message to notification title + - Added ntfy.sh self hosted domain option to config + - jq fixes to templates (and properly using $jqbin) - **v0.6.5**: Refactored notification logic. See notify_templates/notify_v2.sh for upgrade steps. - Added helper functions to simplify sourcing files and executing functions if they exist. - Created notify_v2.sh wrapper script. @@ -37,10 +44,6 @@ ___ - `-M`, Markdown format url-releasenotes in notification (requires template rework, look at gotify!) - Added [addons/DSM/README.md](./addons/DSM/README.md) for more info Synology DSM info. - Permission checks - graceful exit if no docker permissions + checking if root for pkg-manager. -- **v0.6.2**: Style and colour changes, prometheus hotfix, new options: - - `-u`, Allow auto self update of dockcheck.sh - - `-I`, Print container release URLs in the CLI "choose update" list. (please contribute to `urls.list`) - - Extras: `-m`, Monochrome mode now hides the progress bar. ___ diff --git a/dockcheck.sh b/dockcheck.sh index 3a2a04e..992a267 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.6.5" -# ChangeNotes: Refactored notification logic. See README.md for upgrade steps. +VERSION="v0.6.6" +# ChangeNotes: notify_v2 bugfixes - clarify readme and error messages, better sourcing templates, tweaks. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" diff --git a/notify_templates/urls.list b/notify_templates/urls.list index 31ad44a..2b31c3b 100644 --- a/notify_templates/urls.list +++ b/notify_templates/urls.list @@ -1,49 +1,75 @@ -# This is a list of container names and releasenote urls, separated by space. -# Modify, add and (if necessary) remove to fit your needs. # Additions are welcome! Append your list to the git-repo, use generic names and sensible urls. +# Modify, add and (if necessary) remove to fit your needs. +# This is a list of container names and releasenote urls, separated by space. -apprise-api https://github.com/linuxserver/docker-apprise-api/releases -homer https://github.com/bastienwirtz/homer/releases -nginx https://github.com/docker-library/official-images/blob/master/library/nginx -vaultwarden-server https://github.com/dani-garcia/vaultwarden/releases -bruceforce-vaultwarden-backup https://github.com/Bruceforce/vaultwarden-backup/blob/main/CHANGELOG.md actual_server https://actualbudget.org/blog -gotify https://github.com/gotify/server/releases -traefik https://github.com/traefik/traefik/releases -caddy https://github.com/caddyserver/caddy/releases -homarr https://github.com/homarr-labs/homarr/releases -dozzle https://github.com/amir20/dozzle/releases -beszel https://github.com/henrygd/beszel/releases -forgejo https://codeberg.org/forgejo/forgejo/releases -dockge https://github.com/louislam/dockge/releases -cup https://github.com/sergi0g/cup/releases - -calibre https://github.com/linuxserver/docker-calibre/releases -calibre-web https://github.com/linuxserver/docker-calibre-web/releases -readarr https://github.com/Readarr/Readarr/releases +apprise-api https://github.com/linuxserver/docker-apprise-api/releases audiobookshelf https://github.com/advplyr/audiobookshelf/releases - -gluetun https://github.com/qdm12/gluetun/releases bazarr https://github.com/morpheus65535/bazarr/releases bazarr-ls https://github.com/linuxserver/docker-bazarr/releases +beszel https://github.com/henrygd/beszel/releases +bookstack https://github.com/BookStackApp/BookStack/releases +bruceforce-vaultwarden-backup https://github.com/Bruceforce/vaultwarden-backup/blob/main/CHANGELOG.md +caddy https://github.com/caddyserver/caddy/releases +calibre https://github.com/linuxserver/docker-calibre/releases +calibre-web https://github.com/linuxserver/docker-calibre-web/releases +cleanuperr https://github.com/flmorg/cleanuperr/releases +cross-seed https://github.com/cross-seed/cross-seed/releases +cup https://github.com/sergi0g/cup/releases +dockge https://github.com/louislam/dockge/releases +dozzle https://github.com/amir20/dozzle/releases +flatnotes https://github.com/dullage/flatnotes/releases +forgejo https://codeberg.org/forgejo/forgejo/releases +fressrss https://github.com/FreshRSS/FreshRSS/releases +gluetun https://github.com/qdm12/gluetun/releases +go2rtc https://github.com/AlexxIT/go2rtc/releases +gotify https://github.com/gotify/server/releases +hbbr https://github.com/rustdesk/rustdesk-server/releases +hbbs https://github.com/rustdesk/rustdesk-server/releases +homarr https://github.com/homarr-labs/homarr/releases +home-assistant https://github.com/home-assistant/core/releases/ +homer https://github.com/bastienwirtz/homer/releases +immich_machine_learning https://github.com/immich-app/immich/releases +immich_postgres https://github.com/tensorchord/VectorChord/releases +immich_redis https://github.com/valkey-io/valkey/releases +immich_server https://github.com/immich-app/immich/releases +jellyfin https://github.com/jellyfin/jellyfin/releases +jellyseerr https://github.com/Fallenbagel/jellyseerr/releases +jellystat https://github.com/CyferShepard/Jellystat/releases +librespeed https://github.com/librespeed/speedtest/releases +lidarr https://github.com/Lidarr/Lidarr/releases/ +lidarr-ls https://github.com/linuxserver/docker-lidarr/releases +lubelogger https://github.com/hargata/lubelog/releases +mattermost https://github.com/mattermost/mattermost/releases +mealie https://github.com/mealie-recipes/mealie/releases +meilisearch https://github.com/meilisearch/meilisearch/releases +monica https://github.com/monicahq/monica/releases +mqtt https://github.com/eclipse/mosquitto/tags +nextcloud-aio-mastercontainer https://github.com/nextcloud/all-in-one/releases +nginx https://github.com/docker-library/official-images/blob/master/library/nginx +owncast https://github.com/owncast/owncast/releases prowlarr https://github.com/Prowlarr/Prowlarr/releases prowlarr-ls https://github.com/linuxserver/docker-prowlarr/releases +qbittorrent https://www.qbittorrent.org/news +qbittorrent-nox https://www.qbittorrent.org/news +radarr https://github.com/Radarr/Radarr/releases/ +radarr-ls https://github.com/linuxserver/docker-radarr/releases +readarr https://github.com/Readarr/Readarr/releases +readeck https://codeberg.org/readeck/readeck/releases recyclarr https://github.com/recyclarr/recyclarr/releases +roundcubemail https://github.com/roundcube/roundcubemail/releases sabnzbd https://github.com/linuxserver/docker-sabnzbd/releases -sonarr https://github.com/linuxserver/docker-sonarr/releases -radarr https://github.com/linuxserver/docker-radarr/releases -lidarr https://github.com/linuxserver/docker-lidarr/releases -jellyseerr https://github.com/Fallenbagel/jellyseerr/releases -jellyfin https://github.com/jellyfin/jellyfin/releases -tautulli https://github.com/Tautulli/Tautulli/releases -cleanuperr https://github.com/flmorg/cleanuperr/releases +scrutiny https://github.com/AnalogJ/scrutiny/releases +sftpgo https://github.com/drakkan/sftpgo/releases slskd https://github.com/slskd/slskd/releases - -home-assistant https://github.com/home-assistant/docker/releases +snappymail https://github.com/the-djmaze/snappymail/releases +sonarr https://github.com/Sonarr/Sonarr/releases/ +sonarr-ls https://github.com/linuxserver/docker-sonarr/releases +syncthing https://github.com/syncthing/syncthing/releases +tautulli https://github.com/Tautulli/Tautulli/releases +thelounge https://github.com/thelounge/thelounge/releases +traefik https://github.com/traefik/traefik/releases +vaultwarden-server https://github.com/dani-garcia/vaultwarden/releases +watchtower https://github.com/beatkind/watchtower/releases +wud https://github.com/getwud/wud/releases zigbee2mqtt https://github.com/Koenkk/zigbee2mqtt/releases -mqtt https://github.com/eclipse/mosquitto/tags - -bookstack https://github.com/BookStackApp/BookStack/releases -lubelogger https://github.com/hargata/lubelog/releases -mealie https://github.com/mealie-recipes/mealie/releases -flatnotes https://github.com/dullage/flatnotes/releases From 67648efbe87467e94e1dfe785ec5883b4953f5bb Mon Sep 17 00:00:00 2001 From: Christopher Berg Date: Sat, 31 May 2025 21:55:21 +0200 Subject: [PATCH 15/55] ntfy notification bug fixes (#197) --- default.config | 2 +- notify_templates/{notify_ntfy-sh.sh => notify_ntfy.sh} | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename notify_templates/{notify_ntfy-sh.sh => notify_ntfy.sh} (81%) diff --git a/default.config b/default.config index cd26f57..0e590c2 100644 --- a/default.config +++ b/default.config @@ -28,7 +28,7 @@ ## All commented values are examples only. Modify as needed. ## ## Uncomment the line below and specify the notification channels you wish to enable in a space separated string -# NOTIFY_CHANNELS="apprise discord DSM generic gotify matrix ntfy-sh pushbullet pushover slack smtp telegram" +# NOTIFY_CHANNELS="apprise discord DSM generic gotify matrix ntfy pushbullet pushover slack smtp telegram" # ## Uncomment to not send notifications when dockcheck itself has updates. # DISABLE_DOCKCHECK_NOTIFICATION=false diff --git a/notify_templates/notify_ntfy-sh.sh b/notify_templates/notify_ntfy.sh similarity index 81% rename from notify_templates/notify_ntfy-sh.sh rename to notify_templates/notify_ntfy.sh index 5a189c5..b977d6d 100644 --- a/notify_templates/notify_ntfy-sh.sh +++ b/notify_templates/notify_ntfy.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_NTFYSH_VERSION="v0.4" +NOTIFY_NTFY_VERSION="v0.4" # # Setup app and subscription at https://ntfy.sh # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -7,12 +7,12 @@ NOTIFY_NTFYSH_VERSION="v0.4" # Do not modify this file directly within the "notify_templates" subdirectory. Set NTFY_DOMAIN and NTFY_TOPIC_NAME in your dockcheck.config file. if [[ -z "${NTFY_DOMAIN:-}" ]] || [[ -z "${NTFY_TOPIC_NAME:-}" ]]; then - printf "Ntfy.sh notification channel enabled, but required configuration variables are missing. Ntfy.sh notifications will not be sent.\n" + printf "Ntfy notification channel enabled, but required configuration variables are missing. Ntfy notifications will not be sent.\n" - remove_channel ntfy-sh + remove_channel ntfy fi -trigger_ntfy-sh_notification() { +trigger_ntfy_notification() { NtfyUrl="${NTFY_DOMAIN}/${NTFY_TOPIC_NAME}" # e.g. # NTFY_DOMAIN=ntfy.sh From 4a16d2f1eaa0498ed24a7bf5abba49bba4a5d348 Mon Sep 17 00:00:00 2001 From: mag37 Date: Sat, 31 May 2025 22:02:52 +0200 Subject: [PATCH 16/55] -r clarification Clarified the help message for the -r option. --- dockcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockcheck.sh b/dockcheck.sh index 992a267..45e03cc 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -49,7 +49,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 "-r Allow updating images for docker run; won't update the container." + echo "-r Allow checking for updates/updating images for docker run containers. Won't update the container." echo "-s Include stopped containers in the check. (Logic: docker ps -a)." echo "-t Set a timeout (in seconds) per container for registry checkups, 10 is default." echo "-u Allow automatic self updates - caution as this will pull new code and autorun it." From 272615166e4c4d6953f6a027a2736f406ee37195 Mon Sep 17 00:00:00 2001 From: mag37 Date: Sat, 31 May 2025 22:05:35 +0200 Subject: [PATCH 17/55] ntfy rename Corrected all mentions of the ntfy template with its new name. Also clarified the help message of the -r option. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index af2f45d..ce036c3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ ___ - Sourcing templates from either project root or subdirectory - Consistent newline handling - Added (when using `-d`) days old message to notification title - - Added ntfy.sh self hosted domain option to config + - Added ntfy self hosted domain option to config - jq fixes to templates (and properly using $jqbin) - **v0.6.5**: Refactored notification logic. See notify_templates/notify_v2.sh for upgrade steps. - Added helper functions to simplify sourcing files and executing functions if they exist. @@ -38,7 +38,7 @@ ___ - Added logic to skip update check on non-compose containers (unless `-r` option). - Added option `-F` to revert to `compose up -d ` targeting specific container and not the stack. - Also added corresponding label and config-option. - - Added markdown formatting to `notify_ntfy-sh.sh` template. + - Added markdown formatting to `notify_ntfy.sh` template. - **v0.6.3**: Some fixes and changes: - Stops when a container recreation (compose up -d) fails, also `up`s the whole stack now. - `-M`, Markdown format url-releasenotes in notification (requires template rework, look at gotify!) @@ -70,7 +70,7 @@ Options: -M Prints custom releasenote urls as markdown (requires template support). -n No updates, only checking availability. -p Auto-Prune dangling images after update. --r Allow updating images for docker run, wont update the container. +-r Allow checking for updates/updating images for docker run containers. Won't update the container. -s Include stopped containers in the check. (Logic: docker ps -a). -t N Set a timeout (in seconds) per container for registry checkups, 10 is default. -u Allow automatic self updates - caution as this will pull new code and autorun it. @@ -149,7 +149,7 @@ Make certain your project directory is laid out as below. You only need the noti │ ├── notify_generic.sh │ ├── notify_gotify.sh │ ├── notify_matrix.sh -│ ├── notify_ntfy-sh.sh +│ ├── notify_ntfy.sh │ ├── notify_pushbullet.sh │ ├── notify_pushover.sh │ ├── notify_slack.sh @@ -176,7 +176,7 @@ copy it to `notify.sh` alongside the script, modify it to your needs! (notify.sh - Apprise (with it's [multitude](https://github.com/caronc/apprise#supported-notifications) of notifications) - both native [caronc/apprise](https://github.com/caronc/apprise) and the standalone [linuxserver/docker-apprise-api](https://github.com/linuxserver/docker-apprise-api) - Read the [QuickStart](extras/apprise_quickstart.md) -- [ntfy.sh](https://ntfy.sh/) - HTTP-based pub-sub notifications. +- [ntfy](https://ntfy.sh/) - HTTP-based pub-sub notifications. - [Gotify](https://gotify.net/) - a simple server for sending and receiving messages. - [Pushbullet](https://www.pushbullet.com/) - connecting different devices with cross-platform features. - [Telegram](https://telegram.org/) - Telegram chat API. From a0e11de38354b1b0fa798d13ee64bcbb49e702c7 Mon Sep 17 00:00:00 2001 From: vorezal <37914382+vorezal@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:16:48 -0400 Subject: [PATCH 18/55] Snooze feature, curl, and consolidation (#200) * Snooze feature, curl, and consolidation * Added snooze feature to delay notifications * Added configurable default curl arguments * Consolidated and standardized notify template update notifications * Added curl error handling * Snooze comment fix * Grep, curl args, and variable init adjustments * Modified grep commands to make use of word boundaries in order to avoid matching on substrings * Set CurlRetryDelay, CurlRetryCount, and CurlConnectTimeout as individual variables * Used :- for variable initialization where assignment is redundant * Update dockcheck.sh change notes and fix variable collision * Remove unnecessary cat and clarify readme * reformatting --------- Co-authored-by: Matthew Oleksowicz Co-authored-by: mag37 --- .gitignore | 2 + README.md | 45 +++-- default.config | 6 + dockcheck.sh | 76 ++++---- notify_templates/notify_DSM.sh | 7 +- notify_templates/notify_apprise.sh | 12 +- notify_templates/notify_discord.sh | 8 +- notify_templates/notify_gotify.sh | 8 +- notify_templates/notify_matrix.sh | 8 +- notify_templates/notify_ntfy.sh | 8 +- notify_templates/notify_pushbullet.sh | 8 +- notify_templates/notify_pushover.sh | 8 +- notify_templates/notify_slack.sh | 8 +- notify_templates/notify_smtp.sh | 6 +- notify_templates/notify_telegram.sh | 8 +- notify_templates/notify_v2.sh | 250 +++++++++++++++++++++----- 16 files changed, 350 insertions(+), 118 deletions(-) diff --git a/.gitignore b/.gitignore index da6210f..e5a2ded 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /dockcheck.config # ignore the auto-installed regctl regctl +# ignore snooze file +snooze.list diff --git a/README.md b/README.md index ce036c3..751e7b2 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ ___ ## :bell: Changelog +- **v0.6.7**: Snooze feature, curl, and consolidation + - Added snooze feature to delay notifications + - Added configurable default curl arguments + - Consolidated and standardized notify template update notifications + - Added curl error handling - **v0.6.6**: Notify_v2 bugfixes - Clearer readme and error messages - Sourcing templates from either project root or subdirectory @@ -34,16 +39,6 @@ ___ - Added support for notification management via environment variables. - Moved notification secrets to **dockcheck.config**. - Added retries to wget/curl to not get empty responses when github is slow. -- **v0.6.4**: Restructured the update process - first pulls all updates, then recreates all containers. - - Added logic to skip update check on non-compose containers (unless `-r` option). - - Added option `-F` to revert to `compose up -d ` targeting specific container and not the stack. - - Also added corresponding label and config-option. - - Added markdown formatting to `notify_ntfy.sh` template. -- **v0.6.3**: Some fixes and changes: - - Stops when a container recreation (compose up -d) fails, also `up`s the whole stack now. - - `-M`, Markdown format url-releasenotes in notification (requires template rework, look at gotify!) - - Added [addons/DSM/README.md](./addons/DSM/README.md) for more info Synology DSM info. - - Permission checks - graceful exit if no docker permissions + checking if root for pkg-manager. ___ @@ -137,8 +132,7 @@ If `notify.sh` is present and configured, it will be used. Otherwise, `notify_v2 Will send a list of containers with updates available and a notification when `dockcheck.sh` itself has an update. Run it scheduled with `-ni` to only get notified when there's updates available! -V2 installation and configuration (tag v0.6.5 or later): -Remove or rename `notify.sh` if previously configured using the legacy method. +#### Installation and configuration: Make certain your project directory is laid out as below. You only need the notify_v2.sh file and any notification templates you wish to enable, but there is no harm in having all of them present. ``` . @@ -160,17 +154,30 @@ Make certain your project directory is laid out as below. You only need the noti ├── dockcheck.sh └── urls.list # optional ``` -If you wish to customize `notify_v2.sh` or the notify templates yourself, you may copy them to your project root directory alongside the main dockcheck.sh script (where they will also be ignored by git). Uncomment and set the NOTIFY_CHANNELS environment variable in `dockcheck.config` to a space separated string of your desired notification channels to enable. -Uncomment and set the environment variables related to the enabled notification channels. -It is recommended not to make changes directly to the `notify_X.sh` template files within the `notify_templates` subdirectory and instead use only environment variables defined in `dockcheck.config` using this method. +Uncomment and set the environment variables related to the enabled notification channels. +It is recommended to only edit the environmental variables in `dockcheck.config` and not make changes directly to the `notify_X.sh` template files within the `notify_templates` subdirectory. +If you wish to customize the notify templates yourself, you may copy them to your project root directory alongside the main `dockcheck.sh` script (where they will also be ignored by git). +Customizing `notify_v2.sh` is handled the same as customizing the templates, but it must be renamed to `notify.sh` within the `dockcheck.sh` root directory. -Legacy installation and configuration: + +#### Legacy installation and configuration: Use a previous version of a `notify_X.sh` template file (tag v0.6.4 or earlier) from the **notify_templates** directory, copy it to `notify.sh` alongside the script, modify it to your needs! (notify.sh is added to .gitignore) +#### Snooze feature: +**Use case:** You wish to be notified of available updates in a timely manner, but do not require reminders after the initial notification with the same frequency. +e.g. *Dockcheck is scheduled to run every hour. You will receive an update notification within an hour of availability.* +**Snooze enabled:** you will not receive another notification about updates for this container for a configurable period of time. +**Snooze disabled:** you will receive additional notifications every hour. -**Current templates:** +To enable snooze, uncomment the `SNOOZE_SECONDS` variable in your `dockcheck.config` file and set it to the number of seconds you wish to prevent duplicate alerts. +The true snooze duration will be 60 seconds less than your configure value to account for minor scheduling or script run time issues. +If an update becomes available for an item that is not snoozed, notifications will be sent and include all available updates for that item's category, even snoozed items. +`dockcheck.sh` updates, notification template updates, and container updates are considered three separate categories. + + +#### Current notify templates: - Synology [DSM](https://www.synology.com/en-global/dsm) - Email with [mSMTP](https://wiki.debian.org/msmtp) (or deprecated alternative [sSMTP](https://wiki.debian.org/sSMTP)) - Apprise (with it's [multitude](https://github.com/caronc/apprise#supported-notifications) of notifications) @@ -185,7 +192,7 @@ copy it to `notify.sh` alongside the script, modify it to your needs! (notify.sh - [Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) - Discord webhooks. - [Slack](https://api.slack.com/tutorials/tracks/posting-messages-with-curl) - Slack curl api -Further additions are welcome - suggestions or PR! +Further additions are welcome - suggestions or PRs! Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). ### :date: Release notes addon @@ -228,7 +235,7 @@ See the [README.md](./addons/prometheus/README.md) for more detailed information Contributed by [tdralle](https://github.com/tdralle). ### :small_orange_diamond: Zabbix config to monitor docker image updates -If you already use Zabbix - this config will Shows number of available docker image updates on host. +If you already use Zabbix - this config will show numbers of available docker image updates on host. Example: *2 Docker Image updates on host-xyz* See project: [thetorminal/zabbix-docker-image-updates](https://github.com/thetorminal/zabbix-docker-image-updates) diff --git a/default.config b/default.config index 0e590c2..2831591 100644 --- a/default.config +++ b/default.config @@ -23,6 +23,9 @@ #PrintReleaseURL=true # Prints custom releasenote urls alongside each container with updates (requires urls.list)` #PrintMarkdownURL=true # Prints custom releasenote urls as markdown #OnlySpecific=true # Only compose up the specific container, not the whole compose. (useful for master-compose structure). +#CurlRetryDelay=1 # Time between curl retries +#CurlRetryCount=3 # Max number of curl retries +#CurlConnectTimeout=5 # Time to wait for curl to establish a connection before failing ### Notify settings ## All commented values are examples only. Modify as needed. @@ -30,6 +33,9 @@ ## Uncomment the line below and specify the notification channels you wish to enable in a space separated string # NOTIFY_CHANNELS="apprise discord DSM generic gotify matrix ntfy pushbullet pushover slack smtp telegram" # +## Uncomment the line below and specify the number of seconds to delay notifications to enable snooze +# SNOOZE_SECONDS=86400 +# ## Uncomment to not send notifications when dockcheck itself has updates. # DISABLE_DOCKCHECK_NOTIFICATION=false ## Uncomment to not send notifications when notify scripts themselves have updates. diff --git a/dockcheck.sh b/dockcheck.sh index 45e03cc..d341ce9 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.6.6" -# ChangeNotes: notify_v2 bugfixes - clarify readme and error messages, better sourcing templates, tweaks. +VERSION="v0.6.7" +# ChangeNotes: snooze feature (see readme), curl arguments, cleanup. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -13,10 +13,6 @@ ScriptArgs=( "$@" ) ScriptPath="$(readlink -f "$0")" ScriptWorkDir="$(dirname "$ScriptPath")" -# Check if there's a new release of the script -LatestRelease="$(curl --retry 3 --retry-delay 1 --retry-max-time 10 -s -r 0-50 "$RawUrl" | sed -n "/VERSION/s/VERSION=//p" | tr -d '"')" -LatestChanges="$(curl --retry 3 --retry-delay 1 --retry-max-time 10 -s -r 0-200 "$RawUrl" | sed -n "/ChangeNotes/s/# ChangeNotes: //p")" - # Source helper functions source_if_exists() { if [[ -s "$1" ]]; then source "$1"; fi @@ -60,31 +56,32 @@ Help() { } # Initialise variables -Timeout=${Timeout:=10} -MaxAsync=${MaxAsync:=1} -BarWidth=${BarWidth:=50} -AutoMode=${AutoMode:=false} -DontUpdate=${DontUpdate:=false} -AutoPrune=${AutoPrune:=false} -AutoSelfUpdate=${AutoSelfUpdate:=false} -OnlyLabel=${OnlyLabel:=false} -Notify=${Notify:=false} -ForceRestartStacks=${ForceRestartStacks:=false} -DRunUp=${DRunUp:=false} -MonoMode=${MonoMode:=false} -PrintReleaseURL=${PrintReleaseURL:=false} -PrintMarkdownURL=${PrintMarkdownURL:=false} -Stopped=${Stopped:=""} +Timeout=${Timeout:-10} +MaxAsync=${MaxAsync:-1} +BarWidth=${BarWidth:-50} +AutoMode=${AutoMode:-false} +DontUpdate=${DontUpdate:-false} +AutoPrune=${AutoPrune:-false} +AutoSelfUpdate=${AutoSelfUpdate:-false} +OnlyLabel=${OnlyLabel:-false} +Notify=${Notify:-false} +ForceRestartStacks=${ForceRestartStacks:-false} +DRunUp=${DRunUp:-false} +MonoMode=${MonoMode:-false} +PrintReleaseURL=${PrintReleaseURL:-false} +PrintMarkdownURL=${PrintMarkdownURL:-false} +Stopped=${Stopped:-""} CollectorTextFileDirectory=${CollectorTextFileDirectory:-} Exclude=${Exclude:-} DaysOld=${DaysOld:-} -OnlySpecific=${OnlySpecific:=false} -SpecificContainer=${SpecificContainer:=""} +OnlySpecific=${OnlySpecific:-false} +SpecificContainer=${SpecificContainer:-""} Excludes=() GotUpdates=() NoUpdates=() GotErrors=() SelectedUpdates=() +CurlArgs="--retry ${CurlRetryCount:=3} --retry-delay ${CurlRetryDelay:=1} --connect-timeout ${CurlConnectTimeout:=5} -sf" regbin="" jqbin="" @@ -125,6 +122,11 @@ shift "$((OPTIND-1))" # Set $1 to a variable for name filtering later SearchName="${1:-}" +# Check if there's a new release of the script +LatestSnippet="$(curl ${CurlArgs} -r 0-200 "$RawUrl" || printf "undefined")" +LatestRelease="$(echo "${LatestSnippet}" | sed -n "/VERSION/s/VERSION=//p" | tr -d '"')" +LatestChanges="$(echo "${LatestSnippet}" | sed -n "/ChangeNotes/s/# ChangeNotes: //p")" + # Basic notify configuration check if [[ "${Notify}" == true ]] && [[ ! -s "${ScriptWorkDir}/notify.sh" ]] && [[ -z "${NOTIFY_CHANNELS:-}" ]]; then printf "Using v2 notifications with -i flag passed but no notify channels configured in dockcheck.config. This will result in no notifications being sent.\n" @@ -166,7 +168,7 @@ exec_if_exists_or_fail() { self_update_curl() { cp "$ScriptPath" "$ScriptPath".bak if command -v curl &>/dev/null; then - curl --retry 3 --retry-delay 1 --retry-max-time 10 -L $RawUrl > "$ScriptPath"; chmod +x "$ScriptPath" + curl ${CurlArgs} -L $RawUrl > "$ScriptPath"; chmod +x "$ScriptPath" || { printf "ERROR: Failed to curl updated Dockcheck.sh script. Skipping update.\n"; return 1; } printf "\n%b---%b starting over with the updated version %b---%b\n" "$c_yellow" "$c_teal" "$c_yellow" "$c_reset" exec "$ScriptPath" "${ScriptArgs[@]}" # run the new script with old arguments exit 1 # Exit the old instance @@ -270,7 +272,7 @@ binary_downloader() { *) printf "\n%bArchitecture not supported, exiting.%b\n" "$c_red" "$c_reset"; exit 1;; esac GetUrl="${BinaryUrl/TEMP/"$architecture"}" - if command -v curl &>/dev/null; then curl --retry 3 --retry-delay 1 --retry-max-time 10 -L "$GetUrl" > "$ScriptWorkDir/$BinaryName"; + if command -v curl &>/dev/null; then curl ${CurlArgs} -L "$GetUrl" > "$ScriptWorkDir/$BinaryName" || { printf "ERROR: Failed to curl binary dependency. Rerun the script to retry.\n"; exit 1; } elif command -v wget &>/dev/null; then wget --waitretry=1 --timeout=15 -t 10 "$GetUrl" -O "$ScriptWorkDir/$BinaryName"; else printf "\n%bcurl/wget not available - get %s manually from the repo link, exiting.%b" "$c_red" "$BinaryName" "$c_reset"; exit 1; fi @@ -346,15 +348,19 @@ list_options() { } # Version check & initiate self update -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" - if [[ "$AutoMode" == false ]]; then - read -r -p "Would you like to update? y/[n]: " SelfUpdate - [[ "$SelfUpdate" =~ [yY] ]] && self_update - elif [[ "$AutoMode" == true ]] && [[ "$AutoSelfUpdate" == true ]]; then self_update; - else - [[ "$Notify" == true ]] && { exec_if_exists_or_fail dockcheck_notification "$VERSION" "$LatestRelease" "$LatestChanges" || printf "Could not source notification function.\n"; } +if [[ "$LatestRelease" != "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" + if [[ "$AutoMode" == false ]]; then + read -r -p "Would you like to update? y/[n]: " SelfUpdate + [[ "$SelfUpdate" =~ [yY] ]] && self_update + elif [[ "$AutoMode" == true ]] && [[ "$AutoSelfUpdate" == true ]]; then self_update; + else + [[ "$Notify" == true ]] && { exec_if_exists_or_fail dockcheck_notification "$VERSION" "$LatestRelease" "$LatestChanges" || printf "Could not source notification function.\n"; } + fi fi +else + printf "ERROR: Failed to curl latest Dockcheck.sh release version.\n" fi # Version check for notify templates @@ -420,7 +426,7 @@ check_image() { # Checking for errors while setting the variable if RegHash=$($t_out "$regbin" -v error image digest --list "$RepoUrl" 2>&1); then - if [[ "$LocalHash" = *"$RegHash"* ]]; then + if [[ "$LocalHash" == *"$RegHash"* ]]; then printf "%s\n" "NoUpdates $i" else if [[ -n "${DaysOld:-}" ]] && ! datecheck; then @@ -568,7 +574,7 @@ if [[ -n "${GotUpdates:-}" ]]; then # 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; } ## Reformatting path + multi compose - if [[ $ContConfigFile = '/'* ]]; then + if [[ $ContConfigFile == '/'* ]]; then CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s " "$conf"; done) else CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s/%s " "$ContPath" "$conf"; done) diff --git a/notify_templates/notify_DSM.sh b/notify_templates/notify_DSM.sh index edfb51f..08d85c1 100644 --- a/notify_templates/notify_DSM.sh +++ b/notify_templates/notify_DSM.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DSM_VERSION="v0.2" +NOTIFY_DSM_VERSION="v0.3" # INFO: ssmtp is deprecated - consider to use msmtp instead. # # mSMTP/sSMTP has to be installed and configured manually. @@ -45,6 +45,11 @@ Content-Transfer-Encoding: 7bit $MessageBody From $SenderName __EOF + +if [[ $? -gt 0 ]]; then + NotifyError=true +fi + # This ensures DSM's container manager will also see the update /var/packages/ContainerManager/target/tool/image_upgradable_checker } diff --git a/notify_templates/notify_apprise.sh b/notify_templates/notify_apprise.sh index 4da3d71..1fa94de 100644 --- a/notify_templates/notify_apprise.sh +++ b/notify_templates/notify_apprise.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_APPRISE_VERSION="v0.2" +NOTIFY_APPRISE_VERSION="v0.3" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -18,6 +18,10 @@ trigger_apprise_notification() { if [[ -n "${APPRISE_PAYLOAD:-}" ]]; then apprise -vv -t "$MessageTitle" -b "$MessageBody" \ ${APPRISE_PAYLOAD} + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi fi # e.g. APPRISE_PAYLOAD='mailto://myemail:mypass@gmail.com @@ -27,6 +31,10 @@ trigger_apprise_notification() { if [[ -n "${APPRISE_URL:-}" ]]; then AppriseURL="${APPRISE_URL}" - curl -X POST -F "title=$MessageTitle" -F "body=$MessageBody" -F "tags=all" $AppriseURL # e.g. APPRISE_URL=http://apprise.mydomain.tld:1234/notify/apprise + curl -S -o /dev/null ${CurlArgs} -X POST -F "title=$MessageTitle" -F "body=$MessageBody" -F "tags=all" $AppriseURL # e.g. APPRISE_URL=http://apprise.mydomain.tld:1234/notify/apprise + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi fi } \ No newline at end of file diff --git a/notify_templates/notify_discord.sh b/notify_templates/notify_discord.sh index 97bf1fa..fa1a32d 100644 --- a/notify_templates/notify_discord.sh +++ b/notify_templates/notify_discord.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DISCORD_VERSION="v0.3" +NOTIFY_DISCORD_VERSION="v0.4" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -20,5 +20,9 @@ trigger_discord_notification() { --arg body "$MessageBody" \ '{"username": $username, "content": $body}' ) - curl -sS -o /dev/null --fail -X POST -H "Content-Type: application/json" -d "$JsonData" "$DiscordWebhookUrl" + curl -S -o /dev/null ${CurlArgs} -X POST -H "Content-Type: application/json" -d "$JsonData" "$DiscordWebhookUrl" + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } diff --git a/notify_templates/notify_gotify.sh b/notify_templates/notify_gotify.sh index 4e373d6..d3d2c67 100644 --- a/notify_templates/notify_gotify.sh +++ b/notify_templates/notify_gotify.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_GOTIFY_VERSION="v0.3" +NOTIFY_GOTIFY_VERSION="v0.4" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -28,5 +28,9 @@ trigger_gotify_notification() { --arg type "$ContentType" \ '{message: $body, title: $title, priority: 5, extras: {"client::display": {"contentType": $type}}}' ) - curl -s -S --data "${JsonData}" -H 'Content-Type: application/json' -X POST "${GotifyUrl}" 1> /dev/null + curl -S -o /dev/null ${CurlArgs} --data "${JsonData}" -H 'Content-Type: application/json' -X POST "${GotifyUrl}" 1> /dev/null + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } diff --git a/notify_templates/notify_matrix.sh b/notify_templates/notify_matrix.sh index 8cf20b5..bcff5d2 100644 --- a/notify_templates/notify_matrix.sh +++ b/notify_templates/notify_matrix.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_MATRIX_VERSION="v0.2" +NOTIFY_MATRIX_VERSION="v0.3" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -19,5 +19,9 @@ trigger_matrix_notification() { MsgBody="{\"msgtype\":\"m.text\",\"body\":\"$MessageBody\"}" # URL Example: https://matrix.org/_matrix/client/r0/rooms/!xxxxxx:example.com/send/m.room.message?access_token=xxxxxxxx - curl -sS -o /dev/null --fail -X POST "$MatrixServer/_matrix/client/r0/rooms/$Room_id/send/m.room.message?access_token=$AccessToken" -H 'Content-Type: application/json' -d "$MsgBody" + curl -S -o /dev/null ${CurlArgs} -X POST "$MatrixServer/_matrix/client/r0/rooms/$Room_id/send/m.room.message?access_token=$AccessToken" -H 'Content-Type: application/json' -d "$MsgBody" + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } \ No newline at end of file diff --git a/notify_templates/notify_ntfy.sh b/notify_templates/notify_ntfy.sh index b977d6d..d413a2c 100644 --- a/notify_templates/notify_ntfy.sh +++ b/notify_templates/notify_ntfy.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_NTFY_VERSION="v0.4" +NOTIFY_NTFYSH_VERSION="v0.5" # # Setup app and subscription at https://ntfy.sh # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -24,9 +24,13 @@ trigger_ntfy_notification() { ContentType="Markdown: no" #text/plain fi - curl -sS -o /dev/null --show-error --fail \ + curl -S -o /dev/null ${CurlArgs} \ -H "Title: $MessageTitle" \ -H "$ContentType" \ -d "$MessageBody" \ "$NtfyUrl" + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } diff --git a/notify_templates/notify_pushbullet.sh b/notify_templates/notify_pushbullet.sh index 182c78d..78dec0b 100644 --- a/notify_templates/notify_pushbullet.sh +++ b/notify_templates/notify_pushbullet.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_PUSHBULLET_VERSION="v0.2" +NOTIFY_PUSHBULLET_VERSION="v0.3" # # Required receiving services must already be set up. # Requires jq installed and in PATH. @@ -18,5 +18,9 @@ trigger_pushbullet_notification() { PushToken="${PUSHBULLET_TOKEN}" # e.g. PUSHBULLET_TOKEN=token-value # Requires jq to process json data - "$jqbin" -n --arg title "$MessageTitle" --arg body "$MessageBody" '{body: $body, title: $title, type: "note"}' | curl -sS -o /dev/null --show-error --fail -X POST -H "Access-Token: $PushToken" -H "Content-type: application/json" $PushUrl -d @- + "$jqbin" -n --arg title "$MessageTitle" --arg body "$MessageBody" '{body: $body, title: $title, type: "note"}' | curl -S -o /dev/null ${CurlArgs} -X POST -H "Access-Token: $PushToken" -H "Content-type: application/json" $PushUrl -d @- + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } diff --git a/notify_templates/notify_pushover.sh b/notify_templates/notify_pushover.sh index 2f8bdda..60ffad6 100644 --- a/notify_templates/notify_pushover.sh +++ b/notify_templates/notify_pushover.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_PUSHOVER_VERSION="v0.2" +NOTIFY_PUSHOVER_VERSION="v0.3" # # Required receiving services must already be set up. # Requires jq installed and in PATH. @@ -19,10 +19,14 @@ trigger_pushover_notification() { PushoverToken="${PUSHOVER_TOKEN}" # e.g. PUSHOVER_TOKEN=token-value # Sending the notification via Pushover - curl -sS -o /dev/null --show-error --fail -X POST \ + curl -S -o /dev/null ${CurlArgs} -X POST \ -F "token=$PushoverToken" \ -F "user=$PushoverUserKey" \ -F "title=$MessageTitle" \ -F "message=$MessageBody" \ $PushoverUrl + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } \ No newline at end of file diff --git a/notify_templates/notify_slack.sh b/notify_templates/notify_slack.sh index a760f3d..0a9cd7a 100644 --- a/notify_templates/notify_slack.sh +++ b/notify_templates/notify_slack.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_SLACK_VERSION="v0.2" +NOTIFY_SLACK_VERSION="v0.3" # # Setup app and token at https://api.slack.com/tutorials/tracks/posting-messages-with-curl # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -17,8 +17,12 @@ trigger_slack_notification() { ChannelID="${SLACK_CHANNEL_ID}" # e.g. CHANNEL_ID=mychannel SlackUrl="https://slack.com/api/chat.postMessage" - curl -sS -o /dev/null --show-error --fail \ + curl -S -o /dev/null ${CurlArgs} \ -d "text=$MessageBody" -d "channel=$ChannelID" \ -H "Authorization: Bearer $AccessToken" \ -X POST $SlackUrl + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } diff --git a/notify_templates/notify_smtp.sh b/notify_templates/notify_smtp.sh index 2889475..f07588c 100644 --- a/notify_templates/notify_smtp.sh +++ b/notify_templates/notify_smtp.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_SMTP_VERSION="v0.2" +NOTIFY_SMTP_VERSION="v0.3" # INFO: ssmtp is depcerated - consider to use msmtp instead. # # mSMTP/sSMTP has to be installed and configured manually. @@ -40,4 +40,8 @@ Content-Transfer-Encoding: 7bit $MessageBody __EOF + +if [[ $? -gt 0 ]]; then + NotifyError=true +fi } diff --git a/notify_templates/notify_telegram.sh b/notify_templates/notify_telegram.sh index d254490..4f114eb 100644 --- a/notify_templates/notify_telegram.sh +++ b/notify_templates/notify_telegram.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_TELEGRAM_VERSION="v0.3" +NOTIFY_TELEGRAM_VERSION="v0.4" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -31,5 +31,9 @@ trigger_telegram_notification() { --arg parse_mode "$ParseMode" \ '{"chat_id": $chatid, "text": $text, "message_thread_id": $thread, "disable_notification": false, "parse_mode": $parse_mode, "disable_web_page_preview": true}' ) - curl -sS -o /dev/null --fail -X POST "$TelegramUrl/sendMessage" -H 'Content-Type: application/json' -d "$JsonData" + curl -S -o /dev/null ${CurlArgs} -X POST "$TelegramUrl/sendMessage" -H 'Content-Type: application/json' -d "$JsonData" + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi } diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index 5990e1a..a34acfa 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -1,17 +1,28 @@ -NOTIFY_V2_VERSION="v0.2" +NOTIFY_V2_VERSION="v0.3" # # 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. -# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script and rename to notify.sh. # Enable and configure all required notification variables in your dockcheck.config file, e.g.: # NOTIFY_CHANNELS=apprise gotify slack # SLACK_TOKEN=xoxb-some-token-value # GOTIFY_TOKEN=some.token +# Number of seconds to snooze identical update notifications based on local image name +# or dockcheck.sh/notify.sh template file updates. +# Actual snooze will be 60 seconds less to avoid the chance of missed notifications due to minor scheduling or script run time issues. +snooze="${SNOOZE_SECONDS:-}" +SnoozeFile="${ScriptWorkDir}/snooze.list" + enabled_notify_channels=( ${NOTIFY_CHANNELS:-} ) FromHost=$(cat /etc/hostname) +CurrentEpochTime=$(date +"%Y-%m-%dT%H:%M:%S") +CurrentEpochSeconds=$(date +%s) + +NotifyError=false + remove_channel() { local temp_array=() for channel in "${enabled_notify_channels[@]}"; do @@ -26,71 +37,222 @@ for channel in "${enabled_notify_channels[@]}"; do printf "The notification channel ${channel} is enabled, but notify_${channel}.sh was not found. Check the ${ScriptWorkDir} directory or the notify_templates subdirectory.\n" done +notify_containers_count() { + unset NotifyContainers + NotifyContainers=() + + [[ ! -f "${SnoozeFile}" ]] && touch "${SnoozeFile}" + + for update in "$@" + do + read -a container <<< "${update}" + found=$(grep -w "${container[0]}" "${SnoozeFile}" || printf "") + + if [[ -n "${found}" ]]; then + read -a arr <<< "${found}" + CheckEpochSeconds=$(( $(date -d "${arr[1]}" +%s 2>/dev/null) + ${snooze} - 60 )) || CheckEpochSeconds=$(( $(date -f "%Y-%m-%d" -j "${arr[1]}" +%s) + ${snooze} - 60 )) + if [[ "${CurrentEpochSeconds}" -gt "${CheckEpochSeconds}" ]]; then + NotifyContainers+=("${update}") + fi + else + NotifyContainers+=("${update}") + fi + done + + printf "${#NotifyContainers[@]}" +} + +update_snooze() { + + [[ ! -f "${SnoozeFile}" ]] && touch "${SnoozeFile}" + + for arg in "$@" + do + read -a entry <<< "${arg}" + found=$(grep -w "${entry[0]}" "${SnoozeFile}" || printf "") + + if [[ -n "${found}" ]]; then + sed -e "s/${entry[0]}.*/${entry[0]} ${CurrentEpochTime}/" "${SnoozeFile}" > "${SnoozeFile}.new" + mv "${SnoozeFile}.new" "${SnoozeFile}" + else + printf "${entry[0]} ${CurrentEpochTime}\n" >> "${SnoozeFile}" + fi + done +} + +cleanup_snooze() { + unset NotifyEntries + NotifyEntries=() + switch="" + + [[ ! -f "${SnoozeFile}" ]] && touch "${SnoozeFile}" + + for arg in "$@" + do + read -a entry <<< "${arg}" + NotifyEntries+=("${entry[0]}") + done + + if [[ ! "${NotifyEntries[@]}" == *".sh"* ]]; then + switch="-v" + fi + + while read -r entry datestamp; do + if [[ ! "${NotifyEntries[@]}" == *"$entry"* ]]; then + sed -e "/${entry}/d" "${SnoozeFile}" > "${SnoozeFile}.new" + mv "${SnoozeFile}.new" "${SnoozeFile}" + fi + done <<< "$(grep ${switch} '\.sh ' ${SnoozeFile})" +} + send_notification() { [[ -s "$ScriptWorkDir"/urls.list ]] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - UpdToString=${UpdToString%\\n} - for channel in "${enabled_notify_channels[@]}"; do - printf "\nSending ${channel} notification\n" + if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then + UpdNotifyCount=$(notify_containers_count "${Updates[@]}") + else + UpdNotifyCount="${#Updates[@]}" + fi - # To be added in the MessageBody if "-d X" was used - # leading space is left intentionally for clean output - [[ -n "$DaysOld" ]] && msgdaysold="with images ${DaysOld}+ days old " || msgdaysold="" + NotifyError=false - MessageTitle="$FromHost - updates ${msgdaysold}available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n${UpdToString}\n" + if [[ "${UpdNotifyCount}" -gt 0 ]]; then + UpdToString=$( printf '%s\\n' "${Updates[@]}" ) + UpdToString=${UpdToString%\\n} - exec_if_exists_or_fail trigger_${channel}_notification || \ - printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" - done + for channel in "${enabled_notify_channels[@]}"; do + printf "\nSending ${channel} notification\n" + + # To be added in the MessageBody if "-d X" was used + # leading space is left intentionally for clean output + [[ -n "$DaysOld" ]] && msgdaysold="with images ${DaysOld}+ days old " || msgdaysold="" + + MessageTitle="$FromHost - updates ${msgdaysold}available." + # Setting the MessageBody variable here. + printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n${UpdToString}\n" + + exec_if_exists_or_fail trigger_${channel}_notification || \ + printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" + done + + [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${Updates[@]}" + fi + + [[ -n "${snooze}" ]] && cleanup_snooze "${Updates[@]}" } ### Set DISABLE_DOCKCHECK_NOTIFICATION=false in dockcheck.config ### to not send notifications when dockcheck itself has updates. dockcheck_notification() { - if [[ ! "${DISABLE_DOCKCHECK_NOTIFICATION:-}" = "true" ]]; then - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1\nLatest version: $2\n\nChangenotes: $3\n" + if [[ ! "${DISABLE_DOCKCHECK_NOTIFICATION:-}" == "true" ]]; then + DockcheckNotify=false + NotifyError=false - if [[ ${#enabled_notify_channels[@]} -gt 0 ]]; then printf "\n"; fi - for channel in "${enabled_notify_channels[@]}"; do - printf "Sending dockcheck update notification - ${channel}\n" - exec_if_exists_or_fail trigger_${channel}_notification || \ - printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" - done + if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then + found=$(grep -w "dockcheck\.sh" "${SnoozeFile}" || printf "") + if [[ -n "${found}" ]]; then + read -a arr <<< "${found}" + CheckEpochSeconds=$(( $(date -d "${arr[1]}" +%s 2>/dev/null) + ${snooze} - 60 )) || CheckEpochSeconds=$(( $(date -f "%Y-%m-%d" -j "${arr[1]}" +%s) + ${snooze} - 60 )) + if [[ "${CurrentEpochSeconds}" -gt "${CheckEpochSeconds}" ]]; then + DockcheckNotify=true + fi + else + DockcheckNotify=true + fi + else + DockcheckNotify=true + fi + + if [[ "${DockcheckNotify}" == "true" ]]; then + MessageTitle="$FromHost - New version of dockcheck available." + # Setting the MessageBody variable here. + printf -v MessageBody "Installed version: $1\nLatest version: $2\n\nChangenotes: $3\n" + + if [[ ${#enabled_notify_channels[@]} -gt 0 ]]; then printf "\n"; fi + for channel in "${enabled_notify_channels[@]}"; do + printf "Sending dockcheck update notification - ${channel}\n" + exec_if_exists_or_fail trigger_${channel}_notification || \ + printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" + done + + if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then + if [[ "${NotifyError}" == "false" ]]; then + if [[ -n "${found}" ]]; then + sed -e "s/dockcheck\.sh.*/dockcheck\.sh ${CurrentEpochTime}/" "${SnoozeFile}" > "${SnoozeFile}.new" + mv "${SnoozeFile}.new" "${SnoozeFile}" + else + printf "dockcheck.sh ${CurrentEpochTime}\n" >> "${SnoozeFile}" + fi + fi + fi + fi fi } ### Set DISABLE_NOTIFY_UPDATE_NOTIFICATION=false in dockcheck.config ### to not send notifications when notify scripts themselves have updates. notify_update_notification() { - if [[ ! "${DISABLE_NOTIFY_UPDATE_NOTIFICATION:-}" = "true" ]]; then - update_channels=( "${enabled_notify_channels[@]}" "v2" ) + if [[ ! "${DISABLE_NOTIFY_UPDATE_NOTIFICATION:-}" == "true" ]]; then + NotifyUpdateNotify=false + NotifyError=false - for notify_script in "${update_channels[@]}"; do - upper_channel=$(tr '[:lower:]' '[:upper:]' <<< "$notify_script") - VersionVar="NOTIFY_${upper_channel}_VERSION" + UpdateChannels=( "${enabled_notify_channels[@]}" "v2" ) + + for NotifyScript in "${UpdateChannels[@]}"; do + UpperChannel=$(tr '[:lower:]' '[:upper:]' <<< "$NotifyScript") + VersionVar="NOTIFY_${UpperChannel}_VERSION" if [[ -n "${!VersionVar:-}" ]]; then - RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_${notify_script}.sh" - LatestNotifyRelease="$(curl -s -r 0-150 $RawNotifyUrl | sed -n "/NOTIFY_${upper_channel}_VERSION/s/NOTIFY_${upper_channel}_VERSION=//p" | tr -d '"')" - LatestNotifyRelease=${LatestNotifyRelease:-undefined} - if [[ ! "${LatestNotifyRelease}" = "undefined" ]]; then - if [[ "${!VersionVar}" != "$LatestNotifyRelease" ]] ; then - MessageTitle="$FromHost - New version of notify_${notify_script}.sh available." - - printf -v MessageBody "notify_${notify_script}.sh update available:\n ${!VersionVar} -> $LatestNotifyRelease\n" - - for channel in "${enabled_notify_channels[@]}"; do - printf "Sending notify_${notify_script}.sh update notification - ${channel}\n" - exec_if_exists_or_fail trigger_${channel}_notification || \ - printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" - done + RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_${NotifyScript}.sh" + LatestNotifySnippet="$(curl ${CurlArgs} -r 0-150 "$RawNotifyUrl" || printf "undefined")" + LatestNotifyRelease="$(echo "$LatestNotifySnippet" | sed -n "/${VersionVar}/s/${VersionVar}=//p" | tr -d '"')" + if [[ ! "${LatestNotifyRelease}" == "undefined" ]]; then + if [[ "${!VersionVar}" != "${LatestNotifyRelease}" ]] ; then + Updates+=("${NotifyScript}.sh ${!VersionVar} -> ${LatestNotifyRelease}") fi fi fi done + + if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then + for update in "${Updates[@]}"; do + read -a NotifyScript <<< "${update}" + found=$(grep -w "${NotifyScript}" "${SnoozeFile}" || printf "") + if [[ -n "${found}" ]]; then + read -a arr <<< "${found}" + CheckEpochSeconds=$(( $(date -d "${arr[1]}" +%s 2>/dev/null) + ${snooze} - 60 )) || CheckEpochSeconds=$(( $(date -f "%Y-%m-%d" -j "${arr[1]}" +%s) + ${snooze} - 60 )) + if [[ "${CurrentEpochSeconds}" -gt "${CheckEpochSeconds}" ]]; then + NotifyUpdateNotify=true + fi + else + NotifyUpdateNotify=true + fi + done + else + NotifyUpdateNotify=true + fi + + if [[ "${NotifyUpdateNotify}" == "true" ]]; then + if [[ "${#Updates[@]}" -gt 0 ]]; then + UpdToString=$( printf '%s\\n' "${Updates[@]}" ) + UpdToString=${UpdToString%\\n} + NotifyError=false + + MessageTitle="$FromHost - New version of notify templates available." + + printf -v MessageBody "Notify templates on $FromHost with updates available:\n${UpdToString}\n" + + for channel in "${enabled_notify_channels[@]}"; do + printf "Sending notify template update notification - ${channel}\n" + exec_if_exists_or_fail trigger_${channel}_notification || \ + printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" + done + + [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${Updates[@]}" + fi + fi + + UpdatesPlusDockcheck=("${Updates[@]}") + UpdatesPlusDockcheck+=("dockcheck.sh") + [[ -n "${snooze}" ]] && cleanup_snooze "${UpdatesPlusDockcheck[@]}" fi } From 77f024bb81a741fbc74638ab3e267b17ea8c5e72 Mon Sep 17 00:00:00 2001 From: vorezal <37914382+vorezal@users.noreply.github.com> Date: Fri, 27 Jun 2025 03:10:31 -0400 Subject: [PATCH 19/55] Fix unbound variable, potential collision, and config variable. (#209) * Fix unbound variable, potential collision, and config variable. * Return 0 when notification functions finish successfully --------- Co-authored-by: Matthew Oleksowicz --- notify_templates/notify_v2.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index a34acfa..3329512 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -139,6 +139,8 @@ send_notification() { fi [[ -n "${snooze}" ]] && cleanup_snooze "${Updates[@]}" + + return 0 } ### Set DISABLE_DOCKCHECK_NOTIFICATION=false in dockcheck.config @@ -187,14 +189,17 @@ dockcheck_notification() { fi fi fi + + return 0 } -### Set DISABLE_NOTIFY_UPDATE_NOTIFICATION=false in dockcheck.config +### Set DISABLE_NOTIFY_NOTIFICATION=false in dockcheck.config ### to not send notifications when notify scripts themselves have updates. notify_update_notification() { - if [[ ! "${DISABLE_NOTIFY_UPDATE_NOTIFICATION:-}" == "true" ]]; then + if [[ ! "${DISABLE_NOTIFY_NOTIFICATION:-}" == "true" ]]; then NotifyUpdateNotify=false NotifyError=false + NotifyUpdates=() UpdateChannels=( "${enabled_notify_channels[@]}" "v2" ) @@ -207,14 +212,14 @@ notify_update_notification() { LatestNotifyRelease="$(echo "$LatestNotifySnippet" | sed -n "/${VersionVar}/s/${VersionVar}=//p" | tr -d '"')" if [[ ! "${LatestNotifyRelease}" == "undefined" ]]; then if [[ "${!VersionVar}" != "${LatestNotifyRelease}" ]] ; then - Updates+=("${NotifyScript}.sh ${!VersionVar} -> ${LatestNotifyRelease}") + NotifyUpdates+=("${NotifyScript}.sh ${!VersionVar} -> ${LatestNotifyRelease}") fi fi fi done if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then - for update in "${Updates[@]}"; do + for update in "${NotifyUpdates[@]}"; do read -a NotifyScript <<< "${update}" found=$(grep -w "${NotifyScript}" "${SnoozeFile}" || printf "") if [[ -n "${found}" ]]; then @@ -232,8 +237,8 @@ notify_update_notification() { fi if [[ "${NotifyUpdateNotify}" == "true" ]]; then - if [[ "${#Updates[@]}" -gt 0 ]]; then - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) + if [[ "${#NotifyUpdates[@]}" -gt 0 ]]; then + UpdToString=$( printf '%s\\n' "${NotifyUpdates[@]}" ) UpdToString=${UpdToString%\\n} NotifyError=false @@ -247,12 +252,14 @@ notify_update_notification() { printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" done - [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${Updates[@]}" + [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${NotifyUpdates[@]}" fi fi - UpdatesPlusDockcheck=("${Updates[@]}") + UpdatesPlusDockcheck=("${NotifyUpdates[@]}") UpdatesPlusDockcheck+=("dockcheck.sh") [[ -n "${snooze}" ]] && cleanup_snooze "${UpdatesPlusDockcheck[@]}" fi + + return 0 } From d37e1a102434b17ac9f0e5bcaee607839ecf2262 Mon Sep 17 00:00:00 2001 From: mag37 Date: Fri, 27 Jun 2025 09:22:10 +0200 Subject: [PATCH 20/55] Bugfixes for unbound variable, potential collision and config variable mismatch Version bump to alert users of the latest bugfixes for unbound variable, potential collision and config variable mismatch. --- notify_templates/notify_v2.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index 3329512..c2d8538 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -1,4 +1,4 @@ -NOTIFY_V2_VERSION="v0.3" +NOTIFY_V2_VERSION="v0.4" # # 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. From 69c06de1bddf6ac9a691b476c80434ad6cad360a Mon Sep 17 00:00:00 2001 From: op4lat <155382511+op4lat@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:19:15 -0400 Subject: [PATCH 21/55] Add DisplaySourcedFiles variable (#207) * Add DisplaySourcedFiles variable to be used in source_if_exists and source_if_exists_or_fail functions * Added return 0 as to not throw wrong exit code. * Delete source_if_exists. source_if_exists_or_fail returns success or failure. Failure doesn't stop the script --------- Co-authored-by: Elephant Quater Co-authored-by: mag37 --- default.config | 1 + dockcheck.sh | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/default.config b/default.config index 2831591..27bb52c 100644 --- a/default.config +++ b/default.config @@ -26,6 +26,7 @@ #CurlRetryDelay=1 # Time between curl retries #CurlRetryCount=3 # Max number of curl retries #CurlConnectTimeout=5 # Time to wait for curl to establish a connection before failing +#DisplaySourcedFiles=false # Display what files are being sourced/used ### Notify settings ## All commented values are examples only. Modify as needed. diff --git a/dockcheck.sh b/dockcheck.sh index d341ce9..183027c 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -14,16 +14,18 @@ ScriptPath="$(readlink -f "$0")" ScriptWorkDir="$(dirname "$ScriptPath")" # Source helper functions -source_if_exists() { - if [[ -s "$1" ]]; then source "$1"; fi -} - source_if_exists_or_fail() { - [[ -s "$1" ]] && source "$1" + 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 "${ScriptWorkDir}/dockcheck.config" +source_if_exists_or_fail "${HOME}/.config/dockcheck.config" || source_if_exists_or_fail "${ScriptWorkDir}/dockcheck.config" # Help Function Help() { From a1e7446753d582aac1e41ddec23c960c08812bd0 Mon Sep 17 00:00:00 2001 From: mag37 Date: Tue, 1 Jul 2025 22:27:54 +0200 Subject: [PATCH 22/55] version bump + readme --- README.md | 5 ++++- dockcheck.sh | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 751e7b2..be80a36 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ ___ ## :bell: Changelog +- **v0.6.8**: + - Bugfix: Unbound variable in notify_v2.sh + - New option: "DisplaySourcedFiles" *config* added to list what files get sourced - **v0.6.7**: Snooze feature, curl, and consolidation - Added snooze feature to delay notifications - Added configurable default curl arguments @@ -168,7 +171,7 @@ copy it to `notify.sh` alongside the script, modify it to your needs! (notify.sh #### Snooze feature: **Use case:** You wish to be notified of available updates in a timely manner, but do not require reminders after the initial notification with the same frequency. e.g. *Dockcheck is scheduled to run every hour. You will receive an update notification within an hour of availability.* -**Snooze enabled:** you will not receive another notification about updates for this container for a configurable period of time. +**Snooze enabled:** you will not receive another notification about updates for this container for a configurable period of time. **Snooze disabled:** you will receive additional notifications every hour. To enable snooze, uncomment the `SNOOZE_SECONDS` variable in your `dockcheck.config` file and set it to the number of seconds you wish to prevent duplicate alerts. diff --git a/dockcheck.sh b/dockcheck.sh index 183027c..dafa2ce 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.6.7" -# ChangeNotes: snooze feature (see readme), curl arguments, cleanup. +VERSION="v0.6.8" +# ChangeNotes: bugfix unbound variable in notify_v2, new option "DisplaySourcedFiles" added to config Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" From 98e996caa3cca8e2d048b672aa22f2d33ed90dbc Mon Sep 17 00:00:00 2001 From: mag37 Date: Mon, 7 Jul 2025 10:49:32 +0200 Subject: [PATCH 23/55] added paypay sponsor --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index be80a36..dfac061 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ bash GPLv3 release +
Buy me a Coffee LiberaPay Github Sponsor + PayPal donation

CLI tool to automate docker image updates or notifying when updates are available.

From e2dbd69c5e428b94b1b7966b257f304a890e87e1 Mon Sep 17 00:00:00 2001 From: Rasmus Lundsgaard Date: Mon, 14 Jul 2025 13:59:07 +0200 Subject: [PATCH 24/55] first version of notification to Home Assistant (#213) * first working version of notification to Home Assistant * add documentation links * update readme for notify_HA --- README.md | 2 ++ default.config | 7 ++++++- notify_templates/notify_HA.sh | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100755 notify_templates/notify_HA.sh diff --git a/README.md b/README.md index dfac061..6fec3fc 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Make certain your project directory is laid out as below. You only need the noti │ ├── notify_discord.sh │ ├── notify_generic.sh │ ├── notify_gotify.sh +│ ├── notify_HA.sh │ ├── notify_matrix.sh │ ├── notify_ntfy.sh │ ├── notify_pushbullet.sh @@ -190,6 +191,7 @@ If an update becomes available for an item that is not snoozed, notifications wi - Read the [QuickStart](extras/apprise_quickstart.md) - [ntfy](https://ntfy.sh/) - HTTP-based pub-sub notifications. - [Gotify](https://gotify.net/) - a simple server for sending and receiving messages. +- [Home Assistant](https://www.home-assistant.io/integrations/notify/) - Connection to the notify [integrations](https://www.home-assistant.io/integrations/#notifications). - [Pushbullet](https://www.pushbullet.com/) - connecting different devices with cross-platform features. - [Telegram](https://telegram.org/) - Telegram chat API. - [Matrix-Synapse](https://github.com/element-hq/synapse) - [Matrix](https://matrix.org/), open, secure, decentralised communication. diff --git a/default.config b/default.config index 27bb52c..49457f0 100644 --- a/default.config +++ b/default.config @@ -32,7 +32,7 @@ ## All commented values are examples only. Modify as needed. ## ## Uncomment the line below and specify the notification channels you wish to enable in a space separated string -# NOTIFY_CHANNELS="apprise discord DSM generic gotify matrix ntfy pushbullet pushover slack smtp telegram" +# NOTIFY_CHANNELS="apprise discord DSM generic HA gotify matrix ntfy pushbullet pushover slack smtp telegram" # ## Uncomment the line below and specify the number of seconds to delay notifications to enable snooze # SNOOZE_SECONDS=86400 @@ -57,6 +57,10 @@ # GOTIFY_DOMAIN="https://gotify.domain.tld" # GOTIFY_TOKEN="token-value" # +# HA_ENTITY="entity" +# HA_TOKEN="token" +# HA_URL="https://your.homeassistant.url" +# # MATRIX_ACCESS_TOKEN="token-value" # MATRIX_ROOM_ID="myroom" # MATRIX_SERVER_URL="https://matrix.yourdomain.tld" @@ -82,3 +86,4 @@ # TELEGRAM_CHAT_ID="mychatid" # TELEGRAM_TOKEN="token-value" # TELEGRAM_TOPIC_ID="0" + diff --git a/notify_templates/notify_HA.sh b/notify_templates/notify_HA.sh new file mode 100755 index 0000000..dda74be --- /dev/null +++ b/notify_templates/notify_HA.sh @@ -0,0 +1,31 @@ +### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. +NOTIFY_HA_VERSION="v0.1" +# +# This is an integration that makes it possible to send notifications via Home Assistant (https://www.home-assistant.io/integrations/notify/) +# You need to generate a long-lived access token in Home Sssistant to be used here (https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token) +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. +# Do not modify this file directly within the "notify_templates" subdirectory. Set HA_ENTITY, HA_URL and HA_TOKEN in your dockcheck.config file. + +if [[ -z "${HA_ENTITY:-}" ]] || [[ -z "${HA_URL:-}" ]] || [[ -z "${HA_TOKEN:-}" ]]; then + printf "Home Assistant notification channel enabled, but required configuration variables are missing. Home assistant notifications will not be sent.\n" + + remove_channel HA +fi + +trigger_HA_notification() { + AccessToken="${HA_TOKEN}" + Url="${HA_URL}/api/services/notify/${HA_ENTITY}" + JsonData=$( "$jqbin" -n \ + --arg body "$MessageBody" \ + '{"title": "dockcheck update", "message": $body}' ) + + curl -S -o /dev/null ${CurlArgs} \ + -H "Authorization: Bearer $AccessToken" \ + -H "Content-Type: application/json" \ + -d "$JsonData" -X POST $Url + + if [[ $? -gt 0 ]]; then + NotifyError=true + fi +} From cfa74adc3db524e4e89b5dcfb562f8d0d666d4fa Mon Sep 17 00:00:00 2001 From: mag37 Date: Sat, 19 Jul 2025 00:40:50 +0200 Subject: [PATCH 25/55] added new logo --- extras/dockcheck_colour.png | Bin 0 -> 147760 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 extras/dockcheck_colour.png diff --git a/extras/dockcheck_colour.png b/extras/dockcheck_colour.png new file mode 100644 index 0000000000000000000000000000000000000000..1ae6f8f14c0ce2c007c4756d896e0d0c72a59f8d GIT binary patch literal 147760 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®*&Uw|w2$-m$orsgXG?{b5w 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 ye?35tfhT|#`M(~BKn(f+-{ODP Date: Sat, 19 Jul 2025 00:41:43 +0200 Subject: [PATCH 26/55] new logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fec3fc..db217e3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

bash From 563dbb8b4285bc687cf22e2da5c20836fbd713c6 Mon Sep 17 00:00:00 2001 From: mag37 Date: Fri, 25 Jul 2025 10:35:49 +0200 Subject: [PATCH 27/55] Label bugfix, search filtering fix (#216) * search filtering fix * skip recreation if no label when -l option used + clarification * changed readme + help to correctly show help example --- README.md | 20 +++++--------------- dockcheck.sh | 17 +++++++++++------ 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index db217e3..5719489 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ ___ ## :bell: Changelog +- **v0.6.9**: # + - Bugfix: label logic didn't skip recreation (skipped pulling). + - Added comma separated search filtering so you can selectively search exactly which containers to check/update. + - eg: `dockcheck.sh -yp homer,dozzle` - **v0.6.8**: - Bugfix: Unbound variable in notify_v2.sh - New option: "DisplaySourcedFiles" *config* added to list what files get sourced @@ -30,20 +34,6 @@ ___ - Added configurable default curl arguments - Consolidated and standardized notify template update notifications - Added curl error handling -- **v0.6.6**: Notify_v2 bugfixes - - Clearer readme and error messages - - Sourcing templates from either project root or subdirectory - - Consistent newline handling - - Added (when using `-d`) days old message to notification title - - Added ntfy self hosted domain option to config - - jq fixes to templates (and properly using $jqbin) -- **v0.6.5**: Refactored notification logic. See notify_templates/notify_v2.sh for upgrade steps. - - Added helper functions to simplify sourcing files and executing functions if they exist. - - Created notify_v2.sh wrapper script. - - Simplified and consolidated notification logic within notify_v2.sh. - - Added support for notification management via environment variables. - - Moved notification secrets to **dockcheck.config**. - - Added retries to wget/curl to not get empty responses when github is slow. ___ @@ -52,7 +42,7 @@ ___ ## :mag_right: `dockcheck.sh` ``` $ ./dockcheck.sh -h -Syntax: dockcheck.sh [OPTION] [part of name to filter] +Syntax: dockcheck.sh [OPTION] [comma separated names to include] Example: dockcheck.sh -y -x 10 -d 10 -e nextcloud,heimdall Options: diff --git a/dockcheck.sh b/dockcheck.sh index dafa2ce..c4a8587 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.6.8" -# ChangeNotes: bugfix unbound variable in notify_v2, new option "DisplaySourcedFiles" added to config +VERSION="v0.6.9" +# ChangeNotes: bugfix label logic and added comma separated search filtering Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -29,7 +29,7 @@ source_if_exists_or_fail "${HOME}/.config/dockcheck.config" || source_if_exists_ # Help Function Help() { - echo "Syntax: dockcheck.sh [OPTION] [part of name to filter]" + echo "Syntax: dockcheck.sh [OPTION] [comma separated names to include]" echo "Example: dockcheck.sh -y -x 10 -d 10 -e nextcloud,heimdall" echo echo "Options:" @@ -121,8 +121,11 @@ while getopts "ayfFhiIlmMnprsuvc:e:d:t:x:" options; do done shift "$((OPTIND-1))" -# Set $1 to a variable for name filtering later +# Set $1 to a variable for name filtering later, rewriting if multiple SearchName="${1:-}" +if [[ ! -z "$SearchName" ]]; then + SearchName="^(${SearchName//,/|})$" +fi # Check if there's a new release of the script LatestSnippet="$(curl ${CurlArgs} -r 0-200 "$RawUrl" || printf "undefined")" @@ -570,8 +573,11 @@ if [[ -n "${GotUpdates:-}" ]]; then ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") [[ "$ContOnlySpecific" == "null" ]] && ContRestartStack="" + printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" # Checking if compose-values are empty - hence started with docker run - [[ -z "$ContPath" ]] && continue + [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; continue; } + # Checking if Label Only -option is set, and if container got the label + [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != true ]] && { echo "No update label, skipping."; 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; } @@ -587,7 +593,6 @@ if [[ -n "${GotUpdates:-}" ]]; then # Set variable when compose up should only target the specific container, not the stack if [[ $OnlySpecific == true ]] || [[ $ContOnlySpecific == true ]]; then SpecificContainer="$ContName"; fi - printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" # 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; } From 1f374b5003f802975d346abf02152d7233d09c59 Mon Sep 17 00:00:00 2001 From: mag37 Date: Sun, 27 Jul 2025 15:06:35 +0200 Subject: [PATCH 28/55] clarifying ntfy.sh "domain" is a bit misleading, so clarifying that https:// is needed. Might change to "NTFY_URL" in the future. --- default.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default.config b/default.config index 49457f0..cecb493 100644 --- a/default.config +++ b/default.config @@ -65,8 +65,8 @@ # MATRIX_ROOM_ID="myroom" # MATRIX_SERVER_URL="https://matrix.yourdomain.tld" # -## ntfy.sh or your custom domain with no trailing / -# NTFY_DOMAIN="ntfy.sh" +## https://ntfy.sh or your custom domain with https:// and no trailing / +# NTFY_DOMAIN="https://ntfy.sh" # NTFY_TOPIC_NAME="YourUniqueTopicName" # # PUSHBULLET_URL="https://api.pushbullet.com/v2/pushes" From fbc9a252f555384c7602dc7ef38418b6431490c5 Mon Sep 17 00:00:00 2001 From: xmirakulix Date: Sat, 2 Aug 2025 08:04:43 +0200 Subject: [PATCH 29/55] update SMTP template, added suport for sendmail (#219) * update smtp template, add suport for sendmail * add sendmail to DSM and bump version * correct errormsg and version number --- notify_templates/notify_DSM.sh | 7 +++++-- notify_templates/notify_smtp.sh | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/notify_templates/notify_DSM.sh b/notify_templates/notify_DSM.sh index 08d85c1..3afe0ab 100644 --- a/notify_templates/notify_DSM.sh +++ b/notify_templates/notify_DSM.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DSM_VERSION="v0.3" +NOTIFY_DSM_VERSION="v0.4" # INFO: ssmtp is deprecated - consider to use msmtp instead. # # mSMTP/sSMTP has to be installed and configured manually. @@ -10,13 +10,16 @@ NOTIFY_DSM_VERSION="v0.3" MSMTP=$(which msmtp) SSMTP=$(which ssmtp) +SENDMAIL=$(which sendmail) if [ -n "$MSMTP" ] ; then MailPkg=$MSMTP elif [ -n "$SSMTP" ] ; then MailPkg=$SSMTP +elif [ -n "$SENDMAIL" ] ; then + MailPkg=$SENDMAIL else - echo "No msmtp or ssmtp binary found in PATH: $PATH" ; exit 1 + echo "No msmtp, ssmtp or sendmail binary found in PATH: $PATH" ; exit 1 fi trigger_DSM_notification() { diff --git a/notify_templates/notify_smtp.sh b/notify_templates/notify_smtp.sh index f07588c..9cfc76c 100644 --- a/notify_templates/notify_smtp.sh +++ b/notify_templates/notify_smtp.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_SMTP_VERSION="v0.3" +NOTIFY_SMTP_VERSION="v0.4" # INFO: ssmtp is depcerated - consider to use msmtp instead. # # mSMTP/sSMTP has to be installed and configured manually. @@ -15,13 +15,16 @@ fi MSMTP=$(which msmtp) SSMTP=$(which ssmtp) +SENDMAIL=$(which sendmail) if [ -n "$MSMTP" ] ; then MailPkg=$MSMTP elif [ -n "$SSMTP" ] ; then MailPkg=$SSMTP +elif [ -n "$SENDMAIL" ] ; then + MailPkg=$SENDMAIL else - echo "No msmtp or ssmtp binary found in PATH: $PATH" ; exit 1 + echo "No msmtp, ssmtp or sendmail binary found in PATH: $PATH" ; exit 1 fi trigger_smtp_notification() { From 9156cc44e1a9bd788f7a6638d7dff7d48299faec Mon Sep 17 00:00:00 2001 From: op4lat <155382511+op4lat@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:16:38 -0400 Subject: [PATCH 30/55] Ntfy.sh and authentication (#220) * default.config: add NtfyAuth= * notify_templates/notify_ntfy.sh: implement NtfyAuth --------- Co-authored-by: Lat Co-authored-by: mag37 --- default.config | 1 + notify_templates/notify_ntfy.sh | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/default.config b/default.config index cecb493..3ecc4d8 100644 --- a/default.config +++ b/default.config @@ -68,6 +68,7 @@ ## https://ntfy.sh or your custom domain with https:// and no trailing / # NTFY_DOMAIN="https://ntfy.sh" # NTFY_TOPIC_NAME="YourUniqueTopicName" +# NTFY_AUTH="" # set to either format -> "user:password" OR ":tk_12345678". If using tokens, don't forget the ":" # # PUSHBULLET_URL="https://api.pushbullet.com/v2/pushes" # PUSHBULLET_TOKEN="token-value" diff --git a/notify_templates/notify_ntfy.sh b/notify_templates/notify_ntfy.sh index d413a2c..e19f455 100644 --- a/notify_templates/notify_ntfy.sh +++ b/notify_templates/notify_ntfy.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_NTFYSH_VERSION="v0.5" +NOTIFY_NTFYSH_VERSION="v0.6" # # Setup app and subscription at https://ntfy.sh # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -24,11 +24,18 @@ trigger_ntfy_notification() { ContentType="Markdown: no" #text/plain fi + if [[ -n "${NTFY_AUTH:-}" ]]; then + NtfyAuth="-u $NTFY_AUTH" + else + NtfyAuth="" + fi + curl -S -o /dev/null ${CurlArgs} \ -H "Title: $MessageTitle" \ -H "$ContentType" \ -d "$MessageBody" \ - "$NtfyUrl" + $NtfyAuth \ + -L "$NtfyUrl" if [[ $? -gt 0 ]]; then NotifyError=true From 732a5e69cda2676849db9bcbaa9cb95001d0c71c Mon Sep 17 00:00:00 2001 From: vorezal <37914382+vorezal@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:17:01 -0400 Subject: [PATCH 31/55] Reword disable notification comment for clarity and use update_snooze for dockcheck notifications. (#221) Co-authored-by: Matthew Oleksowicz --- default.config | 4 ++-- notify_templates/notify_v2.sh | 13 ++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/default.config b/default.config index 3ecc4d8..47b6189 100644 --- a/default.config +++ b/default.config @@ -37,9 +37,9 @@ ## Uncomment the line below and specify the number of seconds to delay notifications to enable snooze # SNOOZE_SECONDS=86400 # -## Uncomment to not send notifications when dockcheck itself has updates. +## Uncomment and set to true to disable notifications when dockcheck itself has updates. # DISABLE_DOCKCHECK_NOTIFICATION=false -## Uncomment to not send notifications when notify scripts themselves have updates. +## Uncomment and set to true to disable notifications when notify scripts themselves have updates. # DISABLE_NOTIFY_NOTIFICATION=false # ## Apprise configuration variables. Set APPRISE_PAYLOAD to make a CLI call or set APPRISE_URL to make an API request instead. diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index c2d8538..a256311 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -1,4 +1,4 @@ -NOTIFY_V2_VERSION="v0.4" +NOTIFY_V2_VERSION="v0.5" # # 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. @@ -177,16 +177,7 @@ dockcheck_notification() { printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" done - if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then - if [[ "${NotifyError}" == "false" ]]; then - if [[ -n "${found}" ]]; then - sed -e "s/dockcheck\.sh.*/dockcheck\.sh ${CurrentEpochTime}/" "${SnoozeFile}" > "${SnoozeFile}.new" - mv "${SnoozeFile}.new" "${SnoozeFile}" - else - printf "dockcheck.sh ${CurrentEpochTime}\n" >> "${SnoozeFile}" - fi - fi - fi + [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "dockcheck.sh" fi fi From 37f33d7a063ebfd892979ba7786f226162a96448 Mon Sep 17 00:00:00 2001 From: mag37 Date: Mon, 11 Aug 2025 21:36:51 +0200 Subject: [PATCH 32/55] Snooze bugfix, added auth support to ntfy.sh and sendmail support to SMTP --- README.md | 6 +++++- dockcheck.sh | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5719489..4499857 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,11 @@ ___ ## :bell: Changelog -- **v0.6.9**: # +- **v0.7.0**: + - Bugfix: snooze dockcheck.sh-self-notification and some config clarification. + - Added authentication support to Ntfy.sh. + - Added suport for sendmail in the SMTP-template. +- **v0.6.9**: - Bugfix: label logic didn't skip recreation (skipped pulling). - Added comma separated search filtering so you can selectively search exactly which containers to check/update. - eg: `dockcheck.sh -yp homer,dozzle` diff --git a/dockcheck.sh b/dockcheck.sh index c4a8587..9de58f5 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.6.9" -# ChangeNotes: bugfix label logic and added comma separated search filtering +VERSION="v0.7.0" +# ChangeNotes: Snooze bugfix, added auth support to ntfy.sh and sendmail support to SMTP Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -601,7 +601,7 @@ if [[ -n "${GotUpdates:-}" ]]; then fi done if [[ "$AutoPrune" == false ]] && [[ "$AutoMode" == false ]]; then printf "\n"; read -rep "Would you like to prune dangling images? y/[n]: " AutoPrune; fi - if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then printf "\n Auto pruning.."; docker image prune -f; fi + if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then printf "\nAuto pruning.."; docker image prune -f; fi printf "\n%bAll done!%b\n" "$c_green" "$c_reset" else printf "\nNo updates installed, exiting.\n" From 31a45f1d84f6fbd2bff65e71ddb20d9d8ef5ff40 Mon Sep 17 00:00:00 2001 From: vorezal <37914382+vorezal@users.noreply.github.com> Date: Mon, 15 Sep 2025 05:25:23 -0400 Subject: [PATCH 33/55] Add file notification channel (#222) * Add file notification channel * Bypass file channel notifications for dockcheck.sh script * Implement notification channel template reuse and advanced configuration variables. * Fix text insertion formatting for dockcheck script and container updates. * Fix dockcheck.sh notification csv and text output. * Fix ntfy variable references and replace tr for uppercase conversion. * Fix ALLOWEMPTY logic, undefined snippet case, and README formatting. * Refactor notification send/skip logic. Adjust missing variable return codes. * Adjust notifications README section for clarity and readability. --------- Co-authored-by: Matthew Oleksowicz --- .gitignore | 2 + README.md | 82 ++++-- default.config | 4 +- dockcheck.sh | 6 +- notify_templates/notify_DSM.sh | 17 +- notify_templates/notify_HA.sh | 31 +- notify_templates/notify_apprise.sh | 33 ++- notify_templates/notify_discord.sh | 27 +- notify_templates/notify_file.sh | 22 ++ notify_templates/notify_gotify.sh | 30 +- notify_templates/notify_matrix.sh | 35 ++- notify_templates/notify_ntfy.sh | 33 ++- notify_templates/notify_pushbullet.sh | 30 +- notify_templates/notify_pushover.sh | 33 ++- notify_templates/notify_slack.sh | 30 +- notify_templates/notify_smtp.sh | 33 ++- notify_templates/notify_telegram.sh | 33 ++- notify_templates/notify_v2.sh | 406 ++++++++++++++++++-------- 18 files changed, 620 insertions(+), 267 deletions(-) create mode 100644 notify_templates/notify_file.sh diff --git a/.gitignore b/.gitignore index e5a2ded..182c4aa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ regctl # ignore snooze file snooze.list +# ignore updates file +updates_available.txt \ No newline at end of file diff --git a/README.md b/README.md index 4499857..f8f4d17 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,16 @@ ___ ## :bell: Changelog +- **v0.7.1**: + - Added support for multiple notifications using the same template + - Added support for notification output format + - Added support for file output + - Added optional configuration variables per channel to (replace <channel> with any channel name): + - <channel>\_TEMPLATE : Specify a template + - <channel>\_SKIPSNOOZE : Skip snooze + - <channel>\_CONTAINERSONLY : Only notify for docker container related updates + - <channel>\_ALLOWEMPTY : Always send notifications, even when empty + - <channel>\_OUTPUT : Define output format - **v0.7.0**: - Bugfix: snooze dockcheck.sh-self-notification and some config clarification. - Added authentication support to Ntfy.sh. @@ -126,13 +136,15 @@ Alternatively create an alias where specific flags and values are set. Example `alias dc=dockcheck.sh -p -x 10 -t 3`. ## :loudspeaker: Notifications -Trigger with the `-i` flag. -If `notify.sh` is present and configured, it will be used. Otherwise, `notify_v2.sh` will be enabled. -Will send a list of containers with updates available and a notification when `dockcheck.sh` itself has an update. -Run it scheduled with `-ni` to only get notified when there's updates available! +Triggered with the `-i` flag. Will send a list of containers with updates available and a notification when `dockcheck.sh` itself has an update. +`notify_templates/notify_v2.sh` is the default notification wrapper, if `notify.sh` is present and configured, it will override. + +Example of a cron scheduled job running non-interactive at 10'oclock excluding 1 container and sending notifications: +`0 10 * * * /home/user123/.local/bin/dockcheck.sh -nix 10 -e excluded_container1` #### Installation and configuration: -Make certain your project directory is laid out as below. You only need the notify_v2.sh file and any notification templates you wish to enable, but there is no harm in having all of them present. +Set up a directory structure as below. +You only need the `notify_templates/notify_v2.sh` file and any notification templates you wish to enable, but there is no harm in having all of them present. ``` . ├── notify_templates/ @@ -154,27 +166,27 @@ Make certain your project directory is laid out as below. You only need the noti ├── dockcheck.sh └── urls.list # optional ``` -Uncomment and set the NOTIFY_CHANNELS environment variable in `dockcheck.config` to a space separated string of your desired notification channels to enable. -Uncomment and set the environment variables related to the enabled notification channels. -It is recommended to only edit the environmental variables in `dockcheck.config` and not make changes directly to the `notify_X.sh` template files within the `notify_templates` subdirectory. -If you wish to customize the notify templates yourself, you may copy them to your project root directory alongside the main `dockcheck.sh` script (where they will also be ignored by git). +- Uncomment and set the `NOTIFY_CHANNELS=""` environment variable in `dockcheck.config` to a space separated string of your desired notification channels to enable. +- Uncomment and set the environment variables related to the enabled notification channels. Eg. `GOTIFY_DOMAIN=""` + `GOTIFY_TOKEN=""`. + +It's recommended to only do configuration with variables within `dockcheck.config` and not modify `notify_templates/notify_X.sh` directly. +If you wish to customize the notify templates yourself, you may copy them to your project root directory alongside the main `dockcheck.sh` (where they're also ignored by git). Customizing `notify_v2.sh` is handled the same as customizing the templates, but it must be renamed to `notify.sh` within the `dockcheck.sh` root directory. -#### Legacy installation and configuration: -Use a previous version of a `notify_X.sh` template file (tag v0.6.4 or earlier) from the **notify_templates** directory, -copy it to `notify.sh` alongside the script, modify it to your needs! (notify.sh is added to .gitignore) - #### Snooze feature: -**Use case:** You wish to be notified of available updates in a timely manner, but do not require reminders after the initial notification with the same frequency. -e.g. *Dockcheck is scheduled to run every hour. You will receive an update notification within an hour of availability.* -**Snooze enabled:** you will not receive another notification about updates for this container for a configurable period of time. -**Snooze disabled:** you will receive additional notifications every hour. +Configure to receive scheduled notifications only if they're new since the last notification - within a set time frame. + +**Example:** *Dockcheck is scheduled to run every hour. You will receive an update notification within an hour of availability.* +**Snooze enabled:** You will not receive a repeated notification about an already notified update within the snooze duration. +**Snooze disabled:** You will receive additional (possibly repeated) notifications every hour. + +To enable snooze uncomment the `SNOOZE_SECONDS` variable in your `dockcheck.config` and set it to the number of seconds you wish to prevent duplicate alerts. +Snooze is split into three categories; container updates, `dockcheck.sh` self updates and notification template updates. -To enable snooze, uncomment the `SNOOZE_SECONDS` variable in your `dockcheck.config` file and set it to the number of seconds you wish to prevent duplicate alerts. -The true snooze duration will be 60 seconds less than your configure value to account for minor scheduling or script run time issues. If an update becomes available for an item that is not snoozed, notifications will be sent and include all available updates for that item's category, even snoozed items. -`dockcheck.sh` updates, notification template updates, and container updates are considered three separate categories. + +The actual snooze duration will be 60 seconds less than `SNOOZE_SECONDS` to account for minor scheduling or run time issues. #### Current notify templates: @@ -193,15 +205,31 @@ If an update becomes available for an item that is not snoozed, notifications wi - [Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) - Discord webhooks. - [Slack](https://api.slack.com/tutorials/tracks/posting-messages-with-curl) - Slack curl api -Further additions are welcome - suggestions or PRs! -Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). +Further additions are welcome - suggestions or PRs! +Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). + +#### Notification channel configuration: +All required environment variables for each notification channel are provided in the default.config file as comments and must be uncommented and modified for your requirements. +For advanced users, additional functionality is available via custom configurations and environment variables. +Use cases - all configured in `dockcheck.config`: +(replace `` with the upper case name of the of the channel as listed in `NOTIFY_CHANNELS` variable, eg `TELEGRAM_SKIPSNOOZE`) +- To bypass the snooze feature, even when enabled, add the variable `_SKIPSNOOZE` and set it to `true`. +- To configure the channel to only send container update notifications, add the variable `_CONTAINERSONLY` and set it to `true`. +- To send notifications even when there are no updates available, add the variable `_ALLOWEMPTY` and set it to `true`. +- To use another notification output format, add the variable `_OUTPUT` and set it to `csv`, `json`, or `text`. If unset or set to an invalid value, defaults to `text`. +- To send multiple notifications using the same notification template: + - Strings in the `NOTIFY_CHANNELS` list are now treated as unique names and do not necessarily refer to the notification template that will be called, though they do by default. + - Add another notification channel to `NOTIFY_CHANNELS` in `dockcheck.config`. The name can contain upper and lower case letters, numbers and underscores, but can't start with a number. + - Add the variable `_TEMPLATE` to `dockcheck.config` where `` is the name of the channel added above and set the value to an available notification template script (`slack`, `apprise`, `gotify`, etc.) + - Add all other environment variables required for the chosen template to function with `` in upper case as the prefix rather than the template name. + - For example, if `` is `mynotification` and the template configured is `slack`, you would need to set `MYNOTIFICATION_CHANNEL_ID` and `MYNOTIFICATION_ACCESS_TOKEN`. ### :date: Release notes addon -There's a function to use a lookup-file to add release note URL's to the notification message. -Copy the notify_templates/`urls.list` file to the script directory, it will be used automatically if it's there. -Modify it as necessary, the names of interest in the left column needs to match your container names. -To also list the URL's in the CLI output (choose containers list) use the `-I` option or variable config. -For Markdown formatting also add the `-M` option. (**this requires the template to be compatible - see gotify for example**) +There's a function to use a lookup-file to add release note URL's to the notification message. +Copy the notify_templates/`urls.list` file to the script directory, it will be used automatically if it's there. +Modify it as necessary, the names of interest in the left column needs to match your container names. +To also list the URL's in the CLI output (choose containers list) use the `-I` option or variable config. +For Markdown formatting also add the `-M` option. (**this requires the template to be compatible - see gotify for example**) The output of the notification will look something like this: ``` diff --git a/default.config b/default.config index 47b6189..d5d21aa 100644 --- a/default.config +++ b/default.config @@ -32,7 +32,7 @@ ## All commented values are examples only. Modify as needed. ## ## Uncomment the line below and specify the notification channels you wish to enable in a space separated string -# NOTIFY_CHANNELS="apprise discord DSM generic HA gotify matrix ntfy pushbullet pushover slack smtp telegram" +# NOTIFY_CHANNELS="apprise discord DSM file generic HA gotify matrix ntfy pushbullet pushover slack smtp telegram file" # ## Uncomment the line below and specify the number of seconds to delay notifications to enable snooze # SNOOZE_SECONDS=86400 @@ -87,4 +87,6 @@ # TELEGRAM_CHAT_ID="mychatid" # TELEGRAM_TOKEN="token-value" # TELEGRAM_TOPIC_ID="0" +# +# FILE_PATH="${ScriptWorkDir}/updates_available.txt" diff --git a/dockcheck.sh b/dockcheck.sh index 9de58f5..d46f4da 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.7.0" -# ChangeNotes: Snooze bugfix, added auth support to ntfy.sh and sendmail support to SMTP +VERSION="v0.7.1" +# ChangeNotes: Add support for multiple notifications of the same type, output formatting, and file output Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -507,6 +507,8 @@ if [[ -n ${GotUpdates[*]:-} ]]; then if [[ -s "$ScriptWorkDir/urls.list" ]] && [[ "$PrintReleaseURL" == true ]]; then releasenotes; else Updates=("${GotUpdates[@]}"); fi [[ "$AutoMode" == false ]] && list_options || printf "%s\n" "${Updates[@]}" [[ "$Notify" == true ]] && { exec_if_exists_or_fail send_notification "${GotUpdates[@]}" || printf "\nCould not source notification function.\n"; } +else + [[ "$Notify" == true ]] && [[ ! -s "${ScriptWorkDir}/notify.sh" ]] && { exec_if_exists_or_fail send_notification "${GotUpdates[@]}" || printf "\nCould not source notification function.\n"; } fi # Optionally get updates if there's any diff --git a/notify_templates/notify_DSM.sh b/notify_templates/notify_DSM.sh index 3afe0ab..8da3c54 100644 --- a/notify_templates/notify_DSM.sh +++ b/notify_templates/notify_DSM.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DSM_VERSION="v0.4" +NOTIFY_DSM_VERSION="v0.5" # INFO: ssmtp is deprecated - consider to use msmtp instead. # # mSMTP/sSMTP has to be installed and configured manually. @@ -23,15 +23,26 @@ else fi trigger_DSM_notification() { + if [[ -n "$1" ]]; then + DSM_channel="$1" + else + DSM_channel="DSM" + fi + +UpperChannel="${DSM_channel^^}" + +DSMSendmailToVar="${UpperChannel}_SENDMAILTO" +DSMSubjectTagVar="${UpperChannel}_SUBJECTTAG" + CfgFile="/usr/syno/etc/synosmtp.conf" # User variables: # Automatically sends to your usual destination for synology DSM notification emails. # You can also manually override by assigning something else to DSM_SENDMAILTO in dockcheck.config. -SendMailTo=${DSM_SENDMAILTO:-$(grep 'eventmail1' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p')} +SendMailTo=${!DSMSendmailToVar:-$(grep 'eventmail1' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p')} # e.g. DSM_SENDMAILTO="me@mydomain.com" -SubjectTag=${DSM_SUBJECTTAG:-$(grep 'eventsubjectprefix' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p')} +SubjectTag=${!DSMSubjectTagVar:-$(grep 'eventsubjectprefix' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p')} # e.g. DSM_SUBJECTTAG="Email Subject Prefix" SenderName=$(grep 'smtp_from_name' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p') SenderMail=$(grep 'smtp_from_mail' $CfgFile | sed -n 's/.*"\([^"]*\)".*/\1/p') diff --git a/notify_templates/notify_HA.sh b/notify_templates/notify_HA.sh index dda74be..e1f52bd 100755 --- a/notify_templates/notify_HA.sh +++ b/notify_templates/notify_HA.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_HA_VERSION="v0.1" +NOTIFY_HA_VERSION="v0.2" # # This is an integration that makes it possible to send notifications via Home Assistant (https://www.home-assistant.io/integrations/notify/) # You need to generate a long-lived access token in Home Sssistant to be used here (https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token) @@ -7,15 +7,28 @@ NOTIFY_HA_VERSION="v0.1" # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set HA_ENTITY, HA_URL and HA_TOKEN in your dockcheck.config file. -if [[ -z "${HA_ENTITY:-}" ]] || [[ -z "${HA_URL:-}" ]] || [[ -z "${HA_TOKEN:-}" ]]; then - printf "Home Assistant notification channel enabled, but required configuration variables are missing. Home assistant notifications will not be sent.\n" - - remove_channel HA -fi - trigger_HA_notification() { - AccessToken="${HA_TOKEN}" - Url="${HA_URL}/api/services/notify/${HA_ENTITY}" + if [[ -n "$1" ]]; then + HA_channel="$1" + else + HA_channel="HA" + fi + + UpperChannel="${HA_channel^^}" + + HAEntityVar="${UpperChannel}_ENTITY" + HAUrlVar="${UpperChannel}_URL" + HATokenVar="${UpperChannel}_TOKEN" + + if [[ -z "${!HAEntityVar:-}" ]] || [[ -z "${!HAUrlVar:-}" ]] || [[ -z "${!HATokenVar:-}" ]]; then + printf "The ${HA_channel} notification channel is enabled, but required configuration variables are missing. Home assistant notifications will not be sent.\n" + + remove_channel HA + return 0 + fi + + AccessToken="${!HATokenVar}" + Url="${!HAUrlVar}/api/services/notify/${!HAEntityVar}" JsonData=$( "$jqbin" -n \ --arg body "$MessageBody" \ '{"title": "dockcheck update", "message": $body}' ) diff --git a/notify_templates/notify_apprise.sh b/notify_templates/notify_apprise.sh index 1fa94de..d2ab0a5 100644 --- a/notify_templates/notify_apprise.sh +++ b/notify_templates/notify_apprise.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_APPRISE_VERSION="v0.3" +NOTIFY_APPRISE_VERSION="v0.4" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -7,17 +7,28 @@ NOTIFY_APPRISE_VERSION="v0.3" # Do not modify this file directly within the "notify_templates" subdirectory. Set APPRISE_PAYLOAD in your dockcheck.config file. # If API, set APPRISE_URL instead. -if [[ -z "${APPRISE_PAYLOAD:-}" ]] && [[ -z "${APPRISE_URL:-}" ]]; then - printf "Apprise notification channel enabled, but required configuration variables are missing. Apprise notifications will not be sent.\n" - - remove_channel apprise -fi - trigger_apprise_notification() { + if [[ -n "$1" ]]; then + apprise_channel="$1" + else + apprise_channel="apprise" + fi - if [[ -n "${APPRISE_PAYLOAD:-}" ]]; then + UpperChannel="${apprise_channel^^}" + + ApprisePayloadVar="${UpperChannel}_PAYLOAD" + AppriseUrlVar="${UpperChannel}_URL" + + if [[ -z "${!ApprisePayloadVar:-}" ]] && [[ -z "${!AppriseUrlVar:-}" ]]; then + printf "The ${apprise_channel} notification channel is enabled, but required configuration variables are missing. Apprise notifications will not be sent.\n" + + remove_channel apprise + return 0 + fi + + if [[ -n "${!ApprisePayloadVar:-}" ]]; then apprise -vv -t "$MessageTitle" -b "$MessageBody" \ - ${APPRISE_PAYLOAD} + ${!ApprisePayloadVar} if [[ $? -gt 0 ]]; then NotifyError=true @@ -29,8 +40,8 @@ trigger_apprise_notification() { # pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b # tgram://{bot_token}/{chat_id}/' - if [[ -n "${APPRISE_URL:-}" ]]; then - AppriseURL="${APPRISE_URL}" + if [[ -n "${!AppriseUrlVar:-}" ]]; then + AppriseURL="${!AppriseUrlVar}" curl -S -o /dev/null ${CurlArgs} -X POST -F "title=$MessageTitle" -F "body=$MessageBody" -F "tags=all" $AppriseURL # e.g. APPRISE_URL=http://apprise.mydomain.tld:1234/notify/apprise if [[ $? -gt 0 ]]; then diff --git a/notify_templates/notify_discord.sh b/notify_templates/notify_discord.sh index fa1a32d..4ac050a 100644 --- a/notify_templates/notify_discord.sh +++ b/notify_templates/notify_discord.sh @@ -1,19 +1,30 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_DISCORD_VERSION="v0.4" +NOTIFY_DISCORD_VERSION="v0.5" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set DISCORD_WEBHOOK_URL in your dockcheck.config file. -if [[ -z "${DISCORD_WEBHOOK_URL:-}" ]]; then - printf "Discord notification channel enabled, but required configuration variables are missing. Discord notifications will not be sent.\n" - - remove_channel discord -fi - trigger_discord_notification() { - DiscordWebhookUrl="${DISCORD_WEBHOOK_URL}" # e.g. DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/ + if [[ -n "$1" ]]; then + discord_channel="$1" + else + discord_channel="discord" + fi + + UpperChannel="${discord_channel^^}" + + DiscordWebhookUrlVar="${UpperChannel}_WEBHOOK_URL" + + if [[ -z "${!DiscordWebhookUrlVar:-}" ]]; then + printf "The ${discord_channel} notification channel is enabled, but required configuration variables are missing. Discord notifications will not be sent.\n" + + remove_channel discord + return 0 + fi + + DiscordWebhookUrl="${!DiscordWebhookUrlVar}" # e.g. DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/ JsonData=$( "$jqbin" -n \ --arg username "$FromHost" \ diff --git a/notify_templates/notify_file.sh b/notify_templates/notify_file.sh new file mode 100644 index 0000000..75aa74a --- /dev/null +++ b/notify_templates/notify_file.sh @@ -0,0 +1,22 @@ +### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. +NOTIFY_FILE_VERSION="v0.1" +# +# Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. +# If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. + +trigger_file_notification() { + if [[ -n "$1" ]]; then + file_channel="$1" + UpperChannel=$(tr '[:lower:]' '[:upper:]' <<< "$file_channel") + else + file_channel="file" + UpperChannel="FILE" + fi + + FilePathVar="${UpperChannel}_PATH" + + NotifyFile="${!FilePathVar:=${ScriptWorkDir}/updates_available.txt}" + + echo "${MessageBody}" > ${NotifyFile} + +} diff --git a/notify_templates/notify_gotify.sh b/notify_templates/notify_gotify.sh index d3d2c67..66e04ef 100644 --- a/notify_templates/notify_gotify.sh +++ b/notify_templates/notify_gotify.sh @@ -1,20 +1,32 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_GOTIFY_VERSION="v0.4" +NOTIFY_GOTIFY_VERSION="v0.5" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set GOTIFY_TOKEN and GOTIFY_DOMAIN in your dockcheck.config file. -if [[ -z "${GOTIFY_TOKEN:-}" ]] || [[ -z "${GOTIFY_DOMAIN:-}" ]]; then - printf "Gotify notification channel enabled, but required configuration variables are missing. Gotify notifications will not be sent.\n" - - remove_channel gotify -fi - trigger_gotify_notification() { - GotifyToken="${GOTIFY_TOKEN}" # e.g. GOTIFY_TOKEN=token-value - GotifyUrl="${GOTIFY_DOMAIN}/message?token=${GotifyToken}" # e.g. GOTIFY_URL=https://gotify.domain.tld + if [[ -n "$1" ]]; then + gotify_channel="$1" + else + gotify_channel="gotify" + fi + + UpperChannel="${gotify_channel^^}" + + GotifyTokenVar="${UpperChannel}_TOKEN" + GotifyUrlVar="${UpperChannel}_DOMAIN" + + if [[ -z "${!GotifyTokenVar:-}" ]] || [[ -z "${!GotifyUrlVar:-}" ]]; then + printf "The ${gotify_channel} notification channel is enabled, but required configuration variables are missing. Gotify notifications will not be sent.\n" + + remove_channel gotify + return 0 + fi + + GotifyToken="${!GotifyTokenVar}" # e.g. GOTIFY_TOKEN=token-value + GotifyUrl="${!GotifyUrlVar}/message?token=${GotifyToken}" # e.g. GOTIFY_URL=https://gotify.domain.tld if [[ "$PrintMarkdownURL" == true ]]; then ContentType="text/markdown" diff --git a/notify_templates/notify_matrix.sh b/notify_templates/notify_matrix.sh index bcff5d2..efdf37d 100644 --- a/notify_templates/notify_matrix.sh +++ b/notify_templates/notify_matrix.sh @@ -1,25 +1,38 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_MATRIX_VERSION="v0.3" +NOTIFY_MATRIX_VERSION="v0.4" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID, and MATRIX_SERVER_URL in your dockcheck.config file. -if [[ -z "${MATRIX_ACCESS_TOKEN:-}" ]] || [[ -z "${MATRIX_ROOM_ID}:-" ]] || [[ -z "${MATRIX_SERVER_URL}:-" ]]; then - printf "Matrix notification channel enabled, but required configuration variables are missing. Matrix notifications will not be sent.\n" - - remove_channel matrix -fi - trigger_matrix_notification() { - AccessToken="${MATRIX_ACCESS_TOKEN}" # e.g. MATRIX_ACCESS_TOKEN=token-value - Room_id="${MATRIX_ROOM_ID}" # e.g. MATRIX_ROOM_ID=myroom - MatrixServer="${MATRIX_SERVER_URL}" # e.g. MATRIX_SERVER_URL=http://matrix.yourdomain.tld + if [[ -n "$1" ]]; then + matrix_channel="$1" + else + matrix_channel="matrix" + fi + + UpperChannel="${matrix_channel^^}" + + AccessTokenVar="${UpperChannel}_ACCESS_TOKEN" + RoomIdVar="${UpperChannel}_ROOM_ID" + MatrixServerVar="${UpperChannel}_SERVER_URL" + + if [[ -z "${!AccessTokenVar:-}" ]] || [[ -z "${!RoomIdVar:-}" ]] || [[ -z "${!MatrixServerVar:-}" ]]; then + printf "The ${matrix_channel} notification channel is enabled, but required configuration variables are missing. Matrix notifications will not be sent.\n" + + remove_channel matrix + return 0 + fi + + AccessToken="${!AccessTokenVar}" # e.g. MATRIX_ACCESS_TOKEN=token-value + RoomId="${!RoomIdVar}" # e.g. MATRIX_ROOM_ID=myroom + MatrixServer="${!MatrixServerVar}" # e.g. MATRIX_SERVER_URL=http://matrix.yourdomain.tld MsgBody="{\"msgtype\":\"m.text\",\"body\":\"$MessageBody\"}" # URL Example: https://matrix.org/_matrix/client/r0/rooms/!xxxxxx:example.com/send/m.room.message?access_token=xxxxxxxx - curl -S -o /dev/null ${CurlArgs} -X POST "$MatrixServer/_matrix/client/r0/rooms/$Room_id/send/m.room.message?access_token=$AccessToken" -H 'Content-Type: application/json' -d "$MsgBody" + curl -S -o /dev/null ${CurlArgs} -X POST "$MatrixServer/_matrix/client/r0/rooms/$RoomId/send/m.room.message?access_token=$AccessToken" -H 'Content-Type: application/json' -d "$MsgBody" if [[ $? -gt 0 ]]; then NotifyError=true diff --git a/notify_templates/notify_ntfy.sh b/notify_templates/notify_ntfy.sh index e19f455..dff7234 100644 --- a/notify_templates/notify_ntfy.sh +++ b/notify_templates/notify_ntfy.sh @@ -1,19 +1,32 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_NTFYSH_VERSION="v0.6" +NOTIFY_NTFYSH_VERSION="v0.7" # # Setup app and subscription at https://ntfy.sh # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set NTFY_DOMAIN and NTFY_TOPIC_NAME in your dockcheck.config file. -if [[ -z "${NTFY_DOMAIN:-}" ]] || [[ -z "${NTFY_TOPIC_NAME:-}" ]]; then - printf "Ntfy notification channel enabled, but required configuration variables are missing. Ntfy notifications will not be sent.\n" - - remove_channel ntfy -fi - trigger_ntfy_notification() { - NtfyUrl="${NTFY_DOMAIN}/${NTFY_TOPIC_NAME}" + if [[ -n "$1" ]]; then + ntfy_channel="$1" + else + ntfy_channel="ntfy" + fi + + UpperChannel="${ntfy_channel^^}" + + NtfyDomainVar="${UpperChannel}_DOMAIN" + NtfyTopicNameVar="${UpperChannel}_TOPIC_NAME" + NtfyAuthVar="${UpperChannel}_AUTH" + + if [[ -z "${!NtfyDomainVar:-}" ]] || [[ -z "${!NtfyTopicNameVar:-}" ]]; then + printf "The ${ntfy_channel} notification channel is enabled, but required configuration variables are missing. Ntfy notifications will not be sent.\n" + + remove_channel ntfy + return 0 + fi + + NtfyUrl="${!NtfyDomainVar}/${!NtfyTopicNameVar}" # e.g. # NTFY_DOMAIN=ntfy.sh # NTFY_TOPIC_NAME=YourUniqueTopicName @@ -24,8 +37,8 @@ trigger_ntfy_notification() { ContentType="Markdown: no" #text/plain fi - if [[ -n "${NTFY_AUTH:-}" ]]; then - NtfyAuth="-u $NTFY_AUTH" + if [[ -n "${!NtfyAuthVar:-}" ]]; then + NtfyAuth="-u ${!NtfyAuthVar}" else NtfyAuth="" fi diff --git a/notify_templates/notify_pushbullet.sh b/notify_templates/notify_pushbullet.sh index 78dec0b..b061e1c 100644 --- a/notify_templates/notify_pushbullet.sh +++ b/notify_templates/notify_pushbullet.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_PUSHBULLET_VERSION="v0.3" +NOTIFY_PUSHBULLET_VERSION="v0.4" # # Required receiving services must already be set up. # Requires jq installed and in PATH. @@ -7,15 +7,27 @@ NOTIFY_PUSHBULLET_VERSION="v0.3" # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set PUSHBULLET_TOKEN and PUSHBULLET_URL in your dockcheck.config file. -if [[ -z "${PUSHBULLET_URL:-}" ]] || [[ -z "${PUSHBULLET_TOKEN:-}" ]]; then - printf "Pushbullet notification channel enabled, but required configuration variables are missing. Pushbullet notifications will not be sent.\n" - - remove_channel pushbullet -fi - trigger_pushbullet_notification() { - PushUrl="${PUSHBULLET_URL}" # e.g. PUSHBULLET_URL=https://api.pushbullet.com/v2/pushes - PushToken="${PUSHBULLET_TOKEN}" # e.g. PUSHBULLET_TOKEN=token-value + if [[ -n "$1" ]]; then + pushbullet_channel="$1" + else + pushbullet_channel="pushbullet" + fi + + UpperChannel="${pushbullet_channel^^}" + + PushUrlVar="${UpperChannel}_URL" + PushTokenVar="${UpperChannel}_TOKEN" + + if [[ -z "${!PushUrlVar:-}" ]] || [[ -z "${!PushTokenVar:-}" ]]; then + printf "The ${pushbullet_channel} notification channel is enabled, but required configuration variables are missing. Pushbullet notifications will not be sent.\n" + + remove_channel pushbullet + return 0 + fi + + PushUrl="${!PushUrlVar}" # e.g. PUSHBULLET_URL=https://api.pushbullet.com/v2/pushes + PushToken="${!PushTokenVar}" # e.g. PUSHBULLET_TOKEN=token-value # Requires jq to process json data "$jqbin" -n --arg title "$MessageTitle" --arg body "$MessageBody" '{body: $body, title: $title, type: "note"}' | curl -S -o /dev/null ${CurlArgs} -X POST -H "Access-Token: $PushToken" -H "Content-type: application/json" $PushUrl -d @- diff --git a/notify_templates/notify_pushover.sh b/notify_templates/notify_pushover.sh index 60ffad6..92eea46 100644 --- a/notify_templates/notify_pushover.sh +++ b/notify_templates/notify_pushover.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_PUSHOVER_VERSION="v0.3" +NOTIFY_PUSHOVER_VERSION="v0.4" # # Required receiving services must already be set up. # Requires jq installed and in PATH. @@ -7,16 +7,29 @@ NOTIFY_PUSHOVER_VERSION="v0.3" # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set PUSHOVER_USER_KEY, PUSHOVER_TOKEN, and PUSHOVER_URL in your dockcheck.config file. -if [[ -z "${PUSHOVER_URL:-}" ]] || [[ -z "${PUSHOVER_USER_KEY:-}" ]] || [[ -z "${PUSHOVER_TOKEN:-}" ]]; then - printf "Pushover notification channel enabled, but required configuration variables are missing. Pushover notifications will not be sent.\n" - - remove_channel pushover -fi - trigger_pushover_notification() { - PushoverUrl="${PUSHOVER_URL}" # e.g. PUSHOVER_URL=https://api.pushover.net/1/messages.json - PushoverUserKey="${PUSHOVER_USER_KEY}" # e.g. PUSHOVER_USER_KEY=userkey - PushoverToken="${PUSHOVER_TOKEN}" # e.g. PUSHOVER_TOKEN=token-value + if [[ -n "$1" ]]; then + pushover_channel="$1" + else + pushover_channel="pushover" + fi + + UpperChannel="${pushover_channel^^}" + + PushoverUrlVar="${UpperChannel}_URL" + PushoverUserKeyVar="${UpperChannel}_USER_KEY" + PushoverTokenVar="${UpperChannel}_TOKEN" + + if [[ -z "${!PushoverUrlVar:-}" ]] || [[ -z "${!PushoverUserKeyVar:-}" ]] || [[ -z "${!PushoverTokenVar:-}" ]]; then + printf "The ${pushover_channel} notification channel is enabled, but required configuration variables are missing. Pushover notifications will not be sent.\n" + + remove_channel pushover + return 0 + fi + + PushoverUrl="${!PushoverUrlVar}" # e.g. PUSHOVER_URL=https://api.pushover.net/1/messages.json + PushoverUserKey="${!PushoverUserKeyVar}" # e.g. PUSHOVER_USER_KEY=userkey + PushoverToken="${!PushoverTokenVar}" # e.g. PUSHOVER_TOKEN=token-value # Sending the notification via Pushover curl -S -o /dev/null ${CurlArgs} -X POST \ diff --git a/notify_templates/notify_slack.sh b/notify_templates/notify_slack.sh index 0a9cd7a..e2616e2 100644 --- a/notify_templates/notify_slack.sh +++ b/notify_templates/notify_slack.sh @@ -1,20 +1,32 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_SLACK_VERSION="v0.3" +NOTIFY_SLACK_VERSION="v0.4" # # Setup app and token at https://api.slack.com/tutorials/tracks/posting-messages-with-curl # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set SLACK_ACCESS_TOKEN, and SLACK_CHANNEL_ID in your dockcheck.config file. -if [[ -z "${SLACK_ACCESS_TOKEN:-}" ]] || [[ -z "${SLACK_CHANNEL_ID:-}" ]]; then - printf "Slack notification channel enabled, but required configuration variables are missing. Slack notifications will not be sent.\n" - - remove_channel slack -fi - trigger_slack_notification() { - AccessToken="${SLACK_ACCESS_TOKEN}" # e.g. SLACK_ACCESS_TOKEN=some-token - ChannelID="${SLACK_CHANNEL_ID}" # e.g. CHANNEL_ID=mychannel + if [[ -n "$1" ]]; then + slack_channel="$1" + else + slack_channel="slack" + fi + + UpperChannel="${slack_channel^^}" + + AccessTokenVar="${UpperChannel}_ACCESS_TOKEN" + ChannelIDVar="${UpperChannel}_CHANNEL_ID" + + if [[ -z "${!AccessTokenVar:-}" ]] || [[ -z "${!ChannelIDVar:-}" ]]; then + printf "The ${slack_channel} notification channel is enabled, but required configuration variables are missing. Slack notifications will not be sent.\n" + + remove_channel slack + return 0 + fi + + AccessToken="${!AccessTokenVar}" # e.g. SLACK_ACCESS_TOKEN=some-token + ChannelID="${!ChannelIDVar}" # e.g. CHANNEL_ID=mychannel SlackUrl="https://slack.com/api/chat.postMessage" curl -S -o /dev/null ${CurlArgs} \ diff --git a/notify_templates/notify_smtp.sh b/notify_templates/notify_smtp.sh index 9cfc76c..89bd9bc 100644 --- a/notify_templates/notify_smtp.sh +++ b/notify_templates/notify_smtp.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_SMTP_VERSION="v0.4" +NOTIFY_SMTP_VERSION="v0.5" # INFO: ssmtp is depcerated - consider to use msmtp instead. # # mSMTP/sSMTP has to be installed and configured manually. @@ -7,12 +7,6 @@ NOTIFY_SMTP_VERSION="v0.4" # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set SMTP_MAIL_FROM, SMTP_MAIL_TO, and SMTP_SUBJECT_TAG in your dockcheck.config file. -if [[ -z "${SMTP_MAIL_FROM:-}" ]] || [[ -z "${SMTP_MAIL_TO:-}" ]] || [[ -z "${SMTP_SUBJECT_TAG:-}" ]]; then - printf "SMTP notification channel enabled, but required configuration variables are missing. SMTP notifications will not be sent.\n" - - remove_channel smtp -fi - MSMTP=$(which msmtp) SSMTP=$(which ssmtp) SENDMAIL=$(which sendmail) @@ -28,9 +22,28 @@ else fi trigger_smtp_notification() { -SendMailFrom="${SMTP_MAIL_FROM}" # e.g. MAIL_FROM=me@mydomain.tld -SendMailTo="${SMTP_MAIL_TO}" # e.g. MAIL_TO=me@mydomain.tld -SubjectTag="${SMTP_SUBJECT_TAG}" # e.g. SUBJECT_TAG=dockcheck + if [[ -n "$1" ]]; then + smtp_channel="$1" + else + smtp_channel="smtp" + fi + + UpperChannel="${smtp_channel^^}" + + SendMailFromVar="${UpperChannel}_MAIL_FROM" + SendMailToVar="${UpperChannel}_MAIL_TO" + SubjectTagVar="${UpperChannel}_SUBJECT_TAG" + + if [[ -z "${!SendMailFromVar:-}" ]] || [[ -z "${!SendMailToVar:-}" ]] || [[ -z "${!SubjectTagVar:-}" ]]; then + printf "The ${smtp_channel} notification channel is enabled, but required configuration variables are missing. SMTP notifications will not be sent.\n" + + remove_channel smtp + return 0 + fi + + SendMailFrom="${!SendMailFromVar}" # e.g. MAIL_FROM=me@mydomain.tld + SendMailTo="${!SendMailToVar}" # e.g. MAIL_TO=me@mydomain.tld + SubjectTag="${!SubjectTagVar}" # e.g. SUBJECT_TAG=dockcheck $MailPkg $SendMailTo << __EOF From: "$FromHost" <$SendMailFrom> diff --git a/notify_templates/notify_telegram.sh b/notify_templates/notify_telegram.sh index 4f114eb..785a037 100644 --- a/notify_templates/notify_telegram.sh +++ b/notify_templates/notify_telegram.sh @@ -1,28 +1,41 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_TELEGRAM_VERSION="v0.4" +NOTIFY_TELEGRAM_VERSION="v0.5" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. # If you instead wish make your own modifications, make a copy in the same directory as the main dockcheck.sh script. # Do not modify this file directly within the "notify_templates" subdirectory. Set TELEGRAM_CHAT_ID and TELEGRAM_TOKEN in your dockcheck.config file. -if [[ -z "${TELEGRAM_CHAT_ID:-}" ]] || [[ -z "${TELEGRAM_TOKEN:-}" ]]; then - printf "Telegram notification channel enabled, but required configuration variables are missing. Telegram notifications will not be sent.\n" - - remove_channel telegram -fi - trigger_telegram_notification() { + if [[ -n "$1" ]]; then + telegram_channel="$1" + else + telegram_channel="telegram" + fi + + UpperChannel="${telegram_channel^^}" + + TelegramTokenVar="${UpperChannel}_TOKEN" + TelegramChatIdVar="${UpperChannel}_CHAT_ID" + TelegramTopicIdVar="${UpperChannel}_TOPIC_ID" + + if [[ -z "${!TelegramChatIdVar:-}" ]] || [[ -z "${!TelegramTokenVar:-}" ]]; then + printf "The ${telegram_channel} notification channel is enabled, but required configuration variables are missing. Telegram notifications will not be sent.\n" + + remove_channel telegram + return 0 + fi + if [[ "$PrintMarkdownURL" == true ]]; then ParseMode="Markdown" else ParseMode="HTML" fi - TelegramToken="${TELEGRAM_TOKEN}" # e.g. TELEGRAM_TOKEN=token-value - TelegramChatId="${TELEGRAM_CHAT_ID}" # e.g. TELEGRAM_CHAT_ID=mychatid + TelegramToken="${!TelegramTokenVar}" # e.g. TELEGRAM_TOKEN=token-value + TelegramChatId="${!TelegramChatIdVar}" # e.g. TELEGRAM_CHAT_ID=mychatid TelegramUrl="https://api.telegram.org/bot$TelegramToken" - TelegramTopicID=${TELEGRAM_TOPIC_ID:="0"} + TelegramTopicID=${!TelegramTopicIdVar:="0"} JsonData=$( "$jqbin" -n \ --arg chatid "$TelegramChatId" \ diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index a256311..5ff088f 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -1,4 +1,4 @@ -NOTIFY_V2_VERSION="v0.5" +NOTIFY_V2_VERSION="v0.6" # # 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. @@ -13,9 +13,33 @@ NOTIFY_V2_VERSION="v0.5" # Actual snooze will be 60 seconds less to avoid the chance of missed notifications due to minor scheduling or script run time issues. snooze="${SNOOZE_SECONDS:-}" SnoozeFile="${ScriptWorkDir}/snooze.list" +[[ ! -f "${SnoozeFile}" ]] && touch "${SnoozeFile}" enabled_notify_channels=( ${NOTIFY_CHANNELS:-} ) +# Global output string variable for modification by functions +UpdToString="" +FormattedOutput="" + +get_channel_template() { + local UpperChannel="${1^^}" + local TemplateVar="${UpperChannel}_TEMPLATE" + if [[ -n "${!TemplateVar:-}" ]]; then + printf "${!TemplateVar}" + else + printf "$1" + fi +} + +declare -A unique_templates + +for channel in "${enabled_notify_channels[@]}"; do + template=$(get_channel_template "${channel}") + unique_templates["${template}"]=1 +done + +enabled_notify_templates=( "${!unique_templates[@]}" ) + FromHost=$(cat /etc/hostname) CurrentEpochTime=$(date +"%Y-%m-%dT%H:%M:%S") @@ -23,49 +47,96 @@ CurrentEpochSeconds=$(date +%s) NotifyError=false +for template in "${enabled_notify_templates[@]}"; do + source_if_exists_or_fail "${ScriptWorkDir}/notify_${template}.sh" || \ + source_if_exists_or_fail "${ScriptWorkDir}/notify_templates/notify_${template}.sh" || \ + printf "The notification channel template ${template} is enabled, but notify_${template}.sh was not found. Check the ${ScriptWorkDir} directory or the notify_templates subdirectory.\n" +done + +skip_snooze() { + local UpperChannel="${1^^}" + local SkipSnoozeVar="${UpperChannel}_SKIPSNOOZE" + if [[ "${!SkipSnoozeVar:-}" == "true" ]]; then + printf "true" + else + printf "false" + fi +} + +allow_empty() { + local UpperChannel="${1^^}" + local AllowEmptyVar="${UpperChannel}_ALLOWEMPTY" + if [[ "${!AllowEmptyVar:-}" == "true" ]]; then + printf "true" + else + printf "false" + fi +} + +containers_only() { + local UpperChannel="${1^^}" + local ContainersOnlyVar="${UpperChannel}_CONTAINERSONLY" + if [[ "${!ContainersOnlyVar:-}" == "true" ]]; then + printf "true" + else + printf "false" + fi +} + +output_format() { + local UpperChannel="${1^^}" + local OutputFormatVar="${UpperChannel}_OUTPUT" + if [[ -z "${!OutputFormatVar:-}" ]]; then + printf "text" + else + printf "${!OutputFormatVar:-}" + fi +} + remove_channel() { local temp_array=() for channel in "${enabled_notify_channels[@]}"; do - [[ "${channel}" != "$1" ]] && temp_array+=("${channel}") + local channel_template=$(get_channel_template "${channel}") + [[ "${channel_template}" != "$1" ]] && temp_array+=("${channel}") done enabled_notify_channels=( "${temp_array[@]}" ) } -for channel in "${enabled_notify_channels[@]}"; do - source_if_exists_or_fail "${ScriptWorkDir}/notify_${channel}.sh" || \ - source_if_exists_or_fail "${ScriptWorkDir}/notify_templates/notify_${channel}.sh" || \ - printf "The notification channel ${channel} is enabled, but notify_${channel}.sh was not found. Check the ${ScriptWorkDir} directory or the notify_templates subdirectory.\n" -done - -notify_containers_count() { - unset NotifyContainers - NotifyContainers=() - - [[ ! -f "${SnoozeFile}" ]] && touch "${SnoozeFile}" - - for update in "$@" - do - read -a container <<< "${update}" - found=$(grep -w "${container[0]}" "${SnoozeFile}" || printf "") - +is_snoozed() { + if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then + local found=$(grep -w "$1" "${SnoozeFile}" || printf "") if [[ -n "${found}" ]]; then read -a arr <<< "${found}" CheckEpochSeconds=$(( $(date -d "${arr[1]}" +%s 2>/dev/null) + ${snooze} - 60 )) || CheckEpochSeconds=$(( $(date -f "%Y-%m-%d" -j "${arr[1]}" +%s) + ${snooze} - 60 )) - if [[ "${CurrentEpochSeconds}" -gt "${CheckEpochSeconds}" ]]; then - NotifyContainers+=("${update}") + if [[ "${CurrentEpochSeconds}" -le "${CheckEpochSeconds}" ]]; then + printf "true" + else + printf "false" fi else - NotifyContainers+=("${update}") + printf "false" + fi + else + printf "false" + fi +} + +unsnoozed_count() { + unset Unsnoozed + Unsnoozed=() + + for element in "$@" + do + read -a item <<< "${element}" + if [[ $(is_snoozed "${item[0]}") == "false" ]]; then + Unsnoozed+=("${element}") fi done - printf "${#NotifyContainers[@]}" + printf "${#Unsnoozed[@]}" } update_snooze() { - - [[ ! -f "${SnoozeFile}" ]] && touch "${SnoozeFile}" - for arg in "$@" do read -a entry <<< "${arg}" @@ -85,8 +156,6 @@ cleanup_snooze() { NotifyEntries=() switch="" - [[ ! -f "${SnoozeFile}" ]] && touch "${SnoozeFile}" - for arg in "$@" do read -a entry <<< "${arg}" @@ -105,41 +174,130 @@ cleanup_snooze() { done <<< "$(grep ${switch} '\.sh ' ${SnoozeFile})" } +format_output() { + local UpdateType="$1" + local OutputFormat="$2" + local FormattedTextTemplate="$3" + local tempcsv="" + + if [[ ! "${UpdateType}" == "dockcheck_update" ]]; then + tempcsv="${UpdToString// -> /,}" + tempcsv="${tempcsv//.sh /.sh,}" + else + tempcsv="${UpdToString}" + fi + + if [[ "${OutputFormat}" == "csv" ]]; then + if [[ -z "${UpdToString}" ]]; then + FormattedOutput="None" + else + FormattedOutput="${tempcsv}" + fi + elif [[ "${OutputFormat}" == "json" ]]; then + if [[ -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}" == "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]})}') + elif [[ "${UpdateType}" == "dockcheck_update" ]]; then + # dockcheck update 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], "release_notes": (.[3:] | join(","))})}') + else + FormattedOutput="Invalid input" + fi + fi + else + if [[ -z "${UpdToString}" ]]; then + FormattedOutput="None" + else + if [[ "${UpdateType}" == "container_update" ]]; then + FormattedOutput="${FormattedTextTemplate//${UpdToString}}" + elif [[ "${UpdateType}" == "notify_update" ]]; then + FormattedOutput="${FormattedTextTemplate//${UpdToString}}" + elif [[ "${UpdateType}" == "dockcheck_update" ]]; then + FormattedOutput="${FormattedTextTemplate//$4}" + FormattedOutput="${FormattedOutput//$5}" + FormattedOutput="${FormattedOutput//$6}" + else + FormattedOutput="Invalid input" + fi + fi + fi +} + +skip_notification() { + # Skip notification logic. Default to false. Handle all cases, and only those cases, where notifications should be skipped. + local SkipNotification="false" + local Channel="$1" + local UnsnoozedCount="$2" + local NotificationType="$3" + + if [[ $(containers_only "${Channel}") == "true" ]] && [[ "${NotificationType}" != "container" ]]; then + # Do not send notifications through channels only configured for container update notifications + SkipNotification="true" + else + # Handle empty update cases separately + if [[ -z "${UpdToString}" ]]; then + if [[ $(allow_empty "${Channel}") == "false" ]]; then + # Do not send notifications if there are none and allow_empty is false + SkipNotification="true" + fi + else + if [[ $(skip_snooze "${Channel}") == "false" ]] && [[ ${UnsnoozedCount} -eq 0 ]]; then + # Do not send notifications if there are any, they are all snoozed, and skip_snooze is false + SkipNotification="true" + fi + fi + fi + + printf "${SkipNotification}" +} + send_notification() { [[ -s "$ScriptWorkDir"/urls.list ]] && releasenotes || Updates=("$@") - if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then - UpdNotifyCount=$(notify_containers_count "${Updates[@]}") - else - UpdNotifyCount="${#Updates[@]}" - fi - - NotifyError=false - - if [[ "${UpdNotifyCount}" -gt 0 ]]; then - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - UpdToString=${UpdToString%\\n} - - for channel in "${enabled_notify_channels[@]}"; do - printf "\nSending ${channel} notification\n" - - # To be added in the MessageBody if "-d X" was used - # leading space is left intentionally for clean output - [[ -n "$DaysOld" ]] && msgdaysold="with images ${DaysOld}+ days old " || msgdaysold="" - - MessageTitle="$FromHost - updates ${msgdaysold}available." - # Setting the MessageBody variable here. - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n${UpdToString}\n" - - exec_if_exists_or_fail trigger_${channel}_notification || \ - printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" - done - - [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${Updates[@]}" - fi - [[ -n "${snooze}" ]] && cleanup_snooze "${Updates[@]}" + UnsnoozedContainers=$(unsnoozed_count "${Updates[@]}") + NotifyError=false + Notified="false" + + # To be added in the MessageBody if "-d X" was used + # Trailing space is left intentionally for clean output + [[ -n "$DaysOld" ]] && msgdaysold="with images ${DaysOld}+ days old " || msgdaysold="" + MessageTitle="$FromHost - updates ${msgdaysold}available." + + UpdToString=$( printf '%s\\n' "${Updates[@]}" ) + UpdToString="${UpdToString%, }" + UpdToString=${UpdToString%\\n} + + for channel in "${enabled_notify_channels[@]}"; do + local SkipNotification=$(skip_notification "${channel}" "${UnsnoozedContainers}" "container") + if [[ "${SkipNotification}" == "false" ]]; then + local template=$(get_channel_template "${channel}") + + # Formats UpdToString variable per channel settings + format_output "container_update" "$(output_format "${channel}")" "🐋 Containers on $FromHost with updates available:\n\n" + + # Setting the MessageBody variable here. + printf -v MessageBody "${FormattedOutput}" + + printf "\nSending ${channel} notification" + exec_if_exists_or_fail trigger_${template}_notification "${channel}" || \ + printf "\nAttempted to send 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 + [[ -n "${snooze}" ]] && [[ -n "${UpdToString}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${Updates[@]}" + printf "\n" + fi + return 0 } @@ -147,37 +305,34 @@ send_notification() { ### to not send notifications when dockcheck itself has updates. dockcheck_notification() { if [[ ! "${DISABLE_DOCKCHECK_NOTIFICATION:-}" == "true" ]]; then - DockcheckNotify=false + UnsnoozedDockcheck=$(unsnoozed_count "dockcheck\.sh") NotifyError=false + Notified=false - if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then - found=$(grep -w "dockcheck\.sh" "${SnoozeFile}" || printf "") - if [[ -n "${found}" ]]; then - read -a arr <<< "${found}" - CheckEpochSeconds=$(( $(date -d "${arr[1]}" +%s 2>/dev/null) + ${snooze} - 60 )) || CheckEpochSeconds=$(( $(date -f "%Y-%m-%d" -j "${arr[1]}" +%s) + ${snooze} - 60 )) - if [[ "${CurrentEpochSeconds}" -gt "${CheckEpochSeconds}" ]]; then - DockcheckNotify=true - fi - else - DockcheckNotify=true + MessageTitle="$FromHost - New version of dockcheck available." + UpdToString="dockcheck.sh,$1,$2,\"$3\"" + + for channel in "${enabled_notify_channels[@]}"; do + local SkipNotification=$(skip_notification "${channel}" "${UnsnoozedDockcheck}" "dockcheck") + if [[ "${SkipNotification}" == "false" ]]; then + local template=$(get_channel_template "${channel}") + + # Formats UpdToString variable per channel settings + format_output "dockcheck_update" "$(output_format "${channel}")" "Installed version: \nLatest version: \n\nChangenotes: \n" "$1" "$2" "$3" + + # Setting the MessageBody variable here. + printf -v MessageBody "${FormattedOutput}" + + printf "\nSending dockcheck update notification - ${channel}" + exec_if_exists_or_fail trigger_${template}_notification "${channel}" || \ + printf "\nAttempted to send 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 - else - DockcheckNotify=true - fi - - if [[ "${DockcheckNotify}" == "true" ]]; then - MessageTitle="$FromHost - New version of dockcheck available." - # Setting the MessageBody variable here. - printf -v MessageBody "Installed version: $1\nLatest version: $2\n\nChangenotes: $3\n" - - if [[ ${#enabled_notify_channels[@]} -gt 0 ]]; then printf "\n"; fi - for channel in "${enabled_notify_channels[@]}"; do - printf "Sending dockcheck update notification - ${channel}\n" - exec_if_exists_or_fail trigger_${channel}_notification || \ - printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" - done + done + if [[ "${Notified}" == "true" ]]; then [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "dockcheck.sh" + printf "\n" fi fi @@ -188,68 +343,63 @@ dockcheck_notification() { ### to not send notifications when notify scripts themselves have updates. notify_update_notification() { if [[ ! "${DISABLE_NOTIFY_NOTIFICATION:-}" == "true" ]]; then - NotifyUpdateNotify=false NotifyError=false NotifyUpdates=() + Notified=false - UpdateChannels=( "${enabled_notify_channels[@]}" "v2" ) + UpdateChannels=( "${enabled_notify_templates[@]}" "v2" ) for NotifyScript in "${UpdateChannels[@]}"; do - UpperChannel=$(tr '[:lower:]' '[:upper:]' <<< "$NotifyScript") + UpperChannel="${NotifyScript^^}" VersionVar="NOTIFY_${UpperChannel}_VERSION" if [[ -n "${!VersionVar:-}" ]]; then RawNotifyUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/notify_templates/notify_${NotifyScript}.sh" LatestNotifySnippet="$(curl ${CurlArgs} -r 0-150 "$RawNotifyUrl" || printf "undefined")" - LatestNotifyRelease="$(echo "$LatestNotifySnippet" | sed -n "/${VersionVar}/s/${VersionVar}=//p" | tr -d '"')" - if [[ ! "${LatestNotifyRelease}" == "undefined" ]]; then + if [[ ! "${LatestNotifySnippet}" == "undefined" ]]; then + LatestNotifyRelease="$(echo "$LatestNotifySnippet" | sed -n "/${VersionVar}/s/${VersionVar}=//p" | tr -d '"')" + if [[ "${!VersionVar}" != "${LatestNotifyRelease}" ]] ; then - NotifyUpdates+=("${NotifyScript}.sh ${!VersionVar} -> ${LatestNotifyRelease}") + NotifyUpdates+=("${NotifyScript}.sh ${!VersionVar} -> ${LatestNotifyRelease}") fi fi fi done - if [[ -n "${snooze}" ]] && [[ -f "${SnoozeFile}" ]]; then - for update in "${NotifyUpdates[@]}"; do - read -a NotifyScript <<< "${update}" - found=$(grep -w "${NotifyScript}" "${SnoozeFile}" || printf "") - if [[ -n "${found}" ]]; then - read -a arr <<< "${found}" - CheckEpochSeconds=$(( $(date -d "${arr[1]}" +%s 2>/dev/null) + ${snooze} - 60 )) || CheckEpochSeconds=$(( $(date -f "%Y-%m-%d" -j "${arr[1]}" +%s) + ${snooze} - 60 )) - if [[ "${CurrentEpochSeconds}" -gt "${CheckEpochSeconds}" ]]; then - NotifyUpdateNotify=true - fi - else - NotifyUpdateNotify=true - fi - done - else - NotifyUpdateNotify=true - fi - - if [[ "${NotifyUpdateNotify}" == "true" ]]; then - if [[ "${#NotifyUpdates[@]}" -gt 0 ]]; then - UpdToString=$( printf '%s\\n' "${NotifyUpdates[@]}" ) - UpdToString=${UpdToString%\\n} - NotifyError=false - - MessageTitle="$FromHost - New version of notify templates available." - - printf -v MessageBody "Notify templates on $FromHost with updates available:\n${UpdToString}\n" - - for channel in "${enabled_notify_channels[@]}"; do - printf "Sending notify template update notification - ${channel}\n" - exec_if_exists_or_fail trigger_${channel}_notification || \ - printf "Attempted to send notification to channel ${channel}, but the function was not found. Make sure notify_${channel}.sh is available in the ${ScriptWorkDir} directory or notify_templates subdirectory.\n" - done - - [[ -n "${snooze}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${NotifyUpdates[@]}" - fi - fi - UpdatesPlusDockcheck=("${NotifyUpdates[@]}") UpdatesPlusDockcheck+=("dockcheck.sh") [[ -n "${snooze}" ]] && cleanup_snooze "${UpdatesPlusDockcheck[@]}" + + UnsnoozedTemplates=$(unsnoozed_count "${NotifyUpdates[@]}") + + MessageTitle="$FromHost - New version of notify templates available." + + UpdToString=$( printf '%s\\n' "${NotifyUpdates[@]}" ) + UpdToString="${UpdToString%, }" + UpdToString=${UpdToString%\\n} + + for channel in "${enabled_notify_channels[@]}"; do + local SkipNotification=$(skip_notification "${channel}" "${UnsnoozedTemplates}" "notify") + + if [[ "${SkipNotification}" == "false" ]]; then + local template=$(get_channel_template "${channel}") + + # Formats UpdToString variable per channel settings + format_output "notify_update" "$(output_format "${channel}")" "Notify templates on $FromHost with updates available:\n\n" + + # Setting the MessageBody variable here. + printf -v MessageBody "${FormattedOutput}" + + printf "\nSending notify template update notification - ${channel}" + exec_if_exists_or_fail trigger_${template}_notification "${channel}" || \ + printf "\nAttempted to send 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 + [[ -n "${snooze}" ]] && [[ -n "${UpdToString}" ]] && [[ "${NotifyError}" == "false" ]] && update_snooze "${NotifyUpdates[@]}" + printf "\n" + fi fi return 0 From 7d1e1637f93a62f09ef741a4289624f49d6544a4 Mon Sep 17 00:00:00 2001 From: mag37 Date: Mon, 15 Sep 2025 11:49:09 +0200 Subject: [PATCH 34/55] formatting + sponsors + removed emojis Removed emojis - don't want it to look like just about any AI-slop. Added more sponsors. Changed some formatting. --- README.md | 92 +++++++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index f8f4d17..c3bb07c 100644 --- a/README.md +++ b/README.md @@ -20,18 +20,18 @@

For Podman - see the fork sudo-kraken/podcheck!
___ -## :bell: Changelog +## Changelog - **v0.7.1**: - Added support for multiple notifications using the same template - Added support for notification output format - Added support for file output - - Added optional configuration variables per channel to (replace <channel> with any channel name): - - <channel>\_TEMPLATE : Specify a template - - <channel>\_SKIPSNOOZE : Skip snooze - - <channel>\_CONTAINERSONLY : Only notify for docker container related updates - - <channel>\_ALLOWEMPTY : Always send notifications, even when empty - - <channel>\_OUTPUT : Define output format + - Added optional configuration variables per channel to (replace `` with any channel name): + - `_TEMPLATE` : Specify a template + - `_SKIPSNOOZE` : Skip snooze + - `_CONTAINERSONLY` : Only notify for docker container related updates + - `_ALLOWEMPTY` : Always send notifications, even when empty + - `_OUTPUT` : Define output format - **v0.7.0**: - Bugfix: snooze dockcheck.sh-self-notification and some config clarification. - Added authentication support to Ntfy.sh. @@ -53,7 +53,7 @@ ___ ![](extras/example.gif) -## :mag_right: `dockcheck.sh` +## `dockcheck.sh` ``` $ ./dockcheck.sh -h Syntax: dockcheck.sh [OPTION] [comma separated names to include] @@ -103,7 +103,7 @@ After the updates are complete, you'll get prompted if you'd like to prune dangl ___ -## :nut_and_bolt: Dependencies +## Dependencies - Running docker (duh) and compose, either standalone or plugin. (see [Podman fork](https://github.com/sudo-kraken/podcheck) - Bash shell or compatible shell of at least v4.3 - POSIX `xargs`, usually default but can be installed with the `findutils` package - to enable async. @@ -113,7 +113,7 @@ ___ - User will be prompted to download `regctl` if not in `PATH` or `PWD`. - regctl requires `amd64/arm64` - see [workaround](#roller_coaster-workaround-for-non-amd64--arm64) if other architecture is used. -## :tent: Install Instructions +## Install Instructions Download the script to a directory in **PATH**, I'd suggest using `~/.local/bin` as that's usually in **PATH**. For OSX/macOS preferably use `/usr/local/bin`. ```sh @@ -130,12 +130,12 @@ wget -O ~/.local/bin/dockcheck.sh "https://raw.githubusercontent.com/mag37/dockc Then call the script anywhere with just `dockcheck.sh`. Add preferred `notify.sh`-template to the same directory - this will not be touched by the scripts self-update function. -## :handbag: Configuration +## Configuration To modify settings and have them persist through updates - copy the `default.config` to `dockcheck.config` alongside the script or in `~/.config/`. Alternatively create an alias where specific flags and values are set. Example `alias dc=dockcheck.sh -p -x 10 -t 3`. -## :loudspeaker: Notifications +## Notifications Triggered with the `-i` flag. Will send a list of containers with updates available and a notification when `dockcheck.sh` itself has an update. `notify_templates/notify_v2.sh` is the default notification wrapper, if `notify.sh` is present and configured, it will override. @@ -169,19 +169,18 @@ You only need the `notify_templates/notify_v2.sh` file and any notification temp - Uncomment and set the `NOTIFY_CHANNELS=""` environment variable in `dockcheck.config` to a space separated string of your desired notification channels to enable. - Uncomment and set the environment variables related to the enabled notification channels. Eg. `GOTIFY_DOMAIN=""` + `GOTIFY_TOKEN=""`. -It's recommended to only do configuration with variables within `dockcheck.config` and not modify `notify_templates/notify_X.sh` directly. -If you wish to customize the notify templates yourself, you may copy them to your project root directory alongside the main `dockcheck.sh` (where they're also ignored by git). -Customizing `notify_v2.sh` is handled the same as customizing the templates, but it must be renamed to `notify.sh` within the `dockcheck.sh` root directory. +It's recommended to only do configuration with variables within `dockcheck.config` and not modify `notify_templates/notify_X.sh` directly. If you wish to customize the notify templates yourself, you may copy them to your project root directory alongside the main `dockcheck.sh` (where they're also ignored by git). +Customizing `notify_v2.sh` is handled the same as customizing the templates, but it must be renamed to `notify.sh` within the `dockcheck.sh` root directory. #### Snooze feature: Configure to receive scheduled notifications only if they're new since the last notification - within a set time frame. -**Example:** *Dockcheck is scheduled to run every hour. You will receive an update notification within an hour of availability.* -**Snooze enabled:** You will not receive a repeated notification about an already notified update within the snooze duration. -**Snooze disabled:** You will receive additional (possibly repeated) notifications every hour. +**Example:** *Dockcheck is scheduled to run every hour. You will receive an update notification within an hour of availability.* +**Snooze enabled:** You will not receive a repeated notification about an already notified update within the snooze duration. +**Snooze disabled:** You will receive additional (possibly repeated) notifications every hour. -To enable snooze uncomment the `SNOOZE_SECONDS` variable in your `dockcheck.config` and set it to the number of seconds you wish to prevent duplicate alerts. +To enable snooze uncomment the `SNOOZE_SECONDS` variable in your `dockcheck.config` and set it to the number of seconds you wish to prevent duplicate alerts. Snooze is split into three categories; container updates, `dockcheck.sh` self updates and notification template updates. If an update becomes available for an item that is not snoozed, notifications will be sent and include all available updates for that item's category, even snoozed items. @@ -209,9 +208,9 @@ Further additions are welcome - suggestions or PRs! Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). #### Notification channel configuration: -All required environment variables for each notification channel are provided in the default.config file as comments and must be uncommented and modified for your requirements. -For advanced users, additional functionality is available via custom configurations and environment variables. -Use cases - all configured in `dockcheck.config`: +All required environment variables for each notification channel are provided in the default.config file as comments and must be uncommented and modified for your requirements. +For advanced users, additional functionality is available via custom configurations and environment variables. +Use cases - all configured in `dockcheck.config`: (replace `` with the upper case name of the of the channel as listed in `NOTIFY_CHANNELS` variable, eg `TELEGRAM_SKIPSNOOZE`) - To bypass the snooze feature, even when enabled, add the variable `_SKIPSNOOZE` and set it to `true`. - To configure the channel to only send container update notifications, add the variable `_CONTAINERSONLY` and set it to `true`. @@ -224,12 +223,12 @@ Use cases - all configured in `dockcheck.config`: - Add all other environment variables required for the chosen template to function with `` in upper case as the prefix rather than the template name. - For example, if `` is `mynotification` and the template configured is `slack`, you would need to set `MYNOTIFICATION_CHANNEL_ID` and `MYNOTIFICATION_ACCESS_TOKEN`. -### :date: Release notes addon -There's a function to use a lookup-file to add release note URL's to the notification message. -Copy the notify_templates/`urls.list` file to the script directory, it will be used automatically if it's there. -Modify it as necessary, the names of interest in the left column needs to match your container names. -To also list the URL's in the CLI output (choose containers list) use the `-I` option or variable config. -For Markdown formatting also add the `-M` option. (**this requires the template to be compatible - see gotify for example**) +### Release notes addon +There's a function to use a lookup-file to add release note URL's to the notification message. +Copy the notify_templates/`urls.list` file to the script directory, it will be used automatically if it's there. +Modify it as necessary, the names of interest in the left column needs to match your container names. +To also list the URL's in the CLI output (choose containers list) use the `-I` option or variable config. +For Markdown formatting also add the `-M` option. (**this requires the template to be compatible - see gotify for example**) The output of the notification will look something like this: ``` @@ -241,19 +240,19 @@ nginx -> https://github.com/docker-library/official-images/blob/master/library ``` The `urls.list` file is just an example and I'd gladly see that people contribute back when they add their preferred URLs to their lists. -## :fast_forward: Asyncronous update checks with **xargs**; `-x N` option. (default=1) +## Asyncronous update checks with **xargs**; `-x N` option. (default=1) Pass `-x N` where N is number of subprocesses allowed, experiment in your environment to find a suitable max! Change the default value by editing the `MaxAsync=N` variable in `dockcheck.sh`. To disable the subprocess function set `MaxAsync=0`. -## :chart_with_upwards_trend: Extra plugins and tools: +## Extra plugins and tools: -### :small_orange_diamond: Using dockcheck.sh with the Synology DSM +### Using dockcheck.sh with the Synology DSM If you run your container through the *Container Manager GUI* - only notifications are supported. While if running manual (vanilla docker compose CLI) will allow you to use the update function too. Some extra setup to tie together with Synology DSM - check out the [addons/DSM/README.md](./addons/DSM/README.md). -### :small_orange_diamond: Prometheus and node_exporter +### Prometheus and node_exporter Dockcheck can be used together with [Prometheus](https://github.com/prometheus/prometheus) and [node_exporter](https://github.com/prometheus/node_exporter) to export metrics via the file collector, scheduled with cron or likely. This is done with the `-c` option, like this: ``` @@ -263,20 +262,20 @@ dockcheck.sh -c /path/to/exporter/directory See the [README.md](./addons/prometheus/README.md) for more detailed information on how to set it up! Contributed by [tdralle](https://github.com/tdralle). -### :small_orange_diamond: Zabbix config to monitor docker image updates +### Zabbix config to monitor docker image updates If you already use Zabbix - this config will show numbers of available docker image updates on host. Example: *2 Docker Image updates on host-xyz* See project: [thetorminal/zabbix-docker-image-updates](https://github.com/thetorminal/zabbix-docker-image-updates) -### :small_orange_diamond: Serve REST API to list all available updates +### Serve REST API to list all available updates A custom python script to serve a REST API to get pulled into other monitoring tools like [homepage](https://github.com/gethomepage/homepage). See [discussion here](https://github.com/mag37/dockcheck/discussions/146). -### :small_orange_diamond: Wrapper Script for Unraid's User Scripts +### Wrapper Script for Unraid's User Scripts A custom bash wrapper script to allow the usage of dockcheck as a Unraid User Script plugin. See [discussion here](https://github.com/mag37/dockcheck/discussions/145). -## :bookmark: Labels +## Labels Optionally add labels to compose-files. Currently these are the usable labels: ``` labels: @@ -288,7 +287,7 @@ Optionally add labels to compose-files. Currently these are the usable labels: - `mag37.dockcheck.only-specific-container: true` works instead of the `-F` option, specifying the updated container when doing compose up, like `docker compose up -d homer`. - `mag37.dockcheck.restart-stack: true` works instead of the `-f` option, forcing stop+restart on the whole compose-stack (Caution: Will restart on every updated container within stack). -## :roller_coaster: Workaround for non **amd64** / **arm64** +## Workaround for non **amd64** / **arm64** `regctl` provides binaries for amd64/arm64, to use on other architecture you could try this workaround. Run regctl in a container wrapped in a shell script. Copied from [regclient/docs/install.md](https://github.com/regclient/regclient/blob/main/docs/install.md): @@ -308,7 +307,7 @@ chmod 755 regctl ``` Test it with `./regctl --help` and then either add the file to the same path as *dockcheck.sh* or in your path (eg. `~/.local/bin/regctl`). -## :whale: Docker Hub pull limit :chart_with_downwards_trend: not an issue for checks but for actual pulls +## Docker Hub pull limit :chart_with_downwards_trend: not an issue for checks but for actual pulls Due to recent changes in [Docker Hub usage and limits](https://docs.docker.com/docker-hub/usage/) >Unauthenticated users: 10 pulls/hour >Authenticated users with a free account: 100 pulls/hour @@ -316,7 +315,7 @@ Due to recent changes in [Docker Hub usage and limits](https://docs.docker.com/d This is not an issue for registry checks. But if you have a large stack and pull more than 10 updates at once consider updating more often or to create a free account. You could use/modify the login-wrapper function in the example below to automate the login prior to running `dockcheck.sh`. -### :guardsman: Function to auth with docker hub before running +### Function to auth with docker hub before running **Example** - Change names, paths, and remove cat+password flag if you rather get prompted: ```sh function dchk { @@ -325,31 +324,32 @@ function dchk { } ``` -## :warning: `-r flag` disclaimer and warning +## `-r flag` disclaimer and warning **Wont auto-update the containers, only their images. (compose is recommended)** `docker run` dont support using new images just by restarting a container. Containers need to be manually stopped, removed and created again to run on the new image. Using the `-r` option together with eg. `-i` and `-n` to just check for updates and send notifications and not update is safe though! -## :hammer: Known issues +## Known issues - No detailed error feedback (just skip + list what's skipped). - Not respecting `--profile` options when re-creating the container. - Not working well with containers created by **Portainer**. - **Watchtower** might cause issues due to retagging images when checking for updates (and thereby pulling new images). -## :wrench: Debugging +## Debugging If you hit issues, you could check the output of the `extras/errorCheck.sh` script for clues. Another option is to run the main script with debugging in a subshell `bash -x dockcheck.sh` - if there's a particular container/image that's causing issues you can filter for just that through `bash -x dockcheck.sh nginx`. -## :scroll: License +## License dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/licenses/gpl-3.0-standalone.html) license. -## :heartpulse: Sponsorlist +## Sponsorlist - [avegy](https://github.com/avegy) - [eichhorn](https://github.com/eichhorn) - [stepdg](https://github.com/stepdg) - +- [acer2220](https://github.com/acer2220) +- [shgew](https://github.com/shgew) ___ -### :floppy_disk: The [story](https://mag37.org/posts/project_dockcheck/) behind it. 1 year in retrospect. +### The [story](https://mag37.org/posts/project_dockcheck/) behind it. 1 year in retrospect. From af0d0d3f6ea8b6c3869fe2784a34ba947c7ae01f Mon Sep 17 00:00:00 2001 From: mag37 Date: Fri, 3 Oct 2025 09:22:17 +0200 Subject: [PATCH 35/55] label and update list rework (#229) * Reformatting the updates available list * rewritten list padding to be dynamic * Label rework + clearer messages (#228) - Moved up label check and logic to earlier in the process, to iterate the whole run the same way if `-l` option is passed. - Added messaging to make it clearer. - Clarified Readme and --help message. - Clarified prune message (to mean ALL dangling, not just currently updated). --- README.md | 27 +++++++++++++-------------- dockcheck.sh | 33 ++++++++++++++++----------------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c3bb07c..c677456 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@ ___ ## Changelog + +- **v0.7.2**: + - Label rework: + - Moved up label logic to work globally on the current run. + - Only iterating on labeled containers when used with `-l` option, not listing others. + - Clarified messaging and readme/help texts. + - List reformatting for "available updates" numbering to easier highlight and copy: + - Padded with zero, changed `)` to `-`, example: `02 - homer` + - Can be selected by writing `2,3,4` or `02,03,04`. - **v0.7.1**: - Added support for multiple notifications using the same template - Added support for notification output format @@ -36,18 +45,6 @@ ___ - Bugfix: snooze dockcheck.sh-self-notification and some config clarification. - Added authentication support to Ntfy.sh. - Added suport for sendmail in the SMTP-template. -- **v0.6.9**: - - Bugfix: label logic didn't skip recreation (skipped pulling). - - Added comma separated search filtering so you can selectively search exactly which containers to check/update. - - eg: `dockcheck.sh -yp homer,dozzle` -- **v0.6.8**: - - Bugfix: Unbound variable in notify_v2.sh - - New option: "DisplaySourcedFiles" *config* added to list what files get sourced -- **v0.6.7**: Snooze feature, curl, and consolidation - - Added snooze feature to delay notifications - - Added configurable default curl arguments - - Consolidated and standardized notify template update notifications - - Added curl error handling ___ @@ -69,7 +66,7 @@ Options: -h Print this Help. -i Inform - send a preconfigured notification. -I Prints custom releasenote urls alongside each container with updates in CLI output (requires urls.list). --l Only update if label is set. See readme. +-l Only include containers with label set. See readme. -m Monochrome mode, no printf colour codes and hides progress bar. -M Prints custom releasenote urls as markdown (requires template support). -n No updates, only checking availability. @@ -283,10 +280,12 @@ Optionally add labels to compose-files. Currently these are the usable labels: mag37.dockcheck.only-specific-container: true mag37.dockcheck.restart-stack: true ``` -- `mag37.dockcheck.update: true` will when used with the `-l` option only update containers with this label and skip the rest. Will still list updates as usual. +- `mag37.dockcheck.update: true` will when used with the `-l` option only check and update containers with this label set and skip the rest. - `mag37.dockcheck.only-specific-container: true` works instead of the `-F` option, specifying the updated container when doing compose up, like `docker compose up -d homer`. - `mag37.dockcheck.restart-stack: true` works instead of the `-f` option, forcing stop+restart on the whole compose-stack (Caution: Will restart on every updated container within stack). +Adding or modifying labels in compose-files requires a restart of the container to take effect. + ## Workaround for non **amd64** / **arm64** `regctl` provides binaries for amd64/arm64, to use on other architecture you could try this workaround. Run regctl in a container wrapped in a shell script. Copied from [regclient/docs/install.md](https://github.com/regclient/regclient/blob/main/docs/install.md): diff --git a/dockcheck.sh b/dockcheck.sh index d46f4da..fec5085 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.7.1" -# ChangeNotes: Add support for multiple notifications of the same type, output formatting, and file output +VERSION="v0.7.2" +# ChangeNotes: Reformatted updates list, rewrote label logic to work globally when used with `-l`. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -42,7 +42,7 @@ Help() { echo "-h Print this Help." echo "-i Inform - send a preconfigured notification." echo "-I Prints custom releasenote urls alongside each container with updates in CLI output (requires urls.list)." - echo "-l Only update if label is set. See readme." + echo "-l Only include containers with label set. See readme." echo "-m Monochrome mode, no printf colour codes and hides progress bar." echo "-M Prints custom releasenote urls as markdown (requires template support)." echo "-n No updates; only checking availability without interaction." @@ -342,12 +342,13 @@ dependency_check() { dependency_check "regctl" "regbin" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" dependency_check "jq" "jqbin" "https://github.com/jqlang/jq/releases/latest/download/jq-linux-TEMP" -# Numbered List function -# if urls.list exists add release note url per line +# Numbered List function - pads with zero list_options() { - num=1 + local total="${#Updates[@]}" + [[ ${#total} < 2 ]] && local pads=2 || local pads="${#total}" + local num=1 for update in "${Updates[@]}"; do - echo "$num) $update" + printf "%0*d - %s\n" $pads $num $update ((num++)) done } @@ -423,6 +424,10 @@ check_image() { printf "%s\n" "NoUpdates !$i - not checked, no compose file" return fi + # Checking if Label Only -option is set, and if container got the label + ContUpdateLabel=$($jqbin -r '."mag37.dockcheck.update"' <<< "$ContLabels") + [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" + [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != true ]] && { echo "Skip $i"; return; } } local NoUpdates GotUpdates GotErrors ImageId=$(docker inspect "$i" --format='{{.Image}}') @@ -448,7 +453,7 @@ check_image() { # Make required functions and variables available to subprocesses export -f check_image datecheck export Excludes_string="${Excludes[*]:-}" # Can only export scalar variables -export t_out regbin RepoUrl DaysOld DRunUp jqbin +export t_out regbin RepoUrl DaysOld DRunUp jqbin OnlyLabel # Check for POSIX xargs with -P option, fallback without async if (echo "test" | xargs -P 2 >/dev/null 2>&1) && [[ "$MaxAsync" != 0 ]]; then @@ -478,6 +483,8 @@ done < <( \ xargs $XargsAsync -I {} bash -c 'check_image "{}"' \ ) +[[ "$OnlyLabel" == true ]] && printf "\n%bLabel option active:%b Only checking containers with labels set.\n" "$c_blue" "$c_reset" + # Sort arrays alphabetically IFS=$'\n' NoUpdates=($(sort <<<"${NoUpdates[*]:-}")) @@ -533,10 +540,6 @@ if [[ -n "${GotUpdates:-}" ]]; then ContImage=$(docker inspect "$i" --format='{{.Config.Image}}') ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") [[ "$ContPath" == "null" ]] && ContPath="" - ContUpdateLabel=$($jqbin -r '."mag37.dockcheck.update"' <<< "$ContLabels") - [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" - # Checking if Label Only -option is set, and if container got the label - [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != true ]] && { echo "No update label, skipping."; continue; } } # Checking if compose-values are empty - hence started with docker run if [[ -z "$ContPath" ]]; then @@ -568,8 +571,6 @@ if [[ -n "${GotUpdates:-}" ]]; then [[ "$ContName" == "null" ]] && ContName="" ContEnv=$($jqbin -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels") [[ "$ContEnv" == "null" ]] && ContEnv="" - ContUpdateLabel=$($jqbin -r '."mag37.dockcheck.update"' <<< "$ContLabels") - [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" ContRestartStack=$($jqbin -r '."mag37.dockcheck.restart-stack"' <<< "$ContLabels") [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") @@ -578,8 +579,6 @@ if [[ -n "${GotUpdates:-}" ]]; then printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" # Checking if compose-values are empty - hence started with docker run [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; continue; } - # Checking if Label Only -option is set, and if container got the label - [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != true ]] && { echo "No update label, skipping."; 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; } @@ -602,7 +601,7 @@ if [[ -n "${GotUpdates:-}" ]]; then ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } fi done - if [[ "$AutoPrune" == false ]] && [[ "$AutoMode" == false ]]; then printf "\n"; read -rep "Would you like to prune dangling images? y/[n]: " AutoPrune; fi + 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 printf "\n%bAll done!%b\n" "$c_green" "$c_reset" else From be58805824c75d3a97154bb3902f0b68c50dca30 Mon Sep 17 00:00:00 2001 From: mag37 Date: Mon, 6 Oct 2025 10:18:38 +0200 Subject: [PATCH 36/55] hot-patch unquoted variable in updates list --- dockcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockcheck.sh b/dockcheck.sh index fec5085..65e0882 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -348,7 +348,7 @@ list_options() { [[ ${#total} < 2 ]] && local pads=2 || local pads="${#total}" local num=1 for update in "${Updates[@]}"; do - printf "%0*d - %s\n" $pads $num $update + printf "%0*d - %s\n" "$pads" "$num" "$update" ((num++)) done } From 05e5b23e7bb9cf0cf5f5930576cf9fda6ed76e06 Mon Sep 17 00:00:00 2001 From: mag37 Date: Tue, 7 Oct 2025 08:24:31 +0200 Subject: [PATCH 37/55] bugfix - unquoted var in list Versionbump. --- dockcheck.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dockcheck.sh b/dockcheck.sh index 65e0882..42c241a 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.7.2" -# ChangeNotes: Reformatted updates list, rewrote label logic to work globally when used with `-l`. +VERSION="v0.7.3" +# ChangeNotes: Bugfix - unquoted variable in list. Also: Please consider donating. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" From 24cae63b61e033640cb12891144126f12b61e603 Mon Sep 17 00:00:00 2001 From: mag37 Date: Tue, 7 Oct 2025 08:25:44 +0200 Subject: [PATCH 38/55] bugfix - unquoted var in list Versionbump. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c677456..9f7e04e 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ___ ## Changelog - +- **v0.7.3**: Bugfix - unquoted variable in printf list caused occasional issues. - **v0.7.2**: - Label rework: - Moved up label logic to work globally on the current run. From 12a51d8e8351dd560972b3d584c8f2b6d45604c1 Mon Sep 17 00:00:00 2001 From: mag37 Date: Wed, 8 Oct 2025 19:00:29 +0200 Subject: [PATCH 39/55] added new sponsors --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9f7e04e..60733bf 100644 --- a/README.md +++ b/README.md @@ -344,11 +344,15 @@ dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/l ## Sponsorlist -- [avegy](https://github.com/avegy) -- [eichhorn](https://github.com/eichhorn) -- [stepdg](https://github.com/stepdg) -- [acer2220](https://github.com/acer2220) -- [shgew](https://github.com/shgew) +:small_orange_diamond: [avegy](https://github.com/avegy) +:small_orange_diamond: [eichhorn](https://github.com/eichhorn) +:small_orange_diamond: [stepdg](https://github.com/stepdg) +:small_orange_diamond: [acer2220](https://github.com/acer2220) +:small_orange_diamond: [shgew](https://github.com/shgew) +:small_orange_diamond: [jonas3456](https://github.com/jonas3456) +:small_orange_diamond: [4ndreasH](https://github.com/4ndreasH) +:small_orange_diamond: + ___ ### The [story](https://mag37.org/posts/project_dockcheck/) behind it. 1 year in retrospect. From 8970ee3f20c31ef9a61e0e5795b0647250f78941 Mon Sep 17 00:00:00 2001 From: mag37 Date: Tue, 21 Oct 2025 20:47:10 +0200 Subject: [PATCH 40/55] added to the sponsorlist --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 60733bf..a9fc38f 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,9 @@ dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/l :small_orange_diamond: [shgew](https://github.com/shgew) :small_orange_diamond: [jonas3456](https://github.com/jonas3456) :small_orange_diamond: [4ndreasH](https://github.com/4ndreasH) -:small_orange_diamond: +:small_orange_diamond: [markoe01](https://github.com/markoe01) +:small_orange_diamond: [mushrowan](https://github.com/mushrowan) +:small_orange_diamond: ___ From 7ea97d06ce21dd164f641348cec415b6ae3309b8 Mon Sep 17 00:00:00 2001 From: mag37 Date: Sat, 1 Nov 2025 09:14:49 +0100 Subject: [PATCH 41/55] New option -R and bugfix + cleanup (#236) * Cleaned up legacy structure * Add -R flag to skip container recreation after pulling images (#235) * Added new -R option: Skip Container recreation --------- Co-authored-by: mag37 Co-authored-by: NapalmZ --- README.md | 16 +++--- default.config | 2 +- dockcheck.sh | 101 +++++++++++++++++++---------------- extras/apprise_quickstart.md | 30 ++--------- 4 files changed, 71 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index a9fc38f..a4228cd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,14 @@ ___ ## Changelog -- **v0.7.3**: Bugfix - unquoted variable in printf list caused occasional issues. +- **v0.7.4**: + - Added new option `-R`: + - Will skip container recreation after pulling images. + - Allows for more control and possible pipeline integration. + - Fixes: + - Bugfix for *value too great* error due to leading zeroes - solved with base10 conversion. + - Clean up of some legacy readme sections. +- **v0.7.3**: Bugfix - unquoted variable in printf list caused occasional issues. - **v0.7.2**: - Label rework: - Moved up label logic to work globally on the current run. @@ -41,10 +48,6 @@ ___ - `_CONTAINERSONLY` : Only notify for docker container related updates - `_ALLOWEMPTY` : Always send notifications, even when empty - `_OUTPUT` : Define output format -- **v0.7.0**: - - Bugfix: snooze dockcheck.sh-self-notification and some config clarification. - - Added authentication support to Ntfy.sh. - - Added suport for sendmail in the SMTP-template. ___ @@ -72,6 +75,7 @@ Options: -n No updates, only checking availability. -p Auto-Prune dangling images after update. -r Allow checking for updates/updating images for docker run containers. Won't update the container. +-R Skip container recreation after pulling images. -s Include stopped containers in the check. (Logic: docker ps -a). -t N Set a timeout (in seconds) per container for registry checkups, 10 is default. -u Allow automatic self updates - caution as this will pull new code and autorun it. @@ -353,7 +357,7 @@ dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/l :small_orange_diamond: [4ndreasH](https://github.com/4ndreasH) :small_orange_diamond: [markoe01](https://github.com/markoe01) :small_orange_diamond: [mushrowan](https://github.com/mushrowan) -:small_orange_diamond: +:small_orange_diamond: ___ diff --git a/default.config b/default.config index d5d21aa..2b5f75c 100644 --- a/default.config +++ b/default.config @@ -19,6 +19,7 @@ #OnlyLabel=true # Only update if label is set. See readme. #ForceRestartStacks=true # Force stop+start stack after update. Caution: restarts once for every updated container within stack. #DRunUp=true # Allow updating images for docker run, wont update the container. +#SkipRecreate # Skip container recreation after pulling images. #MonoMode=true # Monochrome mode, no printf colour codes and hides progress bar. #PrintReleaseURL=true # Prints custom releasenote urls alongside each container with updates (requires urls.list)` #PrintMarkdownURL=true # Prints custom releasenote urls as markdown @@ -89,4 +90,3 @@ # TELEGRAM_TOPIC_ID="0" # # FILE_PATH="${ScriptWorkDir}/updates_available.txt" - diff --git a/dockcheck.sh b/dockcheck.sh index 42c241a..a3ebef0 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.7.3" -# ChangeNotes: Bugfix - unquoted variable in list. Also: Please consider donating. +VERSION="v0.7.4" +# ChangeNotes: New option -R to pull without recreation. Fixes: value too great error, legacy cleanups. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -47,6 +47,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 "-R Skip container recreation after pulling images." echo "-r Allow checking for updates/updating images for docker run containers. Won't update the container." echo "-s Include stopped containers in the check. (Logic: docker ps -a)." echo "-t Set a timeout (in seconds) per container for registry checkups, 10 is default." @@ -78,6 +79,7 @@ Exclude=${Exclude:-} DaysOld=${DaysOld:-} OnlySpecific=${OnlySpecific:-false} SpecificContainer=${SpecificContainer:-""} +SkipRecreate=${SkipRecreate:-false} Excludes=() GotUpdates=() NoUpdates=() @@ -95,7 +97,7 @@ c_blue="\033[0;34m" c_teal="\033[0;36m" c_reset="\033[0m" -while getopts "ayfFhiIlmMnprsuvc:e:d:t:x:" options; do +while getopts "ayfFhiIlmMnprsuvc:e:d:t:x:R" options; do case "${options}" in a|y) AutoMode=true ;; c) CollectorTextFileDirectory="${OPTARG}" ;; @@ -110,6 +112,7 @@ while getopts "ayfFhiIlmMnprsuvc:e:d:t:x:" options; do M) PrintMarkdownURL=true ;; n) DontUpdate=true; AutoMode=true;; p) AutoPrune=true ;; + R) SkipRecreate=true ;; r) DRunUp=true ;; s) Stopped="-a" ;; t) Timeout="${OPTARG}" ;; @@ -213,6 +216,7 @@ choosecontainers() { else ChoiceClean=${Choice//[,.:;]/ } for CC in $ChoiceClean; do + CC=$((10#$CC)) # Base 10 interpretation to strip leading zeroes if [[ "$CC" -lt 1 || "$CC" -gt $UpdCount ]]; then # Reset choice if out of bounds echo "Number not in list: $CC"; unset ChoiceClean; break 1 else @@ -554,53 +558,58 @@ if [[ -n "${GotUpdates:-}" ]]; then docker pull "$ContImage" || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } done - printf "\n%bDone pulling updates. %bRecreating updated containers.%b\n" "$c_green" "$c_blue" "$c_reset" + printf "\n%bDone pulling updates.%b\n" "$c_green" "$c_reset" - CurrentQue=0 - for i in "${SelectedUpdates[@]}"; do - ((CurrentQue+=1)) - unset CompleteConfs - # Extract labels and metadata - ContLabels=$(docker inspect "$i" --format '{{json .Config.Labels}}') - ContImage=$(docker inspect "$i" --format='{{.Config.Image}}') - ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") - [[ "$ContPath" == "null" ]] && ContPath="" - ContConfigFile=$($jqbin -r '."com.docker.compose.project.config_files"' <<< "$ContLabels") - [[ "$ContConfigFile" == "null" ]] && ContConfigFile="" - ContName=$($jqbin -r '."com.docker.compose.service"' <<< "$ContLabels") - [[ "$ContName" == "null" ]] && ContName="" - ContEnv=$($jqbin -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels") - [[ "$ContEnv" == "null" ]] && ContEnv="" - ContRestartStack=$($jqbin -r '."mag37.dockcheck.restart-stack"' <<< "$ContLabels") - [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" - ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") - [[ "$ContOnlySpecific" == "null" ]] && ContRestartStack="" + if [[ "$SkipRecreate" == true ]]; 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" + CurrentQue=0 + for i in "${SelectedUpdates[@]}"; do + ((CurrentQue+=1)) + unset CompleteConfs + # Extract labels and metadata + ContLabels=$(docker inspect "$i" --format '{{json .Config.Labels}}') + ContImage=$(docker inspect "$i" --format='{{.Config.Image}}') + ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") + [[ "$ContPath" == "null" ]] && ContPath="" + ContConfigFile=$($jqbin -r '."com.docker.compose.project.config_files"' <<< "$ContLabels") + [[ "$ContConfigFile" == "null" ]] && ContConfigFile="" + ContName=$($jqbin -r '."com.docker.compose.service"' <<< "$ContLabels") + [[ "$ContName" == "null" ]] && ContName="" + ContEnv=$($jqbin -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels") + [[ "$ContEnv" == "null" ]] && ContEnv="" + ContRestartStack=$($jqbin -r '."mag37.dockcheck.restart-stack"' <<< "$ContLabels") + [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" + ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") + [[ "$ContOnlySpecific" == "null" ]] && ContRestartStack="" - printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" - # Checking if compose-values are empty - hence started with docker run - [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; continue; } + printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" + # Checking if compose-values are empty - hence started with docker run + [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; 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; } - ## Reformatting path + multi compose - if [[ $ContConfigFile == '/'* ]]; then - CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s " "$conf"; done) - else - CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s/%s " "$ContPath" "$conf"; done) - fi - # Check if the container got an environment file set and reformat it - ContEnvs="" - if [[ -n "$ContEnv" ]]; then ContEnvs=$(for env in ${ContEnv//,/ }; do printf -- "--env-file %s " "$env"; done); fi - # Set variable when compose up should only target the specific container, not the stack - if [[ $OnlySpecific == true ]] || [[ $ContOnlySpecific == true ]]; then SpecificContainer="$ContName"; fi + # 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; } + ## Reformatting path + multi compose + if [[ $ContConfigFile == '/'* ]]; then + CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s " "$conf"; done) + else + CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s/%s " "$ContPath" "$conf"; done) + fi + # Check if the container got an environment file set and reformat it + ContEnvs="" + if [[ -n "$ContEnv" ]]; then ContEnvs=$(for env in ${ContEnv//,/ }; do printf -- "--env-file %s " "$env"; done); fi + # Set variable when compose up should only target the specific container, not the stack + if [[ $OnlySpecific == true ]] || [[ $ContOnlySpecific == true ]]; then SpecificContainer="$ContName"; fi - # 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; } - else - ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } - fi - done + # 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; } + else + ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } + fi + done + fi 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 printf "\n%bAll done!%b\n" "$c_green" "$c_reset" diff --git a/extras/apprise_quickstart.md b/extras/apprise_quickstart.md index 2d1ddd6..c526492 100644 --- a/extras/apprise_quickstart.md +++ b/extras/apprise_quickstart.md @@ -38,35 +38,15 @@ You can also use the [caronc/apprise-api](https://github.com/caronc/apprise-api) ### Customize the **notify.sh** file. -After you're done with the setup of the container and tried your notifications, you can copy the `notify_apprise.sh` file to `notify.sh` and start editing it. +After you're done with the setup of the container and tried your notifications, you need to follow the configuration setup (explained in detail in the README). +Briefly: Copy `default.config` to `dockcheck.config` then edit it to change the following, `APPRISE_URL` matching your environment: -Comment out/remove the bare metal apprise-command (starting with `apprise -vv -t...`). -Uncomment and edit the `AppriseURL` variable and *curl* line -It should look something like this when curling the API: ```bash -send_notification() { -Updates=("$@") -UpdToString=$( printf "%s\n" "${Updates[@]}" ) -FromHost=$(hostname) - -printf "\nSending Apprise notification\n" - -MessageTitle="$FromHost - updates available." -# Setting the MessageBody variable here. -read -d '\n' MessageBody << __EOF -Containers on $FromHost with updates available: - -$UpdToString - -__EOF - -AppriseURL="http://IP.or.mydomain.tld:8000/notify/apprise" -curl -X POST -F "title=$MessageTitle" -F "body=$MessageBody" -F "tags=all" $AppriseURL - -} +NOTIFY_CHANNELS="apprise" +APPRISE_URL="http://apprise.mydomain.tld:1234/notify/apprise" ``` -That's all! +That's it! ___ ___ From c34d52bde064f1dc7506891c17ad995c2bc0e909 Mon Sep 17 00:00:00 2001 From: mag37 Date: Sat, 1 Nov 2025 09:25:11 +0100 Subject: [PATCH 42/55] the missing ) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4228cd..3b0209e 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ After the updates are complete, you'll get prompted if you'd like to prune dangl ___ ## Dependencies -- Running docker (duh) and compose, either standalone or plugin. (see [Podman fork](https://github.com/sudo-kraken/podcheck) +- Running docker (duh) and compose, either standalone or plugin. (see [Podman fork](https://github.com/sudo-kraken/podcheck)) - Bash shell or compatible shell of at least v4.3 - POSIX `xargs`, usually default but can be installed with the `findutils` package - to enable async. - [jq](https://github.com/jqlang/jq) From c33c9f4387a5a9a40496a0107f8abff24ab26e64 Mon Sep 17 00:00:00 2001 From: Oleh Astappiev <4512729+astappiev@users.noreply.github.com> Date: Thu, 13 Nov 2025 06:17:25 +0100 Subject: [PATCH 43/55] Fix version check condition (#239) --- dockcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockcheck.sh b/dockcheck.sh index a3ebef0..a0150df 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -358,7 +358,7 @@ list_options() { } # Version check & initiate self update -if [[ "$LatestRelease" != "undefined" ]]; then +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" if [[ "$AutoMode" == false ]]; then From f1cc8190f9426c2208189872d5099b4c596d54ba Mon Sep 17 00:00:00 2001 From: Andrei Mateescu Date: Fri, 12 Dec 2025 12:00:42 +0200 Subject: [PATCH 44/55] Add the Pangolin stack to urls.list (#241) Adds a few items from the Pangolin stack (https://github.com/fosrl/) and others that are usually used together. --- notify_templates/urls.list | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/notify_templates/urls.list b/notify_templates/urls.list index 2b31c3b..aeba433 100644 --- a/notify_templates/urls.list +++ b/notify_templates/urls.list @@ -15,12 +15,14 @@ calibre https://github.com/linuxserver/docker-calibre/releases calibre-web https://github.com/linuxserver/docker-calibre-web/releases cleanuperr https://github.com/flmorg/cleanuperr/releases cross-seed https://github.com/cross-seed/cross-seed/releases +crowdsec https://github.com/crowdsecurity/crowdsec/releases cup https://github.com/sergi0g/cup/releases dockge https://github.com/louislam/dockge/releases dozzle https://github.com/amir20/dozzle/releases flatnotes https://github.com/dullage/flatnotes/releases forgejo https://codeberg.org/forgejo/forgejo/releases fressrss https://github.com/FreshRSS/FreshRSS/releases +gerbil https://github.com/fosrl/gerbil/releases gluetun https://github.com/qdm12/gluetun/releases go2rtc https://github.com/AlexxIT/go2rtc/releases gotify https://github.com/gotify/server/releases @@ -45,9 +47,11 @@ mealie https://github.com/mealie-recipes/mealie/releases meilisearch https://github.com/meilisearch/meilisearch/releases monica https://github.com/monicahq/monica/releases mqtt https://github.com/eclipse/mosquitto/tags +newt https://github.com/fosrl/newt/releases nextcloud-aio-mastercontainer https://github.com/nextcloud/all-in-one/releases nginx https://github.com/docker-library/official-images/blob/master/library/nginx owncast https://github.com/owncast/owncast/releases +pangolin https://github.com/fosrl/pangolin/releases prowlarr https://github.com/Prowlarr/Prowlarr/releases prowlarr-ls https://github.com/linuxserver/docker-prowlarr/releases qbittorrent https://www.qbittorrent.org/news @@ -66,6 +70,7 @@ snappymail https://github.com/the-djmaze/snappymail/releases sonarr https://github.com/Sonarr/Sonarr/releases/ sonarr-ls https://github.com/linuxserver/docker-sonarr/releases syncthing https://github.com/syncthing/syncthing/releases +tailscale https://github.com/tailscale/tailscale/releases tautulli https://github.com/Tautulli/Tautulli/releases thelounge https://github.com/thelounge/thelounge/releases traefik https://github.com/traefik/traefik/releases From 8ee5575081ecf2c4bc712307b0119d78b088069a Mon Sep 17 00:00:00 2001 From: mag37 Date: Fri, 12 Dec 2025 11:12:57 +0100 Subject: [PATCH 45/55] Added option -b to enable image backups pre pull. (#242) * added new variables, options and setup * datecheck function rewrite * moved the cleanup and prune logic to always run. Changed some wording on messages. * added function to print currently backed up images * Patched bugfix to not recreate stopped containers * changed the RepoDigests grab and logic * Moved the backup - cleanup to always trigger if -b option is used. Added -p&-b warning. * version bump and readme fixes --- README.md | 51 +++++++++++++------- default.config | 1 + dockcheck.sh | 128 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 140 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 3b0209e..f811796 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,22 @@

CLI tool to automate docker image updates or notifying when updates are available.

-

selective updates, exclude containers, custom labels, notification plugins, prune when done etc.

+

selective updates, include/exclude containers, image backups, custom labels, notification plugins, prune when done etc.

-

:whale: Docker Hub pull limit :chart_with_downwards_trend: not an issue for checks but for actual pulls - read more

+

:whale: Docker Hub pull limit :chart_with_downwards_trend: not an issue for checks only for actual pulls - read more

For Podman - see the fork sudo-kraken/podcheck!
___ ## Changelog +- **v0.7.5**: + - Added new option **BackupForDays**; `-b N` and `-B`: + - Backup an image before pulling a new version for easy rollback in case of breakage. + - Removes backed up images older than *N* days. + - List currently backed up images with `-B`. + - Fixes: + - Bugfix for `-s` *Stopped* to not recreate stopped containers after update. - **v0.7.4**: - Added new option `-R`: - Will skip container recreation after pulling images. @@ -38,16 +45,6 @@ ___ - List reformatting for "available updates" numbering to easier highlight and copy: - Padded with zero, changed `)` to `-`, example: `02 - homer` - Can be selected by writing `2,3,4` or `02,03,04`. -- **v0.7.1**: - - Added support for multiple notifications using the same template - - Added support for notification output format - - Added support for file output - - Added optional configuration variables per channel to (replace `` with any channel name): - - `_TEMPLATE` : Specify a template - - `_SKIPSNOOZE` : Skip snooze - - `_CONTAINERSONLY` : Only notify for docker container related updates - - `_ALLOWEMPTY` : Always send notifications, even when empty - - `_OUTPUT` : Define output format ___ @@ -61,6 +58,8 @@ Example: dockcheck.sh -y -x 10 -d 10 -e nextcloud,heimdall Options: -a|y Automatic updates, without interaction. +-b N Enable image backups and sets number of days to keep from pruning. +-B List currently backed up images, then exit. -c D Exports metrics as prom file for the prometheus node_exporter. Provide the collector textfile directory. -d N Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower. -e X Exclude containers, separated by comma. @@ -86,18 +85,19 @@ Options: ### Basic example: ``` $ ./dockcheck.sh -. . . +[##################################################] 5/5 + Containers on latest version: glances homer Containers with updates available: -1) adguardhome -2) syncthing -3) whoogle-search +01) adguardhome +02) syncthing +03) whoogle-search Choose what containers to update: -Enter number(s) separated by comma, [a] for all - [q] to quit: +Enter number(s) separated by comma, [a] for all - [q] to quit: 1,2 ``` Then it proceeds to run `pull` and `up -d` on every container with updates. After the updates are complete, you'll get prompted if you'd like to prune dangling images. @@ -245,6 +245,23 @@ The `urls.list` file is just an example and I'd gladly see that people contribut Pass `-x N` where N is number of subprocesses allowed, experiment in your environment to find a suitable max! Change the default value by editing the `MaxAsync=N` variable in `dockcheck.sh`. To disable the subprocess function set `MaxAsync=0`. +## Image Backups; `-b N` to backup previous images as custom (retagged) images for easy rollback +When the option `BackupForDays` is set **dockcheck** will store the image being updated as a backup, retagged with a different name and removed due to age configured (*BackupForDays*) in a future run. +Let's say we're updating `b4bz/homer:latest` - then before replacing the current image it will be retagged with the name `dockcheck/homer:2025-10-26_1132_latest` +- `dockcheck` as repo name to not interfere with others. +- `homer` is the image. +- `2025-10-26_1132` is the time when running the script. +- `latest` is the tag of the image. + +Then if an update breaks, you could restore the image by stopping the container, delete the new image, eg. `docker rmi b4bz/homer:latest`, then retag the backup as latest `docker tag dockcheck/homer:_latest b4bz/homer:latest`. +After that, start the container again (now with the backup image active) and it will be updated as usual next time you run dockcheck or other updates. + +The backed up images will be removed if they're older than *BackupForDays* value (passed as `-b N` or set in the `dockcheck.config` with `BackupForDays=N`) and then pruned. +If configured for eg. 7 days, force earlier cleaning by just passing a lower number of days, eg. `-b 2` to clean everything older than 2 days. +Backed up images will not be removed if neither `-b` flag nor `BackupForDays` config variable is set. + +Use the capital option `-B` to list currently backed up images. Or list all images with `docker images`. +To manually remove any backed up images, do `docker rmi dockcheck/homer:2025-10-26_1132_latest`. ## Extra plugins and tools: diff --git a/default.config b/default.config index 2b5f75c..90abfa5 100644 --- a/default.config +++ b/default.config @@ -28,6 +28,7 @@ #CurlRetryCount=3 # Max number of curl retries #CurlConnectTimeout=5 # Time to wait for curl to establish a connection before failing #DisplaySourcedFiles=false # Display what files are being sourced/used +#BackupForDays=7 # Enable backups of images and removes backups older than N days. ### Notify settings ## All commented values are examples only. Modify as needed. diff --git a/dockcheck.sh b/dockcheck.sh index a0150df..906ed0d 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.7.4" -# ChangeNotes: New option -R to pull without recreation. Fixes: value too great error, legacy cleanups. +VERSION="v0.7.5" +# ChangeNotes: New option -b N to backup image before pulling for easy rollback. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" @@ -34,6 +34,8 @@ Help() { echo echo "Options:" echo "-a|y Automatic updates, without interaction." + echo "-b N Enable image backups and sets number of days to keep from pruning." + echo "-B List currently backed up images, then exit." echo "-c Exports metrics as prom file for the prometheus node_exporter. Provide the collector textfile directory." echo "-d N Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower." echo "-e X Exclude containers, separated by comma." @@ -58,6 +60,12 @@ 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' +} + # Initialise variables Timeout=${Timeout:-10} MaxAsync=${MaxAsync:-1} @@ -77,6 +85,7 @@ Stopped=${Stopped:-""} CollectorTextFileDirectory=${CollectorTextFileDirectory:-} Exclude=${Exclude:-} DaysOld=${DaysOld:-} +BackupForDays=${BackupForDays:-} OnlySpecific=${OnlySpecific:-false} SpecificContainer=${SpecificContainer:-""} SkipRecreate=${SkipRecreate:-false} @@ -97,9 +106,15 @@ c_blue="\033[0;34m" c_teal="\033[0;36m" c_reset="\033[0m" -while getopts "ayfFhiIlmMnprsuvc:e:d:t:x:R" options; do +# Timestamps +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} ;; @@ -156,6 +171,13 @@ if [[ -n "$DaysOld" ]]; then exit 2 fi fi +if [[ -n "$BackupForDays" ]]; then + if ! [[ $BackupForDays =~ ^[0-9]+$ ]]; then + printf "-b argument given (%s) is not a number.\n" "$BackupForDays" + exit 2 + fi + [[ "$AutoPrune" == true ]] && printf "%bWARNING: When -b option is used, -p has no function.%b\n" "$c_yellow" "$c_reset" +fi if [[ -n "$CollectorTextFileDirectory" ]]; then if ! [[ -d $CollectorTextFileDirectory ]]; then printf "The directory (%s) does not exist.\n" "$CollectorTextFileDirectory" @@ -196,11 +218,11 @@ self_update() { printf "\n%s\n" "Pulling the latest version." git pull --force || { printf "%bGit error,%b manually pull/clone.\n" "$c_red" "$c_reset"; return; } printf "\n%s\n" "--- starting over with the updated version ---" - cd - || { printf "%bPath error.%b\n" "$c_red"; return; } + cd - || { printf "%bPath error.%b\n" "$c_red" "$c_reset"; return; } exec "$ScriptPath" "${ScriptArgs[@]}" # run the new script with old arguments exit 0 # exit the old instance else - cd - || { printf "%bPath error.%b\n" "$c_red"; return; } + cd - || { printf "%bPath error.%b\n" "$c_red" "$c_reset"; return; } self_update_curl fi } @@ -209,6 +231,7 @@ choosecontainers() { while [[ -z "${ChoiceClean:-}" ]]; do read -r -p "Enter number(s) separated by comma, [a] for all - [q] to quit: " Choice if [[ "$Choice" =~ [qQnN] ]]; then + [[ -n "${BackupForDays:-}" ]] && remove_backups exit 0 elif [[ "$Choice" =~ [aAyY] ]]; then SelectedUpdates=( "${GotUpdates[@]}" ) @@ -228,16 +251,39 @@ choosecontainers() { } datecheck() { - ImageDate=$("$regbin" -v error image inspect "$RepoUrl" --format='{{.Created}}' | cut -d" " -f1) + ImageDate="$1" + DaysMax="$2" ImageEpoch=$(date -d "$ImageDate" +%s 2>/dev/null) || ImageEpoch=$(date -f "%Y-%m-%d" -j "$ImageDate" +%s) - ImageAge=$(( ( $(date +%s) - ImageEpoch )/86400 )) - if [[ "$ImageAge" -gt "$DaysOld" ]]; then + ImageAge=$(( ( RunEpoch - ImageEpoch )/86400 )) + if [[ "$ImageAge" -gt "$DaysMax" ]]; then return 0 else return 1 fi } +remove_backups() { + IFS=$'\n' + CleanupCount=0 + for backup_img in $(docker images --format "{{.Repository}} {{.Tag}}" | sed -n '/^dockcheck/p'); do + repo_name=${backup_img% *} + backup_tag=${backup_img#* } + backup_date=${backup_tag%%_*} + # UNTAGGING HERE + if datecheck "$backup_date" "$BackupForDays"; then + [[ "$CleanupCount" == 0 ]] && printf "\n%bRemoving backed up images older then %s days.%b\n" "$c_blue" "$BackupForDays" "$c_reset" + docker rmi "${repo_name}:${backup_tag}" && ((CleanupCount+=1)) + fi + done + unset IFS + if [[ "$CleanupCount" == 0 ]]; then + printf "\nNo backup images to remove.\n" + else + [[ "$CleanupCount" -gt 1 ]] && b_phrase="backups" || b_phrase="backup" + printf "\n%b%s%b %s removed.%b\n" "$c_green" "$CleanupCount" "$c_teal" "$b_phrase" "$c_reset" + fi +} + progress_bar() { QueCurrent="$1" QueTotal="$2" @@ -443,7 +489,7 @@ check_image() { if [[ "$LocalHash" == *"$RegHash"* ]]; then printf "%s\n" "NoUpdates $i" else - if [[ -n "${DaysOld:-}" ]] && ! datecheck; then + if [[ -n "${DaysOld:-}" ]] && ! datecheck $("$regbin" -v error image inspect "$RepoUrl" --format='{{.Created}}' | cut -d" " -f1) "$DaysOld" ; then printf "%s\n" "NoUpdates +$i ${ImageAge}d" else printf "%s\n" "GotUpdates $i" @@ -457,7 +503,7 @@ check_image() { # Make required functions and variables available to subprocesses export -f check_image datecheck export Excludes_string="${Excludes[*]:-}" # Can only export scalar variables -export t_out regbin RepoUrl DaysOld DRunUp jqbin OnlyLabel +export t_out regbin RepoUrl DaysOld DRunUp jqbin OnlyLabel RunTimestamp RunEpoch # Check for POSIX xargs with -P option, fallback without async if (echo "test" | xargs -P 2 >/dev/null 2>&1) && [[ "$MaxAsync" != 0 ]]; then @@ -540,11 +586,25 @@ if [[ -n "${GotUpdates:-}" ]]; then for i in "${SelectedUpdates[@]}"; do ((CurrentQue+=1)) printf "\n%bNow updating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" - ContLabels=$(docker inspect "$i" --format '{{json .Config.Labels}}') - ContImage=$(docker inspect "$i" --format='{{.Config.Image}}') - ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") + ContConfig=$(docker inspect "$i" --format '{{json .}}') + ContImage=$($jqbin -r '."Config"."Image"' <<< "$ContConfig") + ImageId=$($jqbin -r '."Image"' <<< "$ContConfig") + ContPath=$($jqbin -r '."Config"."Labels"."com.docker.compose.project.working_dir"' <<< "$ContConfig") [[ "$ContPath" == "null" ]] && ContPath="" + # Add new backup tag prior to pulling if option is set + if [[ -n "${BackupForDays:-}" ]]; then + ImageConfig=$(docker image inspect "$ImageId" --format '{{ json . }}') + ContRepoDigests=$($jqbin -r '.RepoDigests[0]' <<< "$ImageConfig") + [[ "$ContRepoDigests" == "null" ]] && ContRepoDigests="" + ContRepo=${ContImage%:*} + ContApp=${ContRepo#*/} + [[ "$ContImage" =~ ":" ]] && ContTag=${ContImage#*:} || ContTag="latest" + BackupName="dockcheck/${ContApp}:${RunTimestamp}_${ContTag}" + docker tag "$ImageId" "$BackupName" + printf "%b%s backed up as %s%b\n" "$c_teal" "$i" "$BackupName" "$c_reset" + fi + # Checking if compose-values are empty - hence started with docker run if [[ -z "$ContPath" ]]; then if [[ "$DRunUp" == true ]]; then @@ -556,7 +616,13 @@ if [[ -n "${GotUpdates:-}" ]]; then continue fi - docker pull "$ContImage" || { printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; } + if docker pull "$ContImage"; then + # Removal of the -tag image left behind from backup + if [[ ! -z "${ContRepoDigests:-}" ]] && [[ -n "${BackupForDays:-}" ]]; then docker rmi "$ContRepoDigests"; fi + else + printf "\n%bDocker error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1 + fi + done printf "\n%bDone pulling updates.%b\n" "$c_green" "$c_reset" @@ -569,8 +635,8 @@ if [[ -n "${GotUpdates:-}" ]]; then ((CurrentQue+=1)) unset CompleteConfs # Extract labels and metadata - ContLabels=$(docker inspect "$i" --format '{{json .Config.Labels}}') - ContImage=$(docker inspect "$i" --format='{{.Config.Image}}') + ContConfig=$(docker inspect "$i" --format '{{json .}}') + ContLabels=$($jqbin -r '."Config"."Labels"' <<< "$ContConfig") ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") [[ "$ContPath" == "null" ]] && ContPath="" ContConfigFile=$($jqbin -r '."com.docker.compose.project.config_files"' <<< "$ContLabels") @@ -583,14 +649,22 @@ if [[ -n "${GotUpdates:-}" ]]; then [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") [[ "$ContOnlySpecific" == "null" ]] && ContRestartStack="" + ContStateRunning=$($jqbin -r '."State"."Running"' <<< "$ContConfig") + [[ "$ContStateRunning" == "null" ]] && ContStateRunning="" + + if [[ "$ContStateRunning" == "true" ]]; then + printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$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" + continue + fi - printf "\n%bNow recreating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset" # Checking if compose-values are empty - hence started with docker run [[ -z "$ContPath" ]] && { echo "Not a compose container, skipping."; 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; } - ## Reformatting path + multi compose + # Reformatting path + multi compose if [[ $ContConfigFile == '/'* ]]; then CompleteConfs=$(for conf in ${ContConfigFile//,/ }; do printf -- "-f %s " "$conf"; done) else @@ -610,14 +684,22 @@ if [[ -n "${GotUpdates:-}" ]]; then fi done fi - 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 - printf "\n%bAll done!%b\n" "$c_green" "$c_reset" + printf "\n%bAll updates done!%b\n" "$c_green" "$c_reset" + + # 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 + fi + else - printf "\nNo updates installed, exiting.\n" + printf "\nNo updates installed.\n" fi else - printf "\nNo updates available, exiting.\n" + printf "\nNo updates available.\n" fi +# Clean up old backup image tags if -b is used +[[ -n "${BackupForDays:-}" ]] && remove_backups + exit 0 From 4e0b705b8ba9e6af124bd26526441f2430952803 Mon Sep 17 00:00:00 2001 From: singularity0821 <17620644+singularity0821@users.noreply.github.com> Date: Sun, 14 Dec 2025 11:49:56 +0100 Subject: [PATCH 46/55] Sanitize message for Matrix notifications (#243) * Sanitize message for Matrix notifications * Use variable for jq and increment version of Matrix script --------- Co-authored-by: martin --- notify_templates/notify_matrix.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notify_templates/notify_matrix.sh b/notify_templates/notify_matrix.sh index efdf37d..fbdb332 100644 --- a/notify_templates/notify_matrix.sh +++ b/notify_templates/notify_matrix.sh @@ -1,5 +1,5 @@ ### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -NOTIFY_MATRIX_VERSION="v0.4" +NOTIFY_MATRIX_VERSION="v0.5" # # Required receiving services must already be set up. # Leave (or place) this file in the "notify_templates" subdirectory within the same directory as the main dockcheck.sh script. @@ -29,7 +29,7 @@ trigger_matrix_notification() { AccessToken="${!AccessTokenVar}" # e.g. MATRIX_ACCESS_TOKEN=token-value RoomId="${!RoomIdVar}" # e.g. MATRIX_ROOM_ID=myroom MatrixServer="${!MatrixServerVar}" # e.g. MATRIX_SERVER_URL=http://matrix.yourdomain.tld - MsgBody="{\"msgtype\":\"m.text\",\"body\":\"$MessageBody\"}" + MsgBody=$($jqbin -Rn --arg body "$MessageBody" '{msgtype:"m.text", body:$body}') # URL Example: https://matrix.org/_matrix/client/r0/rooms/!xxxxxx:example.com/send/m.room.message?access_token=xxxxxxxx curl -S -o /dev/null ${CurlArgs} -X POST "$MatrixServer/_matrix/client/r0/rooms/$RoomId/send/m.room.message?access_token=$AccessToken" -H 'Content-Type: application/json' -d "$MsgBody" @@ -37,4 +37,4 @@ trigger_matrix_notification() { if [[ $? -gt 0 ]]; then NotifyError=true fi -} \ No newline at end of file +} From fc5c5db72bfa0bf5a676341e7c99661ebd17b043 Mon Sep 17 00:00:00 2001 From: smoochy <34371932+smoochy@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:47:48 +0100 Subject: [PATCH 47/55] [Issue 255] Fix Notifiy also checking for /etc/HOSTNAME (#256) * - Adjusted **FromHost** variable to have fallback options when `cat /etc/hostname` or `hostname` command fails. * - Bumped Version to 0.7 --- notify_templates/notify_v2.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh index 5ff088f..a44870a 100644 --- a/notify_templates/notify_v2.sh +++ b/notify_templates/notify_v2.sh @@ -1,4 +1,4 @@ -NOTIFY_V2_VERSION="v0.6" +NOTIFY_V2_VERSION="v0.7" # # 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. @@ -40,7 +40,7 @@ done enabled_notify_templates=( "${!unique_templates[@]}" ) -FromHost=$(cat /etc/hostname) +FromHost="$(cat /etc/hostname 2>/dev/null)" || FromHost="$(hostname 2>/dev/null)" || FromHost="UNKNOWN" CurrentEpochTime=$(date +"%Y-%m-%dT%H:%M:%S") CurrentEpochSeconds=$(date +%s) From c9a4150b6732573e27594277b6ad2e171dfae8a0 Mon Sep 17 00:00:00 2001 From: smoochy Date: Mon, 26 Jan 2026 20:14:47 +0100 Subject: [PATCH 48/55] Adjusted default.config & readme --- README.md | 134 ++++++++++++++++++++++++++++++++----------------- default.config | 46 ++++++++--------- 2 files changed, 111 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index f811796..5346b53 100644 --- a/README.md +++ b/README.md @@ -18,40 +18,41 @@

:whale: Docker Hub pull limit :chart_with_downwards_trend: not an issue for checks only for actual pulls - read more

For Podman - see the fork sudo-kraken/podcheck!
- ___ + ## Changelog - **v0.7.5**: - - Added new option **BackupForDays**; `-b N` and `-B`: - - Backup an image before pulling a new version for easy rollback in case of breakage. - - Removes backed up images older than *N* days. - - List currently backed up images with `-B`. - - Fixes: - - Bugfix for `-s` *Stopped* to not recreate stopped containers after update. + - Added new option **BackupForDays**; `-b N` and `-B`: + - Backup an image before pulling a new version for easy rollback in case of breakage. + - Removes backed up images older than *N* days. + - List currently backed up images with `-B`. + - Fixes: + - Bugfix for `-s` *Stopped* to not recreate stopped containers after update. - **v0.7.4**: - - Added new option `-R`: - - Will skip container recreation after pulling images. - - Allows for more control and possible pipeline integration. - - Fixes: - - Bugfix for *value too great* error due to leading zeroes - solved with base10 conversion. - - Clean up of some legacy readme sections. + - Added new option `-R`: + - Will skip container recreation after pulling images. + - Allows for more control and possible pipeline integration. + - Fixes: + - Bugfix for *value too great* error due to leading zeroes - solved with base10 conversion. + - Clean up of some legacy readme sections. - **v0.7.3**: Bugfix - unquoted variable in printf list caused occasional issues. - **v0.7.2**: - - Label rework: - - Moved up label logic to work globally on the current run. - - Only iterating on labeled containers when used with `-l` option, not listing others. - - Clarified messaging and readme/help texts. - - List reformatting for "available updates" numbering to easier highlight and copy: - - Padded with zero, changed `)` to `-`, example: `02 - homer` - - Can be selected by writing `2,3,4` or `02,03,04`. + - Label rework: + - Moved up label logic to work globally on the current run. + - Only iterating on labeled containers when used with `-l` option, not listing others. + - Clarified messaging and readme/help texts. + - List reformatting for "available updates" numbering to easier highlight and copy: + - Padded with zero, changed `)` to `-`, example: `02 - homer` + - Can be selected by writing `2,3,4` or `02,03,04`. + ___ - -![](extras/example.gif) +![example.gif](extras/example.gif) ## `dockcheck.sh` -``` + +```shell $ ./dockcheck.sh -h Syntax: dockcheck.sh [OPTION] [comma separated names to include] Example: dockcheck.sh -y -x 10 -d 10 -e nextcloud,heimdall @@ -82,8 +83,9 @@ Options: -x N Set max asynchronous subprocesses, 1 default, 0 to disable, 32+ tested. ``` -### Basic example: -``` +### Basic example + +```shell $ ./dockcheck.sh [##################################################] 5/5 @@ -99,12 +101,14 @@ Containers with updates available: Choose what containers to update: Enter number(s) separated by comma, [a] for all - [q] to quit: 1,2 ``` + Then it proceeds to run `pull` and `up -d` on every container with updates. After the updates are complete, you'll get prompted if you'd like to prune dangling images. ___ ## Dependencies + - Running docker (duh) and compose, either standalone or plugin. (see [Podman fork](https://github.com/sudo-kraken/podcheck)) - Bash shell or compatible shell of at least v4.3 - POSIX `xargs`, usually default but can be installed with the `findutils` package - to enable async. @@ -112,12 +116,14 @@ ___ - User will be prompted to install with package manager or download static binary. - [regclient/regctl](https://github.com/regclient/regclient) (Licensed under [Apache-2.0 License](http://www.apache.org/licenses/LICENSE-2.0)) - User will be prompted to download `regctl` if not in `PATH` or `PWD`. - - regctl requires `amd64/arm64` - see [workaround](#roller_coaster-workaround-for-non-amd64--arm64) if other architecture is used. + - regctl requires `amd64/arm64` - see [workaround](#workaround-for-non-amd64--arm64) if other architecture is used. ## Install Instructions + Download the script to a directory in **PATH**, I'd suggest using `~/.local/bin` as that's usually in **PATH**. For OSX/macOS preferably use `/usr/local/bin`. -```sh + +```shell # basic example with curl: curl -L https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh -o ~/.local/bin/dockcheck.sh chmod +x ~/.local/bin/dockcheck.sh @@ -128,25 +134,31 @@ wget -O ~/.local/bin/dockcheck.sh "https://raw.githubusercontent.com/mag37/dockc # OSX or macOS version with curl: curl -L https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh -o /usr/local/bin/dockcheck.sh && chmod +x /usr/local/bin/dockcheck.sh ``` + Then call the script anywhere with just `dockcheck.sh`. Add preferred `notify.sh`-template to the same directory - this will not be touched by the scripts self-update function. ## Configuration + To modify settings and have them persist through updates - copy the `default.config` to `dockcheck.config` alongside the script or in `~/.config/`. Alternatively create an alias where specific flags and values are set. Example `alias dc=dockcheck.sh -p -x 10 -t 3`. ## Notifications + Triggered with the `-i` flag. Will send a list of containers with updates available and a notification when `dockcheck.sh` itself has an update. `notify_templates/notify_v2.sh` is the default notification wrapper, if `notify.sh` is present and configured, it will override. Example of a cron scheduled job running non-interactive at 10'oclock excluding 1 container and sending notifications: `0 10 * * * /home/user123/.local/bin/dockcheck.sh -nix 10 -e excluded_container1` -#### Installation and configuration: +## Installation and configuration + Set up a directory structure as below. -You only need the `notify_templates/notify_v2.sh` file and any notification templates you wish to enable, but there is no harm in having all of them present. -``` +You only need the `notify_templates/notify_v2.sh` file and any notification templates +you wish to enable, but there is no harm in having all of them present. + +```shell . ├── notify_templates/ │ ├── notify_DSM.sh @@ -167,14 +179,15 @@ You only need the `notify_templates/notify_v2.sh` file and any notification temp ├── dockcheck.sh └── urls.list # optional ``` + - Uncomment and set the `NOTIFY_CHANNELS=""` environment variable in `dockcheck.config` to a space separated string of your desired notification channels to enable. - Uncomment and set the environment variables related to the enabled notification channels. Eg. `GOTIFY_DOMAIN=""` + `GOTIFY_TOKEN=""`. It's recommended to only do configuration with variables within `dockcheck.config` and not modify `notify_templates/notify_X.sh` directly. If you wish to customize the notify templates yourself, you may copy them to your project root directory alongside the main `dockcheck.sh` (where they're also ignored by git). Customizing `notify_v2.sh` is handled the same as customizing the templates, but it must be renamed to `notify.sh` within the `dockcheck.sh` root directory. +### Snooze feature -#### Snooze feature: Configure to receive scheduled notifications only if they're new since the last notification - within a set time frame. **Example:** *Dockcheck is scheduled to run every hour. You will receive an update notification within an hour of availability.* @@ -188,8 +201,8 @@ If an update becomes available for an item that is not snoozed, notifications wi The actual snooze duration will be 60 seconds less than `SNOOZE_SECONDS` to account for minor scheduling or run time issues. +### Current notify templates -#### Current notify templates: - Synology [DSM](https://www.synology.com/en-global/dsm) - Email with [mSMTP](https://wiki.debian.org/msmtp) (or deprecated alternative [sSMTP](https://wiki.debian.org/sSMTP)) - Apprise (with it's [multitude](https://github.com/caronc/apprise#supported-notifications) of notifications) @@ -208,11 +221,14 @@ The actual snooze duration will be 60 seconds less than `SNOOZE_SECONDS` to acco Further additions are welcome - suggestions or PRs! Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). -#### Notification channel configuration: +### Notification channel configuration + All required environment variables for each notification channel are provided in the default.config file as comments and must be uncommented and modified for your requirements. For advanced users, additional functionality is available via custom configurations and environment variables. Use cases - all configured in `dockcheck.config`: -(replace `` with the upper case name of the of the channel as listed in `NOTIFY_CHANNELS` variable, eg `TELEGRAM_SKIPSNOOZE`) +(replace `` with the upper case name of the of the channel as listed in +`NOTIFY_CHANNELS` variable, eg `TELEGRAM_SKIPSNOOZE`) + - To bypass the snooze feature, even when enabled, add the variable `_SKIPSNOOZE` and set it to `true`. - To configure the channel to only send container update notifications, add the variable `_CONTAINERSONLY` and set it to `true`. - To send notifications even when there are no updates available, add the variable `_ALLOWEMPTY` and set it to `true`. @@ -224,7 +240,8 @@ Use cases - all configured in `dockcheck.config`: - Add all other environment variables required for the chosen template to function with `` in upper case as the prefix rather than the template name. - For example, if `` is `mynotification` and the template configured is `slack`, you would need to set `MYNOTIFICATION_CHANNEL_ID` and `MYNOTIFICATION_ACCESS_TOKEN`. -### Release notes addon +## Release notes addon + There's a function to use a lookup-file to add release note URL's to the notification message. Copy the notify_templates/`urls.list` file to the script directory, it will be used automatically if it's there. Modify it as necessary, the names of interest in the left column needs to match your container names. @@ -232,22 +249,28 @@ To also list the URL's in the CLI output (choose containers list) use the `-I` o For Markdown formatting also add the `-M` option. (**this requires the template to be compatible - see gotify for example**) The output of the notification will look something like this: -``` + +```shell Containers on hostname with updates available: apprise-api -> https://github.com/linuxserver/docker-apprise-api/releases homer -> https://github.com/bastienwirtz/homer/releases nginx -> https://github.com/docker-library/official-images/blob/master/library/nginx ... ``` + The `urls.list` file is just an example and I'd gladly see that people contribute back when they add their preferred URLs to their lists. ## Asyncronous update checks with **xargs**; `-x N` option. (default=1) + Pass `-x N` where N is number of subprocesses allowed, experiment in your environment to find a suitable max! -Change the default value by editing the `MaxAsync=N` variable in `dockcheck.sh`. To disable the subprocess function set `MaxAsync=0`. +Change the default value by editing the `MaxAsync=N` variable in `dockcheck.config`. To disable the subprocess function set `MaxAsync=0`. ## Image Backups; `-b N` to backup previous images as custom (retagged) images for easy rollback + When the option `BackupForDays` is set **dockcheck** will store the image being updated as a backup, retagged with a different name and removed due to age configured (*BackupForDays*) in a future run. -Let's say we're updating `b4bz/homer:latest` - then before replacing the current image it will be retagged with the name `dockcheck/homer:2025-10-26_1132_latest` +Let's say we're updating `b4bz/homer:latest` - then before replacing the current image +it will be retagged with the name `dockcheck/homer:2025-10-26_1132_latest` + - `dockcheck` as repo name to not interfere with others. - `homer` is the image. - `2025-10-26_1132` is the time when running the script. @@ -263,17 +286,20 @@ Backed up images will not be removed if neither `-b` flag nor `BackupForDays` co Use the capital option `-B` to list currently backed up images. Or list all images with `docker images`. To manually remove any backed up images, do `docker rmi dockcheck/homer:2025-10-26_1132_latest`. -## Extra plugins and tools: +## Extra plugins and tools ### Using dockcheck.sh with the Synology DSM + If you run your container through the *Container Manager GUI* - only notifications are supported. While if running manual (vanilla docker compose CLI) will allow you to use the update function too. Some extra setup to tie together with Synology DSM - check out the [addons/DSM/README.md](./addons/DSM/README.md). ### Prometheus and node_exporter + Dockcheck can be used together with [Prometheus](https://github.com/prometheus/prometheus) and [node_exporter](https://github.com/prometheus/node_exporter) to export metrics via the file collector, scheduled with cron or likely. This is done with the `-c` option, like this: -``` + +```shell dockcheck.sh -c /path/to/exporter/directory ``` @@ -281,26 +307,32 @@ See the [README.md](./addons/prometheus/README.md) for more detailed information Contributed by [tdralle](https://github.com/tdralle). ### Zabbix config to monitor docker image updates + If you already use Zabbix - this config will show numbers of available docker image updates on host. Example: *2 Docker Image updates on host-xyz* See project: [thetorminal/zabbix-docker-image-updates](https://github.com/thetorminal/zabbix-docker-image-updates) ### Serve REST API to list all available updates + A custom python script to serve a REST API to get pulled into other monitoring tools like [homepage](https://github.com/gethomepage/homepage). See [discussion here](https://github.com/mag37/dockcheck/discussions/146). ### Wrapper Script for Unraid's User Scripts + A custom bash wrapper script to allow the usage of dockcheck as a Unraid User Script plugin. See [discussion here](https://github.com/mag37/dockcheck/discussions/145). ## Labels + Optionally add labels to compose-files. Currently these are the usable labels: -``` + +```yaml labels: mag37.dockcheck.update: true mag37.dockcheck.only-specific-container: true mag37.dockcheck.restart-stack: true ``` + - `mag37.dockcheck.update: true` will when used with the `-l` option only check and update containers with this label set and skip the rest. - `mag37.dockcheck.only-specific-container: true` works instead of the `-F` option, specifying the updated container when doing compose up, like `docker compose up -d homer`. - `mag37.dockcheck.restart-stack: true` works instead of the `-f` option, forcing stop+restart on the whole compose-stack (Caution: Will restart on every updated container within stack). @@ -308,10 +340,11 @@ Optionally add labels to compose-files. Currently these are the usable labels: Adding or modifying labels in compose-files requires a restart of the container to take effect. ## Workaround for non **amd64** / **arm64** + `regctl` provides binaries for amd64/arm64, to use on other architecture you could try this workaround. Run regctl in a container wrapped in a shell script. Copied from [regclient/docs/install.md](https://github.com/regclient/regclient/blob/main/docs/install.md): -```sh +```shell cat >regctl <Unauthenticated users: 10 pulls/hour >Authenticated users with a free account: 100 pulls/hour @@ -336,8 +371,11 @@ This is not an issue for registry checks. But if you have a large stack and pull You could use/modify the login-wrapper function in the example below to automate the login prior to running `dockcheck.sh`. ### Function to auth with docker hub before running -**Example** - Change names, paths, and remove cat+password flag if you rather get prompted: -```sh + +**Example** - Change names, paths, and remove cat+password flag if you rather get +prompted: + +```shell function dchk { cat ~/pwd.txt | docker login --username YourUser --password-stdin ~/dockcheck.sh "$@" @@ -345,22 +383,26 @@ function dchk { ``` ## `-r flag` disclaimer and warning + **Wont auto-update the containers, only their images. (compose is recommended)** -`docker run` dont support using new images just by restarting a container. +`docker run` doesn't support using new images just by restarting a container. Containers need to be manually stopped, removed and created again to run on the new image. Using the `-r` option together with eg. `-i` and `-n` to just check for updates and send notifications and not update is safe though! ## Known issues + - No detailed error feedback (just skip + list what's skipped). - Not respecting `--profile` options when re-creating the container. - Not working well with containers created by **Portainer**. - **Watchtower** might cause issues due to retagging images when checking for updates (and thereby pulling new images). ## Debugging + If you hit issues, you could check the output of the `extras/errorCheck.sh` script for clues. Another option is to run the main script with debugging in a subshell `bash -x dockcheck.sh` - if there's a particular container/image that's causing issues you can filter for just that through `bash -x dockcheck.sh nginx`. ## License + dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/licenses/gpl-3.0-standalone.html) license. ## Sponsorlist @@ -378,4 +420,4 @@ dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/l ___ -### The [story](https://mag37.org/posts/project_dockcheck/) behind it. 1 year in retrospect. +## The [story](https://mag37.org/posts/project_dockcheck/) behind it. 1 year in retrospect diff --git a/default.config b/default.config index 90abfa5..43baea6 100644 --- a/default.config +++ b/default.config @@ -5,30 +5,30 @@ ## Uncomment and set your preferred configuration variables here ## This will not be replaced on updates -#Timeout=10 # Set a timeout (in seconds) per container for registry checkups. -#MaxAsync=10 # Set max asynchronous subprocesses, 1 default, 0 to disable. -#BarWidth=50 # The character width of the progress bar -#AutoMode=true # Automatic updates, without interaction. -#DontUpdate=true # No updates; only checking availability without interaction. -#AutoPrune=true # Auto-Prune dangling images after update. -#AutoSelfUpdate=true # Allow automatic self updates - caution as this will pull new code and autorun it. -#Notify=true # Inform - send a preconfigured notification. -#Exclude="one,two" # Exclude containers, separated by comma. -#DaysOld="5" # Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower. -#Stopped="-a" # Include stopped containers in the check. (Logic: docker ps -a). -#OnlyLabel=true # Only update if label is set. See readme. +#AutoMode=true # Automatic updates, without interaction. +#AutoPrune=true # Auto-Prune dangling images after update. +#AutoSelfUpdate=true # Allow automatic self updates - caution as this will pull new code and autorun it. +#BackupForDays=7 # Enable backups of images and removes backups older than N days. +#BarWidth=50 # The character width of the progress bar +#CurlConnectTimeout=5 # Time to wait for curl to establish a connection before failing +#CurlRetryCount=3 # Max number of curl retries +#CurlRetryDelay=1 # Time between curl retries +#DaysOld="5" # Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower. +#DisplaySourcedFiles=false # Display what files are being sourced/used +#DontUpdate=true # No updates; only checking availability without interaction. +#DRunUp=true # Allows for checking containers, which had been created using 'docker run' and don't use docker compose. Won't update the containers. +#Exclude="one,two" # Exclude containers, separated by comma. #ForceRestartStacks=true # Force stop+start stack after update. Caution: restarts once for every updated container within stack. -#DRunUp=true # Allow updating images for docker run, wont update the container. -#SkipRecreate # Skip container recreation after pulling images. -#MonoMode=true # Monochrome mode, no printf colour codes and hides progress bar. -#PrintReleaseURL=true # Prints custom releasenote urls alongside each container with updates (requires urls.list)` -#PrintMarkdownURL=true # Prints custom releasenote urls as markdown -#OnlySpecific=true # Only compose up the specific container, not the whole compose. (useful for master-compose structure). -#CurlRetryDelay=1 # Time between curl retries -#CurlRetryCount=3 # Max number of curl retries -#CurlConnectTimeout=5 # Time to wait for curl to establish a connection before failing -#DisplaySourcedFiles=false # Display what files are being sourced/used -#BackupForDays=7 # Enable backups of images and removes backups older than N days. +#MaxAsync=10 # Set max asynchronous subprocesses, 1 default, 0 to disable. +#MonoMode=true # Monochrome mode, no printf colour codes and hides progress bar. +#Notify=true # Inform - send a preconfigured notification. +#OnlyLabel=true # Only update if label is set. See readme. +#OnlySpecific=true # Only compose up the specific container, not the whole compose. (useful for master-compose structure). +#PrintMarkdownURL=true # Prints custom releasenote urls as markdown +#PrintReleaseURL=true # Prints custom releasenote urls alongside each container with updates (requires urls.list)` +#SkipRecreate # Skip container recreation after pulling images. +#Stopped="-a" # Include stopped containers in the check. (Logic: docker ps -a). +#Timeout=10 # Set a timeout (in seconds) per container for registry checkups. ### Notify settings ## All commented values are examples only. Modify as needed. From 9c780b8b3566161d0a5fc7ec9da4f77b41dc6eb8 Mon Sep 17 00:00:00 2001 From: mag37 Date: Tue, 27 Jan 2026 09:31:19 +0100 Subject: [PATCH 49/55] Consistent sub-headers --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5346b53..1d1e660 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@

:whale: Docker Hub pull limit :chart_with_downwards_trend: not an issue for checks only for actual pulls - read more

For Podman - see the fork sudo-kraken/podcheck!
+ ___ ## Changelog @@ -152,7 +153,7 @@ Triggered with the `-i` flag. Will send a list of containers with updates availa Example of a cron scheduled job running non-interactive at 10'oclock excluding 1 container and sending notifications: `0 10 * * * /home/user123/.local/bin/dockcheck.sh -nix 10 -e excluded_container1` -## Installation and configuration +#### Installation and configuration Set up a directory structure as below. You only need the `notify_templates/notify_v2.sh` file and any notification templates @@ -186,7 +187,7 @@ you wish to enable, but there is no harm in having all of them present. It's recommended to only do configuration with variables within `dockcheck.config` and not modify `notify_templates/notify_X.sh` directly. If you wish to customize the notify templates yourself, you may copy them to your project root directory alongside the main `dockcheck.sh` (where they're also ignored by git). Customizing `notify_v2.sh` is handled the same as customizing the templates, but it must be renamed to `notify.sh` within the `dockcheck.sh` root directory. -### Snooze feature +#### Snooze feature Configure to receive scheduled notifications only if they're new since the last notification - within a set time frame. @@ -201,7 +202,7 @@ If an update becomes available for an item that is not snoozed, notifications wi The actual snooze duration will be 60 seconds less than `SNOOZE_SECONDS` to account for minor scheduling or run time issues. -### Current notify templates +#### Current notify templates - Synology [DSM](https://www.synology.com/en-global/dsm) - Email with [mSMTP](https://wiki.debian.org/msmtp) (or deprecated alternative [sSMTP](https://wiki.debian.org/sSMTP)) @@ -221,7 +222,7 @@ The actual snooze duration will be 60 seconds less than `SNOOZE_SECONDS` to acco Further additions are welcome - suggestions or PRs! Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). -### Notification channel configuration +#### Notification channel configuration All required environment variables for each notification channel are provided in the default.config file as comments and must be uncommented and modified for your requirements. For advanced users, additional functionality is available via custom configurations and environment variables. @@ -240,7 +241,7 @@ Use cases - all configured in `dockcheck.config`: - Add all other environment variables required for the chosen template to function with `` in upper case as the prefix rather than the template name. - For example, if `` is `mynotification` and the template configured is `slack`, you would need to set `MYNOTIFICATION_CHANNEL_ID` and `MYNOTIFICATION_ACCESS_TOKEN`. -## Release notes addon +#### Release notes addon There's a function to use a lookup-file to add release note URL's to the notification message. Copy the notify_templates/`urls.list` file to the script directory, it will be used automatically if it's there. From 9755c32f8cc02b4eb924b4c7a89465af8186ee60 Mon Sep 17 00:00:00 2001 From: solitudechn Date: Wed, 28 Jan 2026 17:19:34 +0800 Subject: [PATCH 50/55] add-bark-notify (#259) --- default.config | 4 ++- notify_templates/notify_bark.sh | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 notify_templates/notify_bark.sh diff --git a/default.config b/default.config index 90abfa5..77e18d1 100644 --- a/default.config +++ b/default.config @@ -34,7 +34,7 @@ ## All commented values are examples only. Modify as needed. ## ## Uncomment the line below and specify the notification channels you wish to enable in a space separated string -# NOTIFY_CHANNELS="apprise discord DSM file generic HA gotify matrix ntfy pushbullet pushover slack smtp telegram file" +# NOTIFY_CHANNELS="apprise discord DSM file generic HA gotify matrix ntfy pushbullet pushover slack smtp telegram bark file" # ## Uncomment the line below and specify the number of seconds to delay notifications to enable snooze # SNOOZE_SECONDS=86400 @@ -90,4 +90,6 @@ # TELEGRAM_TOKEN="token-value" # TELEGRAM_TOPIC_ID="0" # +# BARK_KEY="" +# # FILE_PATH="${ScriptWorkDir}/updates_available.txt" diff --git a/notify_templates/notify_bark.sh b/notify_templates/notify_bark.sh new file mode 100644 index 0000000..c720cf2 --- /dev/null +++ b/notify_templates/notify_bark.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# NOTIFY_BARK_VERSION="v1.0" + +trigger_bark_notification() { + local channel="$1" + + if [[ -z "$jqbin" ]]; then + for path in "$jqbin" "jq" "./jq" "../jq" "./jq-linux-TEMP" "../jq-linux-TEMP"; do + if command -v "$path" &>/dev/null; then jqbin="$path"; break; fi + done + fi + [[ -z "$jqbin" ]] && { echo "Error: jq missing"; return 1; } + + [[ -z "$BARK_KEY" ]] && { echo "Error: Key not set"; return 1; } + + local sound="${BARK_SOUND:-hello}" + local group="${BARK_GROUP:-Dockcheck}" + local icon_url="${BARK_ICON_URL:-https://raw.githubusercontent.com/mag37/dockcheck/main/logo.png}" + + + local title="${MessageTitle%.}" + local newline=$'\n' + local formatted_body="## $title${newline}${newline}---${newline}${newline}$MessageBody" + + local json_payload=$( "$jqbin" -n \ + --arg title "$title" \ + --arg body "$formatted_body" \ + --arg group "$group" \ + --arg sound "$sound" \ + --arg icon "$icon_url" \ + '{ + "title": $title, + "markdown": $body, + "group": $group, + "sound": $sound, + "icon": $icon, + }' ) + + + if curl -s -f -X POST "https://api.day.app/$BARK_KEY" \ + -H "Content-Type: application/json; charset=utf-8" \ + -d "$json_payload" > /dev/null 2>&1; then + echo "Bark notification sent successfully (Markdown): $title" + fi +} \ No newline at end of file From 1584f23d330c6181263b3c17f789df63b3d29b10 Mon Sep 17 00:00:00 2001 From: smoochy Date: Wed, 28 Jan 2026 21:39:51 +0100 Subject: [PATCH 51/55] - Adjusted description of "-r" parameter - Adjusted order of parameters between dockcheck.sh and readme - Added missing parameter values from readme into dockcheck.sh --- README.md | 2 +- default.config | 2 +- dockcheck.sh | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1d1e660..0f93af5 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Options: -M Prints custom releasenote urls as markdown (requires template support). -n No updates, only checking availability. -p Auto-Prune dangling images after update. --r Allow checking for updates/updating images for docker run containers. Won't update the container. +-r Allow checking/updating images created by `docker run`, containers need to be recreated manually. -R Skip container recreation after pulling images. -s Include stopped containers in the check. (Logic: docker ps -a). -t N Set a timeout (in seconds) per container for registry checkups, 10 is default. diff --git a/default.config b/default.config index 43baea6..9920947 100644 --- a/default.config +++ b/default.config @@ -16,7 +16,7 @@ #DaysOld="5" # Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower. #DisplaySourcedFiles=false # Display what files are being sourced/used #DontUpdate=true # No updates; only checking availability without interaction. -#DRunUp=true # Allows for checking containers, which had been created using 'docker run' and don't use docker compose. Won't update the containers. +#DRunUp=true # Allow checking/updating images created by `docker run`, containers need to be recreated manually. #Exclude="one,two" # Exclude containers, separated by comma. #ForceRestartStacks=true # Force stop+start stack after update. Caution: restarts once for every updated container within stack. #MaxAsync=10 # Set max asynchronous subprocesses, 1 default, 0 to disable. diff --git a/dockcheck.sh b/dockcheck.sh index 906ed0d..7115571 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -36,7 +36,7 @@ Help() { echo "-a|y Automatic updates, without interaction." echo "-b N Enable image backups and sets number of days to keep from pruning." echo "-B List currently backed up images, then exit." - echo "-c Exports metrics as prom file for the prometheus node_exporter. Provide the collector textfile directory." + echo "-c D Exports metrics as prom file for the prometheus node_exporter. Provide the collector textfile directory." echo "-d N Only update to new images that are N+ days old. Lists too recent with +prefix and age. 2xSlower." echo "-e X Exclude containers, separated by comma." echo "-f Force stop+start stack after update. Caution: restarts once for every updated container within stack." @@ -49,10 +49,10 @@ 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 "-r Allow checking/updating images created by `docker run`, containers need to be recreated manually." echo "-R Skip container recreation after pulling images." - echo "-r Allow checking for updates/updating images for docker run containers. Won't update the container." echo "-s Include stopped containers in the check. (Logic: docker ps -a)." - echo "-t Set a timeout (in seconds) per container for registry checkups, 10 is default." + echo "-t N Set a timeout (in seconds) per container for registry checkups, 10 is default." echo "-u Allow automatic self updates - caution as this will pull new code and autorun it." echo "-v Prints current version." echo "-x N Set max asynchronous subprocesses, 1 default, 0 to disable, 32+ tested." From 6e3b7ee419241110187ba63a5aaa635b2fabc1f9 Mon Sep 17 00:00:00 2001 From: mag37 Date: Thu, 29 Jan 2026 09:42:33 +0100 Subject: [PATCH 52/55] Added Bark to README and minor cleanups --- README.md | 14 ++++++++------ default.config | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0f93af5..9810726 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ you wish to enable, but there is no harm in having all of them present. ├── notify_templates/ │ ├── notify_DSM.sh │ ├── notify_apprise.sh +│ ├── notify_bark.sh │ ├── notify_discord.sh │ ├── notify_generic.sh │ ├── notify_gotify.sh @@ -204,20 +205,21 @@ The actual snooze duration will be 60 seconds less than `SNOOZE_SECONDS` to acco #### Current notify templates -- Synology [DSM](https://www.synology.com/en-global/dsm) -- Email with [mSMTP](https://wiki.debian.org/msmtp) (or deprecated alternative [sSMTP](https://wiki.debian.org/sSMTP)) - Apprise (with it's [multitude](https://github.com/caronc/apprise#supported-notifications) of notifications) - both native [caronc/apprise](https://github.com/caronc/apprise) and the standalone [linuxserver/docker-apprise-api](https://github.com/linuxserver/docker-apprise-api) - Read the [QuickStart](extras/apprise_quickstart.md) -- [ntfy](https://ntfy.sh/) - HTTP-based pub-sub notifications. +- [Bark](https://bark.day.app/) - iOS Push notifications. +- [Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) - Discord webhooks. +- [DSM](https://www.synology.com/en-global/dsm) - Synology. - [Gotify](https://gotify.net/) - a simple server for sending and receiving messages. - [Home Assistant](https://www.home-assistant.io/integrations/notify/) - Connection to the notify [integrations](https://www.home-assistant.io/integrations/#notifications). -- [Pushbullet](https://www.pushbullet.com/) - connecting different devices with cross-platform features. -- [Telegram](https://telegram.org/) - Telegram chat API. - [Matrix-Synapse](https://github.com/element-hq/synapse) - [Matrix](https://matrix.org/), open, secure, decentralised communication. +- [ntfy](https://ntfy.sh/) - HTTP-based pub-sub notifications. +- [Pushbullet](https://www.pushbullet.com/) - connecting different devices with cross-platform features. - [Pushover](https://pushover.net/) - Simple Notifications (to your phone, wearables, desktops) -- [Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) - Discord webhooks. - [Slack](https://api.slack.com/tutorials/tracks/posting-messages-with-curl) - Slack curl api +- SMTP Email with [mSMTP](https://wiki.debian.org/msmtp) (or deprecated alternative [sSMTP](https://wiki.debian.org/sSMTP)) +- [Telegram](https://telegram.org/) - Telegram chat API. Further additions are welcome - suggestions or PRs! Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). diff --git a/default.config b/default.config index 330d45f..f9076a2 100644 --- a/default.config +++ b/default.config @@ -1,4 +1,4 @@ -### Custom user variables +##### Custom user variables ##### ## Copy this file to "dockcheck.config" to make it active ## Can be placed in ~/.config/ or alongside dockcheck.sh ## @@ -30,11 +30,12 @@ #Stopped="-a" # Include stopped containers in the check. (Logic: docker ps -a). #Timeout=10 # Set a timeout (in seconds) per container for registry checkups. -### Notify settings +##### NOTIFY SETTINGS BELOW ##### +## ## All commented values are examples only. Modify as needed. ## ## Uncomment the line below and specify the notification channels you wish to enable in a space separated string -# NOTIFY_CHANNELS="apprise discord DSM file generic HA gotify matrix ntfy pushbullet pushover slack smtp telegram bark file" +# NOTIFY_CHANNELS="apprise bark discord DSM file generic gotify HA matrix ntfy pushbullet pushover slack smtp telegram" # ## Uncomment the line below and specify the number of seconds to delay notifications to enable snooze # SNOOZE_SECONDS=86400 @@ -51,11 +52,15 @@ # tgram://{bot_token}/{chat_id}/' # APPRISE_URL="http://apprise.mydomain.tld:1234/notify/apprise" # +# BARK_KEY="key-value" +# # DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/" # # DSM_SENDMAILTO="me@mydomain.com" # DSM_SUBJECTTAG="Email Subject Prefix" # +# FILE_PATH="${ScriptWorkDir}/updates_available.txt" +# # GOTIFY_DOMAIN="https://gotify.domain.tld" # GOTIFY_TOKEN="token-value" # @@ -70,7 +75,8 @@ ## https://ntfy.sh or your custom domain with https:// and no trailing / # NTFY_DOMAIN="https://ntfy.sh" # NTFY_TOPIC_NAME="YourUniqueTopicName" -# NTFY_AUTH="" # set to either format -> "user:password" OR ":tk_12345678". If using tokens, don't forget the ":" +## Auth method, set to either format -> "user:password" OR ":tk_12345678". If using tokens, don't forget the ":" +# NTFY_AUTH="" # # PUSHBULLET_URL="https://api.pushbullet.com/v2/pushes" # PUSHBULLET_TOKEN="token-value" @@ -89,7 +95,3 @@ # TELEGRAM_CHAT_ID="mychatid" # TELEGRAM_TOKEN="token-value" # TELEGRAM_TOPIC_ID="0" -# -# BARK_KEY="" -# -# FILE_PATH="${ScriptWorkDir}/updates_available.txt" From 6c969c91695ff6cab90e4c43ebcd42f9168acbe8 Mon Sep 17 00:00:00 2001 From: mag37 Date: Thu, 29 Jan 2026 09:59:45 +0100 Subject: [PATCH 53/55] mend --- dockcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockcheck.sh b/dockcheck.sh index 7115571..aa895bb 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -49,7 +49,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 "-r Allow checking/updating images created by `docker run`, containers need to be recreated manually." + 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)." echo "-t N Set a timeout (in seconds) per container for registry checkups, 10 is default." From fc0b1a2505942ce51582ce384d684f47df9f22c9 Mon Sep 17 00:00:00 2001 From: mag37 Date: Thu, 29 Jan 2026 13:06:07 +0100 Subject: [PATCH 54/55] Versionbump, changenotes --- README.md | 17 ++++++++--------- dockcheck.sh | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9810726..418b50b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,14 @@ ___ ## Changelog +- **v0.7.6**: + - New: + - Added Bark notify-template. + - Fixes: + - Sanitized message for Matrix notification. + - Fixed hostname fallback for notifications. + - Clenaed up README.md some. + - Sorted and clarified `default.config` - migrate your settings manually (optional). - **v0.7.5**: - Added new option **BackupForDays**; `-b N` and `-B`: - Backup an image before pulling a new version for easy rollback in case of breakage. @@ -37,15 +45,6 @@ ___ - Fixes: - Bugfix for *value too great* error due to leading zeroes - solved with base10 conversion. - Clean up of some legacy readme sections. -- **v0.7.3**: Bugfix - unquoted variable in printf list caused occasional issues. -- **v0.7.2**: - - Label rework: - - Moved up label logic to work globally on the current run. - - Only iterating on labeled containers when used with `-l` option, not listing others. - - Clarified messaging and readme/help texts. - - List reformatting for "available updates" numbering to easier highlight and copy: - - Padded with zero, changed `)` to `-`, example: `02 - homer` - - Can be selected by writing `2,3,4` or `02,03,04`. ___ diff --git a/dockcheck.sh b/dockcheck.sh index aa895bb..24e7c62 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="v0.7.5" -# ChangeNotes: New option -b N to backup image before pulling for easy rollback. +VERSION="v0.7.6" +# ChangeNotes: Bugfixes and sanitation. Cleanup of default.config - migrate settings manually (optional). Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" From 7785e869d3ca5e2b9fd1d6e84ec80c18fee668fa Mon Sep 17 00:00:00 2001 From: singularity0821 <17620644+singularity0821@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:07:58 +0100 Subject: [PATCH 55/55] Add additional URLs to urls.list (#263) * Sanitize message for Matrix notifications * Use variable for jq and increment version of Matrix script * Add additional URLs to urls.list --------- Co-authored-by: martin --- notify_templates/urls.list | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/notify_templates/urls.list b/notify_templates/urls.list index aeba433..e69838a 100644 --- a/notify_templates/urls.list +++ b/notify_templates/urls.list @@ -3,33 +3,47 @@ # This is a list of container names and releasenote urls, separated by space. actual_server https://actualbudget.org/blog +adguardhome https://github.com/AdguardTeam/AdGuardHome/releases apprise-api https://github.com/linuxserver/docker-apprise-api/releases audiobookshelf https://github.com/advplyr/audiobookshelf/releases +authentik_server https://github.com/goauthentik/authentik/releases +authentik_worker https://github.com/goauthentik/authentik/releases +barassistant https://github.com/karlomikus/bar-assistant//releases +barassistant_api https://github.com/karlomikus/bar-assistant/releases bazarr https://github.com/morpheus65535/bazarr/releases bazarr-ls https://github.com/linuxserver/docker-bazarr/releases beszel https://github.com/henrygd/beszel/releases +booklore https://github.com/booklore-app/BookLore/releases bookstack https://github.com/BookStackApp/BookStack/releases bruceforce-vaultwarden-backup https://github.com/Bruceforce/vaultwarden-backup/blob/main/CHANGELOG.md caddy https://github.com/caddyserver/caddy/releases calibre https://github.com/linuxserver/docker-calibre/releases calibre-web https://github.com/linuxserver/docker-calibre-web/releases cleanuperr https://github.com/flmorg/cleanuperr/releases +collabora https://github.com/CollaboraOnline/online/releases cross-seed https://github.com/cross-seed/cross-seed/releases crowdsec https://github.com/crowdsecurity/crowdsec/releases cup https://github.com/sergi0g/cup/releases +databasus https://github.com/databasus/databasus/releases dockge https://github.com/louislam/dockge/releases dozzle https://github.com/amir20/dozzle/releases +esphome https://github.com/esphome/esphome/releases +feishin https://github.com/jeffvli/feishin/releases flatnotes https://github.com/dullage/flatnotes/releases forgejo https://codeberg.org/forgejo/forgejo/releases fressrss https://github.com/FreshRSS/FreshRSS/releases +frigate https://github.com/blakeblackshear/frigate/releases gerbil https://github.com/fosrl/gerbil/releases +glances https://github.com/nicolargo/glances/releases gluetun https://github.com/qdm12/gluetun/releases go2rtc https://github.com/AlexxIT/go2rtc/releases +godoxy https://github.com/yusing/godoxy/releases gotify https://github.com/gotify/server/releases hbbr https://github.com/rustdesk/rustdesk-server/releases hbbs https://github.com/rustdesk/rustdesk-server/releases homarr https://github.com/homarr-labs/homarr/releases home-assistant https://github.com/home-assistant/core/releases/ +homepage https://github.com/gethomepage/homepage/releases homer https://github.com/bastienwirtz/homer/releases immich_machine_learning https://github.com/immich-app/immich/releases immich_postgres https://github.com/tensorchord/VectorChord/releases @@ -38,6 +52,7 @@ immich_server https://github.com/immich-app/immich/releases jellyfin https://github.com/jellyfin/jellyfin/releases jellyseerr https://github.com/Fallenbagel/jellyseerr/releases jellystat https://github.com/CyferShepard/Jellystat/releases +karakeep https://github.com/karakeep-app/karakeep/releases librespeed https://github.com/librespeed/speedtest/releases lidarr https://github.com/Lidarr/Lidarr/releases/ lidarr-ls https://github.com/linuxserver/docker-lidarr/releases @@ -47,9 +62,12 @@ mealie https://github.com/mealie-recipes/mealie/releases meilisearch https://github.com/meilisearch/meilisearch/releases monica https://github.com/monicahq/monica/releases mqtt https://github.com/eclipse/mosquitto/tags +navidrome https://github.com/navidrome/navidrome/releases newt https://github.com/fosrl/newt/releases nextcloud-aio-mastercontainer https://github.com/nextcloud/all-in-one/releases nginx https://github.com/docker-library/official-images/blob/master/library/nginx +opencloud https://github.com/opencloud-eu/opencloud/releases +outline https://github.com/outline/outline/releases owncast https://github.com/owncast/owncast/releases pangolin https://github.com/fosrl/pangolin/releases prowlarr https://github.com/Prowlarr/Prowlarr/releases @@ -63,18 +81,25 @@ readeck https://codeberg.org/readeck/readeck/releases recyclarr https://github.com/recyclarr/recyclarr/releases roundcubemail https://github.com/roundcube/roundcubemail/releases sabnzbd https://github.com/linuxserver/docker-sabnzbd/releases +scanopy https://github.com/scanopy/scanopy/releases scrutiny https://github.com/AnalogJ/scrutiny/releases sftpgo https://github.com/drakkan/sftpgo/releases slskd https://github.com/slskd/slskd/releases snappymail https://github.com/the-djmaze/snappymail/releases +stirling-pdf https://github.com/Stirling-Tools/Stirling-PDF/releases sonarr https://github.com/Sonarr/Sonarr/releases/ sonarr-ls https://github.com/linuxserver/docker-sonarr/releases +synapse https://github.com/element-hq/synapse/releases syncthing https://github.com/syncthing/syncthing/releases tailscale https://github.com/tailscale/tailscale/releases tautulli https://github.com/Tautulli/Tautulli/releases thelounge https://github.com/thelounge/thelounge/releases traefik https://github.com/traefik/traefik/releases +uptime-kuma https://github.com/louislam/uptime-kuma/releases vaultwarden-server https://github.com/dani-garcia/vaultwarden/releases +vikunja https://github.com/go-vikunja/vikunja/releases +wallos https://github.com/ellite/Wallos/releases watchtower https://github.com/beatkind/watchtower/releases +wopiserver https://github.com/cs3org/wopiserver/releases wud https://github.com/getwud/wud/releases zigbee2mqtt https://github.com/Koenkk/zigbee2mqtt/releases