image-checker/go/main.go

270 lines
6.4 KiB
Go
Raw Permalink 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"
"os"
"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 // Default wird ggf. überschrieben durch CHECK_INTERVAL
excludeContainers = map[string]struct{}{}
)
// Helper: image ohne ":tag" erhält "latest"
func normalizeImageTag(tag string) string {
if !strings.Contains(tag, ":") {
return tag + ":latest"
}
return tag
}
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 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
}
}
}
for tag, digest := range imageTagToDigest {
fmt.Printf("imageTagToDigest: %s -> %s\n", tag, digest)
}
results := make([]ImageStatus, 0)
for _, ctr := range containers {
containerName := "unknown"
if len(ctr.Names) > 0 {
containerName = strings.TrimPrefix(ctr.Names[0], "/")
}
// Überspringen, wenn im EXCLUDE_CONTAINERS enthalten
if _, excluded := excludeContainers[containerName]; excluded {
fmt.Printf("⏭️ Container '%s' ist ausgeschlossen.\n", containerName)
continue
}
rawTag := normalizeImageTag(ctr.Image)
localDigest := imageTagToDigest[rawTag]
imageRef, err := toRegistryImage(rawTag)
if err != nil {
log.Printf("Ungültige Image-Referenz (%s): %v", rawTag, err)
continue
}
refObj, err := ref.New(imageRef)
if err != nil {
log.Printf("Fehler beim Erzeugen der Referenz (%s): %v", rawTag, err)
continue
}
desc, err := rc.ManifestHead(ctx, refObj)
if err != nil {
log.Printf("Remote-Manifest nicht gefunden (%s): %v", rawTag, err)
continue
}
remoteDigest := desc.GetDigest().String()
update := 0.0
fmt.Printf("Container: %s\n", containerName)
fmt.Printf(" Image: %s\n", rawTag)
fmt.Printf(" Local Digest: %s\n", localDigest)
fmt.Printf(" Remote Digest: %s\n", remoteDigest)
if localDigest != remoteDigest {
update = 1.0
fmt.Println(" -> ⚠️ Update verfügbar!")
} else {
fmt.Println(" -> ✅ Kein Update erforderlich.")
}
imageName, imageTag := rawTag, "latest"
if cp := strings.Split(rawTag, ":"); 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 loadIntervalFromEnv() {
if val := os.Getenv("CHECK_INTERVAL"); val != "" {
dur, err := time.ParseDuration(val)
if err != nil {
log.Printf("❌ Ungültiger CHECK_INTERVAL: %s verwende Default (%s)", val, interval)
} else {
interval = dur
}
}
}
func loadExclusionsFromEnv() {
raw := os.Getenv("EXCLUDE_CONTAINERS")
if raw == "" {
return
}
list := strings.Split(raw, ",")
for _, name := range list {
clean := strings.TrimSpace(name)
if clean != "" {
excludeContainers[clean] = struct{}{}
}
}
}
func main() {
log.Println("🚀 Docker Image Update Exporter startet...")
loadIntervalFromEnv()
loadExclusionsFromEnv()
log.Printf("🔁 Prüfintervall: %v", interval)
if len(excludeContainers) > 0 {
log.Printf("🙈 Ignorierte Container: %v", keys(excludeContainers))
}
go func() {
for {
checkImageUpdates()
time.Sleep(interval)
}
}()
exporter := newImageUpdateCollector()
prometheus.MustRegister(exporter)
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":9788", nil))
}
func keys(m map[string]struct{}) []string {
var result []string
for k := range m {
result = append(result, k)
}
return result
}