129 lines
3.5 KiB
Go
129 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/client"
|
|
)
|
|
|
|
// Zerlegt einen Image-Namen in Registry, Repository und Tag
|
|
func parseImageName(imageName string) (registry, repo, tag string) {
|
|
defaultRegistry := "registry-1.docker.io"
|
|
tag = "latest"
|
|
r := regexp.MustCompile(`^(?:(?P<registry>[^/]+)/)?(?P<repo>[^:]+)(?::(?P<tag>.+))?$`)
|
|
match := r.FindStringSubmatch(imageName)
|
|
if len(match) == 0 {
|
|
return defaultRegistry, imageName, tag
|
|
}
|
|
index := r.SubexpIndex("registry")
|
|
if match[index] != "" {
|
|
registry = match[index]
|
|
} else {
|
|
registry = defaultRegistry
|
|
}
|
|
index = r.SubexpIndex("repo")
|
|
repo = match[index]
|
|
index = r.SubexpIndex("tag")
|
|
if match[index] != "" {
|
|
tag = match[index]
|
|
}
|
|
return
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", 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
|
|
}
|
|
|
|
// 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})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, img := range images {
|
|
for _, t := range img.RepoTags {
|
|
if t == imageName {
|
|
if len(img.RepoDigests) > 0 {
|
|
return img.RepoDigests[0], nil
|
|
}
|
|
return img.ID, nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("Image %s nicht lokal gefunden", imageName)
|
|
}
|
|
|
|
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})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
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)
|
|
|
|
localDigest, err := getLocalDigestOfImage(ctx, apiClient, ctr.Image)
|
|
if err != nil {
|
|
fmt.Printf(" Fehler beim lokalen Digest: %v\n", err)
|
|
continue
|
|
}
|
|
|
|
remoteDigest, err := getRemoteDigest(registry, repo, tag)
|
|
if err != nil {
|
|
fmt.Printf(" Fehler beim Registry-Digest: %v\n", err)
|
|
continue
|
|
}
|
|
|
|
fmt.Println(" Local Digest: ", localDigest)
|
|
fmt.Println(" Remote Digest:", remoteDigest)
|
|
if localDigest == remoteDigest {
|
|
fmt.Println(" -> Image ist aktuell")
|
|
} else {
|
|
fmt.Println(" -> Update verfügbar!")
|
|
}
|
|
}
|
|
}
|