image-checker/go/main.go

264 lines
6.2 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"
"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
}
}
}
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
if localDigest != remoteDigest {
update = 1.0
fmt.Printf("⚠️ Container: %s\n -> Image: %s\n -> Update verfügbar!\n", containerName, rawTag)
} else {
fmt.Printf("✅ Container: %s\n -> Image: %s\n -> Aktuell\n", containerName, rawTag)
}
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)
}
}()
checkImageUpdates()
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
}