diff --git a/podcheck.sh b/podcheck.sh index 0c6c0b6..a5ee5db 100755 --- a/podcheck.sh +++ b/podcheck.sh @@ -375,8 +375,6 @@ distro_checker() { fi } -} - # Static binary downloader for dependencies binary_downloader() { BinaryName="$1" @@ -461,25 +459,48 @@ dependency_check() { dependency_check "regctl" "regbin" "https://github.com/regclient/regclient/releases/latest/download/regctl-linux-TEMP" dependency_check "jq" "jqbin" "https://github.com/jqlang/jq/releases/latest/download/jq-linux-TEMP" +# 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 -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" +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 -options() { - num=1 - for i in "${GotUpdates[@]}"; do - echo "$num) $i" - ((num++)) - done -} +# 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" @@ -493,118 +514,245 @@ start_time=$(date +%s) printf "\n%bStarting container update check%b\n" "$c_blue" "$c_reset" -process_container() { - local container="$1" - ((RegCheckQue++)) - progress_bar "$RegCheckQue" "$ContCount" - - for e in "${Excludes[@]}"; do - if [[ "$container" == "$e" ]]; then - return 0 +# 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 - - local ImageId RepoUrl LocalHash RegHash - if ! ImageId=$(podman inspect "$container" --format='{{.Image}}'); then - echo "Error: Failed to get image ID for container $container" - return 0 + + # 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 - 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 - # 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 + + 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 - GotErrors+=("$container - No digest returned") + if [[ -n "${DaysOld:-}" ]] && ! datecheck; then + printf "%s\n" "NoUpdates +$i ${ImageAge}d" + else + printf "%s\n" "GotUpdates $i" + fi fi else - GotErrors+=("$container - Error checking registry") + printf "%s\n" "GotErrors $i - ${RegHash}" # Reghash contains an error code here fi } -# Main loop to process all containers -for container in $(podman ps $Stopped --filter "name=$SearchName" --format '{{.Names}}'); do - process_container "$container" || true -done +# 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[*]}")) +NoUpdates=($(sort <<<"${NoUpdates[*]:-}")) +GotUpdates=($(sort <<<"${GotUpdates[*]:-}")) unset IFS -echo "" -echo "===== Summary =====" -if [[ -n "${NoUpdates[*]}" ]]; then +# 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" +if [[ -n ${GotErrors[*]:-} ]]; then + printf "\n%bContainers with errors, won't get updated:%b\n" "$c_red" "$c_reset" printf "%s\n" "${GotErrors[@]}" printf "%binfo:%b 'unauthorized' often means not found in a public registry.\n" "$c_blue" "$c_reset" fi -if [[ -n "${GotUpdates[*]}" ]]; then +if [[ -n ${GotUpdates[*]:-} ]]; then printf "\n%bContainers with updates available:%b\n" "$c_yellow" "$c_reset" - printf "%s\n" "${GotUpdates[@]}" + 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 -echo "Found ${#GotUpdates[@]} containers with updates available" - -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 +# 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[@]}" - if [ "${#SelectedUpdates[@]}" -gt 0 ]; then 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 '."mag37.dockcheck.update"' <<< "$ContLabels") + [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" + # Checking if Label Only -option is set, and if container got the label + [[ "$OnlyLabel" == true ]] && { [[ "$ContUpdateLabel" != true ]] && { echo "No update label, skipping."; continue; } } + + # Checking if compose-values are empty - hence started with 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='{{.ImageName}}') + ContImage=$(podman inspect "$i" --format='{{.Config.Image}}') ContPath=$($jqbin -r '."com.docker.compose.project.working_dir"' <<< "$ContLabels") - [ "$ContPath" == "null" ] && ContPath="" + [[ "$ContPath" == "null" ]] && ContPath="" ContConfigFile=$($jqbin -r '."com.docker.compose.project.config_files"' <<< "$ContLabels") - [ "$ContConfigFile" == "null" ] && ContConfigFile="" + [[ "$ContConfigFile" == "null" ]] && ContConfigFile="" + ContName=$($jqbin -r '."com.docker.compose.service"' <<< "$ContLabels") + [[ "$ContName" == "null" ]] && ContName="" + ContEnv=$($jqbin -r '."com.docker.compose.project.environment_file"' <<< "$ContLabels") + [[ "$ContEnv" == "null" ]] && ContEnv="" + ContUpdateLabel=$($jqbin -r '."mag37.dockcheck.update"' <<< "$ContLabels") + [[ "$ContUpdateLabel" == "null" ]] && ContUpdateLabel="" + ContRestartStack=$($jqbin -r '."mag37.dockcheck.restart-stack"' <<< "$ContLabels") + [[ "$ContRestartStack" == "null" ]] && ContRestartStack="" + ContOnlySpecific=$($jqbin -r '."mag37.dockcheck.only-specific-container"' <<< "$ContLabels") + [[ "$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") @@ -659,22 +807,19 @@ if [[ -n "${GotUpdates[*]}" ]]; 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 + ${PodmanBin} ${CompleteConfs} down + ${PodmanBin} ${CompleteConfs} ${ContEnvs} up -d else - $PodmanComposeBin ${CompleteConfs} ${ContEnvs} up -d ${ContName} + ${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" - if [[ -z "$AutoPrune" ]] && [[ "$AutoUp" == "no" ]]; then - read -r -p "Would you like to prune dangling images? y/[n]: " AutoPrune - fi - - if [[ "$AutoPrune" =~ [yY] ]] || [[ "$AutoUp" == "yes" ]]; then - printf "\n%bCleaning up failed update images...%b\n\n" "$c_teal" "$c_reset" - podman image prune -f - printf "\n" - fi else printf "\nNo updates installed, exiting.\n" fi @@ -682,42 +827,4 @@ 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" <