From 520cfad72ddf7da8da9b49a4ab5d4eee70879c81 Mon Sep 17 00:00:00 2001 From: Yan Lin Date: Sun, 14 Sep 2025 16:59:33 +0200 Subject: [PATCH] Add container updater --- hosts/nixos/hs/system.nix | 11 +++ modules/container-updater.nix | 101 +++++++++++++++++++++ scripts/container-update.sh | 159 ++++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 modules/container-updater.nix create mode 100755 scripts/container-update.sh diff --git a/hosts/nixos/hs/system.nix b/hosts/nixos/hs/system.nix index c844849..eb9f1c1 100644 --- a/hosts/nixos/hs/system.nix +++ b/hosts/nixos/hs/system.nix @@ -10,6 +10,7 @@ ../../../modules/samba.nix ../../../modules/borg-client.nix ../../../modules/webdav.nix + ../../../modules/container-updater.nix ]; # GRUB bootloader with ZFS support @@ -123,6 +124,16 @@ # Enable sudo for wheel group security.sudo.wheelNeedsPassword = false; + # Container auto-updater configuration + services.containerUpdater = { + enable = true; + schedule = "*-*-* 03:00:00"; # Daily at 3 AM + excludeContainers = []; # Update all containers + enableNotifications = true; + gotifyUrl = "https://notify.yanlincs.com"; + gotifyToken = "Ac9qKFH5cA.7Yly"; # Same token as borg backups + }; + # List packages installed in system profile environment.systemPackages = with pkgs; [ vim diff --git a/modules/container-updater.nix b/modules/container-updater.nix new file mode 100644 index 0000000..862822d --- /dev/null +++ b/modules/container-updater.nix @@ -0,0 +1,101 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.containerUpdater; +in +{ + options.services.containerUpdater = { + enable = mkEnableOption "automatic container updates"; + + schedule = mkOption { + type = types.str; + default = "*-*-* 03:00:00"; + example = "daily"; + description = '' + Systemd timer schedule for container updates. + Can be a systemd time specification or alias like "daily", "weekly". + ''; + }; + + excludeContainers = mkOption { + type = types.listOf types.str; + default = []; + example = [ "traefik" "wireguard" ]; + description = '' + List of container names to exclude from automatic updates. + ''; + }; + + enableNotifications = mkOption { + type = types.bool; + default = false; + description = "Enable Gotify notifications for update status"; + }; + + gotifyUrl = mkOption { + type = types.str; + default = ""; + example = "https://notify.yanlincs.com"; + description = "Gotify server URL for notifications"; + }; + + gotifyToken = mkOption { + type = types.str; + default = ""; + example = "Ac9qKFH5cA.7Yly"; + description = "Gotify API token for notifications"; + }; + }; + + config = mkIf cfg.enable { + # Ensure the update script exists and is executable + system.activationScripts.container-updater = '' + chmod +x /home/yanlin/.config/nix/scripts/container-update.sh + chmod +x /home/yanlin/.config/nix/scripts/gotify-notify.sh + ''; + + # Systemd service for container updates + systemd.services.container-updater = { + description = "Update podman containers to latest images"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + + environment = { + GOTIFY_URL = mkIf cfg.enableNotifications cfg.gotifyUrl; + GOTIFY_TOKEN = mkIf cfg.enableNotifications cfg.gotifyToken; + EXCLUDE_CONTAINERS = concatStringsSep "," cfg.excludeContainers; + }; + + path = [ pkgs.podman pkgs.curl pkgs.coreutils pkgs.nettools ]; + + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.bash}/bin/bash /home/yanlin/.config/nix/scripts/container-update.sh"; + User = "root"; + StandardOutput = "journal"; + StandardError = "journal"; + + # Restart policy + Restart = "on-failure"; + RestartSec = "5min"; + + # Timeout for the update process (30 minutes should be enough) + TimeoutStartSec = "30min"; + }; + }; + + # Systemd timer for scheduled updates + systemd.timers.container-updater = { + description = "Timer for automatic container updates"; + wantedBy = [ "timers.target" ]; + + timerConfig = { + OnCalendar = cfg.schedule; + Persistent = true; + RandomizedDelaySec = "5min"; + }; + }; + }; +} \ No newline at end of file diff --git a/scripts/container-update.sh b/scripts/container-update.sh new file mode 100755 index 0000000..baddcca --- /dev/null +++ b/scripts/container-update.sh @@ -0,0 +1,159 @@ +# Container update script with Gotify notifications +# Updates all podman containers to latest images + +set -euo pipefail + +# Configuration from environment (set by systemd service) +GOTIFY_URL="${GOTIFY_URL:-}" +GOTIFY_TOKEN="${GOTIFY_TOKEN:-}" +EXCLUDE_CONTAINERS="${EXCLUDE_CONTAINERS:-}" + +# Convert excluded containers to array +IFS=',' read -ra EXCLUDED <<< "$EXCLUDE_CONTAINERS" + +# Function to send Gotify notification +send_notification() { + local priority="$1" + local title="$2" + local message="$3" + + if [[ -n "$GOTIFY_URL" ]] && [[ -n "$GOTIFY_TOKEN" ]]; then + /home/yanlin/.config/nix/scripts/gotify-notify.sh \ + "$GOTIFY_URL" \ + "$GOTIFY_TOKEN" \ + "$priority" \ + "$title" \ + "$message" 2>&1 || echo "Failed to send notification" + fi +} + +# Get all running containers +echo "Getting list of running containers..." +containers=$(podman ps --format "{{.Names}}") + +if [[ -z "$containers" ]]; then + echo "No running containers found" + exit 0 +fi + +# Arrays to track updates +updated_containers=() +failed_containers=() +skipped_containers=() + +# Update each container +for container in $containers; do + # Check if container is in exclude list + skip=false + for excluded in "${EXCLUDED[@]}"; do + if [[ "$container" == "$excluded" ]]; then + echo "Skipping excluded container: $container" + skipped_containers+=("$container") + skip=true + break + fi + done + + if [[ "$skip" == true ]]; then + continue + fi + + echo "Processing container: $container" + + # Get current image + image=$(podman inspect "$container" --format '{{.ImageName}}') + + if [[ -z "$image" ]]; then + echo "ERROR: Could not get image for container $container" + failed_containers+=("$container (no image)") + continue + fi + + echo " Current image: $image" + + # Get current image ID before pull + old_image_id=$(podman inspect "$container" --format '{{.Image}}') + + # Pull latest image + echo " Pulling latest image..." + if podman pull "$image" 2>&1; then + echo " Image pulled successfully" + + # Get new image ID after pull + new_image_id=$(podman inspect "$image" --format '{{.Id}}') + + # Check if image actually changed + if [[ "$old_image_id" != "$new_image_id" ]]; then + echo " New image detected, restarting container..." + + # Restart container + if podman restart "$container" 2>&1; then + echo " Container updated successfully" + updated_containers+=("$container") + else + echo " ERROR: Failed to restart container" + failed_containers+=("$container (restart failed)") + fi + else + echo " Image unchanged, skipping restart" + skipped_containers+=("$container (no update)") + fi + else + echo " ERROR: Failed to pull image" + failed_containers+=("$container (pull failed)") + fi + + echo "" +done + +# Prepare notification message +notification_lines=() +notification_priority="normal" + +if [[ ${#updated_containers[@]} -gt 0 ]]; then + notification_lines+=("✅ Updated (${#updated_containers[@]}):") + for container in "${updated_containers[@]}"; do + notification_lines+=(" • $container") + done +fi + +if [[ ${#failed_containers[@]} -gt 0 ]]; then + notification_priority="high" + notification_lines+=("") + notification_lines+=("❌ Failed (${#failed_containers[@]}):") + for container in "${failed_containers[@]}"; do + notification_lines+=(" • $container") + done +fi + +if [[ ${#skipped_containers[@]} -gt 0 ]]; then + notification_lines+=("") + notification_lines+=("⏭️ No updates (${#skipped_containers[@]}):") + for container in "${skipped_containers[@]}"; do + notification_lines+=(" • $container") + done +fi + +# Send notification if there were any updates or failures +if [[ ${#notification_lines[@]} -gt 0 ]]; then + # Build multi-line message similar to borg-client + message="" + for line in "${notification_lines[@]}"; do + if [[ -n "$message" ]]; then + message="${message}\n${line}" + else + message="$line" + fi + done + + hostname_val=$(hostname 2>/dev/null || echo "hs") + send_notification "$notification_priority" "Container Update - $hostname_val" "$message" +fi + +# Exit with error if any containers failed +if [[ ${#failed_containers[@]} -gt 0 ]]; then + echo "ERROR: Some containers failed to update" + exit 1 +fi + +echo "Container update completed successfully" \ No newline at end of file