diff --git a/notify_templates/notify_discord.sh b/notify_templates/notify_discord.sh index b29966a..c027420 100644 --- a/notify_templates/notify_discord.sh +++ b/notify_templates/notify_discord.sh @@ -1,28 +1,49 @@ -### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -# -# 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 +#!/usr/bin/env bash -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) +# Discord notification template for podcheck v2 +# Requires: DISCORD_WEBHOOK_URL - echo "$UpdToString" - FromHost=$(hostname) - - # platform specific notification code would go here - printf "\nSending Discord notification\n" - - # Setting the MessageBody variable here. - MessageBody="🐋 Containers on $FromHost with updates available: \n$UpdToString" - - # Modify to fit your setup: - DiscordWebhookUrl="PasteYourFullDiscordWebhookURL" - - MsgBody="{\"username\":\"$FromHost\",\"content\":\"$MessageBody\"}" - - curl -sS -o /dev/null --fail -X POST -H "Content-Type: application/json" -d "$MsgBody" "$DiscordWebhookUrl" +if [[ -z "${DISCORD_WEBHOOK_URL:-}" ]]; then + echo "Error: DISCORD_WEBHOOK_URL not configured" + return 1 +fi +# Prepare the Discord message +if [[ -n "${NOTIFICATION_MESSAGE:-}" ]]; then + # Format message for Discord - escape quotes and newlines + discord_content="${NOTIFICATION_MESSAGE}" + discord_content="${discord_content//\"/\\\"}" + discord_content="${discord_content//$'\n'/\\n}" + + # Create Discord webhook payload + discord_payload=$(cat </dev/null; then + return 0 + else + echo "Failed to send Discord notification" + return 1 + fi +else + echo "No notification message provided" + return 1 +fi diff --git a/notify_templates/notify_gotify.sh b/notify_templates/notify_gotify.sh index 49d07ae..ebf176d 100644 --- a/notify_templates/notify_gotify.sh +++ b/notify_templates/notify_gotify.sh @@ -1,29 +1,30 @@ -### DISCLAIMER: This is a third party addition to dockcheck - best effort testing. -# -# 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. +#!/usr/bin/env bash -send_notification() { - [ -s "$ScriptWorkDir"/urls.list ] && releasenotes || Updates=("$@") - UpdToString=$( printf '%s\\n' "${Updates[@]}" ) - FromHost=$(hostname) +# Gotify notification template for podcheck v2 +# Requires: GOTIFY_DOMAIN, GOTIFY_TOKEN - # platform specific notification code would go here - printf "\nSending Gotify notification\n" +if [[ -z "${GOTIFY_DOMAIN:-}" ]] || [[ -z "${GOTIFY_TOKEN:-}" ]]; then + echo "Error: GOTIFY_DOMAIN and GOTIFY_TOKEN must be configured" + return 1 +fi - # Setting the MessageTitle and MessageBody variable here. - MessageTitle="${FromHost} - updates available." - printf -v MessageBody "Containers on $FromHost with updates available:\n$UpdToString" - - # Modify to fit your setup: - GotifyToken="Your Gotify token here" - GotifyUrl="https://api.gotify/message?token=${GotifyToken}" - - curl \ - -F "title=${MessageTitle}" \ - -F "message=${MessageBody}" \ - -F "priority=5" \ - -X POST "${GotifyUrl}" 1> /dev/null - -} +# Prepare the Gotify message +if [[ -n "${NOTIFICATION_MESSAGE:-}" ]]; then + # Build Gotify URL + gotify_url="${GOTIFY_DOMAIN}/message?token=${GOTIFY_TOKEN}" + + # Send to Gotify + if curl -F "title=${NOTIFICATION_TITLE:-Podcheck Notification}" \ + -F "message=${NOTIFICATION_MESSAGE}" \ + -F "priority=5" \ + -X POST "${gotify_url}" \ + ${CurlArgs:-} &>/dev/null; then + return 0 + else + echo "Failed to send Gotify notification" + return 1 + fi +else + echo "No notification message provided" + return 1 +fi diff --git a/notify_templates/notify_v2.sh b/notify_templates/notify_v2.sh new file mode 100644 index 0000000..ef3556c --- /dev/null +++ b/notify_templates/notify_v2.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash + +# notify_v2.sh - Advanced notification wrapper for podcheck +# This is the main notification dispatch system + +# Get hostname for notifications +# Try multiple methods to get hostname +if [[ -s "/etc/hostname" ]]; then + HOSTNAME=$(cat /etc/hostname) +elif command -v hostname &>/dev/null; then + HOSTNAME=$(hostname) +else + HOSTNAME="podcheck-host" +fi + +# Default notification channels if not configured +NOTIFY_CHANNELS=${NOTIFY_CHANNELS:-""} + +# Check if any channels are configured +if [[ -z "${NOTIFY_CHANNELS}" ]]; then + echo "No notification channels configured. Set NOTIFY_CHANNELS in podcheck.config" + return 1 +fi + +# Function to check if a notification should be sent based on snooze +should_send_notification() { + local notification_type="$1" + local snooze_file="${ScriptWorkDir}/.snooze_${notification_type}" + local current_time=$(date +%s) + + # If snooze is not enabled, always send + if [[ -z "${SNOOZE_SECONDS:-}" ]]; then + return 0 + fi + + # Check if snooze file exists and is recent + if [[ -f "$snooze_file" ]]; then + local last_notification=$(cat "$snooze_file" 2>/dev/null || echo "0") + local time_diff=$((current_time - last_notification)) + + if [[ $time_diff -lt ${SNOOZE_SECONDS} ]]; then + return 1 # Don't send notification + fi + fi + + # Update snooze file + echo "$current_time" > "$snooze_file" + return 0 # Send notification +} + +# Function to format output based on type +format_output() { + local format="$1" + local title="$2" + shift 2 + local containers=("$@") + + case "$format" in + "json") + local container_json="" + for container in "${containers[@]}"; do + if [[ -n "$container_json" ]]; then + container_json="${container_json}," + fi + container_json="${container_json}\"${container}\"" + done + echo "{\"title\":\"${title}\",\"hostname\":\"${HOSTNAME}\",\"containers\":[${container_json}]}" + ;; + "csv") + local container_csv="" + for container in "${containers[@]}"; do + if [[ -n "$container_csv" ]]; then + container_csv="${container_csv}," + fi + container_csv="${container_csv}${container}" + done + echo "${title},${HOSTNAME},${container_csv}" + ;; + "text"|*) + echo "${title} on ${HOSTNAME}:" + for container in "${containers[@]}"; do + echo " - ${container}" + done + ;; + esac +} + +# Main notification function for container updates +send_notification() { + local containers=("$@") + + # If no containers provided, exit early unless ALLOWEMPTY is set + if [[ ${#containers[@]} -eq 0 ]]; then + # Check if any channel allows empty notifications + local send_empty=false + for channel in ${NOTIFY_CHANNELS}; do + local channel_upper=$(echo "$channel" | tr '[:lower:]' '[:upper:]') + local allow_empty_var="${channel_upper}_ALLOWEMPTY" + if [[ "${!allow_empty_var:-false}" == "true" ]]; then + send_empty=true + break + fi + done + + if [[ "$send_empty" == "false" ]]; then + return 0 + fi + fi + + # Check snooze for container notifications + if ! should_send_notification "containers"; then + echo "Container update notification snoozed" + return 0 + fi + + # Send notifications to each configured channel + for channel in ${NOTIFY_CHANNELS}; do + local channel_upper=$(echo "$channel" | tr '[:lower:]' '[:upper:]') + + # Check if this channel should skip snooze + local skip_snooze_var="${channel_upper}_SKIPSNOOZE" + if [[ "${!skip_snooze_var:-false}" == "true" ]]; then + # Force send by updating snooze file + echo "$(date +%s)" > "${ScriptWorkDir}/.snooze_containers" + fi + + # Check if this channel is containers only + local containers_only_var="${channel_upper}_CONTAINERSONLY" + if [[ "${!containers_only_var:-false}" == "true" && ${#containers[@]} -eq 0 ]]; then + continue + fi + + # Get the template to use (default to channel name) + local template_var="${channel_upper}_TEMPLATE" + local template="${!template_var:-$channel}" + + # Get output format + local output_var="${channel_upper}_OUTPUT" + local output_format="${!output_var:-text}" + + # Format the message + local title="Containers with updates available" + if [[ ${#containers[@]} -eq 0 ]]; then + title="No container updates available" + fi + + local message=$(format_output "$output_format" "$title" "${containers[@]}") + + # Source and execute the notification template + local template_file="${ScriptWorkDir}/notify_templates/notify_${template}.sh" + if [[ -f "$template_file" ]]; then + # Export message for template to use + export NOTIFICATION_MESSAGE="$message" + export NOTIFICATION_TITLE="$title" + export NOTIFICATION_CONTAINERS=("${containers[@]}") + + if source "$template_file"; then + echo "Notification sent via $channel ($template)" + else + echo "Failed to send notification via $channel ($template)" + fi + else + echo "Notification template not found: $template_file" + fi + done +} + +# Function for podcheck self-update notifications +podcheck_notification() { + local current_version="$1" + local latest_version="$2" + local changes="$3" + + # Check if podcheck notifications are disabled + if [[ "${DISABLE_PODCHECK_NOTIFICATION:-false}" == "true" ]]; then + return 0 + fi + + # Check snooze + if ! should_send_notification "podcheck"; then + echo "Podcheck update notification snoozed" + return 0 + fi + + local title="Podcheck update available" + local message="$title: $current_version → $latest_version" + if [[ -n "$changes" ]]; then + message="$message\nChanges: $changes" + fi + + # Send to configured channels + for channel in ${NOTIFY_CHANNELS}; do + local channel_upper=$(echo "$channel" | tr '[:lower:]' '[:upper:]') + + # Get the template to use + local template_var="${channel_upper}_TEMPLATE" + local template="${!template_var:-$channel}" + + # Get output format + local output_var="${channel_upper}_OUTPUT" + local output_format="${!output_var:-text}" + + local formatted_message=$(format_output "$output_format" "$title" "podcheck: $current_version → $latest_version") + + # Source and execute the notification template + local template_file="${ScriptWorkDir}/notify_templates/notify_${template}.sh" + if [[ -f "$template_file" ]]; then + export NOTIFICATION_MESSAGE="$formatted_message" + export NOTIFICATION_TITLE="$title" + + if source "$template_file"; then + echo "Podcheck update notification sent via $channel" + else + echo "Failed to send podcheck notification via $channel" + fi + fi + done +} + +# Function for notify template update notifications +notify_update_notification() { + # Check if notify notifications are disabled + if [[ "${DISABLE_NOTIFY_NOTIFICATION:-false}" == "true" ]]; then + return 0 + fi + + # Check snooze + if ! should_send_notification "notify"; then + echo "Notify template update notification snoozed" + return 0 + fi + + local title="Notification templates updated" + local message="Notification templates have been updated" + + # Send to configured channels + for channel in ${NOTIFY_CHANNELS}; do + local channel_upper=$(echo "$channel" | tr '[:lower:]' '[:upper:]') + + local template_var="${channel_upper}_TEMPLATE" + local template="${!template_var:-$channel}" + + local template_file="${ScriptWorkDir}/notify_templates/notify_${template}.sh" + if [[ -f "$template_file" ]]; then + export NOTIFICATION_MESSAGE="$message" + export NOTIFICATION_TITLE="$title" + + if source "$template_file"; then + echo "Notify update notification sent via $channel" + fi + fi + done +} \ No newline at end of file diff --git a/podcheck.sh b/podcheck.sh index ba8fe18..0c6c0b6 100755 --- a/podcheck.sh +++ b/podcheck.sh @@ -3,7 +3,7 @@ set -euo pipefail shopt -s nullglob shopt -s failglob -VERSION="v0.7.1-podman" +VERSION="v0.7.1" # Variables for self-updating ScriptArgs=( "$@" ) @@ -267,34 +267,66 @@ choosecontainers() { 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() { - if [[ -z "${DaysOld:-}" ]]; then - return 0 - fi - if ! ImageDate=$($regbin -v error image inspect "$RepoUrl" --format='{{.Created}}' 2>/dev/null | cut -d" " -f1); then - return 1 - fi - ImageAge=$(( ( $(date +%s) - $(date -d "$ImageDate" +%s) ) / 86400 )) - if [ "$ImageAge" -gt "$DaysOld" ]; then + 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 } -progress_bar() { - QueCurrent="$1" - QueTotal="$2" - ((Percent=100*QueCurrent/QueTotal)) - ((Complete=50*Percent/100)) - ((Left=50-Complete)) - BarComplete=$(printf "%${Complete}s" | tr " " "#") - BarLeft=$(printf "%${Left}s" | tr " " "-") - # Remove the duplicate "Processing container" output - printf "\r[%s%s] %s/%s %bProcessing container: %s%b\n" \ - "$BarComplete" "$BarLeft" "$QueCurrent" "$QueTotal" \ - "$c_blue" "$container" "$c_reset" -} + t_out=$(command -v timeout 2>/dev/null || echo "") if [[ -n "$t_out" ]]; then @@ -343,56 +375,91 @@ distro_checker() { fi } -# Dependency check for jq -if command -v jq &>/dev/null; then - jqbin="jq" -elif [[ -f "$ScriptWorkDir/jq" ]]; then - jqbin="$ScriptWorkDir/jq" -else - printf "%s\n" "Required dependency 'jq' missing, do you want to install it?" - read -r -p "y: With packagemanager (sudo). / s: Download static binary. y/s/[n] " GetJq - GetJq=${GetJq:-no} - if [[ "$GetJq" =~ [yYsS] ]]; then - [[ "$GetJq" =~ [yY] ]] && distro_checker - if [[ -n "$PkgInstaller" && "$PkgInstaller" != "ERROR" ]]; then - (sudo $PkgInstaller jq) - PkgExitcode="$?" - [[ "$PkgExitcode" == 0 ]] && jqbin="jq" || printf "\n%bPackagemanager install failed%b, falling back to static binary.\n" "$c_yellow" "$c_reset" - fi - if [[ "$GetJq" =~ [nN] || "$PkgInstaller" == "ERROR" || "$PkgExitcode" != 0 ]]; then - binary_downloader "jq" "https://github.com/jqlang/jq/releases/latest/download/jq-linux-TEMP" - [[ -f "$ScriptWorkDir/jq" ]] && jqbin="$ScriptWorkDir/jq" - fi - else - printf "\n%bDependency missing, exiting.%b\n" "$c_red" "$c_reset" - exit 1 +} + +# 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 -fi + [[ -f "$ScriptWorkDir/$BinaryName" ]] && chmod +x "$ScriptWorkDir/$BinaryName" +} -$jqbin --version &>/dev/null || { printf "%s\n" "jq is not working - try to remove it and re-download it, exiting."; exit 1; } - -# Dependency check for regctl -if command -v regctl &>/dev/null; then - regbin="regctl" -elif [[ -f "$ScriptWorkDir/regctl" ]]; then - regbin="$ScriptWorkDir/regctl" -else - read -r -p "Required dependency 'regctl' missing, do you want it downloaded? y/[n] " GetRegctl - if [[ "$GetRegctl" =~ [yY] ]]; then - binary_downloader "regctl" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" - if [[ -f "$ScriptWorkDir/regctl" ]]; then - regbin="$ScriptWorkDir/regctl" - else - printf "\n%bFailed to download regctl, exiting.%b\n" "$c_red" "$c_reset" - exit 1 - fi - else - printf "\n%bDependency missing, exiting.%b\n" "$c_red" "$c_reset" - exit 1 +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 -fi +} -$regbin version &>/dev/null || { printf "%s\n" "regctl is not working - try to remove it and re-download it, exiting."; exit 1; } +# 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" # Check podman compose binary if podman compose version &>/dev/null; then