diff --git a/cmd/ctr/commands/images/images.go b/cmd/ctr/commands/images/images.go index e5caa96f4..2856c7898 100644 --- a/cmd/ctr/commands/images/images.go +++ b/cmd/ctr/commands/images/images.go @@ -42,6 +42,7 @@ var Command = cli.Command{ checkCommand, exportCommand, importCommand, + inspectCommand, listCommand, mountCommand, unmountCommand, diff --git a/cmd/ctr/commands/images/inspect.go b/cmd/ctr/commands/images/inspect.go new file mode 100644 index 000000000..7dff057d1 --- /dev/null +++ b/cmd/ctr/commands/images/inspect.go @@ -0,0 +1,65 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package images + +import ( + "os" + + "github.com/containerd/containerd/cmd/ctr/commands" + "github.com/containerd/containerd/pkg/display" + "github.com/urfave/cli" +) + +var inspectCommand = cli.Command{ + Name: "inspect", + Aliases: []string{"i"}, + Usage: "inspect an image", + ArgsUsage: " [flags]", + Description: `Inspect an image`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "content", + Usage: "Show JSON content", + }, + }, + Action: func(clicontext *cli.Context) error { + client, ctx, cancel, err := commands.NewClient(clicontext) + if err != nil { + return err + } + defer cancel() + var ( + ref = clicontext.Args().First() + imageStore = client.ImageService() + cs = client.ContentStore() + ) + + img, err := imageStore.Get(ctx, ref) + if err != nil { + return err + } + + opts := []display.PrintOpt{ + display.WithWriter(os.Stdout), + } + if clicontext.Bool("content") { + opts = append(opts, display.Verbose) + } + + return display.NewImageTreePrinter(opts...).PrintImageTree(ctx, img, cs) + }, +} diff --git a/pkg/display/manifest_printer.go b/pkg/display/manifest_printer.go new file mode 100644 index 000000000..f762d9621 --- /dev/null +++ b/pkg/display/manifest_printer.go @@ -0,0 +1,211 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package display + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/images" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// TreeFormat is used to format tree based output using 4 values. +// Each value must display with the same total width to format correctly. +// +// MiddleDrop is used to show a child element which is not the last child +// LastDrop is used to show the last child element +// SkipLine is used for displaying data from a previous child before the next child +// Spacer is used to display child data for the last child +type TreeFormat struct { + MiddleDrop string + LastDrop string + SkipLine string + Spacer string +} + +// LineTreeFormat uses line drawing characters to format a tree +// +// TreeRoot +// ├── First child # MiddleDrop = "├── " +// │ Skipped line # SkipLine = "│ " +// └── Last child # LastDrop = "└── " +// ....└── Only child # Spacer="....", LastDrop = "└── " +var LineTreeFormat = TreeFormat{ + MiddleDrop: "├── ", + LastDrop: "└── ", + SkipLine: "│ ", + Spacer: " ", +} + +type ContentReader interface { + content.Provider + + Info(ctx context.Context, dgst digest.Digest) (content.Info, error) +} + +type ImageTreePrinter struct { + verbose bool + w io.Writer + format TreeFormat +} + +type PrintOpt func(*ImageTreePrinter) + +func Verbose(p *ImageTreePrinter) { + p.verbose = true +} + +func WithWriter(w io.Writer) PrintOpt { + return func(p *ImageTreePrinter) { + p.w = w + } +} + +func WithFormat(format TreeFormat) PrintOpt { + return func(p *ImageTreePrinter) { + p.format = format + } +} + +func NewImageTreePrinter(opts ...PrintOpt) *ImageTreePrinter { + p := &ImageTreePrinter{ + verbose: false, + w: os.Stdout, + format: LineTreeFormat, + } + for _, opt := range opts { + opt(p) + } + + return p +} + +// PrintImageTree prints an image and all its sub elements +func (p *ImageTreePrinter) PrintImageTree(ctx context.Context, img images.Image, store ContentReader) error { + fmt.Fprintln(p.w, img.Name) + subchild := p.format.SkipLine + fmt.Fprintf(p.w, "%s Created: %s\n", subchild, img.CreatedAt) + fmt.Fprintf(p.w, "%s Updated: %s\n", subchild, img.UpdatedAt) + for k, v := range img.Labels { + fmt.Fprintf(p.w, "%s Label %q: %q\n", subchild, k, v) + } + return p.printManifestTree(ctx, img.Target, store, p.format.LastDrop, p.format.Spacer) +} + +// PrintManifestTree prints a manifest and all its sub elements +func (p *ImageTreePrinter) PrintManifestTree(ctx context.Context, desc ocispec.Descriptor, store ContentReader) error { + // start displaying tree from the root descriptor perspective, which is a single child view + return p.printManifestTree(ctx, desc, store, p.format.LastDrop, p.format.Spacer) +} + +func (p *ImageTreePrinter) printManifestTree(ctx context.Context, desc ocispec.Descriptor, store ContentReader, prefix, childprefix string) error { + subprefix := childprefix + p.format.MiddleDrop + subchild := childprefix + p.format.SkipLine + fmt.Fprintf(p.w, "%s%s @%s (%d bytes)\n", prefix, desc.MediaType, desc.Digest, desc.Size) + + if desc.Platform != nil && desc.Platform.Architecture != "" { + // TODO: Use containerd platform library to format + fmt.Fprintf(p.w, "%s Platform: %s/%s\n", subchild, desc.Platform.OS, desc.Platform.Architecture) + } + b, err := content.ReadBlob(ctx, store, desc) + if err != nil { + return err + } + if err := p.showContent(ctx, store, desc, subchild); err != nil { + return err + } + + switch desc.MediaType { + case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: + var manifest ocispec.Manifest + if err := json.Unmarshal(b, &manifest); err != nil { + return err + } + + if len(manifest.Layers) == 0 { + subprefix = childprefix + p.format.LastDrop + subchild = childprefix + p.format.Spacer + } + fmt.Fprintf(p.w, "%s%s @%s (%d bytes)\n", subprefix, manifest.Config.MediaType, manifest.Config.Digest, manifest.Config.Size) + + if err := p.showContent(ctx, store, manifest.Config, subchild); err != nil { + return err + } + + for i := range manifest.Layers { + if len(manifest.Layers) == i+1 { + subprefix = childprefix + p.format.LastDrop + } + fmt.Fprintf(p.w, "%s%s @%s (%d bytes)\n", subprefix, manifest.Layers[i].MediaType, manifest.Layers[i].Digest, manifest.Layers[i].Size) + } + + case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: + var idx ocispec.Index + if err := json.Unmarshal(b, &idx); err != nil { + return err + } + + for i := range idx.Manifests { + if len(idx.Manifests) == i+1 { + subprefix = childprefix + p.format.LastDrop + subchild = childprefix + p.format.Spacer + } + if err := p.printManifestTree(ctx, idx.Manifests[i], store, subprefix, subchild); err != nil { + return err + } + } + } + + return nil +} + +func (p *ImageTreePrinter) showContent(ctx context.Context, store ContentReader, desc ocispec.Descriptor, prefix string) error { + if p.verbose { + info, err := store.Info(ctx, desc.Digest) + if err != nil { + return err + } + if len(info.Labels) > 0 { + fmt.Fprintf(p.w, "%s┌────────Labels─────────\n", prefix) + for k, v := range info.Labels { + fmt.Fprintf(p.w, "%s│%q: %q\n", prefix, k, v) + } + fmt.Fprintf(p.w, "%s└───────────────────────\n", prefix) + } + } + if p.verbose && strings.HasSuffix(desc.MediaType, "json") { + // Print content for config + cb, err := content.ReadBlob(ctx, store, desc) + if err != nil { + return err + } + dst := bytes.NewBuffer(nil) + json.Indent(dst, cb, prefix+"│", " ") + fmt.Fprintf(p.w, "%s┌────────Content────────\n", prefix) + fmt.Fprintf(p.w, "%s│%s\n", prefix, strings.TrimSpace(dst.String())) + fmt.Fprintf(p.w, "%s└───────────────────────\n", prefix) + } + return nil +}