package main import ( "context" "fmt" "log" "net/http" "regexp" "strings" "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" ) // Hilfsfunktion: registrykompatibles Format aus repo:tag erzeugen 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 } // Extrahiert nur den sha256:... Digest func extractDigest(s string) string { for _, part := range strings.Split(s, "@") { if strings.HasPrefix(part, "sha256:") { return part } } return s } // Prometheus Collector-Struktur type dockerImageUpdateCollector struct { desc *prometheus.Desc } func newDockerImageUpdateCollector() prometheus.Collector { return &dockerImageUpdateCollector{ desc: prometheus.NewDesc( "docker_image_update_available", "Whether a new image digest is available for this local image tag (1=update, 0=current)", []string{"image"}, nil, ), } } func (c *dockerImageUpdateCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.desc } func (c *dockerImageUpdateCollector) Collect(ch chan<- prometheus.Metric) { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { log.Println("Docker-Client-Fehler:", err) return } defer cli.Close() rc := regclient.New() images, err := cli.ImageList(ctx, image.ListOptions{All: true}) if err != nil { log.Println("Docker-ImageList-Fehler:", err) return } for _, img := range images { for _, tag := range img.RepoTags { imageRef, err := toRegistryImage(tag) if err != nil { continue } var localDigest string if len(img.RepoDigests) > 0 { localDigest = extractDigest(img.RepoDigests[0]) } else { localDigest = img.ID } refObj, err := ref.New(imageRef) if err != nil { continue } desc, err := rc.ManifestHead(ctx, refObj) if err != nil { continue } remoteDigest := desc.GetDigest().String() var value float64 = 0 if localDigest != remoteDigest { value = 1 } ch <- prometheus.MustNewConstMetric( c.desc, prometheus.GaugeValue, value, tag, ) } } } func main() { prometheus.MustRegister(newDockerImageUpdateCollector()) http.Handle("/metrics", promhttp.Handler()) log.Println("Prometheus Exporter läuft auf :9888/metrics") log.Fatal(http.ListenAndServe(":9888", nil)) }