From 1dc2b2a5b7b82121161cce4362ca7c731eb0d529 Mon Sep 17 00:00:00 2001 From: Simon Rieger Date: Thu, 17 Jul 2025 02:53:36 +0200 Subject: [PATCH] use regclient --- go.mod | 9 ++++- go.sum | 16 ++++++++ main.go | 116 ++++++++++++++++++++++++++------------------------------ 3 files changed, 77 insertions(+), 64 deletions(-) diff --git a/go.mod b/go.mod index 95a29af..cab85d6 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module image-checker go 1.24.1 -require github.com/docker/docker v28.3.2+incompatible +require ( + github.com/docker/docker v28.3.2+incompatible + github.com/regclient/regclient v0.9.0 +) require ( github.com/Microsoft/go-winio v0.4.14 // indirect @@ -12,10 +15,12 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect @@ -23,6 +28,8 @@ require ( 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/sirupsen/logrus v1.9.3 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect diff --git a/go.sum b/go.sum index 7032ea7..bbe776a 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -20,6 +21,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -37,6 +40,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/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= @@ -48,6 +53,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/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= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -57,13 +64,19 @@ 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/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= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -105,6 +118,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -129,6 +143,8 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= diff --git a/main.go b/main.go index 2a862d4..a7955c1 100644 --- a/main.go +++ b/main.go @@ -2,76 +2,56 @@ package main import ( "context" - "encoding/json" "fmt" - "io" - "net/http" + "log" + "os" "regexp" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" + "github.com/regclient/regclient" + "github.com/regclient/regclient/types/ref" ) -// Zerlegt einen Image-Namen in Registry, Repository und Tag -func parseImageName(imageName string) (registry, repo, tag string) { - defaultRegistry := "registry-1.docker.io" - tag = "latest" +// Zerlegt einen Image-Namen in die Form registry/repo:tag für regclient +func parseImageName(imageName string) (string, error) { r := regexp.MustCompile(`^(?:(?P[^/]+)/)?(?P[^:]+)(?::(?P.+))?$`) match := r.FindStringSubmatch(imageName) if len(match) == 0 { - return defaultRegistry, imageName, tag + return "", fmt.Errorf("Fehler beim Parsen des Image-Namens: %s", imageName) } - index := r.SubexpIndex("registry") - if match[index] != "" { - registry = match[index] - } else { - registry = defaultRegistry + registry := match[r.SubexpIndex("registry")] + repo := match[r.SubexpIndex("repo")] + tag := match[r.SubexpIndex("tag")] + if repo == "" { + return "", fmt.Errorf("Kein Repo erkannt in %s", imageName) } - index = r.SubexpIndex("repo") - repo = match[index] - index = r.SubexpIndex("tag") - if match[index] != "" { - tag = match[index] + if registry == "" { + registry = "registry-1.docker.io" } - return + if tag == "" { + tag = "latest" + } + return fmt.Sprintf("%s/%s:%s", registry, repo, tag), nil } -// Holt Digest aus Registry (funktioniert für öffentliche OCI-kompatible Registries) -func getRemoteDigest(registry, repo, tag string) (string, error) { - manifestURL := fmt.Sprintf("https://%s/v2/%s/manifests/%s", registry, repo, tag) - req, err := http.NewRequest("GET", manifestURL, nil) +// Digest remote ermitteln (regclient) +func getRemoteDigest(ctx context.Context, rc *regclient.RegClient, image string) (string, error) { + refObj, err := ref.New(image) if err != nil { - return "", err + return "", fmt.Errorf("image-ref ungültig: %w", err) } - req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") - resp, err := http.DefaultClient.Do(req) + desc, err := rc.ManifestHead(ctx, refObj) if err != nil { - return "", err + return "", fmt.Errorf("Fehler ManifestHead %s: %w", image, err) } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("%s [%d]: %s", manifestURL, resp.StatusCode, body) - } - digest := resp.Header.Get("Docker-Content-Digest") - if digest == "" { - var manifest map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&manifest); err == nil { - if config, ok := manifest["config"].(map[string]interface{}); ok { - if dgst, ok := config["digest"].(string); ok { - return dgst, nil - } - } - } - return "", fmt.Errorf("Kein Digest in Registry-Manifest gefunden") - } - return digest, nil + return desc.GetDigest().String(), nil } -// Gibt den Digest zu einem Image-Namen zurück (verwendet image.ListOptions) -func getLocalDigestOfImage(ctx context.Context, apiClient *client.Client, imageName string) (string, error) { - images, err := apiClient.ImageList(ctx, image.ListOptions{All: true}) +// Lokalen Digest via Docker-API holen +func getLocalDigestOfImage(ctx context.Context, cli *client.Client, imageName string) (string, error) { + images, err := cli.ImageList(ctx, image.ListOptions{All: true}) if err != nil { return "", err } @@ -89,32 +69,42 @@ func getLocalDigestOfImage(ctx context.Context, apiClient *client.Client, imageN } func main() { - apiClient, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - panic(err) - } - defer apiClient.Close() - ctx := context.Background() - containers, err := apiClient.ContainerList(ctx, container.ListOptions{All: true}) + + cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { - panic(err) + log.Fatal(err) + } + defer cli.Close() + + rc := regclient.New() + + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + log.Fatal(err) + } + if len(containers) == 0 { + fmt.Println("Keine laufenden Container gefunden") + os.Exit(0) } for _, ctr := range containers { - fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status) - registry, repo, tag := parseImageName(ctr.Image) - fmt.Printf(" Registry: %s\n Repo: %s\n Tag: %s\n", registry, repo, tag) + fmt.Printf("Container: %s %s (status: %s)\n", ctr.ID[:12], ctr.Image, ctr.Status) + imageFull, err := parseImageName(ctr.Image) + if err != nil { + fmt.Printf(" Fehler beim Parsen des Image-Namens: %v\n", err) + continue + } - localDigest, err := getLocalDigestOfImage(ctx, apiClient, ctr.Image) + localDigest, err := getLocalDigestOfImage(ctx, cli, ctr.Image) if err != nil { fmt.Printf(" Fehler beim lokalen Digest: %v\n", err) continue } - remoteDigest, err := getRemoteDigest(registry, repo, tag) + remoteDigest, err := getRemoteDigest(ctx, rc, imageFull) if err != nil { - fmt.Printf(" Fehler beim Registry-Digest: %v\n", err) + fmt.Printf(" Fehler beim Remote-Digest: %v\n", err) continue } @@ -123,7 +113,7 @@ func main() { if localDigest == remoteDigest { fmt.Println(" -> Image ist aktuell") } else { - fmt.Println(" -> Update verfügbar!") + fmt.Println(" -> Update verfügbar!\n") } } }