From 7530cc18eb19ab36dd448db134f52936e942bcba Mon Sep 17 00:00:00 2001 From: Simon Rieger Date: Mon, 21 Jul 2025 10:39:29 +0200 Subject: [PATCH] add env for interval and exclude containers --- docker-compose.yml | 3 ++ go/main.go | 106 +++++++++++++++++++++++++++++++-------------- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2b253b3..d98e3ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,9 @@ services: - /var/run/docker.sock:/var/run/docker.sock #ports: # - "9788:9788" + environment: + - CHECK_INTERVAL=6h + - EXCLUDE_CONTAINERS=immich_redis,mailcowdockerized-ofelia-mailcow-1 restart: always networks: default: diff --git a/go/main.go b/go/main.go index f51a165..3ad3ca2 100644 --- a/go/main.go +++ b/go/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "os" "regexp" "strings" "sync" @@ -35,10 +36,19 @@ type StatusCache struct { } var ( - cache = &StatusCache{Data: []ImageStatus{}, LastCheck: time.Time{}} - interval = 6 * time.Hour + cache = &StatusCache{Data: []ImageStatus{}, LastCheck: time.Time{}} + interval = 6 * time.Hour // Default wird ggf. überschrieben durch CHECK_INTERVAL + excludeContainers = map[string]struct{}{} ) +// Helper: image ohne ":tag" erhält "latest" +func normalizeImageTag(tag string) string { + if !strings.Contains(tag, ":") { + return tag + ":latest" + } + return tag +} + func toRegistryImage(imageTag string) (string, error) { r := regexp.MustCompile(`^(?:(?P[^/]+)/)?(?P[^:]+)(?::(?P.+))?$`) match := r.FindStringSubmatch(imageTag) @@ -66,13 +76,6 @@ func extractDigest(s string) string { return s } -func normalizeImageTag(tag string) string { - if !strings.Contains(tag, ":") { - return tag + ":latest" - } - return tag -} - func checkImageUpdates() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) @@ -107,55 +110,51 @@ func checkImageUpdates() { } } } - // Debug-Ausgabe aller bekannten imageTagToDigest - fmt.Println("DEBUG: Alle bekannten imageTagToDigest:") - for k, v := range imageTagToDigest { - fmt.Printf(" %s -> %s\n", k, v) - } results := make([]ImageStatus, 0) for _, ctr := range containers { - rawTag := ctr.Image - tag := normalizeImageTag(rawTag) - containerName := "unknown" if len(ctr.Names) > 0 { - containerName = ctr.Names[0] + containerName = strings.TrimPrefix(ctr.Names[0], "/") } - imageRef, err := toRegistryImage(tag) - if err != nil { - log.Printf("Fehler beim Parsen des Image-Namens (%s): %v", tag, err) + // Überspringen, wenn im EXCLUDE_CONTAINERS enthalten + if _, excluded := excludeContainers[containerName]; excluded { + fmt.Printf("⏭️ Container '%s' ist ausgeschlossen.\n", containerName) continue } - localDigest := imageTagToDigest[tag] + rawTag := normalizeImageTag(ctr.Image) + localDigest := imageTagToDigest[rawTag] + imageRef, err := toRegistryImage(rawTag) + if err != nil { + log.Printf("Ungültige Image-Referenz (%s): %v", rawTag, err) + continue + } refObj, err := ref.New(imageRef) if err != nil { - log.Printf("Ungültige Image-Referenz (%s): %v", tag, err) + log.Printf("Fehler beim Erzeugen der Referenz (%s): %v", rawTag, err) continue } - desc, err := rc.ManifestHead(ctx, refObj) if err != nil { - log.Printf("Fehler beim Abrufen des Remote-Manifests (%s): %v", tag, err) + log.Printf("Remote-Manifest nicht gefunden (%s): %v", rawTag, err) continue } - remoteDigest := desc.GetDigest().String() + remoteDigest := desc.GetDigest().String() update := 0.0 + if localDigest != remoteDigest { update = 1.0 - fmt.Printf("Container: %s\n Image: %s\n Local Digest: %s\n Remote Digest: %s\n -> Update verfügbar!\n", - containerName, tag, localDigest, remoteDigest) + fmt.Printf("⚠️ Container: %s\n -> Image: %s\n -> Update verfügbar!\n", containerName, rawTag) } else { - fmt.Printf("Container: %s\n Image: %s\n Local Digest: %s\n Remote Digest: %s\n -> Kein Update verfügbar.\n", - containerName, tag, localDigest, remoteDigest) + fmt.Printf("✅ Container: %s\n -> Image: %s\n -> Aktuell\n", containerName, rawTag) } - imageName, imageTag := tag, "latest" - if cp := strings.Split(tag, ":"); len(cp) == 2 { + imageName, imageTag := rawTag, "latest" + if cp := strings.Split(rawTag, ":"); len(cp) == 2 { imageName, imageTag = cp[0], cp[1] } @@ -205,8 +204,40 @@ func (c *imageUpdateCollector) Collect(ch chan<- prometheus.Metric) { } } +func loadIntervalFromEnv() { + if val := os.Getenv("CHECK_INTERVAL"); val != "" { + dur, err := time.ParseDuration(val) + if err != nil { + log.Printf("❌ Ungültiger CHECK_INTERVAL: %s – verwende Default (%s)", val, interval) + } else { + interval = dur + } + } +} + +func loadExclusionsFromEnv() { + raw := os.Getenv("EXCLUDE_CONTAINERS") + if raw == "" { + return + } + list := strings.Split(raw, ",") + for _, name := range list { + clean := strings.TrimSpace(name) + if clean != "" { + excludeContainers[clean] = struct{}{} + } + } +} + func main() { - log.Printf("🚀 Docker Image Update Exporter gestartet – Intervall: %v", interval) + log.Println("🚀 Docker Image Update Exporter startet...") + + loadIntervalFromEnv() + loadExclusionsFromEnv() + log.Printf("🔁 Prüfintervall: %v", interval) + if len(excludeContainers) > 0 { + log.Printf("🙈 Ignorierte Container: %v", keys(excludeContainers)) + } go func() { for { @@ -214,6 +245,7 @@ func main() { time.Sleep(interval) } }() + checkImageUpdates() exporter := newImageUpdateCollector() @@ -222,3 +254,11 @@ func main() { http.Handle("/metrics", promhttp.Handler()) log.Fatal(http.ListenAndServe(":9788", nil)) } + +func keys(m map[string]struct{}) []string { + var result []string + for k := range m { + result = append(result, k) + } + return result +}