From 3ff5a90be334123c6cd3cc6820f3a73ef3513de5 Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 15 May 2024 00:48:43 -0700 Subject: [PATCH] image inspect fixes & cleanup Signed-off-by: apostasie --- pkg/cmd/image/inspect.go | 95 ++++++++++++------- pkg/inspecttypes/dockercompat/dockercompat.go | 69 +++++++------- 2 files changed, 97 insertions(+), 67 deletions(-) diff --git a/pkg/cmd/image/inspect.go b/pkg/cmd/image/inspect.go index f37e9e0e217..55a06670586 100644 --- a/pkg/cmd/image/inspect.go +++ b/pkg/cmd/image/inspect.go @@ -19,58 +19,87 @@ package image import ( "context" "fmt" + "regexp" "time" "github.com/containerd/containerd" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/formatter" - "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/imageinspector" + "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/referenceutil" ) // Inspect prints detailed information of each image in `images`. -func Inspect(ctx context.Context, client *containerd.Client, images []string, options types.ImageInspectOptions) error { - f := &imageInspector{ - mode: options.Mode, +func Inspect(ctx context.Context, client *containerd.Client, identifiers []string, options types.ImageInspectOptions) error { + // Verify we have a valid mode + // TODO: move this up to Cobra arg line validation + if options.Mode != "native" && options.Mode != "dockercompat" { + return fmt.Errorf("unknown mode %q", options.Mode) } - walker := &imagewalker.ImageWalker{ - Client: client, - OnFound: func(ctx context.Context, found imagewalker.Found) error { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - n, err := imageinspector.Inspect(ctx, client, found.Image, options.GOptions.Snapshotter) - if err != nil { - return err - } - switch f.mode { - case "native": - f.entries = append(f.entries, n) - case "dockercompat": - d, err := dockercompat.ImageFromNative(n) + objects := make(map[string]*dockercompat.Image) + var entries []interface{} + + // Construct the filters + filters := []string{} + for _, identifier := range identifiers { + if canonicalRef, err := referenceutil.ParseAny(identifier); err == nil { + filters = append(filters, fmt.Sprintf("name==%s", canonicalRef.String())) + } + filters = append(filters, + fmt.Sprintf("name==%s", identifier), + fmt.Sprintf("target.digest~=^sha256:%s.*$", regexp.QuoteMeta(identifier)), + fmt.Sprintf("target.digest~=^%s.*$", regexp.QuoteMeta(identifier)), + ) + } + + // Set a timeout + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + // Query containerd image service to retrieve a slice of containerd images + images, err := client.ImageService().List(ctx, filters...) + if err != nil { + return fmt.Errorf("image inspect errored while trying to query containerd ImageService: %w", err) + } + + // Iterate over the results + for _, image := range images { + // Query each image + nativeImage, err := imageinspector.Inspect(ctx, client, image, options.GOptions.Snapshotter) + if err != nil { + return fmt.Errorf("image inspect errored while trying to inspect image %s: %w", image.Name, err) + } + switch options.Mode { + case "native": + // If native, add the entry as-is + entries = append(entries, nativeImage) + case "dockercompat": + // If docker compat, possibly coalesce entries. First, get the image digest + newDigest := nativeImage.ImageConfigDesc.Digest.String() + // If we do not know about this yet, add it + if objects[newDigest] == nil { + dockerCompatImage, err := dockercompat.ImageFromNative(nativeImage) if err != nil { - return err + return fmt.Errorf("image inspect failed to marshall native image: %w", err) } - f.entries = append(f.entries, d) - default: - return fmt.Errorf("unknown mode %q", f.mode) + objects[newDigest] = dockerCompatImage + entries = append(entries, dockerCompatImage) + } else { + // If we do know about it, add the RepoTags to the existing entry + repository, tag := imgutil.ParseRepoTag(nativeImage.Image.Name) + objects[newDigest].RepoTags = append(objects[newDigest].RepoTags, fmt.Sprintf("%s:%s", repository, tag)) } - return nil - }, + } } - err := walker.WalkAll(ctx, images, true) - if len(f.entries) > 0 { - if formatErr := formatter.FormatSlice(options.Format, options.Stdout, f.entries); formatErr != nil { + if len(entries) > 0 { + if formatErr := formatter.FormatSlice(options.Format, options.Stdout, entries); formatErr != nil { log.G(ctx).Error(formatErr) } } - return err -} - -type imageInspector struct { - mode string - entries []interface{} + return nil } diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index da9f714c9df..1b6d83a9281 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -37,7 +37,7 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/runtime/restart" - gocni "github.com/containerd/go-cni" + "github.com/containerd/go-cni" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" @@ -294,48 +294,49 @@ func ContainerFromNative(n *native.Container) (*Container, error) { return c, nil } -func ImageFromNative(n *native.Image) (*Image, error) { - i := &Image{} - - imgoci := n.ImageConfig +func ImageFromNative(nativeImage *native.Image) (*Image, error) { + imgOCI := nativeImage.ImageConfig + repository, tag := imgutil.ParseRepoTag(nativeImage.Image.Name) + + image := &Image{ + // Docker ID (digest of platform-specific config), not containerd ID (digest of multi-platform index or manifest) + ID: nativeImage.ImageConfigDesc.Digest.String(), + // Parent: nativeImage.Image.Labels["org.mobyproject.image.parent"], + Architecture: imgOCI.Architecture, + Os: imgOCI.OS, + Size: nativeImage.Size, + RepoTags: []string{fmt.Sprintf("%s:%s", repository, tag)}, + RepoDigests: []string{fmt.Sprintf("%s@%s", repository, nativeImage.Image.Target.Digest.String())}, + } - i.RootFS.Type = imgoci.RootFS.Type - diffIDs := imgoci.RootFS.DiffIDs - for _, d := range diffIDs { - i.RootFS.Layers = append(i.RootFS.Layers, d.String()) + if len(imgOCI.History) > 0 { + image.Comment = imgOCI.History[len(imgOCI.History)-1].Comment + image.Created = imgOCI.History[len(imgOCI.History)-1].Created.Format(time.RFC3339Nano) + image.Author = imgOCI.History[len(imgOCI.History)-1].Author } - if len(imgoci.History) > 0 { - i.Comment = imgoci.History[len(imgoci.History)-1].Comment - i.Created = imgoci.History[len(imgoci.History)-1].Created.Format(time.RFC3339Nano) - i.Author = imgoci.History[len(imgoci.History)-1].Author + + image.RootFS.Type = imgOCI.RootFS.Type + for _, d := range imgOCI.RootFS.DiffIDs { + image.RootFS.Layers = append(image.RootFS.Layers, d.String()) } - i.Architecture = imgoci.Architecture - i.Os = imgoci.OS portSet := make(nat.PortSet) - for k := range imgoci.Config.ExposedPorts { + for k := range imgOCI.Config.ExposedPorts { portSet[nat.Port(k)] = struct{}{} } - i.Config = &Config{ - Cmd: imgoci.Config.Cmd, - Volumes: imgoci.Config.Volumes, - Env: imgoci.Config.Env, - User: imgoci.Config.User, - WorkingDir: imgoci.Config.WorkingDir, - Entrypoint: imgoci.Config.Entrypoint, - Labels: imgoci.Config.Labels, + image.Config = &Config{ + Cmd: imgOCI.Config.Cmd, + Volumes: imgOCI.Config.Volumes, + Env: imgOCI.Config.Env, + User: imgOCI.Config.User, + WorkingDir: imgOCI.Config.WorkingDir, + Entrypoint: imgOCI.Config.Entrypoint, + Labels: imgOCI.Config.Labels, ExposedPorts: portSet, } - i.ID = n.ImageConfigDesc.Digest.String() // Docker ID (digest of platform-specific config), not containerd ID (digest of multi-platform index or manifest) - - repository, tag := imgutil.ParseRepoTag(n.Image.Name) - - i.RepoTags = []string{fmt.Sprintf("%s:%s", repository, tag)} - i.RepoDigests = []string{fmt.Sprintf("%s@%s", repository, n.Image.Target.Digest.String())} - i.Size = n.Size - return i, nil + return image, nil } // mountsFromNative only filters bind mount to transform from native container. @@ -411,7 +412,7 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting res.Networks[fakeDockerNetworkName] = nes if portsLabel, ok := sp.Annotations[labels.Ports]; ok { - var ports []gocni.PortMapping + var ports []cni.PortMapping err := json.Unmarshal([]byte(portsLabel), &ports) if err != nil { return nil, err @@ -437,7 +438,7 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting return res, nil } -func convertToNatPort(portMappings []gocni.PortMapping) (*nat.PortMap, error) { +func convertToNatPort(portMappings []cni.PortMapping) (*nat.PortMap, error) { portMap := make(nat.PortMap) for _, portMapping := range portMappings { ports := []nat.PortBinding{}