Merge pull request #39 from docker/improve-tag-inspect

Improve tag inspect command
This commit is contained in:
Silvin Lubecki 2020-10-14 10:15:12 +02:00 коммит произвёл GitHub
Родитель 940ef126a6 6de02e4ce2
Коммит ee87a2cccb
5 изменённых файлов: 306 добавлений и 67 удалений

Просмотреть файл

@ -17,11 +17,15 @@
package tag
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"text/tabwriter"
"time"
"github.com/cli/cli/utils"
"github.com/containerd/containerd/images"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/cli/cli"
@ -45,6 +49,14 @@ type inspectOptions struct {
format string
}
//Image is the combination of a manifest and its config object
type Image struct {
Name reference.Named
Manifest ocispec.Manifest
Config ocispec.Image
Descriptor ocispec.Descriptor
}
func newInspectCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
@ -58,42 +70,38 @@ func newInspectCmd(streams command.Streams, hubClient *hub.Client, parent string
return runInspect(streams, hubClient, opts, args[0])
},
}
cmd.Flags().StringVar(&opts.format, "format", "", `Print original manifest ("json")`)
cmd.Flags().StringVar(&opts.format, "format", "", `Print original manifest ("json|raw")`)
cmd.Flags().SetInterspersed(false)
return cmd
}
func runInspect(streams command.Streams, hubClient *hub.Client, opts inspectOptions, image string) error {
func runInspect(streams command.Streams, hubClient *hub.Client, opts inspectOptions, imageRef string) error {
resolver := imagetools.New(imagetools.Opt{
Auth: &authResolver{
authConfig: convert(hubClient.AuthConfig),
},
})
raw, descriptor, err := resolver.Get(hubClient.Ctx, image)
raw, descriptor, err := resolver.Get(hubClient.Ctx, imageRef)
if err != nil {
return err
}
if opts.format == "json" {
fmt.Fprintf(streams.Out(), "%s", raw) // avoid newline to keep digest
return nil
} else if opts.format != "" {
return fmt.Errorf("unsupported format type: %q", opts.format)
ref, err := reference.ParseNormalizedNamed(imageRef)
if err != nil {
return err
}
ref = reference.TagNameOnly(ref)
switch descriptor.MediaType {
// case images.MediaTypeDockerSchema2Manifest, specs.MediaTypeImageManifest:
// TODO: handle distribution manifest and schema1
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
if err := imagetools.PrintManifestList(raw, descriptor, image, streams.Out()); err != nil {
return err
}
return formatManifestlist(streams, opts.format, raw, descriptor, imageRef)
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
if err := printManifest(raw, descriptor, image, streams.Out()); err != nil {
return err
}
return formatManifest(hubClient.Ctx, streams, resolver, opts.format, raw, descriptor, ref)
default:
fmt.Println("Other mediatype")
fmt.Fprintf(streams.Out(), "%s\n", raw)
}
@ -104,52 +112,211 @@ const (
pfx = " "
)
func printManifest(raw []byte, descriptor ocispec.Descriptor, image string, out io.Writer) error {
ref, err := reference.ParseNormalizedNamed(image)
func formatManifestlist(streams command.Streams, format string, raw []byte, descriptor ocispec.Descriptor, imageRef string) error {
switch format {
case "json", "raw":
_, err := fmt.Fprintf(streams.Out(), "%s", raw) // avoid newline to keep digest
return err
case "":
return imagetools.PrintManifestList(raw, descriptor, imageRef, streams.Out())
default:
return fmt.Errorf("unsupported format type: %q", format)
}
}
func formatManifest(ctx context.Context, streams command.Streams, resolver *imagetools.Resolver, format string, raw []byte, descriptor ocispec.Descriptor, ref reference.Named) error {
image, err := readImage(ctx, resolver, raw, descriptor, ref)
if err != nil {
return err
}
ref = reference.TagNameOnly(ref)
switch format {
case "raw":
_, err := fmt.Fprintf(streams.Out(), "%s", raw) // avoid newline to keep digest
return err
case "json":
buf, err := json.MarshalIndent(image, "", " ")
if err != nil {
return err
}
_, err = fmt.Fprint(streams.Out(), string(buf))
return err
case "":
return printImage(streams.Out(), image)
default:
return fmt.Errorf("unsupported format type: %q", format)
}
}
func readImage(ctx context.Context, resolver *imagetools.Resolver, rawManifest []byte, descriptor ocispec.Descriptor, ref reference.Named) (*Image, error) {
var manifest ocispec.Manifest
if err := json.Unmarshal(raw, &manifest); err != nil {
if err := json.Unmarshal(rawManifest, &manifest); err != nil {
return nil, err
}
configRef := fmt.Sprintf("%s@%s", ref.Name(), manifest.Config.Digest)
configRaw, err := resolver.GetDescriptor(ctx, configRef, manifest.Config)
if err != nil {
return nil, err
}
var config ocispec.Image
if err := json.Unmarshal(configRaw, &config); err != nil {
return nil, err
}
return &Image{ref, manifest, config, descriptor}, nil
}
func printImage(out io.Writer, image *Image) error {
if err := printManifest(out, image); err != nil {
return err
}
if err := printConfig(out, image); err != nil {
return err
}
return printLayers(out, image)
}
func printManifest(out io.Writer, image *Image) error {
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
fmt.Fprintf(w, "Name:\t%s\n", ref.String())
fmt.Fprintf(w, "MediaType:\t%s\n", descriptor.MediaType)
fmt.Fprintf(w, "Digest:\t%s\n", descriptor.Digest)
fmt.Fprintf(w, utils.Green("Manifest:")+"\n")
fmt.Fprintf(w, utils.Blue("%sName:")+"\t%s\n", pfx, image.Name)
fmt.Fprintf(w, utils.Blue("%sMediaType:")+"\t%s\n", pfx, image.Descriptor.MediaType)
fmt.Fprintf(w, utils.Blue("%sDigest:")+"\t%s\n", pfx, image.Descriptor.Digest)
if image.Descriptor.Platform != nil {
fmt.Fprintf(w, utils.Blue("%sPlatform:")+"\t%s\n", pfx, getPlatform(image.Descriptor.Platform))
}
if len(image.Manifest.Annotations) > 0 {
printAnnotations(w, image.Manifest.Annotations)
} else if len(image.Descriptor.Annotations) > 0 {
printAnnotations(w, image.Descriptor.Annotations)
}
if image.Config.Architecture != "" {
fmt.Fprintf(w, utils.Blue("%sOs/Arch:")+"\t%s/%s\n", pfx, image.Config.OS, image.Config.Architecture)
}
if image.Config.Author != "" {
fmt.Fprintf(w, utils.Blue("%sAuthor:")+"\t%s\n", pfx, image.Config.Author)
}
if image.Config.Created != nil {
fmt.Fprintf(w, utils.Blue("%sCreated:")+"\t%s ago\n", pfx, units.HumanDuration(time.Since(*image.Config.Created)))
}
fmt.Fprintf(w, "\n")
if err := w.Flush(); err != nil {
return err
}
w = tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
fmt.Fprintf(w, "Config:\n")
fmt.Fprintf(w, "%sMediaType:\t%s\n", pfx, manifest.Config.MediaType)
fmt.Fprintf(w, "%sSize:\t%v\n", pfx, units.HumanSize(float64(manifest.Config.Size)))
fmt.Fprintf(w, "%sDigest:\t%s\n", pfx, manifest.Config.Digest)
fmt.Fprintf(w, "\n")
if err := w.Flush(); err != nil {
return err
}
w = tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
fmt.Fprintf(w, "Layers:\n")
for i, layer := range manifest.Layers {
if i != 0 {
fmt.Fprintf(w, "\n")
}
fmt.Fprintf(w, "%sMediaType:\t%s\n", pfx, layer.MediaType)
fmt.Fprintf(w, "%sSize:\t%v\n", pfx, units.HumanSize(float64(layer.Size)))
fmt.Fprintf(w, "%sDigest:\t%s\n", pfx, layer.Digest)
}
return w.Flush()
}
func printConfig(out io.Writer, image *Image) error {
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
fmt.Fprintf(w, utils.Green("Config:")+"\n")
fmt.Fprintf(w, utils.Blue("%sMediaType:")+"\t%s\n", pfx, image.Manifest.Config.MediaType)
fmt.Fprintf(w, utils.Blue("%sSize:")+"\t%v\n", pfx, units.HumanSize(float64(image.Manifest.Config.Size)))
fmt.Fprintf(w, utils.Blue("%sDigest:")+"\t%s\n", pfx, image.Manifest.Config.Digest)
if len(image.Config.Config.Cmd) > 0 {
fmt.Fprintf(w, utils.Blue("%sCommand:")+"\t%q\n", pfx, strings.TrimPrefix(strings.Join(image.Config.Config.Cmd, " "), "/bin/sh -c "))
}
if len(image.Config.Config.Entrypoint) > 0 {
fmt.Fprintf(w, utils.Blue("%sEntrypoint:")+"\t%q\n", pfx, strings.Join(image.Config.Config.Entrypoint, " "))
}
if image.Config.Config.User != "" {
fmt.Fprintf(w, utils.Blue("%sUser:")+"\t%s\n", pfx, image.Config.Config.User)
}
if len(image.Config.Config.ExposedPorts) > 0 {
fmt.Fprintf(w, utils.Blue("%sExposed ports:")+"\t%s\n", pfx, getExposedPorts(image.Config.Config.ExposedPorts))
}
if len(image.Config.Config.Env) > 0 {
fmt.Fprintf(w, utils.Blue("%sEnvironment:")+"\n", pfx)
for _, env := range image.Config.Config.Env {
fmt.Fprintf(w, "%s%s%s\n", pfx, pfx, env)
}
}
if len(image.Config.Config.Volumes) > 0 {
fmt.Fprintf(w, utils.Blue("%sVolumes:")+"\n", pfx)
for volume := range image.Config.Config.Volumes {
fmt.Fprintf(w, "%s%s%s\n", pfx, pfx, volume)
}
}
if image.Config.Config.WorkingDir != "" {
fmt.Fprintf(w, utils.Blue("%sWorking Directory:")+"\t%q\n", pfx, image.Config.Config.WorkingDir)
}
if len(image.Config.Config.Labels) > 0 {
fmt.Fprintf(w, utils.Blue("%sLabels:")+"\n", pfx)
for k, v := range image.Config.Config.Labels {
fmt.Fprintf(w, "%s%s%s=%q\n", pfx, pfx, k, v)
}
}
if image.Config.Config.StopSignal != "" {
fmt.Fprintf(w, utils.Blue("%sStop signal:")+"\t%s\n", pfx, image.Config.Config.StopSignal)
}
fmt.Fprintf(w, "\n")
return w.Flush()
}
func printLayers(out io.Writer, image *Image) error {
history := filterEmptyLayers(image.Config.History)
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
fmt.Fprintln(w, utils.Green("Layers:"))
for i, layer := range image.Manifest.Layers {
if i != 0 {
fmt.Fprintln(w)
}
fmt.Fprintf(w, utils.Blue("%sMediaType:")+"\t%s\n", pfx, layer.MediaType)
fmt.Fprintf(w, utils.Blue("%sSize:")+"\t%v\n", pfx, units.HumanSize(float64(layer.Size)))
fmt.Fprintf(w, utils.Blue("%sDigest:")+"\t%s\n", pfx, layer.Digest)
if len(image.Manifest.Layers) == len(history) {
fmt.Fprintf(w, utils.Blue("%sCommand:")+"\t%s\n", pfx, cleanCreatedBy(history[i].CreatedBy))
if history[i].Created != nil {
fmt.Fprintf(w, utils.Blue("%sCreated:")+"\t%s ago\n", pfx, units.HumanDuration(time.Since(*history[i].Created)))
}
}
}
return w.Flush()
}
func getPlatform(platform *ocispec.Platform) string {
result := fmt.Sprintf("%s/%s", platform.OS, platform.Architecture)
if platform.Variant != "" {
result += "/" + platform.Variant
}
if platform.OSVersion != "" {
result += "/" + platform.OSVersion
}
if len(platform.OSFeatures) > 0 {
result += "/" + strings.Join(platform.OSFeatures, "/")
}
return result
}
func cleanCreatedBy(history string) string {
history = strings.TrimPrefix(history, "/bin/sh -c #(nop) ")
history = strings.TrimPrefix(history, "/bin/sh -c ")
return strings.TrimSpace(history)
}
func filterEmptyLayers(history []ocispec.History) []ocispec.History {
var result []ocispec.History
for _, h := range history {
if !h.EmptyLayer {
result = append(result, h)
}
}
return result
}
func getExposedPorts(configPorts map[string]struct{}) string {
var ports []string
for port := range configPorts {
ports = append(ports, port)
}
return strings.Join(ports, " ")
}
func printAnnotations(w io.Writer, annotations map[string]string) {
fmt.Fprintf(w, utils.Blue("%sAnnotations:")+"\n", pfx)
for k, v := range annotations {
fmt.Fprintf(w, "%s%s%s:\t%s\n", pfx, pfx, k, v)
}
}
type authResolver struct {
authConfig clitypes.AuthConfig
}

Просмотреть файл

@ -18,16 +18,19 @@ package tag
import (
"bytes"
"encoding/json"
"testing"
"time"
"github.com/docker/distribution/reference"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/golden"
"gotest.tools/v3/assert"
)
func TestPrintManifest(t *testing.T) {
func TestPrintImage(t *testing.T) {
manifest := ocispec.Manifest{
Versioned: specs.Versioned{},
Config: ocispec.Descriptor{
MediaType: "mediatype/config",
Digest: "sha256:beef",
@ -40,17 +43,64 @@ func TestPrintManifest(t *testing.T) {
Size: 456,
},
},
Annotations: map[string]string{
"annotation1": "value1",
"annotation2": "value2",
},
}
now := time.Now()
config := ocispec.Image{
Created: &now,
Author: "author",
Architecture: "arch",
OS: "os",
Config: ocispec.ImageConfig{
User: "user",
ExposedPorts: map[string]struct{}{"80/tcp": {}},
Env: []string{"KEY=VALUE"},
Entrypoint: []string{"./entrypoint", "parameter"},
Cmd: []string{"./cmd", "parameter"},
Volumes: map[string]struct{}{"/volume": {}},
WorkingDir: "/workingdir",
Labels: map[string]string{"label": "value"},
StopSignal: "SIGTERM",
},
History: []ocispec.History{
// empty layer is ignored
{
Created: &now,
CreatedBy: "created-by-ignored",
Author: "author-ignored",
Comment: "comment-ignored",
EmptyLayer: true,
},
{
Created: &now,
CreatedBy: "/bin/sh -c #(nop) apt-get install",
Author: "author-history",
Comment: "comment-history",
EmptyLayer: false,
},
},
}
raw, err := json.Marshal(&manifest)
assert.NilError(t, err)
manifestDescriptor := ocispec.Descriptor{
MediaType: "mediatype/manifest",
Digest: "sha256:abcdef",
Size: 789,
Platform: &ocispec.Platform{
Architecture: "arch",
OS: "os",
OSVersion: "osversion",
OSFeatures: []string{"feature1", "feature2"},
Variant: "variant",
},
}
out := bytes.NewBuffer(nil)
err = printManifest(raw, manifestDescriptor, "image:latest", out)
ref, err := reference.ParseDockerRef("image:latest")
assert.NilError(t, err)
golden.Assert(t, out.String(), "printmanifest.golden")
image := Image{ref, manifest, config, manifestDescriptor}
out := bytes.NewBuffer(nil)
err = printImage(out, &image)
assert.NilError(t, err)
golden.Assert(t, out.String(), "printimage.golden")
}

Просмотреть файл

35
internal/commands/tag/testdata/printimage.golden поставляемый Normal file
Просмотреть файл

@ -0,0 +1,35 @@
Manifest:
Name: docker.io/library/image:latest
MediaType: mediatype/manifest
Digest: sha256:abcdef
Platform: os/arch/variant/osversion/feature1/feature2
Annotations:
annotation1: value1
annotation2: value2
Os/Arch: os/arch
Author: author
Created: Less than a second ago
Config:
MediaType: mediatype/config
Size: 123B
Digest: sha256:beef
Command: "./cmd parameter"
Entrypoint: "./entrypoint parameter"
User: user
Exposed ports: 80/tcp
Environment:
KEY=VALUE
Volumes:
/volume
Working Directory: "/workingdir"
Labels:
label="value"
Stop signal: SIGTERM
Layers:
MediaType: mediatype/layer
Size: 456B
Digest: sha256:c0ffee
Command: apt-get install
Created: Less than a second ago

Просмотреть файл

@ -1,13 +0,0 @@
Name: docker.io/library/image:latest
MediaType: mediatype/manifest
Digest: sha256:abcdef
Config:
MediaType: mediatype/config
Size: 123B
Digest: sha256:beef
Layers:
MediaType: mediatype/layer
Size: 456B
Digest: sha256:c0ffee