Add manifest printer library

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan 2023-08-22 15:54:39 -07:00
parent de066a37dc
commit 78308b4a44
No known key found for this signature in database
GPG Key ID: F58C5D0A4405ACDB

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
}