mirror of
https://github.com/mag37/dockcheck.git
synced 2026-02-15 07:48:14 +01:00
- Added advanced notification system with notify_v2.sh wrapper - Implemented async processing with xargs support (-x option) - Added comprehensive configuration system (default.config) - Enhanced command line options: -F, -I, -M, -u, -x - Added snooze functionality for notifications - Improved dependency management with auto-downloads - Added helper functions: releasenotes(), list_options(), progress_bar() - Updated README to match dockcheck style and documentation - Fixed label references to use sudo-kraken.podcheck namespace - Enhanced logo styling with rounded corners and cropped display - Maintained podman-specific functionality for compose and systemd units Brings podcheck to feature parity with dockcheck v0.7.1 while preserving podman ecosystem compatibility including Quadlet support.
830 lines
32 KiB
Bash
Executable file
830 lines
32 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
shopt -s failglob
|
|
|
|
VERSION="v0.7.1"
|
|
|
|
# Variables for self-updating
|
|
ScriptArgs=( "$@" )
|
|
ScriptPath="$(readlink -f "$0")"
|
|
ScriptWorkDir="$(dirname "$ScriptPath")"
|
|
|
|
# ChangeNotes: Sync with dockcheck v0.7.1 - Added advanced notifications, async processing, configuration system
|
|
Github="https://github.com/sudo-kraken/podcheck"
|
|
RawUrl="https://raw.githubusercontent.com/sudo-kraken/podcheck/main/podcheck.sh"
|
|
|
|
# Source helper functions
|
|
source_if_exists_or_fail() {
|
|
if [[ -s "$1" ]]; then
|
|
source "$1"
|
|
[[ "${DisplaySourcedFiles:-false}" == true ]] && echo " * sourced config: ${1}"
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# User customizable defaults
|
|
source_if_exists_or_fail "${HOME}/.config/podcheck.config" || source_if_exists_or_fail "${ScriptWorkDir}/podcheck.config"
|
|
|
|
cleanup() {
|
|
# Temporarily disable failglob for cleanup
|
|
shopt -u failglob
|
|
|
|
# Remove temporary files if any
|
|
rm -f /tmp/podcheck-* 2>/dev/null
|
|
# Remove backup file if update failed
|
|
[ -f "$ScriptPath.bak" ] && rm -f "$ScriptPath.bak"
|
|
# Clean up any temporary downloaded binaries
|
|
[ -f "/tmp/regctl.tmp" ] && rm -f "/tmp/regctl.tmp"
|
|
[ -f "/tmp/jq.tmp" ] && rm -f "/tmp/jq.tmp"
|
|
|
|
# Re-enable failglob
|
|
shopt -s failglob
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# Check if there's a new release of the script
|
|
LatestRelease="$(curl -s -r 0-100 "$RawUrl" | sed -n "/VERSION/s/VERSION=//p" | tr -d '"')"
|
|
LatestChanges="$(curl -s -r 0-200 "$RawUrl" | sed -n "/ChangeNotes/s/# ChangeNotes: //p")"
|
|
|
|
# After getting LatestRelease
|
|
if [[ -n "$LatestRelease" && "$LatestRelease" != "$VERSION" ]]; then
|
|
printf "\nNew version available: %s\nCurrent version: %s\nChanges: %s\n" \
|
|
"$LatestRelease" "$VERSION" "$LatestChanges"
|
|
read -r -p "Do you want to update? [y/N] " update
|
|
if [[ "$update" =~ [yY] ]]; then
|
|
self_update
|
|
fi
|
|
fi
|
|
|
|
Help() {
|
|
echo "Syntax: podcheck.sh [OPTION] [comma separated names to include]"
|
|
echo "Example: podcheck.sh -y -x 10 -d 10 -e nextcloud,heimdall"
|
|
echo
|
|
echo "Options:"
|
|
echo "-a|y Automatic updates, without interaction."
|
|
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."
|
|
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 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)."
|
|
echo "-n No updates; only checking availability without interaction."
|
|
echo "-p Auto-prune dangling images after update."
|
|
echo "-r Allow checking for updates/updating images for podman run containers. Won't update the container."
|
|
echo "-s Include stopped containers in the check. (Logic: podman 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."
|
|
echo "-v Prints current version."
|
|
echo "-x N Set max asynchronous subprocesses, 1 default, 0 to disable, 32+ tested."
|
|
echo
|
|
echo "Project source: $Github"
|
|
}
|
|
|
|
# Colours
|
|
c_red="\033[0;31m"
|
|
c_green="\033[0;32m"
|
|
c_yellow="\033[0;33m"
|
|
c_blue="\033[0;34m"
|
|
c_teal="\033[0;36m"
|
|
c_reset="\033[0m"
|
|
|
|
# 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:-""}
|
|
CollectorTextFileDirectory=${CollectorTextFileDirectory:-}
|
|
Exclude=${Exclude:-}
|
|
DaysOld=${DaysOld:-}
|
|
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=""
|
|
|
|
set -euo pipefail
|
|
|
|
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 ;;
|
|
m) MonoMode=true ;;
|
|
M) PrintMarkdownURL=true ;;
|
|
n) DontUpdate=true; AutoMode=true;;
|
|
p) AutoPrune=true ;;
|
|
r) DRunUp=true ;;
|
|
s) Stopped="-a" ;;
|
|
t) Timeout="${OPTARG}" ;;
|
|
u) AutoSelfUpdate=true ;;
|
|
v) printf "%s\n" "$VERSION"; exit 0 ;;
|
|
x) MaxAsync=${OPTARG} ;;
|
|
h|*) Help; exit 2 ;;
|
|
esac
|
|
done
|
|
shift "$((OPTIND-1))"
|
|
|
|
# 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")"
|
|
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 podcheck.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
|
|
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"
|
|
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
|
|
|
|
exec_if_exists() {
|
|
if [[ $(type -t $1) == function ]]; then "$@"; fi
|
|
}
|
|
|
|
exec_if_exists_or_fail() {
|
|
[[ $(type -t $1) == function ]] && "$@"
|
|
}
|
|
|
|
# Now get the search name from the first remaining positional parameter
|
|
SearchName="${1:-}"
|
|
|
|
# Self-update functions
|
|
self_update_curl() {
|
|
cp "$ScriptPath" "$ScriptPath".bak
|
|
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[@]}"
|
|
exit 1
|
|
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[@]}"
|
|
exit 1
|
|
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 &>/dev/null && [[ "$(git ls-remote --get-url 2>/dev/null)" =~ .*"sudo-kraken/podcheck".* ]]; then
|
|
printf "\n%s\n" "Pulling the latest version."
|
|
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; }
|
|
exec "$ScriptPath" "${ScriptArgs[@]}"
|
|
exit 1
|
|
else
|
|
cd - || { printf "Path error.\n"; return; }
|
|
self_update_curl
|
|
fi
|
|
}
|
|
|
|
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
|
|
exit 0
|
|
elif [[ "$Choice" =~ [aAyY] ]]; then
|
|
SelectedUpdates=( "${GotUpdates[@]}" )
|
|
ChoiceClean=${Choice//[,.:;]/ }
|
|
else
|
|
ChoiceClean=${Choice//[,.:;]/ }
|
|
for CC in $ChoiceClean; do
|
|
if [[ "$CC" -lt 1 || "$CC" -gt $UpdCount ]]; then
|
|
echo "Number not in list: $CC"
|
|
unset ChoiceClean
|
|
break 1
|
|
else
|
|
SelectedUpdates+=( "${GotUpdates[$CC-1]}" )
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
printf "\nUpdating containers:\n"
|
|
printf "%s\n" "${SelectedUpdates[@]}"
|
|
printf "\n"
|
|
}
|
|
|
|
# Function to add user-provided urls to releasenotes
|
|
releasenotes() {
|
|
unset Updates
|
|
for update in "${GotUpdates[@]}"; do
|
|
found=false
|
|
while read -r container url; do
|
|
if [[ "$update" == "$container" ]] && [[ "$PrintMarkdownURL" == true ]]; then
|
|
Updates+=("- [$update]($url)"); found=true;
|
|
elif [[ "$update" == "$container" ]]; then
|
|
Updates+=("$update -> $url"); found=true;
|
|
fi
|
|
done < "${ScriptWorkDir}/urls.list"
|
|
if [[ "$found" == false ]] && [[ "$PrintMarkdownURL" == true ]]; then
|
|
Updates+=("- $update -> url missing");
|
|
elif [[ "$found" == false ]]; then
|
|
Updates+=("$update -> url missing");
|
|
else
|
|
continue;
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Numbered List function
|
|
# if urls.list exists add release note url per line
|
|
list_options() {
|
|
num=1
|
|
for update in "${Updates[@]}"; do
|
|
echo "$num) $update"
|
|
((num++))
|
|
done
|
|
}
|
|
|
|
progress_bar() {
|
|
QueCurrent="$1"
|
|
QueTotal="$2"
|
|
BarWidth=${BarWidth:-50}
|
|
((Percent=100*QueCurrent/QueTotal))
|
|
((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 " " "-")
|
|
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
|
|
}
|
|
|
|
datecheck() {
|
|
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
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
t_out=$(command -v timeout 2>/dev/null || echo "")
|
|
if [[ -n "$t_out" ]]; then
|
|
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}"
|
|
fi
|
|
else
|
|
t_out=""
|
|
fi
|
|
|
|
binary_downloader() {
|
|
BinaryName="$1"
|
|
BinaryUrl="$2"
|
|
case "$(uname --machine)" in
|
|
x86_64|amd64) architecture="amd64" ;;
|
|
arm64|aarch64) architecture="arm64" ;;
|
|
*) 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"
|
|
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="dnf install"
|
|
elif [[ -f /etc/SuSE-release ]]; then
|
|
PkgInstaller="zypper install"
|
|
elif [[ -f /etc/debian_version ]]; then
|
|
PkgInstaller="apt-get install"
|
|
else
|
|
PkgInstaller="ERROR"
|
|
printf "\n%bNo distribution could be determined%b, falling back to static binary.\n" "$c_yellow" "$c_reset"
|
|
fi
|
|
}
|
|
|
|
# Static binary downloader for dependencies
|
|
binary_downloader() {
|
|
BinaryName="$1"
|
|
BinaryUrl="$2"
|
|
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;;
|
|
esac
|
|
GetUrl="${BinaryUrl/TEMP/"$architecture"}"
|
|
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
|
|
[[ -f "$ScriptWorkDir/$BinaryName" ]] && chmod +x "$ScriptWorkDir/$BinaryName"
|
|
}
|
|
|
|
distro_checker() {
|
|
isRoot=false
|
|
[[ ${EUID:-} == 0 ]] && isRoot=true
|
|
if [[ -f /etc/alpine-release ]] ; then
|
|
[[ "$isRoot" == true ]] && PkgInstaller="apk add" || PkgInstaller="doas apk add"
|
|
elif [[ -f /etc/arch-release ]]; then
|
|
[[ "$isRoot" == true ]] && PkgInstaller="pacman -S" || PkgInstaller="sudo pacman -S"
|
|
elif [[ -f /etc/debian_version ]]; then
|
|
[[ "$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
|
|
[[ "$isRoot" == true ]] && PkgInstaller="zypper install" || PkgInstaller="sudo zypper 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
|
|
}
|
|
|
|
# Dependency check + installer function
|
|
dependency_check() {
|
|
AppName="$1"
|
|
AppVar="$2"
|
|
AppUrl="$3"
|
|
if command -v "$AppName" &>/dev/null; then
|
|
export "$AppVar"="$AppName";
|
|
elif [[ -f "$ScriptWorkDir/$AppName" ]]; then
|
|
export "$AppVar"="$ScriptWorkDir/$AppName";
|
|
else
|
|
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
|
|
[[ "$GetBin" =~ [yY] ]] && distro_checker
|
|
if [[ -n "${PkgInstaller:-}" && "${PkgInstaller:-}" != "ERROR" ]]; then
|
|
[[ $(uname -s) == "Darwin" && "$AppName" == "regctl" ]] && AppName="regclient"
|
|
if $PkgInstaller "$AppName"; then
|
|
AppName="$1"
|
|
export "$AppVar"="$AppName"
|
|
printf "\n%b%b installed.%b\n" "$c_green" "$AppName" "$c_reset"
|
|
else
|
|
PkgInstaller="ERROR"
|
|
printf "\n%bPackagemanager install failed%b, falling back to static binary.\n" "$c_yellow" "$c_reset"
|
|
fi
|
|
fi
|
|
if [[ "$GetBin" =~ [sS] ]] || [[ "$PkgInstaller" == "ERROR" ]]; then
|
|
binary_downloader "$AppName" "$AppUrl"
|
|
[[ -f "$ScriptWorkDir/$AppName" ]] && { export "$AppVar"="$ScriptWorkDir/$1" && printf "\n%b%s downloaded.%b\n" "$c_green" "$AppName" "$c_reset"; }
|
|
fi
|
|
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; }
|
|
}
|
|
|
|
# Use the new dependency management system
|
|
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"
|
|
|
|
# Version check & initiate self update
|
|
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 podcheck_notification "$VERSION" "$LatestRelease" "$LatestChanges" || printf "Could not source notification function.\n"; }
|
|
fi
|
|
fi
|
|
else
|
|
printf "ERROR: Failed to curl latest Podcheck.sh release version.\n"
|
|
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"; }
|
|
|
|
# Check podman compose binary
|
|
podman info &>/dev/null || { printf "\n%bYour current user does not have permissions to the podman socket - may require root / podman group. Exiting.%b\n" "$c_red" "$c_reset"; exit 1; }
|
|
if podman compose version &>/dev/null; then
|
|
PodmanBin="podman compose" ;
|
|
elif podman-compose version &>/dev/null; then
|
|
PodmanBin="podman-compose" ;
|
|
elif podman -v &>/dev/null; then
|
|
printf "%s\n" "No podman compose binary available, using plain podman (Not recommended!)"
|
|
printf "%s\n" "'podman run' will ONLY update images, not the container itself."
|
|
else
|
|
printf "%s\n" "No podman binaries available, exiting."
|
|
exit 1
|
|
fi
|
|
|
|
# Listing typed exclusions
|
|
if [[ -n ${Excludes[*]:-} ]]; then
|
|
printf "\n%bExcluding these names:%b\n" "$c_blue" "$c_reset"
|
|
printf "%s\n" "${Excludes[@]}"
|
|
printf "\n"
|
|
fi
|
|
|
|
|
|
|
|
if [[ -n "${Excludes[*]}" ]]; then
|
|
printf "\n%bExcluding these names:%b\n" "$c_blue" "$c_reset"
|
|
printf "%s\n" "${Excludes[@]}"
|
|
printf "\n"
|
|
fi
|
|
|
|
ContCount=$(podman ps $Stopped --filter "name=$SearchName" --format '{{.Names}}' | wc -l)
|
|
RegCheckQue=0
|
|
start_time=$(date +%s)
|
|
|
|
printf "\n%bStarting container update check%b\n" "$c_blue" "$c_reset"
|
|
|
|
# Testing and setting timeout binary
|
|
t_out=$(command -v timeout || echo "")
|
|
if [[ $t_out ]]; then
|
|
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}"
|
|
fi
|
|
else
|
|
t_out=""
|
|
fi
|
|
|
|
check_image() {
|
|
i="$1"
|
|
local Excludes=($Excludes_string)
|
|
for e in "${Excludes[@]}"; do
|
|
if [[ "$i" == "$e" ]]; then
|
|
printf "%s\n" "Skip $i"
|
|
return
|
|
fi
|
|
done
|
|
|
|
# Skipping non-compose containers unless option is set
|
|
ContLabels=$(podman 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=$(podman inspect "$i" --format='{{.Image}}')
|
|
RepoUrl=$(podman inspect "$i" --format='{{.Config.Image}}')
|
|
LocalHash=$(podman 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
|
|
printf "%s\n" "NoUpdates $i"
|
|
else
|
|
if [[ -n "${DaysOld:-}" ]] && ! datecheck; then
|
|
printf "%s\n" "NoUpdates +$i ${ImageAge}d"
|
|
else
|
|
printf "%s\n" "GotUpdates $i"
|
|
fi
|
|
fi
|
|
else
|
|
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 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
|
|
XargsAsync="-P $MaxAsync"
|
|
else
|
|
XargsAsync=""
|
|
[[ "$MaxAsync" != 0 ]] && printf "%bMissing POSIX xargs, consider installing 'findutils' for asynchronous lookups.%b\n" "$c_yellow" "$c_reset"
|
|
fi
|
|
|
|
# Variables for progress_bar function
|
|
ContCount=$(podman ps $Stopped --filter "name=$SearchName" --format '{{.Names}}' | wc -l)
|
|
RegCheckQue=0
|
|
|
|
# Asynchronously check the image-hash of every running container VS the registry
|
|
while read -r line; do
|
|
((RegCheckQue+=1))
|
|
if [[ "$MonoMode" == false ]]; then progress_bar "$RegCheckQue" "$ContCount"; fi
|
|
|
|
Got=${line%% *} # Extracts the first word (NoUpdates, GotUpdates, GotErrors)
|
|
item=${line#* }
|
|
|
|
case "$Got" in
|
|
NoUpdates) NoUpdates+=("$item") ;;
|
|
GotUpdates) GotUpdates+=("$item") ;;
|
|
GotErrors) GotErrors+=("$item") ;;
|
|
Skip) ;;
|
|
*) echo "Error! Unexpected output from subprocess: ${line}" ;;
|
|
esac
|
|
done < <( \
|
|
podman ps $Stopped --filter "name=$SearchName" --format '{{.Names}}' | \
|
|
xargs $XargsAsync -I {} bash -c 'check_image "{}"' \
|
|
)
|
|
|
|
# Sort arrays alphabetically
|
|
IFS=$'\n'
|
|
NoUpdates=($(sort <<<"${NoUpdates[*]:-}"))
|
|
GotUpdates=($(sort <<<"${GotUpdates[*]:-}"))
|
|
unset IFS
|
|
|
|
# Run the prometheus exporter function
|
|
if [[ -n "${CollectorTextFileDirectory:-}" ]]; then
|
|
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
|
|
UpdCount="${#GotUpdates[@]}"
|
|
|
|
# List what containers got updates or not
|
|
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
|
|
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
|
|
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 ]] && { 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
|
|
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 [[ "$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
|
|
((CurrentQue+=1))
|
|
printf "\n%bNow updating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset"
|
|
ContLabels=$(podman inspect "$i" --format '{{json .Config.Labels}}')
|
|
ContImage=$(podman inspect "$i" --format='{{.Config.Image}}')
|
|
ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels")
|
|
[[ "$ContPath" == "null" ]] && ContPath=""
|
|
ContUpdateLabel=$($jqbin -r '."sudo-kraken.podcheck.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 podman run
|
|
if [[ -z "$ContPath" ]]; then
|
|
if [[ "$DRunUp" == true ]]; then
|
|
podman pull "$ContImage"
|
|
printf "%s\n" "$i got a new image downloaded, rebuild manually with preferred 'podman run'-parameters"
|
|
else
|
|
printf "\n%b%s%b has no compose labels, probably started with podman run - %bskipping%b\n\n" "$c_yellow" "$i" "$c_reset" "$c_yellow" "$c_reset"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
podman pull "$ContImage" || { printf "\n%bPodman 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
|
|
ContLabels=$(podman inspect "$i" --format '{{json .Config.Labels}}')
|
|
ContImage=$(podman 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=""
|
|
ContUpdateLabel=$($jqbin -r '."sudo-kraken.podcheck.update"' <<< "$ContLabels")
|
|
[[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel=""
|
|
ContRestartStack=$($jqbin -r '."sudo-kraken.podcheck.restart-stack"' <<< "$ContLabels")
|
|
[[ "$ContRestartStack" == "null" ]] && ContRestartStack=""
|
|
ContOnlySpecific=$($jqbin -r '."sudo-kraken.podcheck.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 podman 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; }
|
|
## 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
|
|
${PodmanBin} ${CompleteConfs} stop; ${PodmanBin} ${CompleteConfs} ${ContEnvs} up -d || { printf "\n%bPodman error, exiting!%b\n" "$c_red" "$c_reset" ; exit 1; }
|
|
else
|
|
${PodmanBin} ${CompleteConfs} ${ContEnvs} up -d ${SpecificContainer} || { printf "\n%bPodman 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
|
|
printf "\nAuto pruning.."; podman image prune -f;
|
|
fi
|
|
printf "\n%bAll done!%b\n" "$c_green" "$c_reset"
|
|
else
|
|
printf "\nNo updates installed, exiting.\n"
|
|
fi
|
|
else
|
|
printf "\nNo updates available, exiting.\n"
|
|
fi
|
|
|
|
exit 0
|
|
ContName=$($jqbin -r '."com.docker.compose.service"' <<< "$ContLabels")
|
|
[ "$ContName" == "null" ] && ContName=""
|
|
ContEnv=$($jqbin -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels")
|
|
[ "$ContEnv" == "null" ] && ContEnv=""
|
|
ContUpdateLabel=$($jqbin -r '."sudo-kraken.podcheck.update"' <<< "$ContLabels")
|
|
[ "$ContUpdateLabel" == "null" ] && ContUpdateLabel=""
|
|
ContRestartStack=$($jqbin -r '."sudo-kraken.podcheck.restart-stack"' <<< "$ContLabels")
|
|
[ "$ContRestartStack" == "null" ] && ContRestartStack=""
|
|
|
|
# Add spacing and colors to systemd unit detection
|
|
if [ -z "$ContPath" ]; then
|
|
printf "\n%bChecking systemd units for container: %s%b\n\n" \
|
|
"$c_teal" "$i" "$c_reset"
|
|
|
|
unit=$(podman inspect "$i" --format '{{.Config.Labels.PODMAN_SYSTEMD_UNIT}}')
|
|
if [ -n "$unit" ]; then
|
|
printf "%bDetected Quadlet-managed container: %s (unit: %s)%b\n\n" \
|
|
"$c_green" "$i" "$unit" "$c_reset"
|
|
|
|
printf "%bPulling new image...%b\n\n" "$c_teal" "$c_reset"
|
|
|
|
if podman pull "$ContImage"; then
|
|
printf "\n%bSuccessfully pulled new image%b\n\n" "$c_green" "$c_reset"
|
|
else
|
|
printf "\n%bFailed to pull image for %s%b\n\n" "$c_red" "$i" "$c_reset"
|
|
continue
|
|
fi
|
|
printf "%bAttempting to restart unit...%b\n\n" "$c_teal" "$c_reset"
|
|
|
|
if timeout 60 systemctl --user restart "$unit"; then
|
|
printf "\n%bQuadlet container %s updated and restarted (user scope)%b\n\n" \
|
|
"$c_green" "$i" "$c_reset"
|
|
else
|
|
printf "\n%bFailed to restart unit %s%b\n" "$c_red" "$unit" "$c_reset"
|
|
systemctl --user status "$unit"
|
|
fi
|
|
fi
|
|
continue
|
|
fi
|
|
cd "$ContPath" || { echo "Path error - skipping $i"; continue; }
|
|
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
|
|
printf "\n%bNow updating (%s/%s): %b%s%b\n" "$c_teal" "$CurrentQue" "$NumberofUpdates" "$c_blue" "$i" "$c_reset"
|
|
echo "Processing update for container: $i"
|
|
[[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != "true" ]] && { echo "No update label, skipping."; continue; } }
|
|
podman pull "$ContImage"
|
|
ContEnvs=""
|
|
if [ -n "$ContEnv" ]; then
|
|
ContEnvs=$(for env in ${ContEnv//,/ }; do printf -- "--env-file %s " "$env"; done)
|
|
fi
|
|
if [[ "$ContRestartStack" == "true" ]] || [[ "$ForceRestartPods" == true ]]; then
|
|
${PodmanBin} ${CompleteConfs} down
|
|
${PodmanBin} ${CompleteConfs} ${ContEnvs} up -d
|
|
else
|
|
${PodmanBin} ${CompleteConfs} ${ContEnvs} up -d ${ContName}
|
|
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 "\nAuto pruning.."; podman image prune -f;
|
|
fi
|
|
printf "\n%bAll done!%b\n" "$c_green" "$c_reset"
|
|
else
|
|
printf "\nNo updates installed, exiting.\n"
|
|
fi
|
|
else
|
|
printf "\nNo updates available, exiting.\n"
|
|
fi
|
|
|
|
exit 0
|