Merge pull request #8998 from dmcgowan/image-inspect

ctr: images inspect
This commit is contained in:
Phil Estes 2023-08-23 14:12:56 -04:00 committed by GitHub
commit 8e7a25856b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 277 additions and 0 deletions

View File

@ -42,6 +42,7 @@ var Command = cli.Command{
checkCommand, checkCommand,
exportCommand, exportCommand,
importCommand, importCommand,
inspectCommand,
listCommand, listCommand,
mountCommand, mountCommand,
unmountCommand, unmountCommand,

View File

@ -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: "<image> [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)
},
}

View File

@ -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
}