203 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			203 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   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/v2/core/content"
 | 
						|
	"github.com/containerd/containerd/v2/core/images"
 | 
						|
	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 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 content.InfoReaderProvider) 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 content.InfoReaderProvider) 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 content.InfoReaderProvider, 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
 | 
						|
	}
 | 
						|
 | 
						|
	if images.IsManifestType(desc.MediaType) {
 | 
						|
		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)
 | 
						|
		}
 | 
						|
	} else if images.IsIndexType(desc.MediaType) {
 | 
						|
		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 content.InfoReaderProvider, 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
 | 
						|
}
 |