From a92ed9fe8fc4094c4794308797aec3a11935165f Mon Sep 17 00:00:00 2001 From: Simon Rieger Date: Thu, 17 Jul 2025 10:24:53 +0200 Subject: [PATCH] add cache --- go/main.go | 307 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 178 insertions(+), 129 deletions(-) diff --git a/go/main.go b/go/main.go index 7403d5e..ffd5e53 100644 --- a/go/main.go +++ b/go/main.go @@ -1,155 +1,204 @@ package main import ( - "context" - "fmt" - "log" - "net/http" - "regexp" - "strings" - "sync" + "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 +} + +type StatusCache struct { + sync.RWMutex + Data []ImageStatus + LastCheck time.Time +} + +var ( + 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() + + containers, err := cli.ContainerList(ctx, container.ListOptions{All: false}) + if err != nil { + log.Printf("Fehler bei ContainerList: %v", err) + return + } + + images, err := cli.ImageList(ctx, image.ListOptions{All: true}) + if err != nil { + log.Printf("Fehler bei ImageList: %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 + } + } + } + + 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() + + 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] + } + 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() + + // Im Cache speichern + 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 des laufenden Containers für das Tag im Registry 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) { - 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() - - // Hole nur aktive (laufende) Container - containers, err := cli.ContainerList(ctx, container.ListOptions{All: false}) - if err != nil { - log.Printf("Fehler bei ContainerList: %v", err) - return - } - - images, err := cli.ImageList(ctx, image.ListOptions{All: true}) - if err != nil { - log.Printf("Fehler bei ImageList: %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 - } - } - } - - var wg sync.WaitGroup - 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] - - wg.Add(1) - go func(ctr container.Summary, 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() - - fmt.Printf("Container: %s\n Image: %s\n Local Digest: %s\n Remote Digest: %s\n", ctr.Names[0], tag, localDigest, remoteDigest) - var update float64 = 0 - if localDigest != remoteDigest { - fmt.Println(" -> Update verfügbar!") - update = 1 - } else { - fmt.Println(" -> Kein Update verfügbar.") - } - // Labels container_name, image, tag - labelImg, labelTag := tag, "latest" - if cp := strings.Split(tag, ":"); len(cp) == 2 { - labelImg, labelTag = cp[0], cp[1] - } - ch <- prometheus.MustNewConstMetric( - c.metric, prometheus.GaugeValue, - update, ctr.Names[0], labelImg, labelTag, - ) - }(ctr, tag, localDigest, imageRef) - } - wg.Wait() + cache.RLock() + for _, stat := range cache.Data { + ch <- prometheus.MustNewConstMetric( + c.metric, prometheus.GaugeValue, + stat.UpdateAvailable, stat.ContainerName, stat.Image, stat.Tag, + ) + } + cache.RUnlock() } func main() { - log.Println("Starte Prometheus Exporter für laufende Container-Images (Port 9788)...") - exporter := newImageUpdateCollector() - prometheus.MustRegister(exporter) - http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(":9788", nil)) + log.Printf("Starte Docker-Image-Update-Exporter mit 6h-Intervall...") + + // Hintergrund: alle 6h Update, zu Beginn auch direkt + go func() { + for { + checkImageUpdates() + time.Sleep(interval) + } + }() + checkImageUpdates() // Initiales Scrape + + exporter := newImageUpdateCollector() + prometheus.MustRegister(exporter) + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(":9788", nil)) }