diff --git a/go.mod b/go.mod index cab85d6..b63599e 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ go 1.24.1 require ( github.com/docker/docker v28.3.2+incompatible + github.com/prometheus/client_golang v1.22.0 github.com/regclient/regclient v0.9.0 ) require ( github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -25,9 +28,13 @@ require ( github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/ulikunitz/xz v0.5.12 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -38,5 +45,6 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/time v0.12.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gotest.tools/v3 v3.5.2 // indirect ) diff --git a/go.sum b/go.sum index bbe776a..53eb878 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -43,6 +47,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -53,6 +59,8 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI= github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -64,6 +72,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/regclient/regclient v0.9.0 h1:c3hNJZvtv8lMqhP0jGCa4d9j2n4688VCfhCWddGfWfk= github.com/regclient/regclient v0.9.0/go.mod h1:Adv7tukwdX+oDTszfILrjerGk55Pg2nKlbshj94U3rg= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= diff --git a/main.go b/main.go index 8b2f6f6..c7b3d4d 100644 --- a/main.go +++ b/main.go @@ -4,16 +4,19 @@ 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" ) -// Wandelt ein ImageTag in registrykompatibles Format (ggf. registry hinzufügen) +// Hilfsfunktion: registrykompatibles Format aus repo:tag erzeugen func toRegistryImage(imageTag string) (string, error) { r := regexp.MustCompile(`^(?:(?P[^/]+)/)?(?P[^:]+)(?::(?P.+))?$`) match := r.FindStringSubmatch(imageTag) @@ -32,28 +35,50 @@ func toRegistryImage(imageTag string) (string, error) { return fmt.Sprintf("%s/%s:%s", registry, repo, tag), nil } -// Extrahiert nur den reinen sha256:<...>-Digest +// 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 // ggf. nur die ID, wenn kein Digest + return s } -func main() { +// 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.Fatal(err) + 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.Fatal(err) + log.Println("Docker-ImageList-Fehler:", err) + return } for _, img := range images { @@ -62,34 +87,38 @@ func main() { if err != nil { continue } - - // Lokalen Digest extrahieren var localDigest string if len(img.RepoDigests) > 0 { localDigest = extractDigest(img.RepoDigests[0]) } else { localDigest = img.ID } - - // Remote-Digest bestimmen refObj, err := ref.New(imageRef) if err != nil { - fmt.Printf("ImageRef-Fehler bei %s: %v\n", tag, err) continue } desc, err := rc.ManifestHead(ctx, refObj) if err != nil { - fmt.Printf("Manifest nicht gefunden (%s): %v\n", tag, err) continue } remoteDigest := desc.GetDigest().String() - - fmt.Printf("Image: %s\n Local Digest: %s\n Remote Digest: %s\n", tag, localDigest, remoteDigest) - if localDigest == remoteDigest { - fmt.Println(" -> Kein Update verfügbar.") - } else { - fmt.Println(" -> Update verfügbar!") + 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)) +}