From 7cebb77160ffe4d15e480264c7123596f0c87dcb Mon Sep 17 00:00:00 2001 From: Simon Rieger Date: Mon, 21 Jul 2025 09:23:50 +0200 Subject: [PATCH] remove go func --- go/main.go | 323 +++++++++++++++++++++++++++-------------------------- 1 file changed, 166 insertions(+), 157 deletions(-) diff --git a/go/main.go b/go/main.go index ffd5e53..5f5dcf3 100644 --- a/go/main.go +++ b/go/main.go @@ -1,204 +1,213 @@ package main import ( - "context" - "fmt" - "log" - "net/http" - "regexp" - "strings" - "sync" - "time" + "context" + "fmt" + "log" + "net/http" + "regexp" + "strings" + "sync" + "time" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/regclient/regclient" - "github.com/regclient/regclient/types/ref" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/regclient/regclient" + "github.com/regclient/regclient/types/ref" ) type ImageStatus struct { - ContainerName, Image, Tag string - UpdateAvailable float64 - LocalDigest, RemoteDigest string + ContainerName string + Image string + Tag string + UpdateAvailable float64 + LocalDigest string + RemoteDigest string } type StatusCache struct { - sync.RWMutex - Data []ImageStatus - LastCheck time.Time + sync.RWMutex + Data []ImageStatus + LastCheck time.Time } var ( - cache = &StatusCache{Data: []ImageStatus{}, LastCheck: time.Time{}} - interval = 6 * time.Hour + cache = &StatusCache{Data: []ImageStatus{}, LastCheck: time.Time{}} + interval = 6 * time.Hour ) func toRegistryImage(imageTag string) (string, error) { - r := regexp.MustCompile(`^(?:(?P[^/]+)/)?(?P[^:]+)(?::(?P.+))?$`) - match := r.FindStringSubmatch(imageTag) - if len(match) == 0 { - return "", fmt.Errorf("Image-Tag nicht erkannt: %s", imageTag) - } - registry := match[r.SubexpIndex("registry")] - repo := match[r.SubexpIndex("repo")] - tag := match[r.SubexpIndex("tag")] - if registry == "" { - registry = "registry-1.docker.io" - } - if tag == "" { - tag = "latest" - } - return fmt.Sprintf("%s/%s:%s", registry, repo, tag), nil + r := regexp.MustCompile(`^(?:(?P[^/]+)/)?(?P[^:]+)(?::(?P.+))?$`) + match := r.FindStringSubmatch(imageTag) + if len(match) == 0 { + return "", fmt.Errorf("Image-Tag nicht erkannt: %s", imageTag) + } + registry := match[r.SubexpIndex("registry")] + repo := match[r.SubexpIndex("repo")] + tag := match[r.SubexpIndex("tag")] + if registry == "" { + registry = "registry-1.docker.io" + } + if tag == "" { + tag = "latest" + } + return fmt.Sprintf("%s/%s:%s", registry, repo, tag), nil } func extractDigest(s string) string { - for _, part := range strings.Split(s, "@") { - if strings.HasPrefix(part, "sha256:") { - return part - } - } - return s + for _, part := range strings.Split(s, "@") { + if strings.HasPrefix(part, "sha256:") { + return part + } + } + return s } func checkImageUpdates() { - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - log.Printf("Fehler bei Docker-Client: %v", err) - return - } - defer cli.Close() - rc := regclient.New() + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + log.Printf("Fehler bei Docker-Client: %v", err) + return + } + defer cli.Close() - containers, err := cli.ContainerList(ctx, container.ListOptions{All: false}) - if err != nil { - log.Printf("Fehler bei ContainerList: %v", err) - return - } + rc := regclient.New() - images, err := cli.ImageList(ctx, image.ListOptions{All: true}) - if err != nil { - log.Printf("Fehler bei ImageList: %v", err) - return - } + // Nur laufende Container abrufen + containers, err := cli.ContainerList(ctx, container.ListOptions{All: false}) + if err != nil { + log.Printf("Fehler beim ContainerList: %v", err) + return + } - imageTagToDigest := make(map[string]string) - for _, img := range images { - for _, tag := range img.RepoTags { - if len(img.RepoDigests) > 0 { - imageTagToDigest[tag] = extractDigest(img.RepoDigests[0]) - } else { - imageTagToDigest[tag] = img.ID - } - } - } + images, err := cli.ImageList(ctx, image.ListOptions{All: true}) + if err != nil { + log.Printf("Fehler beim ImageList: %v", err) + return + } - var wg sync.WaitGroup - results := make([]ImageStatus, 0) - resultsLock := sync.Mutex{} - for _, ctr := range containers { - tag := ctr.Image - imageRef, err := toRegistryImage(tag) - if err != nil { - log.Printf("ImageRef-Fehler bei %s: %v", tag, err) - continue - } - localDigest := imageTagToDigest[tag] - containerName := "unknown" - if len(ctr.Names) > 0 { - containerName = ctr.Names[0] - } - wg.Add(1) - go func(containerName, tag, localDigest, imageRef string) { - defer wg.Done() - refObj, err := ref.New(imageRef) - if err != nil { - log.Printf("ImageRef (regclient) ungültig (%s): %v", tag, err) - return - } - desc, err := rc.ManifestHead(ctx, refObj) - if err != nil { - log.Printf("Remote-Manifest nicht gefunden (%s): %v", tag, err) - return - } - remoteDigest := desc.GetDigest().String() + // RepoTag → Digest map + imageTagToDigest := make(map[string]string) + for _, img := range images { + for _, tag := range img.RepoTags { + if len(img.RepoDigests) > 0 { + imageTagToDigest[tag] = extractDigest(img.RepoDigests[0]) + } else { + imageTagToDigest[tag] = img.ID + } + } + } - 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) - } 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) - } + results := make([]ImageStatus, 0) + for _, ctr := range containers { + tag := ctr.Image + containerName := "unknown" + if len(ctr.Names) > 0 { + containerName = ctr.Names[0] + } - labelImg, labelTag := tag, "latest" - if cp := strings.Split(tag, ":"); len(cp) == 2 { - labelImg, labelTag = cp[0], cp[1] - } - resultsLock.Lock() - results = append(results, ImageStatus{ - ContainerName: containerName, Image: labelImg, Tag: labelTag, - UpdateAvailable: update, LocalDigest: localDigest, RemoteDigest: remoteDigest, - }) - resultsLock.Unlock() - }(containerName, tag, localDigest, imageRef) - } - wg.Wait() + imageRef, err := toRegistryImage(tag) + if err != nil { + log.Printf("Fehler beim Parsen des Image-Namens (%s): %v", tag, err) + continue + } - // Im Cache speichern - cache.Lock() - cache.Data = results - cache.LastCheck = time.Now() - cache.Unlock() + localDigest := imageTagToDigest[tag] + + refObj, err := ref.New(imageRef) + if err != nil { + log.Printf("Ungültige Image-Referenz (%s): %v", tag, err) + continue + } + + desc, err := rc.ManifestHead(ctx, refObj) + if err != nil { + log.Printf("Fehler beim Abrufen des Remote-Manifests (%s): %v", tag, err) + continue + } + 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) + } 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) + } + + labelImg, labelTag := tag, "latest" + if cp := strings.Split(tag, ":"); len(cp) == 2 { + labelImg, labelTag = cp[0], cp[1] + } + + results = append(results, ImageStatus{ + ContainerName: containerName, + Image: labelImg, + Tag: labelTag, + UpdateAvailable: update, + LocalDigest: localDigest, + RemoteDigest: remoteDigest, + }) + } + + // Speichern in Cache + cache.Lock() + cache.Data = results + cache.LastCheck = time.Now() + cache.Unlock() } type imageUpdateCollector struct { - metric *prometheus.Desc + metric *prometheus.Desc } func newImageUpdateCollector() *imageUpdateCollector { - return &imageUpdateCollector{ - metric: prometheus.NewDesc( - "docker_image_update_available", - "Ob Update für das lokale Docker-Image des laufenden Containers für das Tag im Registry verfügbar ist (1=Update, 0=aktuell)", - []string{"container_name", "image", "tag"}, - nil, - ), - } + return &imageUpdateCollector{ + metric: prometheus.NewDesc( + "docker_image_update_available", + "Ob Update für das lokale Docker-Image eines laufenden Containers verfügbar ist (1 = Update, 0 = aktuell)", + []string{"container_name", "image", "tag"}, + nil, + ), + } } func (c *imageUpdateCollector) Describe(ch chan<- *prometheus.Desc) { - ch <- c.metric + ch <- c.metric } func (c *imageUpdateCollector) Collect(ch chan<- prometheus.Metric) { - cache.RLock() - for _, stat := range cache.Data { - ch <- prometheus.MustNewConstMetric( - c.metric, prometheus.GaugeValue, - stat.UpdateAvailable, stat.ContainerName, stat.Image, stat.Tag, - ) - } - cache.RUnlock() + cache.RLock() + defer cache.RUnlock() + for _, stat := range cache.Data { + ch <- prometheus.MustNewConstMetric( + c.metric, prometheus.GaugeValue, + stat.UpdateAvailable, stat.ContainerName, stat.Image, stat.Tag, + ) + } } func main() { - log.Printf("Starte Docker-Image-Update-Exporter mit 6h-Intervall...") + log.Printf("🚀 Docker Image Update Exporter gestartet (Intervall = %v)", interval) - // Hintergrund: alle 6h Update, zu Beginn auch direkt - go func() { - for { - checkImageUpdates() - time.Sleep(interval) - } - }() - checkImageUpdates() // Initiales Scrape + // Hintergrundprozess zum Aktualisieren des Caches + go func() { + for { + checkImageUpdates() + time.Sleep(interval) + } + }() + checkImageUpdates() // initial - exporter := newImageUpdateCollector() - prometheus.MustRegister(exporter) - http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(":9788", nil)) + exporter := newImageUpdateCollector() + prometheus.MustRegister(exporter) + + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(":9788", nil)) }