From a7dcd975b25732df8a162624542bc212eb934c30 Mon Sep 17 00:00:00 2001 From: Joe Harrison <53116754+sudo-kraken@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:12:01 +0000 Subject: [PATCH] Upstream patches and additional patching (#2) * Ensures DSM GUI refreshes its updates * Removed whale icon and changed verbosity * Added addon for Prometheus+node_exporter * Changed local image check to check on image ID rather than name * Update podcheck.sh changed docker->podman, typo * - **v0.6.0**: - **Grafana & Prometheus Integration:** - Added a detailed Prometheus metrics exporter that now reports not only the number of containers with updates, no-updates, and errors, but also the total number of containers checked, the duration of the update check, and the epoch timestamp of the last check. - Enhanced documentation with instructions on integrating these metrics with Grafana for visual monitoring. - **Improved Error Handling & Code Refactoring:** - Introduced `set -euo pipefail` and local variable scoping within functions to improve reliability and prevent unexpected behaviour. - Standardised container name handling and refined the Quadlet detection logic. - **Self-Update Enhancements:** - Updated the self-update mechanism to support both Git-based and HTTP-based updates, with an automatic restart that preserves the original arguments. - **Miscellaneous Improvements:** - Enhanced dependency installer to support both package manager and static binary installations for `jq` and `regctl`. - General code refactoring across the project for better readability and maintainability. * Update podcheck.sh * increment version * Update Quadlet detection logic Update Quadlet detection logic to support flexible service naming - Modified the quadlet update block to first try an exact match for "$i.service". - If no exact match is found, build a regex pattern from the container name (allowing underscores and hyphens interchangeably) and search user service units. - When multiple candidate units are found, the script attempts to choose the one that exactly matches (ignoring case) or defaults to the first candidate. - This update allows containers like "containera" to match service units named "container_a.service" and supports scenarios with multiple counterparts (e.g., matrix-a, matrix-b, matrix_db). * search name fix * fixes to arg parsing * Logic overhaul, verbose output and better syntax * Added support for prometheus --------- Co-authored-by: mag37 --- README.md | 21 + addons/prometheus/README.md | 61 +++ .../prometheus/grafana/grafana_dashboard.json | 382 +++++++++++++ .../prometheus/grafana/grafana_dashboard.png | Bin 0 -> 51198 bytes addons/prometheus/prometheus_collector.sh | 62 +++ extras/apprise_quickstart.md | 4 +- extras/dc_brief.sh | 51 -- extras/errorCheck.sh | 36 +- extras/pc_brief.sh | 61 +++ notify_templates/notify_DSM.sh | 3 + notify_templates/notify_gotify.sh | 4 +- podcheck.sh | 513 ++++++++++++------ 12 files changed, 953 insertions(+), 245 deletions(-) create mode 100644 addons/prometheus/README.md create mode 100644 addons/prometheus/grafana/grafana_dashboard.json create mode 100644 addons/prometheus/grafana/grafana_dashboard.png create mode 100644 addons/prometheus/prometheus_collector.sh delete mode 100755 extras/dc_brief.sh create mode 100644 extras/pc_brief.sh diff --git a/README.md b/README.md index 4170f35..16ca230 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,18 @@ ___ ## :bell: Changelog +- **v0.6.0**: + - **Grafana & Prometheus Integration:** + - Added a detailed Prometheus metrics exporter that now reports not only the number of containers with updates, no-updates, and errors, but also the total number of containers checked, the duration of the update check, and the epoch timestamp of the last check. + - Enhanced documentation with instructions on integrating these metrics with Grafana for visual monitoring. + - **Improved Error Handling & Code Refactoring:** + - Introduced `set -euo pipefail` and local variable scoping within functions to improve reliability and prevent unexpected behaviour. + - Standardised container name handling and refined the Quadlet detection logic. + - **Self-Update Enhancements:** + - Updated the self-update mechanism to support both Git-based and HTTP-based updates, with an automatic restart that preserves the original arguments. + - **Miscellaneous Improvements:** + - Enhanced dependency installer to support both package manager and static binary installations for `jq` and `regctl`. + - General code refactoring across the project for better readability and maintainability. - **v0.5.7**: Rewrite of dependency downloads, now jq can be installed with package manager or static binary. - **v0.5.6**: Directly checking for systemd units matching container names. - Improved Quadlet detection by checking for systemd units named after the container. @@ -133,6 +145,15 @@ it-tools -> https://github.com/CorentinTh/it-tools/releases ``` 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. +## :chart_with_upwards_trend: 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: +``` +dockcheck.sh -c /path/to/exporter/directory +``` +See the [README_prom.md](./addons/prometheus/README.md) for more detailed information on how to set it up! +Contributed by [tdralle](https://github.com/tdralle). + ## :bookmark: Labels Optionally, you can add labels to your containers to control how Podcheck handles them. Currently, these are the usable labels: diff --git a/addons/prometheus/README.md b/addons/prometheus/README.md new file mode 100644 index 0000000..3bc4f09 --- /dev/null +++ b/addons/prometheus/README.md @@ -0,0 +1,61 @@ +## [Prometheus](https://github.com/prometheus/prometheus) and [node_exporter](https://github.com/prometheus/node_exporter) +Podcheck check is capable to export metrics to prometheus via the text file collector provided by the node_exporter. +In order to do so the -c flag has to be specified followed by the file path that is configured in the text file collector of the node_exporter. +A simple cron job can be configured to export these metrics on a regular interval as shown in the sample below: + +``` +0 1 * * * /root/podcheck.sh -n -c /var/lib/node_exporter/textfile_collector +``` + +The following metrics are exported to prometheus + +``` +# HELP podcheck_images_analyzed Podman images that have been analyzed +# TYPE podcheck_images_analyzed gauge +podcheck_images_analyzed 22 +# HELP podcheck_images_outdated Podman images that are outdated +# TYPE podcheck_images_outdated gauge +podcheck_images_outdated 7 +# HELP podcheck_images_latest Podman images that are outdated +# TYPE podcheck_images_latest gauge +podcheck_images_latest 14 +# HELP podcheck_images_error Podman images with analysis errors +# TYPE podcheck_images_error gauge +podcheck_images_error 1 +# HELP podcheck_images_analyze_timestamp_seconds Last podcheck run time +# TYPE podcheck_images_analyze_timestamp_seconds gauge +podcheck_images_analyze_timestamp_seconds 1737924029 +``` + +Once those metrics are exported they can be used to define alarms as shown below + +``` +- alert: podcheck_images_outdated + expr: sum by(instance) (podcheck_images_outdated) > 0 + for: 15s + labels: + severity: warning + annotations: + summary: "{{ $labels.instance }} has {{ $value }} outdated podman images." + description: "{{ $labels.instance }} has {{ $value }} outdated podman images." +- alert: podcheck_images_error + expr: sum by(instance) (podcheck_images_error) > 0 + for: 15s + labels: + severity: warning + annotations: + summary: "{{ $labels.instance }} has {{ $value }} podman images having an error." + description: "{{ $labels.instance }} has {{ $value }} podman images having an error." +- alert: podcheck_image_last_analyze + expr: (time() - podcheck_images_analyze_timestamp_seconds) > (3600 * 24 * 3) + for: 15s + labels: + severity: warning + annotations: + summary: "{{ $labels.instance }} has not updated the podcheck statistics for more than 3 days." + description: "{{ $labels.instance }} has not updated the podcheck statistics for more than 3 days." +``` + +There is a reference Grafana dashboard in [grafana/grafana_dashboard.json](./grafana/grafana_dashboard.json). + +![](./grafana/grafana_dashboard.png) diff --git a/addons/prometheus/grafana/grafana_dashboard.json b/addons/prometheus/grafana/grafana_dashboard.json new file mode 100644 index 0000000..e731337 --- /dev/null +++ b/addons/prometheus/grafana/grafana_dashboard.json @@ -0,0 +1,382 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "11.4.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "last_analyze_timestamp" + }, + "properties": [ + { + "id": "unit", + "value": "dateTimeAsIso" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "last_analyze_since" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 259200 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "images_outdated" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "images_error" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "color-background" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + } + ] + } + ] + }, + "gridPos": { + "h": 14, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(instance) (podcheck_images_analyzed)", + "format": "table", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": true, + "interval": "", + "legendFormat": "{{instance}}", + "range": false, + "refId": "podcheck_images_analyzed", + "useBackend": false, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + } + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(instance) (podcheck_images_outdated)", + "format": "table", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{instance}}", + "range": false, + "refId": "podcheck_images_outdated", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(instance) (podcheck_images_latest)", + "format": "table", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "{{instance}}", + "range": false, + "refId": "podcheck_images_latest", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(instance) (podcheck_images_error)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{instance}}", + "range": false, + "refId": "podcheck_images_error" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "podcheck_images_analyze_timestamp_seconds * 1000", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{instance}}", + "range": false, + "refId": "podcheck_images_analyze_timestamp_seconds" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "time() - podcheck_images_analyze_timestamp_seconds", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{instance}}", + "range": false, + "refId": "podcheck_images_last_analyze" + } + ], + "title": "podcheck Status", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "job": true + }, + "includeByName": {}, + "indexByName": { + "Time": 0, + "Value #podcheck_images_analyze_timestamp_seconds": 2, + "Value #podcheck_images_analyzed": 4, + "Value #podcheck_images_error": 7, + "Value #podcheck_images_last_analyze": 3, + "Value #podcheck_images_latest": 5, + "Value #podcheck_images_outdated": 6, + "instance": 1, + "job": 8 + }, + "renameByName": { + "Value #A": "analyze_timestamp", + "Value #podcheck_images_analyze_timestamp_seconds": "last_analyze_timestamp", + "Value #podcheck_images_analyzed": "images_analyzed", + "Value #podcheck_images_error": "images_error", + "Value #podcheck_images_last_analyze": "last_analyze_since", + "Value #podcheck_images_latest": "images_latest", + "Value #podcheck_images_outdated": "images_outdated" + } + } + } + ], + "type": "table" + } + ], + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "podcheck Status", + "uid": "feb4pv3kv1hxca", + "version": 17, + "weekStart": "" +} \ No newline at end of file diff --git a/addons/prometheus/grafana/grafana_dashboard.png b/addons/prometheus/grafana/grafana_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..c3878df7814c6a276155436c5ddb7aea5fe81f69 GIT binary patch literal 51198 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 +# +# Metrics: +# podcheck_no_updates: +# Number of containers that are already on the latest image. +# podcheck_updates: +# Number of containers with updates available. +# podcheck_errors: +# Number of containers that encountered errors during the update check. +# podcheck_total: +# Total number of containers checked. +# podcheck_check_duration: +# Duration (in seconds) it took to perform the update check. +# podcheck_last_check_timestamp: +# Epoch timestamp when the update check was performed. +# +# The metrics are written to a file named podcheck.prom in the specified +# CollectorTextFileDirectory, or /tmp if not specified. +# + +prometheus_exporter() { + local no_updates="$1" + local updates="$2" + local errors="$3" + local total="$4" + local check_duration="$5" + local collector_dir="${CollectorTextFileDirectory:-/tmp}" + local last_check_timestamp + last_check_timestamp=$(date +%s) + + { + echo "# HELP podcheck_no_updates Number of containers already on latest image." + echo "# TYPE podcheck_no_updates gauge" + echo "podcheck_no_updates $no_updates" + + echo "# HELP podcheck_updates Number of containers with updates available." + echo "# TYPE podcheck_updates gauge" + echo "podcheck_updates $updates" + + echo "# HELP podcheck_errors Number of containers with errors during update check." + echo "# TYPE podcheck_errors gauge" + echo "podcheck_errors $errors" + + echo "# HELP podcheck_total Total number of containers checked." + echo "# TYPE podcheck_total gauge" + echo "podcheck_total $total" + + echo "# HELP podcheck_check_duration Duration in seconds for the update check." + echo "# TYPE podcheck_check_duration gauge" + echo "podcheck_check_duration $check_duration" + + echo "# HELP podcheck_last_check_timestamp Epoch timestamp of the last update check." + echo "# TYPE podcheck_last_check_timestamp gauge" + echo "podcheck_last_check_timestamp $last_check_timestamp" + } > "$collector_dir/podcheck.prom" +} diff --git a/extras/apprise_quickstart.md b/extras/apprise_quickstart.md index 2d1ddd6..85689f4 100644 --- a/extras/apprise_quickstart.md +++ b/extras/apprise_quickstart.md @@ -1,9 +1,9 @@ # A small guide on getting started with Apprise notifications. -## Standalone docker container: [linuxserver/apprise-api](https://hub.docker.com/r/linuxserver/apprise-api) +## Standalone podman container: [linuxserver/apprise-api](https://hub.docker.com/r/linuxserver/apprise-api) -Set up the docker compose as preferred: +Set up the podman compose as preferred: ```yaml --- version: "2.1" diff --git a/extras/dc_brief.sh b/extras/dc_brief.sh deleted file mode 100755 index 61d56d8..0000000 --- a/extras/dc_brief.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -### If not in PATH, set full path. Else just "regctl" -regbin="regctl" -### options to allow exclude: -while getopts "e:" options; do - case "${options}" in - e) Exclude=${OPTARG} ;; - *) exit 0 ;; - esac -done -shift "$((OPTIND-1))" -### Create array of excludes -IFS=',' read -r -a Excludes <<< "$Exclude" ; unset IFS - -SearchName="$1" - -for i in $(podman ps --filter "name=$SearchName" --format '{{.Names}}') ; do - for e in "${Excludes[@]}" ; do [[ "$i" == "$e" ]] && continue 2 ; done - printf ". " - RepoUrl=$(podman inspect "$i" --format='{{.ImageName}}') - LocalHash=$(podman image inspect "$RepoUrl" --format '{{.Digest}}') - ### Checking for errors while setting the variable: - if RegHash=$($regbin image digest --list "$RepoUrl" 2>/dev/null) ; then - if [[ "$LocalHash" == "$RegHash" ]] ; then NoUpdates+=("$i"); else GotUpdates+=("$i"); fi - else - GotErrors+=("$i") - fi -done - -### Sort arrays alphabetically -IFS=$'\n' -NoUpdates=($(sort <<<"${NoUpdates[*]}")) -GotUpdates=($(sort <<<"${GotUpdates[*]}")) -GotErrors=($(sort <<<"${GotErrors[*]}")) -unset IFS - -### List what containers got updates or not -if [[ -n ${NoUpdates[*]} ]] ; then - printf "\n\033[0;32mContainers on latest version:\033[0m\n" - printf "%s\n" "${NoUpdates[@]}" -fi -if [[ -n ${GotErrors[*]} ]] ; then - printf "\n\033[0;31mContainers with errors; won't get updated:\033[0m\n" - printf "%s\n" "${GotErrors[@]}" -fi -if [[ -n ${GotUpdates[*]} ]] ; then - printf "\n\033[0;33mContainers with updates available:\033[0m\n" - printf "%s\n" "${GotUpdates[@]}" -fi -printf "\n\n" diff --git a/extras/errorCheck.sh b/extras/errorCheck.sh index d4e10bf..4e271c8 100755 --- a/extras/errorCheck.sh +++ b/extras/errorCheck.sh @@ -1,37 +1,55 @@ #!/usr/bin/env bash +# Usage: ./script.sh + SearchName="$1" -for i in $(podman ps --filter "name=$SearchName" --format '{{.Names}}') ; do - echo "------------ $i ------------" - ContLabels=$(podman inspect "$i" --format '{{json .Config.Labels}}') - ContImage=$(podman inspect "$i" --format='{{.ImageName}}') + +# Iterate over containers whose name contains the search term. +podman ps --filter "name=$SearchName" --format '{{.Names}}' | while read -r container; do + echo "------------ $container ------------" + + # Retrieve container labels and image name. + ContLabels=$(podman inspect "$container" --format '{{json .Config.Labels}}') + ContImage=$(podman inspect "$container" --format '{{.ImageName}}') + + # Extract values from labels; if not set, default to an empty string. ContPath=$(jq -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") [ "$ContPath" == "null" ] && ContPath="" + ContConfigFile=$(jq -r '."com.docker.compose.project.config_files"' <<< "$ContLabels") [ "$ContConfigFile" == "null" ] && ContConfigFile="" + ContName=$(jq -r '."com.docker.compose.service"' <<< "$ContLabels") [ "$ContName" == "null" ] && ContName="" + ContEnv=$(jq -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels") [ "$ContEnv" == "null" ] && ContEnv="" + ContUpdateLabel=$(jq -r '."sudo-kraken.podcheck.update"' <<< "$ContLabels") [ "$ContUpdateLabel" == "null" ] && ContUpdateLabel="" + ContRestartStack=$(jq -r '."sudo-kraken.podcheck.restart-stack"' <<< "$ContLabels") [ "$ContRestartStack" == "null" ] && ContRestartStack="" - - if [[ $ContConfigFile = '/'* ]] ; then + + # Determine the compose file location. + if [[ $ContConfigFile = '/'* ]]; then ComposeFile="$ContConfigFile" else ComposeFile="$ContPath/$ContConfigFile" fi - + + # Output the extracted details. echo -e "Service name:\t\t$ContName" echo -e "Project working dir:\t$ContPath" echo -e "Compose files:\t\t$ComposeFile" echo -e "Environment files:\t$ContEnv" echo -e "Container image:\t$ContImage" - echo -e "Update label:\t$ContUpdateLabel" + echo -e "Update label:\t\t$ContUpdateLabel" echo -e "Restart Stack label:\t$ContRestartStack" echo echo "Mounts:" - podman inspect -f '{{ range .Mounts }}{{ .Source }}:{{ .Destination }}{{ printf "\n" }}{{ end }}' "$i" + + # Display container mount points. + podman inspect -f '{{ range .Mounts }}{{ .Source }}:{{ .Destination }}{{ "\n" }}{{ end }}' "$container" echo done + diff --git a/extras/pc_brief.sh b/extras/pc_brief.sh new file mode 100644 index 0000000..77bc577 --- /dev/null +++ b/extras/pc_brief.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# pc_brief.sh - Provides a brief diagnostic summary of Podman Compose containers. +# Usage: pc_brief.sh + +set -euo pipefail + +# Check if a name filter argument was provided +if [ "$#" -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +SearchName="$1" + +# Use a while-read loop to correctly handle container names with spaces +podman ps --filter "name=$SearchName" --format '{{.Names}}' | while IFS= read -r container; do + echo "------------ $container ------------" + + # Retrieve container labels and image name + ContLabels=$(podman inspect "$container" --format '{{json .Config.Labels}}') + ContImage=$(podman inspect "$container" --format '{{.ImageName}}') + + # Extract Docker Compose-related labels via jq; default to empty if null + ContPath=$(jq -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") + [ "$ContPath" == "null" ] && ContPath="" + + ContConfigFile=$(jq -r '."com.docker.compose.project.config_files"' <<< "$ContLabels") + [ "$ContConfigFile" == "null" ] && ContConfigFile="" + + ContName=$(jq -r '."com.docker.compose.service"' <<< "$ContLabels") + [ "$ContName" == "null" ] && ContName="" + + ContEnv=$(jq -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels") + [ "$ContEnv" == "null" ] && ContEnv="" + + ContUpdateLabel=$(jq -r '."sudo-kraken.podcheck.update"' <<< "$ContLabels") + [ "$ContUpdateLabel" == "null" ] && ContUpdateLabel="" + + ContRestartStack=$(jq -r '."sudo-kraken.podcheck.restart-stack"' <<< "$ContLabels") + [ "$ContRestartStack" == "null" ] && ContRestartStack="" + + # Determine the full path to the compose file(s) + if [[ $ContConfigFile = /* ]]; then + ComposeFile="$ContConfigFile" + else + ComposeFile="$ContPath/$ContConfigFile" + fi + + # Output a concise summary of container configuration + echo -e "Service name:\t\t$ContName" + echo -e "Project working dir:\t$ContPath" + echo -e "Compose files:\t\t$ComposeFile" + echo -e "Environment files:\t$ContEnv" + echo -e "Container image:\t$ContImage" + echo -e "Update label:\t\t$ContUpdateLabel" + echo -e "Restart Stack label:\t$ContRestartStack" + echo + echo "Mounts:" + podman inspect -f '{{ range .Mounts }}{{ .Source }}:{{ .Destination }}{{ "\n" }}{{ end }}' "$container" + echo +done diff --git a/notify_templates/notify_DSM.sh b/notify_templates/notify_DSM.sh index d9a0cd9..e96d5f2 100644 --- a/notify_templates/notify_DSM.sh +++ b/notify_templates/notify_DSM.sh @@ -49,4 +49,7 @@ Content-Transfer-Encoding: 7bit $MessageBody From $SenderName __EOF +# 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_gotify.sh b/notify_templates/notify_gotify.sh index b88f597..49d07ae 100644 --- a/notify_templates/notify_gotify.sh +++ b/notify_templates/notify_gotify.sh @@ -14,7 +14,7 @@ send_notification() { # Setting the MessageTitle and MessageBody variable here. MessageTitle="${FromHost} - updates available." - printf -v MessageBody "🐋 Containers on $FromHost with updates available:\n$UpdToString" + printf -v MessageBody "Containers on $FromHost with updates available:\n$UpdToString" # Modify to fit your setup: GotifyToken="Your Gotify token here" @@ -24,6 +24,6 @@ send_notification() { -F "title=${MessageTitle}" \ -F "message=${MessageBody}" \ -F "priority=5" \ - -X POST "${GotifyUrl}" &> /dev/null + -X POST "${GotifyUrl}" 1> /dev/null } diff --git a/podcheck.sh b/podcheck.sh index 14dff5e..3120315 100755 --- a/podcheck.sh +++ b/podcheck.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash -VERSION="v0.5.7" -# ChangeNotes: Rewrite of dependency installer. jq can now be installed via package manager or static binary. +VERSION="v0.6.0" Github="https://github.com/sudo-kraken/podcheck" -RawUrl="https://raw.githubusercontent.com/sudo-kraken/podcheck/main/podcheck.sh" +RawUrl="https://raw.githubusercontent.com/sudo-kraken/podcheck/upstream_patches/podcheck.sh" # Variables for self-updating ScriptArgs=( "$@" ) @@ -10,23 +9,23 @@ ScriptPath="$(readlink -f "$0")" ScriptWorkDir="$(dirname "$ScriptPath")" # 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")" +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")" -# Help Function Help() { echo "Syntax: podcheck.sh [OPTION] [part of name to filter]" echo "Example: podcheck.sh -y -d 10 -e nextcloud,heimdall" echo echo "Options:" echo "-a|y Automatic updates, without interaction." + 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." echo "-e X Exclude containers, separated by comma." echo "-f Force pod restart after update." 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 "-m Monochrome mode, no printf colour codes." echo "-n No updates; only checking availability." echo "-p Auto-prune dangling images after update." echo "-r Allow updating images for podman run; won't update the container." @@ -37,7 +36,7 @@ Help() { echo "Project source: $Github" } -# Colors +# Colours c_red="\033[0;31m" c_green="\033[0;32m" c_yellow="\033[0;33m" @@ -45,76 +44,113 @@ c_blue="\033[0;34m" c_teal="\033[0;36m" c_reset="\033[0m" -Timeout=10 +# Initialise variables first +AutoUp="no" +AutoPrune="" Stopped="" -while getopts "aynpfrhlisvme:d:t:" options; do +Timeout=10 +NoUpdateMode=false +Excludes=() +GotUpdates=() +NoUpdates=() +GotErrors=() +NotifyUpdates=() +SelectedUpdates=() +OnlyLabel=false +ForceRestartPods=false + +# regbin will be set later. +regbin="" + +set -euo pipefail + +while getopts "aynpfrhlisvmc:e:d:t:v" options; do case "${options}" in a|y) AutoUp="yes" ;; - n) AutoUp="no" ;; + c) + CollectorTextFileDirectory="${OPTARG}" + if ! [[ -d $CollectorTextFileDirectory ]]; then + printf "The directory (%s) does not exist.\n" "${CollectorTextFileDirectory}" + exit 2 + fi + ;; + n) NoUpdateMode=true ;; r) DRunUp="yes" ;; p) AutoPrune="yes" ;; l) OnlyLabel=true ;; f) ForceRestartPods=true ;; - i) [ -s "$ScriptWorkDir"/notify.sh ] && { source "$ScriptWorkDir"/notify.sh ; Notify="yes" ; } ;; - e) Exclude=${OPTARG} ;; + i) [ -s "$ScriptWorkDir/notify.sh" ] && { source "$ScriptWorkDir/notify.sh"; Notify="yes"; } ;; + e) Exclude="${OPTARG}" + IFS=',' read -ra Excludes <<< "$Exclude" + ;; m) declare c_{red,green,yellow,blue,teal,reset}="" ;; s) Stopped="-a" ;; t) Timeout="${OPTARG}" ;; - v) printf "%s\n" "$VERSION" ; exit 0 ;; - 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}" + if ! [[ $DaysOld =~ ^[0-9]+$ ]]; then + printf "Days -d argument given (%s) is not a number.\n" "${DaysOld}" + exit 2 + fi + ;; + v) printf "%s\n" "$VERSION"; exit 0 ;; + h|*) Help; exit 2 ;; esac done shift "$((OPTIND-1))" +# 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) ]]; 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" + 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[@]}" # Run the new script with old arguments - exit 1 # Exit the old instance + 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) ]] && [[ "$(git ls-remote --get-url 2>/dev/null)" =~ .*"sudo-kraken/podcheck".* ]] ; then + 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 ; } + 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[@]}" # Run the new script with old arguments - exit 1 # Exit the old instance + cd - || { printf "Path error.\n"; return; } + exec "$ScriptPath" "${ScriptArgs[@]}" + exit 1 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 - echo "Number not in list: $CC" ; unset ChoiceClean ; break 1 + 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 @@ -127,9 +163,14 @@ choosecontainers() { } datecheck() { - ImageDate=$($regbin -v error image inspect "$RepoUrl" --format='{{.Created}}' | cut -d" " -f1 ) - ImageAge=$(( ( $(date +%s) - $(date -d "$ImageDate" +%s) )/86400 )) - if [ "$ImageAge" -gt "$DaysOld" ] ; then + 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 return 0 else return 1 @@ -140,198 +181,249 @@ progress_bar() { QueCurrent="$1" QueTotal="$2" ((Percent=100*QueCurrent/QueTotal)) - ((Complete=50*Percent/100)) # Change first number for width (50) - ((Left=50-Complete)) # Change first number for width (50) + ((Complete=50*Percent/100)) + ((Left=50-Complete)) 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 } -# Static binary downloader for dependencies +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;; + 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) ]]; then curl -L $GetUrl > "$ScriptWorkDir/$BinaryName" ; - elif [[ $(command -v wget) ]]; then wget $GetUrl -O "$ScriptWorkDir/$BinaryName" ; - else printf "%s\n" "curl/wget not available - get $BinaryName manually from the repo link, exiting."; exit 1; + 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" + 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 } -# Version check & initiate self update -if [[ "$VERSION" != "$LatestRelease" ]] && [[ -n "$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 - 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 - -# Dependency check for jq in PATH or directory -if [[ $(command -v jq) ]]; then jqbin="jq" ; -elif [[ -f "$ScriptWorkDir/jq" ]]; then jqbin="$ScriptWorkDir/jq" ; +# 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} # set default to no if nothing is given - if [[ "$GetJq" =~ [yYsS] ]] ; then + GetJq=${GetJq:-no} + if [[ "$GetJq" =~ [yYsS] ]]; then [[ "$GetJq" =~ [yY] ]] && distro_checker - if [[ -n "$PkgInstaller" && "$PkgInstaller" != "ERROR" ]] ; then - (sudo $PkgInstaller jq) ; PkgExitcode="$?" + 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" + 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 ; + else + printf "\n%bDependency missing, exiting.%b\n" "$c_red" "$c_reset" + exit 1 fi fi -# Final check if binary is correct -$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 in PATH or directory -if [[ $(command -v regctl) ]]; then regbin="regctl" ; -elif [[ -f "$ScriptWorkDir/regctl" ]]; then regbin="$ScriptWorkDir/regctl" ; +$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 + if [[ "$GetRegctl" =~ [yY] ]]; then binary_downloader "regctl" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" - [[ -f "$ScriptWorkDir/regctl" ]] && regbin="$ScriptWorkDir/regctl" - else printf "\n%bDependency missing, exiting.%b\n" "$c_red" "$c_reset" ; exit 1 ; + 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 fi fi -# Final check if binary is correct -$regbin version &> /dev/null || { printf "%s\n" "regctl is not working - try to remove it and re-download it, exiting."; exit 1; } + +$regbin version &>/dev/null || { printf "%s\n" "regctl is not working - try to remove it and re-download it, exiting."; exit 1; } # Check podman compose binary -if podman compose version &> /dev/null ; then PodmanComposeBin="podman compose" ; -elif command -v podman-compose &> /dev/null; then PodmanComposeBin="podman-compose" ; -elif podman version &> /dev/null; then +if podman compose version &>/dev/null; then + PodmanComposeBin="podman compose" +elif command -v podman-compose &>/dev/null; then + PodmanComposeBin="podman-compose" +elif podman version &>/dev/null; then printf "%s\n" "No podman-compose binary available, using plain podman" else printf "%s\n" "No podman binaries available, exiting." exit 1 fi -# Numbered List function options() { -num=1 -for i in "${GotUpdates[@]}"; do - echo "$num) $i" - ((num++)) -done + 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" fi -# Variables for progress_bar function ContCount=$(podman ps $Stopped --filter "name=$SearchName" --format '{{.Names}}' | wc -l) RegCheckQue=0 +start_time=$(date +%s) -# Testing and setting timeout binary -t_out=$(command -v timeout) -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 the image-hash of every running container VS the registry -for i in $(podman ps $Stopped --filter "name=$SearchName" --format '{{.Names}}') ; do - ((RegCheckQue+=1)) +process_container() { + local container="$1" + ((RegCheckQue++)) progress_bar "$RegCheckQue" "$ContCount" - # Looping every item over the list of excluded names and skipping - for e in "${Excludes[@]}" ; do [[ "$i" == "$e" ]] && continue 2 ; done - RepoUrl=$(podman inspect "$i" --format='{{.ImageName}}') - LocalHash=$(podman image inspect "$RepoUrl" --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 - NoUpdates+=("$i") - else - if [[ -n "$DaysOld" ]] && ! datecheck ; then - NoUpdates+=("+$i ${ImageAge}d") + >&2 echo "Processing container: $container" + + for e in "${Excludes[@]}"; do + if [[ "$container" == "$e" ]]; then + return 0 + fi + done + + local ImageId RepoUrl LocalHash RegHash + if ! ImageId=$(podman inspect "$container" --format='{{.Image}}'); then + return 0 + fi + if ! RepoUrl=$(podman inspect "$container" --format='{{.ImageName}}'); then + return 0 + fi + if ! LocalHash=$(podman image inspect "$ImageId" --format '{{.RepoDigests}}'); then + return 0 + fi + + if RegHash=$(${t_out} $regbin -v error image digest --list "$RepoUrl" 2>/dev/null | xargs); then + if [[ -n "$RegHash" ]]; then + if [[ "$LocalHash" == *"$RegHash"* ]]; then + NoUpdates+=("$container") else - GotUpdates+=("$i") + # Create a separate array for notifications + NotifyUpdates+=("$container") + # Add to GotUpdates for update logic + GotUpdates+=("$container") + + # If it's too recent based on age check, move it to NoUpdates for display + # but keep it in NotifyUpdates + if [[ -n "${DaysOld:-}" ]] && ! datecheck; then + NoUpdates+=("+$container ${ImageAge}d") + # Remove from GotUpdates for update logic + for i in "${!GotUpdates[@]}"; do + if [[ "${GotUpdates[i]}" = "$container" ]]; then + unset 'GotUpdates[i]' + break + fi + done + # Re-index array after removal + GotUpdates=("${GotUpdates[@]}") + fi fi + else + GotErrors+=("$container - No digest returned") fi else - # Here the RegHash is the result of an error code - GotErrors+=("$i - ${RegHash}") + GotErrors+=("$container - Error checking registry") fi +} + +# Main loop to process all containers +for container in $(podman ps $Stopped --filter "name=$SearchName" --format '{{.Names}}'); do + process_container "$container" || true done -# Sort arrays alphabetically IFS=$'\n' NoUpdates=($(sort <<<"${NoUpdates[*]}")) GotUpdates=($(sort <<<"${GotUpdates[*]}")) unset IFS -# Define how many updates are available -UpdCount="${#GotUpdates[@]}" - -# List what containers got updates or not -if [[ -n ${NoUpdates[*]} ]] ; then +echo "" +echo "===== Summary =====" +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 - 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" ; } +if [[ -n "${GotUpdates[*]}" ]]; then + printf "\n%bContainers with updates available:%b\n" "$c_yellow" "$c_reset" + printf "%s\n" "${GotUpdates[@]}" fi -# Optionally get updates if there's any -if [ -n "$GotUpdates" ] ; then - if [ -z "$AutoUp" ] ; then - printf "\n%bChoose what containers to update.%b\n" "$c_teal" "$c_reset" - choosecontainers - else +if [[ -n "${GotUpdates[*]}" ]]; then + UpdCount="${#GotUpdates[@]}" + + # Send notification if -i flag was used, regardless of other options + [[ "${Notify:-}" == "yes" && -n "${NotifyUpdates[*]}" ]] && send_notification "${NotifyUpdates[@]}" + + if [[ "$NoUpdateMode" == true ]]; then + printf "\n%bNo updates will be performed due to -n flag.%b\n" "$c_blue" "$c_reset" + elif [[ "$AutoUp" == "yes" ]]; then SelectedUpdates=( "${GotUpdates[@]}" ) + else + printf "\n%bChoose what containers to update:%b\n" "$c_teal" "$c_reset" + options + choosecontainers fi - if [ "$AutoUp" == "${AutoUp#[Nn]}" ] ; then + + if [ "${#SelectedUpdates[@]}" -gt 0 ]; then NumberofUpdates="${#SelectedUpdates[@]}" CurrentQue=0 - for i in "${SelectedUpdates[@]}" - do + 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='{{.ImageName}}') ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") @@ -347,52 +439,73 @@ if [ -n "$GotUpdates" ] ; then ContRestartStack=$($jqbin -r '."sudo-kraken.podcheck.restart-stack"' <<< "$ContLabels") [ "$ContRestartStack" == "null" ] && ContRestartStack="" - # Checking if compose-values are empty - possibly started with podman run or managed by Quadlet - if [ -z "$ContPath" ] ; then - # Check if a systemd unit exists with the same name as the container - if systemctl --user status "$i.service" &> /dev/null; then - echo "Detected Quadlet-managed container: $i (unit: $i.service)" - podman pull "$ContImage" - systemctl --user restart "$i.service" - echo "Quadlet container $i updated and restarted." - elif [ "$(id -u)" -eq 0 ] && systemctl status "$i.service" &> /dev/null; then - echo "Detected Quadlet-managed container: $i (unit: $i.service)" - podman pull "$ContImage" - systemctl restart "$i.service" - echo "Quadlet container $i updated and restarted." + if [ -z "$ContPath" ]; then + if systemctl --user status "$i.service" &>/dev/null; then + unit="$i.service" + elif [ "$(id -u)" -eq 0 ] && systemctl status "$i.service" &>/dev/null; then + unit="$i.service" else - if [ "$DRunUp" == "yes" ] ; then + pattern="^$(echo "$i" | sed 's/_/[_-]/g')\.service$" + candidates=$(systemctl --user list-units --type=service --no-legend | awk '{print $1}' | grep -iE "$pattern") + if [ "$(echo "$candidates" | wc -l)" -eq 1 ]; then + unit="$candidates" + elif [ "$(echo "$candidates" | wc -l)" -gt 1 ]; then + for cand in $candidates; do + if [[ "${cand,,}" == "${i,,}.service" ]]; then + unit="$cand" + break + fi + done + if [ -z "${unit:-}" ]; then + unit=$(echo "$candidates" | head -n 1) + fi + fi + fi + + if [ -n "${unit:-}" ]; then + echo "Detected Quadlet-managed container: $i (matched unit: $unit)" + podman pull "$ContImage" + if systemctl --user restart "$unit" &>/dev/null; then + echo "Quadlet container $i updated and restarted (user scope)." + elif [ "$(id -u)" -eq 0 ] && systemctl restart "$unit" &>/dev/null; then + echo "Quadlet container $i updated and restarted (system scope)." + else + echo "Failed to restart unit $unit for container $i." + fi + else + if [ "$DRunUp" == "yes" ]; 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 or associated systemd unit; %bskipping%b\n\n" "$c_yellow" "$i" "$c_reset" "$c_yellow" "$c_reset" fi fi + continue fi - # cd to the compose-file directory to account for people who use relative volumes - 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) + 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) + 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 ; } } + [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != "true" ]] && { echo "No update label, skipping."; continue; } } podman pull "$ContImage" - # 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 - # Check if the whole pod should be restarted - if [[ "$ContRestartStack" == "true" ]] || [[ "$ForceRestartPods" == true ]] ; then - $PodmanComposeBin ${CompleteConfs} down ; $PodmanComposeBin ${CompleteConfs} ${ContEnvs} up -d + if [ -n "$ContEnv" ]; then + ContEnvs=$(for env in ${ContEnv//,/ }; do printf -- "--env-file %s " "$env"; done) + fi + if [[ "$ContRestartStack" == "true" ]] || [[ "$ForceRestartPods" == true ]]; then + $PodmanComposeBin ${CompleteConfs} down + $PodmanComposeBin ${CompleteConfs} ${ContEnvs} up -d else $PodmanComposeBin ${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 + if [[ -z "$AutoPrune" ]] && [[ "$AutoUp" == "no" ]]; then + read -r -p "Would you like to prune dangling images? y/[n]: " AutoPrune + fi [[ "$AutoPrune" =~ [yY] ]] && podman image prune -f else printf "\nNo updates installed, exiting.\n" @@ -401,4 +514,42 @@ else printf "\nNo updates available, exiting.\n" fi +# Export metrics if collector directory was specified +if [[ -n "${CollectorTextFileDirectory:-}" ]]; then + # Calculate check duration + end_time=$(date +%s) + check_duration=$((end_time - start_time)) + + # Source the prometheus collector script if it exists + if [[ -f "$ScriptWorkDir/addons/prometheus/prometheus_collector.sh" ]]; then + source "$ScriptWorkDir/addons/prometheus/prometheus_collector.sh" + # Call the prometheus_exporter with appropriate metrics + prometheus_exporter "${#NoUpdates[@]}" "${#GotUpdates[@]}" "${#GotErrors[@]}" "$ContCount" "$check_duration" + printf "\n%bPrometheus metrics exported to: %s/podcheck.prom%b\n" "$c_teal" "$CollectorTextFileDirectory" "$c_reset" + else + # Fallback if the collector script isn't found + cat > "$CollectorTextFileDirectory/podcheck.prom" <