package main import ( "context" "fmt" "log" "net/http" "regexp" "strings" "sync" "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" ) 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 } func extractDigest(s string) string { for _, part := range strings.Split(s, "@") { if strings.HasPrefix(part, "sha256:") { return part } } return s } type imageUpdateCollector struct { 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, ), } } func (c *imageUpdateCollector) Describe(ch chan<- *prometheus.Desc) { 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() } 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)) }