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[^/]+)/)?(?P[^:]+)(?::(?P.+))?$`) 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!") } } }