image-checker/go/main.go

224 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"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"
)
type ImageStatus struct {
ContainerName string
Image string
Tag string
UpdateAvailable float64
LocalDigest string
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<registry>[^/]+)/)?(?P<repo>[^:]+)(?::(?P<tag>.+))?$`)
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
}
func normalizeImageTag(tag string) string {
if !strings.Contains(tag, ":") {
return tag + ":latest"
}
return tag
}
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 beim ContainerList: %v", err)
return
}
images, err := cli.ImageList(ctx, image.ListOptions{All: true})
if err != nil {
log.Printf("Fehler beim ImageList: %v", err)
return
}
imageTagToDigest := make(map[string]string)
for _, img := range images {
for _, tag := range img.RepoTags {
normalizedTag := normalizeImageTag(tag)
if len(img.RepoDigests) > 0 {
imageTagToDigest[normalizedTag] = extractDigest(img.RepoDigests[0])
} else {
imageTagToDigest[normalizedTag] = img.ID
}
}
}
// Debug-Ausgabe aller bekannten imageTagToDigest
fmt.Println("DEBUG: Alle bekannten imageTagToDigest:")
for k, v := range imageTagToDigest {
fmt.Printf(" %s -> %s\n", k, v)
}
results := make([]ImageStatus, 0)
for _, ctr := range containers {
rawTag := ctr.Image
tag := normalizeImageTag(rawTag)
containerName := "unknown"
if len(ctr.Names) > 0 {
containerName = ctr.Names[0]
}
imageRef, err := toRegistryImage(tag)
if err != nil {
log.Printf("Fehler beim Parsen des Image-Namens (%s): %v", tag, err)
continue
}
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)
}
imageName, imageTag := tag, "latest"
if cp := strings.Split(tag, ":"); len(cp) == 2 {
imageName, imageTag = cp[0], cp[1]
}
results = append(results, ImageStatus{
ContainerName: containerName,
Image: imageName,
Tag: imageTag,
UpdateAvailable: update,
LocalDigest: localDigest,
RemoteDigest: remoteDigest,
})
}
cache.Lock()
cache.Data = results
cache.LastCheck = time.Now()
cache.Unlock()
}
type imageUpdateCollector struct {
metric *prometheus.Desc
}
func newImageUpdateCollector() *imageUpdateCollector {
return &imageUpdateCollector{
metric: prometheus.NewDesc(
"docker_image_update_available",
"Ob ein Update für das verwendete Image eines laufenden Containers verfügbar ist (1 = Update)",
[]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) {
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("🚀 Docker Image Update Exporter gestartet Intervall: %v", interval)
go func() {
for {
checkImageUpdates()
time.Sleep(interval)
}
}()
checkImageUpdates()
exporter := newImageUpdateCollector()
prometheus.MustRegister(exporter)
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":9788", nil))
}