From 8dd1bba75b9f07276a399679498cc7541ea7505e Mon Sep 17 00:00:00 2001 From: mag37 Date: Sun, 30 Mar 2025 13:31:34 +0200 Subject: [PATCH] Clean&refactor (#148) * cleaning spaces and consistent formatting * removed more subshells * progress bar cleanup * moved uservars to a .config file * rewritten options from yes/no to true/false * initialized default variables * added bash options: -euo pipefail, shopt -s nullglob and failglob * quoting variables, cleaning syntax, logic and order * unquoted some variables due to breakage * added exit on pull-fail * added new sponsor * added Slack template --- .gitignore | 2 + README.md | 19 +- default.config | 20 ++ dockcheck.sh | 344 ++++++++++++++++++------------- notify_templates/notify_slack.sh | 52 +++++ 5 files changed, 289 insertions(+), 148 deletions(-) create mode 100644 default.config create mode 100644 notify_templates/notify_slack.sh diff --git a/.gitignore b/.gitignore index f3300e3..da5921c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # ignore users custom notify.sh /notify.sh /urls.list +# ignore user config +/dockcheck.config # ignore the auto-installed regctl regctl diff --git a/README.md b/README.md index 5f6e52f..06ef02c 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,18 @@ ___ ## :bell: Changelog -- **v0.5.8.0**: Added version checks to all templates and a notification if a new template is released. -- **v0.5.7.0**: Rewritten templates - now with a function to notify when there's a new Dockcheck release. +- **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. +- **v0.5.8**: Added version checks to all templates and a notification if a new template is released. +- **v0.5.7**: Rewritten templates - now with a function to notify when there's a new Dockcheck release. - Manually migrate your current `notify.sh` settings to a new template for new functionality. - **v0.5.6.1**: Async xargs hotfix - due to errors `failed to request manifest head ... context canceled` - Defaulted subprocess to 1 with `MaxAsync=1`, increase to find a stable value in your environment. - Added `-x N` option to pass `MaxAsync` value at runtime. - To disable xargs `-P` flag (max processes) all together, set `MaxAsync` to 0. - **v0.5.6.0**: Heavily improved performance due to async checking for updates. -- **v0.5.5.0**: osx and bsd compatibility changes + rewrite of dependency installer ___ @@ -41,7 +44,7 @@ $ ./dockcheck.sh -h Syntax: dockcheck.sh [OPTION] [part of name to filter] Example: dockcheck.sh -y -d 10 -e nextcloud,heimdall -Options:" +Options: -a|y Automatic updates, without interaction. -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. @@ -55,7 +58,7 @@ Options:" -p Auto-Prune dangling images after update. -r Allow updating images for docker run, wont update the container. -s Include stopped containers in the check. (Logic: docker ps -a). --t Set a timeout (in seconds) per container for registry checkups, 10 is default. +-t N Set a timeout (in seconds) per container for registry checkups, 10 is default. -v Prints current version. -x N Set max asynchronous subprocesses, 1 default, 0 to disable, 32+ tested. ``` @@ -108,6 +111,10 @@ 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 +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 Trigger with the `-i` flag. @@ -128,6 +135,7 @@ Use a `notify_X.sh` template file from the **notify_templates** directory, copy - [Matrix-Synapse](https://github.com/element-hq/synapse) - [Matrix](https://matrix.org/), open, secure, decentralised communication. - [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 Further additions are welcome - suggestions or PR! Initiated and first contributed by [yoyoma2](https://github.com/yoyoma2). @@ -229,6 +237,7 @@ dockcheck is created and released under the [GNU GPL v3.0](https://www.gnu.org/l ## :heartpulse: Sponsorlist - [avegy](https://github.com/avegy) +- [eichhorn](https://github.com/eichhorn) ___ diff --git a/default.config b/default.config new file mode 100644 index 0000000..82c0351 --- /dev/null +++ b/default.config @@ -0,0 +1,20 @@ +### 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 + +#Timeout=10 # Set a timeout (in seconds) per container for registry checkups. +#MaxAsync=1 # 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=false # Auto-Prune dangling images after update. +#Notify=false # 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=false # Only update if label is set. See readme. +#ForceRestartStacks=false # Force stack restart after update. Caution. +#DRunUp=false # Allow updating images for docker run, wont update the container. diff --git a/dockcheck.sh b/dockcheck.sh index e060a89..28ba27d 100755 --- a/dockcheck.sh +++ b/dockcheck.sh @@ -1,21 +1,28 @@ #!/usr/bin/env bash -VERSION="v0.5.8.0" -### ChangeNotes: Added version checks to all templates and a notification if a new template is released. +VERSION="v0.6.0" +### ChangeNotes: uservars file added. Lots of code refactoring, please report any bugs. Github="https://github.com/mag37/dockcheck" RawUrl="https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh" +set -euo pipefail +shopt -s nullglob +shopt -s failglob + # Variables for self updating ScriptArgs=( "$@" ) 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 -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")" # User customizable defaults -MaxAsync=1 -Timeout=10 +if [[ -s "${HOME}/.config/dockcheck.config" ]]; then + source "${HOME}/.config/dockcheck.config" +elif [[ -s "${ScriptWorkDir}/dockcheck.config" ]]; then + source "${ScriptWorkDir}/dockcheck.config" +fi # Help Function Help() { @@ -31,8 +38,8 @@ Help() { echo "-h Print this Help." echo "-i Inform - send a preconfigured notification." echo "-l Only update if label is set. See readme." - echo "-m Monochrome mode, no printf color codes." - echo "-n No updates; only checking availability." + echo "-m Monochrome mode, no printf colour codes." + 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 "-s Include stopped containers in the check. (Logic: docker ps -a)." @@ -43,7 +50,30 @@ Help() { echo "Project source: $Github" } -# Colors +# Initialise variables +Timeout=${Timeout:=10} +MaxAsync=${MaxAsync:=1} +BarWidth=${BarWidth:=50} +AutoMode=${AutoMode:=false} +DontUpdate=${DontUpdate:=false} +AutoPrune=${AutoPrune:=false} +OnlyLabel=${OnlyLabel:=false} +Notify=${Notify:=false} +ForceRestartStacks=${ForceRestartStacks:=false} +DRunUp=${DRunUp:=false} +Stopped=${Stopped:=""} +CollectorTextFileDirectory=${CollectorTextFileDirectory:-} +Exclude=${Exclude:-} +DaysOld=${DaysOld:-} +Excludes=() +GotUpdates=() +NoUpdates=() +GotErrors=() +SelectedUpdates=() +regbin="" +jqbin="" + +# Colours c_red="\033[0;31m" c_green="\033[0;32m" c_yellow="\033[0;33m" @@ -51,78 +81,103 @@ c_blue="\033[0;34m" c_teal="\033[0;36m" c_reset="\033[0m" -Stopped="" while getopts "aynpfrhlisvmc:e:d:t:x:" options; do case "${options}" in - a|y) AutoUp="yes" ;; - c) CollectorTextFileDirectory="${OPTARG}" - if ! [[ -d $CollectorTextFileDirectory ]] ; then { printf "The directory (%s) does not exist.\n" "${CollectorTextFileDirectory}" ; exit 2; } fi ;; - n) AutoUp="no" ;; - r) DRunUp="yes" ;; - p) AutoPrune="yes" ;; + a|y) AutoMode=true ;; + c) CollectorTextFileDirectory="${OPTARG}" ;; + n) DontUpdate=true; AutoMode=true;; + r) DRunUp=true ;; + p) AutoPrune=true ;; l) OnlyLabel=true ;; f) ForceRestartStacks=true ;; - i) [ -s "$ScriptWorkDir"/notify.sh ] && { source "$ScriptWorkDir"/notify.sh ; Notify="yes" ; } ;; + i) Notify=true ;; e) Exclude=${OPTARG} ;; m) declare c_{red,green,yellow,blue,teal,reset}="" ;; s) Stopped="-a" ;; t) Timeout="${OPTARG}" ;; - v) printf "%s\n" "$VERSION" ; exit 0 ;; + v) printf "%s\n" "$VERSION"; exit 0 ;; x) MaxAsync=${OPTARG} ;; - d) DaysOld=${OPTARG} - if ! [[ $DaysOld =~ ^[0-9]+$ ]] ; then { printf "Days -d argument given (%s) is not a number.\n" "${DaysOld}" ; exit 2 ; } ; fi ;; - h|*) Help ; exit 2 ;; + d) DaysOld=${OPTARG} ;; + h|*) Help; exit 2 ;; esac done shift "$((OPTIND-1))" -# Self-update function +# Set $1 to a variable for name filtering later +SearchName="${1:-}" + +# Setting up options and sourcing functions +if [[ "$DontUpdate" == true ]]; then AutoMode=true; fi +if [[ "$Notify" == true ]]; then + if [[ -s "${ScriptWorkDir}/notify.sh" ]]; then + source "${ScriptWorkDir}/notify.sh" + else Notify=false + fi +fi +if [[ -n "$Exclude" ]]; then + IFS=',' read -ra Excludes <<< "$Exclude" + unset IFS +fi +if [[ -n "$DaysOld" ]]; then + if ! [[ $DaysOld =~ ^[0-9]+$ ]]; then + printf "Days -d argument given (%s) is not a number.\n" "$DaysOld" + exit 2 + fi +fi +if [[ -n "$CollectorTextFileDirectory" ]]; then + if ! [[ -d $CollectorTextFileDirectory ]]; then + printf "The directory (%s) does not exist.\n" "$CollectorTextFileDirectory" + exit 2 + else + source "${ScriptWorkDir}/addons/prometheus/prometheus_collector.sh" + fi +fi + self_update_curl() { cp "$ScriptPath" "$ScriptPath".bak - if [[ $(command -v curl) ]]; then - curl -L $RawUrl > "$ScriptPath" ; chmod +x "$ScriptPath" + if command -v curl &>/dev/null; then + curl -L $RawUrl > "$ScriptPath"; chmod +x "$ScriptPath" printf "\n%s\n" "--- starting over with the updated version ---" exec "$ScriptPath" "${ScriptArgs[@]}" # run the new script with old arguments exit 1 # Exit the old instance - elif [[ $(command -v wget) ]]; then - wget $RawUrl -O "$ScriptPath" ; chmod +x "$ScriptPath" + elif command -v wget &>/dev/null; then + wget $RawUrl -O "$ScriptPath"; chmod +x "$ScriptPath" printf "\n%s\n" "--- starting over with the updated version ---" exec "$ScriptPath" "${ScriptArgs[@]}" # run the new script with old arguments - exit 1 # Exit the old instance + exit 0 # exit the old instance else printf "curl/wget not available - download the update manually: %s \n" "$Github" fi } self_update() { - cd "$ScriptWorkDir" || { printf "Path error, skipping update.\n" ; return ; } - if [[ $(command -v git) ]] && [[ "$(git ls-remote --get-url 2>/dev/null)" =~ .*"mag37/dockcheck".* ]] ; then + cd "$ScriptWorkDir" || { printf "Path error, skipping update.\n"; return; } + if command -v git &>/dev/null && [[ "$(git ls-remote --get-url 2>/dev/null)" =~ .*"mag37/dockcheck".* ]]; then printf "\n%s\n" "Pulling the latest version." - git pull --force || { printf "Git error, manually pull/clone.\n" ; return ; } + git pull --force || { printf "Git error, manually pull/clone.\n"; return; } printf "\n%s\n" "--- starting over with the updated version ---" - cd - || { printf "Path error.\n" ; return ; } + cd - || { printf "Path error.\n"; return; } exec "$ScriptPath" "${ScriptArgs[@]}" # run the new script with old arguments - exit 1 # exit the old instance + exit 0 # exit the old instance else - cd - || { printf "Path error.\n" ; return ; } + cd - || { printf "Path error.\n"; return; } self_update_curl fi } -# Choose from list function choosecontainers() { - while [[ -z "$ChoiceClean" ]]; do + while [[ -z "${ChoiceClean:-}" ]]; do read -r -p "Enter number(s) separated by comma, [a] for all - [q] to quit: " Choice - if [[ "$Choice" =~ [qQnN] ]] ; then + if [[ "$Choice" =~ [qQnN] ]]; then exit 0 - elif [[ "$Choice" =~ [aAyY] ]] ; then + elif [[ "$Choice" =~ [aAyY] ]]; then SelectedUpdates=( "${GotUpdates[@]}" ) ChoiceClean=${Choice//[,.:;]/ } else ChoiceClean=${Choice//[,.:;]/ } - for CC in $ChoiceClean ; do - if [[ "$CC" -lt 1 || "$CC" -gt $UpdCount ]] ; then # Reset choice if out of bounds - echo "Number not in list: $CC" ; unset ChoiceClean ; break 1 + for CC in $ChoiceClean; do + 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 SelectedUpdates+=( "${GotUpdates[$CC-1]}" ) fi @@ -135,10 +190,10 @@ choosecontainers() { } datecheck() { - ImageDate=$($regbin -v error image inspect "$RepoUrl" --format='{{.Created}}' | cut -d" " -f1 ) + ImageDate=$("$regbin" -v error image inspect "$RepoUrl" --format='{{.Created}}' | cut -d" " -f1) 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=$(( ( $(date +%s) - ImageEpoch )/86400 )) + if [[ "$ImageAge" -gt "$DaysOld" ]]; then return 0 else return 1 @@ -148,42 +203,30 @@ datecheck() { progress_bar() { QueCurrent="$1" QueTotal="$2" + BarWidth=${BarWidth:-50} ((Percent=100*QueCurrent/QueTotal)) - ((Complete=50*Percent/100)) # Change first number for width (50) - ((Left=50-Complete)) # Change first number for width (50) + ((Complete=BarWidth*Percent/100)) + ((Left=BarWidth-Complete)) || true # to not throw error when result is 0 BarComplete=$(printf "%${Complete}s" | tr " " "#") BarLeft=$(printf "%${Left}s" | tr " " "-") - [[ "$QueTotal" == "$QueCurrent" ]] || printf "\r[%s%s] %s/%s " "$BarComplete" "$BarLeft" "$QueCurrent" "$QueTotal" - [[ "$QueTotal" == "$QueCurrent" ]] && printf "\r[%b%s%b] %s/%s \n" "$c_teal" "$BarComplete" "$c_reset" "$QueCurrent" "$QueTotal" + if [[ "$QueTotal" != "$QueCurrent" ]]; then + printf "\r[%s%s] %s/%s " "$BarComplete" "$BarLeft" "$QueCurrent" "$QueTotal" + else + printf "\r[%b%s%b] %s/%s \n" "$c_teal" "$BarComplete" "$c_reset" "$QueCurrent" "$QueTotal" + fi } # Function to add user-provided urls to releasenotes releasenotes() { - for update in ${GotUpdates[@]}; do + for update in "${GotUpdates[@]}"; do found=false while read -r container url; do - [[ $update == $container ]] && Updates+=("$update -> $url") && found=true - done < "$ScriptWorkDir"/urls.list - [[ $found == false ]] && Updates+=("$update -> url missing") || continue + if [[ "$update" == "$container" ]]; then Updates+=("$update -> $url"); found=true; fi + done < "${ScriptWorkDir}/urls.list" + if [[ "$found" == false ]]; then Updates+=("$update -> url missing"); else continue; fi done } -# 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 [[ -z "$AutoUp" ]] ; then - read -r -p "Would you like to update? y/[n]: " SelfUpdate - [[ "$SelfUpdate" =~ [yY] ]] && self_update - else - [[ -n "$Notify" ]] && { [[ $(type -t dockcheck_notification) == function ]] && dockcheck_notification "$VERSION" "$LatestRelease" "$LatestChanges" || printf "Could not source notification function.\n" ; } - fi -fi - -# Set $1 to a variable for name filtering later -SearchName="$1" -# Create array of excludes -IFS=',' read -r -a Excludes <<< "$Exclude" ; unset IFS - # Static binary downloader for dependencies binary_downloader() { BinaryName="$1" @@ -191,23 +234,23 @@ binary_downloader() { case "$(uname -m)" in x86_64|amd64) architecture="amd64" ;; arm64|aarch64) architecture="arm64";; - *) printf "\n%bArchitecture not supported, exiting.%b\n" "$c_red" "$c_reset" ; exit 1;; + *) printf "\n%bArchitecture not supported, exiting.%b\n" "$c_red" "$c_reset"; exit 1;; esac GetUrl="${BinaryUrl/TEMP/"$architecture"}" - if [[ $(command -v curl) ]]; then curl -L $GetUrl > "$ScriptWorkDir/$BinaryName" ; - elif [[ $(command -v wget) ]]; then wget $GetUrl -O "$ScriptWorkDir/$BinaryName" ; + 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; fi [[ -f "$ScriptWorkDir/$BinaryName" ]] && chmod +x "$ScriptWorkDir/$BinaryName" } distro_checker() { - if [[ -f /etc/arch-release ]] ; then PkgInstaller="pacman -S" - elif [[ -f /etc/redhat-release ]] ; then PkgInstaller="sudo dnf install" - elif [[ -f /etc/SuSE-release ]] ; then PkgInstaller="sudo zypper install" - elif [[ -f /etc/debian_version ]] ; then PkgInstaller="sudo apt-get install" - elif [[ $(uname -s) == "Darwin" ]] ; then PkgInstaller="brew install" - else PkgInstaller="ERROR" ; printf "\n%bNo distribution could be determined%b, falling back to static binary.\n" "$c_yellow" "$c_reset" + if [[ -f /etc/arch-release ]]; then PkgInstaller="pacman -S" + elif [[ -f /etc/redhat-release ]]; then PkgInstaller="sudo dnf install" + elif [[ -f /etc/SuSE-release ]]; then PkgInstaller="sudo zypper install" + elif [[ -f /etc/debian_version ]]; then PkgInstaller="sudo apt-get install" + elif [[ $(uname -s) == "Darwin" ]]; then PkgInstaller="brew install" + else PkgInstaller="ERROR"; printf "\n%bNo distribution could be determined%b, falling back to static binary.\n" "$c_yellow" "$c_reset" fi } @@ -216,41 +259,61 @@ dependency_check() { AppName="$1" AppVar="$2" AppUrl="$3" - if [[ $(command -v $AppName) ]]; then export $AppVar="$AppName" ; - elif [[ -f "$ScriptWorkDir/$AppName" ]]; then export $AppVar="$ScriptWorkDir/$AppName" ; + 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 '$AppName' missing, do you want to install it?" 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 + if [[ "$GetBin" =~ [yYsS] ]]; then [[ "$GetBin" =~ [yY] ]] && distro_checker - if [[ -n "$PkgInstaller" && "$PkgInstaller" != "ERROR" ]] ; then + if [[ -n "${PkgInstaller:-}" && "${PkgInstaller:-}" != "ERROR" ]]; then [[ $(uname -s) == "Darwin" && "$AppName" == "regctl" ]] && AppName="regclient" - ($PkgInstaller $AppName) ; PkgExitcode="$?" && AppName="$1" - if [[ "$PkgExitcode" == 0 ]] ; then { export $AppVar="$AppName" && printf "\n%b$AppName installed.%b\n" "$c_green" "$c_reset"; } + ("$PkgInstaller" "$AppName"); PkgExitcode="$?" && AppName="$1" + if [[ "$PkgExitcode" == 0 ]]; then { export "$AppVar"="$AppName" && printf "\n%b%b installed.%b\n" "$c_green" "$AppName" "$c_reset"; } else printf "\n%bPackagemanager install failed%b, falling back to static binary.\n" "$c_yellow" "$c_reset" fi fi - if [[ "$GetBin" =~ [sS] || "$PkgInstaller" == "ERROR" || "$PkgExitcode" != 0 ]] ; then + if [[ "$GetBin" =~ [sS] || "$PkgInstaller" == "ERROR" || "$PkgExitcode" != 0 ]]; then binary_downloader "$AppName" "$AppUrl" - [[ -f "$ScriptWorkDir/$AppName" ]] && { export $AppVar="$ScriptWorkDir/$1" && printf "\n%b$AppName downloaded.%b\n" "$c_green" "$c_reset"; } + [[ -f "$ScriptWorkDir/$AppName" ]] && { export "$AppVar"="$ScriptWorkDir/$1" && printf "\n%b%b downloaded.%b\n" "$c_green" "$AppName" "$c_reset"; } fi - else printf "\n%bDependency missing, exiting.%b\n" "$c_red" "$c_reset" ; exit 1 ; + else printf "\n%bDependency missing, exiting.%b\n" "$c_red" "$c_reset"; exit 1; fi fi # Final check if binary is correct [[ "$1" == "jq" ]] && VerFlag="--version" [[ "$1" == "regctl" ]] && VerFlag="version" - ${!AppVar} $VerFlag &> /dev/null || { printf "%s\n" "$AppName is not working - try to remove it and re-download it, exiting."; exit 1; } + ${!AppVar} "$VerFlag" &> /dev/null || { printf "%s\n" "$AppName is not working - try to remove it and re-download it, exiting."; exit 1; } } +# Numbered List function +options() { + num=1 + for i in "${GotUpdates[@]}"; do + echo "$num) $i" + ((num++)) + done + } + +# 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 + else + [[ "$Notify" == true ]] && { [[ $(type -t dockcheck_notification) == function ]] && dockcheck_notification "$VERSION" "$LatestRelease" "$LatestChanges" || printf "Could not source notification function.\n"; } + fi +fi + 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 -if docker compose version &> /dev/null ; then DockerBin="docker compose" ; -elif docker-compose -v &> /dev/null; then DockerBin="docker-compose" ; -elif docker -v &> /dev/null; then +if docker compose version &>/dev/null; then DockerBin="docker compose" ; +elif docker-compose -v &>/dev/null; then DockerBin="docker-compose" ; +elif docker -v &>/dev/null; then printf "%s\n" "No docker compose binary available, using plain docker (Not recommended!)" printf "%s\n" "'docker run' will ONLY update images, not the container itself." else @@ -258,17 +321,8 @@ else exit 1 fi -# Numbered List function -options() { -num=1 -for i in "${GotUpdates[@]}"; do - echo "$num) $i" - ((num++)) -done -} - # Listing typed exclusions -if [[ -n ${Excludes[*]} ]] ; then +if [[ -n ${Excludes[*]} ]]; then printf "\n%bExcluding these names:%b\n" "$c_blue" "$c_reset" printf "%s\n" "${Excludes[@]}" printf "\n" @@ -279,9 +333,9 @@ ContCount=$(docker ps $Stopped --filter "name=$SearchName" --format '{{.Names}}' RegCheckQue=0 # Testing and setting timeout binary -t_out=$(command -v timeout) +t_out=$(command -v timeout || echo "") if [[ $t_out ]]; then - t_out=$(realpath $t_out 2>/dev/null || readlink -f $t_out) + t_out=$(realpath "$t_out" 2>/dev/null || readlink -f "$t_out") if [[ $t_out =~ "busybox" ]]; then t_out="timeout ${Timeout}" else t_out="timeout --foreground ${Timeout}" @@ -292,9 +346,9 @@ fi check_image() { i="$1" local Excludes=($Excludes_string) - for e in "${Excludes[@]}" ; do + for e in "${Excludes[@]}"; do if [[ "$i" == "$e" ]]; then - echo Skip $i + printf "%s\n" "Skip $i" return fi done @@ -305,25 +359,24 @@ check_image() { LocalHash=$(docker image inspect "$ImageId" --format '{{.RepoDigests}}') # 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 - echo NoUpdates "$i" + if RegHash=$($t_out "$regbin" -v error image digest --list "$RepoUrl" 2>&1); then + if [[ "$LocalHash" = *"$RegHash"* ]]; then + printf "%s\n" "NoUpdates $i" else - if [[ -n "$DaysOld" ]] && ! datecheck ; then - echo NoUpdates "+$i ${ImageAge}d" + if [[ -n "${DaysOld:-}" ]] && ! datecheck; then + printf "%s\n" "NoUpdates +$i ${ImageAge}d" else - echo GotUpdates "$i" + printf "%s\n" "GotUpdates $i" fi fi else - # Here the RegHash is the result of an error code - echo GotErrors "$i - ${RegHash}" + printf "%s\n" "GotErrors $i - ${RegHash}" # Reghash contains an error code here fi } # Make required functions and variables available to subprocesses export -f check_image datecheck -export Excludes_string="${Excludes[@]}" # Can only export scalar variables +export Excludes_string="${Excludes[*]}" # Can only export scalar variables export t_out regbin RepoUrl DaysOld # Check for POSIX xargs with -P option, fallback without async @@ -351,7 +404,7 @@ while read -r line; do esac done < <( \ docker ps $Stopped --filter "name=$SearchName" --format '{{.Names}}' | \ - xargs ${XargsAsync} -I {} bash -c 'check_image "{}"' \ + xargs $XargsAsync -I {} bash -c 'check_image "{}"' \ ) # Sort arrays alphabetically @@ -361,38 +414,42 @@ GotUpdates=($(sort <<<"${GotUpdates[*]}")) unset IFS # Run the prometheus exporter function -if [ -n "$CollectorTextFileDirectory" ] ; then - source "$ScriptWorkDir"/addons/prometheus/prometheus_collector.sh && prometheus_exporter ${#NoUpdates[@]} ${#GotUpdates[@]} ${#GotErrors[@]} +if [[ -n "${CollectorTextFileDirectory:-}" ]]; then + if type -t send_notification &>/dev/null; then + prometheus_exporter ${#NoUpdates[@]} ${#GotUpdates[@]} ${#GotErrors[@]} + else + printf "%s\n" "Could not source prometheus exporter function." + fi fi # Define how many updates are available UpdCount="${#GotUpdates[@]}" # List what containers got updates or not -if [[ -n ${NoUpdates[*]} ]] ; then +if [[ -n ${NoUpdates[*]} ]]; then printf "\n%bContainers on latest version:%b\n" "$c_green" "$c_reset" printf "%s\n" "${NoUpdates[@]}" fi -if [[ -n ${GotErrors[*]} ]] ; then +if [[ -n ${GotErrors[*]} ]]; then printf "\n%bContainers with errors, won't get updated:%b\n" "$c_red" "$c_reset" printf "%s\n" "${GotErrors[@]}" printf "%binfo:%b 'unauthorized' often means not found in a public registry.\n" "$c_blue" "$c_reset" fi -if [[ -n ${GotUpdates[*]} ]] ; then +if [[ -n ${GotUpdates[*]} ]]; then printf "\n%bContainers with updates available:%b\n" "$c_yellow" "$c_reset" - [[ -z "$AutoUp" ]] && options || printf "%s\n" "${GotUpdates[@]}" - [[ -n "$Notify" ]] && { [[ $(type -t send_notification) == function ]] && send_notification "${GotUpdates[@]}" || printf "Could not source notification function.\n" ; } + [[ "$AutoMode" == false ]] && options || printf "%s\n" "${GotUpdates[@]}" + [[ "$Notify" == true ]] && { type -t send_notification &>/dev/null && send_notification "${GotUpdates[@]}" || printf "Could not source notification function.\n"; } fi # Optionally get updates if there's any -if [ -n "$GotUpdates" ] ; then - if [ -z "$AutoUp" ] ; then +if [[ -n "${GotUpdates:-}" ]]; then + if [[ "$AutoMode" == false ]]; then printf "\n%bChoose what containers to update.%b\n" "$c_teal" "$c_reset" choosecontainers else SelectedUpdates=( "${GotUpdates[@]}" ) fi - if [ "$AutoUp" == "${AutoUp#[Nn]}" ] ; then + if [[ "$DontUpdate" == false ]]; then NumberofUpdates="${#SelectedUpdates[@]}" CurrentQue=0 for i in "${SelectedUpdates[@]}" @@ -403,21 +460,21 @@ if [ -n "$GotUpdates" ] ; then 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="" + [[ "$ContPath" == "null" ]] && ContPath="" ContConfigFile=$($jqbin -r '."com.docker.compose.project.config_files"' <<< "$ContLabels") - [ "$ContConfigFile" == "null" ] && ContConfigFile="" + [[ "$ContConfigFile" == "null" ]] && ContConfigFile="" ContName=$($jqbin -r '."com.docker.compose.service"' <<< "$ContLabels") - [ "$ContName" == "null" ] && ContName="" + [[ "$ContName" == "null" ]] && ContName="" ContEnv=$($jqbin -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels") - [ "$ContEnv" == "null" ] && ContEnv="" + [[ "$ContEnv" == "null" ]] && ContEnv="" ContUpdateLabel=$($jqbin -r '."mag37.dockcheck.update"' <<< "$ContLabels") - [ "$ContUpdateLabel" == "null" ] && ContUpdateLabel="" + [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" ContRestartStack=$($jqbin -r '."mag37.dockcheck.restart-stack"' <<< "$ContLabels") - [ "$ContRestartStack" == "null" ] && ContRestartStack="" + [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" # Checking if compose-values are empty - hence started with docker run - if [ -z "$ContPath" ] ; then - if [ "$DRunUp" == "yes" ] ; then + 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 @@ -426,29 +483,30 @@ if [ -n "$GotUpdates" ] ; then continue fi # cd to the compose-file directory to account for people who use relative volumes - cd "$ContPath" || { echo "Path error - skipping $i" ; continue ; } + cd "$ContPath" || { echo "Path error - skipping $i"; continue; } ## Reformatting path + multi compose - if [[ $ContConfigFile = '/'* ]] ; then - CompleteConfs=$(for conf in ${ContConfigFile//,/ } ; do printf -- "-f %s " "$conf"; done) + 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) + 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" + [[ "$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 - if [ -n "$ContEnv" ]; then ContEnvs=$(for env in ${ContEnv//,/ } ; do printf -- "--env-file %s " "$env"; done) ; fi + ContEnvs="" + if [[ -n "$ContEnv" ]]; then ContEnvs=$(for env in ${ContEnv//,/ }; do printf -- "--env-file %s " "$env"; done); fi # Check if the whole stack should be restarted - if [[ "$ContRestartStack" == true ]] || [[ "$ForceRestartStacks" == true ]] ; then - $DockerBin ${CompleteConfs} stop ; $DockerBin ${CompleteConfs} ${ContEnvs} up -d + if [[ "$ContRestartStack" == true ]] || [[ "$ForceRestartStacks" == true ]]; then + ${DockerBin} ${CompleteConfs} stop; ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d else - $DockerBin ${CompleteConfs} ${ContEnvs} up -d ${ContName} + ${DockerBin} ${CompleteConfs} ${ContEnvs} up -d ${ContName} fi done printf "\n%bAll done!%b\n" "$c_green" "$c_reset" - if [[ -z "$AutoPrune" ]] && [[ -z "$AutoUp" ]]; then read -r -p "Would you like to prune dangling images? y/[n]: " AutoPrune ; fi - [[ "$AutoPrune" =~ [yY] ]] && docker image prune -f + if [[ "$AutoPrune" == false ]] && [[ "$AutoMode" == false ]]; then read -r -p "Would you like to prune dangling images? y/[n]: " AutoPrune; fi + if [[ "$AutoPrune" == true ]] || [[ "$AutoPrune" =~ [yY] ]]; then docker image prune -f; fi else printf "\nNo updates installed, exiting.\n" fi diff --git a/notify_templates/notify_slack.sh b/notify_templates/notify_slack.sh new file mode 100644 index 0000000..56ed28b --- /dev/null +++ b/notify_templates/notify_slack.sh @@ -0,0 +1,52 @@ +### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. +NOTIFY_SLACK_VERSION="v0.1" +# +# 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 + +FromHost=$(hostname) + +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" + + 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 +}