summaryrefslogtreecommitdiff
path: root/libpod/image
diff options
context:
space:
mode:
Diffstat (limited to 'libpod/image')
-rw-r--r--libpod/image/config.go6
-rw-r--r--libpod/image/filters.go13
-rw-r--r--libpod/image/image.go154
-rw-r--r--libpod/image/image_test.go2
-rw-r--r--libpod/image/manifests.go154
-rw-r--r--libpod/image/parts.go2
-rw-r--r--libpod/image/pull.go1
-rw-r--r--libpod/image/tree.go138
8 files changed, 390 insertions, 80 deletions
diff --git a/libpod/image/config.go b/libpod/image/config.go
index bb84175a3..efd83d343 100644
--- a/libpod/image/config.go
+++ b/libpod/image/config.go
@@ -1,5 +1,11 @@
package image
+const (
+ // LatestTag describes the tag used to refer to the latest version
+ // of an image
+ LatestTag = "latest"
+)
+
// ImageDeleteResponse is the response for removing an image from storage and containers
// what was untagged vs actually removed
type ImageDeleteResponse struct { //nolint
diff --git a/libpod/image/filters.go b/libpod/image/filters.go
index d545f1bfc..8ca3526a0 100644
--- a/libpod/image/filters.go
+++ b/libpod/image/filters.go
@@ -3,13 +3,13 @@ package image
import (
"context"
"fmt"
- "github.com/pkg/errors"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/containers/libpod/pkg/inspect"
+ "github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@@ -102,6 +102,13 @@ func ReferenceFilter(ctx context.Context, referenceFilter string) ResultFilter {
}
}
+// IdFilter allows you to filter by image Id
+func IdFilter(idFilter string) ResultFilter {
+ return func(i *Image) bool {
+ return i.ID() == idFilter
+ }
+}
+
// OutputImageFilter allows you to filter by an a specific image name
func OutputImageFilter(userImage *Image) ResultFilter {
return func(i *Image) bool {
@@ -141,7 +148,7 @@ func (ir *Runtime) createFilterFuncs(filters []string, img *Image) ([]ResultFilt
return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1])
}
filterFuncs = append(filterFuncs, CreatedBeforeFilter(before.Created()))
- case "after":
+ case "since", "after":
after, err := ir.NewFromLocal(splitFilter[1])
if err != nil {
return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1])
@@ -165,6 +172,8 @@ func (ir *Runtime) createFilterFuncs(filters []string, img *Image) ([]ResultFilt
case "reference":
referenceFilter := strings.Join(splitFilter[1:], "=")
filterFuncs = append(filterFuncs, ReferenceFilter(ctx, referenceFilter))
+ case "id":
+ filterFuncs = append(filterFuncs, IdFilter(splitFilter[1]))
default:
return nil, errors.Errorf("invalid filter %s ", splitFilter[0])
}
diff --git a/libpod/image/image.go b/libpod/image/image.go
index 6ea49e2a9..5f914ed79 100644
--- a/libpod/image/image.go
+++ b/libpod/image/image.go
@@ -99,10 +99,7 @@ func NewImageRuntimeFromOptions(options storage.StoreOptions) (*Runtime, error)
if err != nil {
return nil, err
}
-
- return &Runtime{
- store: store,
- }, nil
+ return NewImageRuntimeFromStore(store), nil
}
func setStore(options storage.StoreOptions) (storage.Store, error) {
@@ -114,30 +111,29 @@ func setStore(options storage.StoreOptions) (storage.Store, error) {
return store, nil
}
-// newFromStorage creates a new image object from a storage.Image
-func (ir *Runtime) newFromStorage(img *storage.Image) *Image {
- image := Image{
- InputName: img.ID,
+// newImage creates a new image object given an "input name" and a storage.Image
+func (ir *Runtime) newImage(inputName string, img *storage.Image) *Image {
+ return &Image{
+ InputName: inputName,
imageruntime: ir,
image: img,
}
- return &image
+}
+
+// newFromStorage creates a new image object from a storage.Image. Its "input name" will be its ID.
+func (ir *Runtime) newFromStorage(img *storage.Image) *Image {
+ return ir.newImage(img.ID, img)
}
// NewFromLocal creates a new image object that is intended
// to only deal with local images already in the store (or
// its aliases)
func (ir *Runtime) NewFromLocal(name string) (*Image, error) {
- image := Image{
- InputName: name,
- imageruntime: ir,
- }
- localImage, err := image.getLocalImage()
+ updatedInputName, localImage, err := ir.getLocalImage(name)
if err != nil {
return nil, err
}
- image.image = localImage
- return &image, nil
+ return ir.newImage(updatedInputName, localImage), nil
}
// New creates a new image object where the image could be local
@@ -148,15 +144,10 @@ func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile
defer span.Finish()
// We don't know if the image is local or not ... check local first
- newImage := Image{
- InputName: name,
- imageruntime: ir,
- }
if pullType != util.PullImageAlways {
- localImage, err := newImage.getLocalImage()
+ newImage, err := ir.NewFromLocal(name)
if err == nil {
- newImage.image = localImage
- return &newImage, nil
+ return newImage, nil
} else if pullType == util.PullImageNever {
return nil, err
}
@@ -171,13 +162,11 @@ func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile
return nil, errors.Wrapf(err, "unable to pull %s", name)
}
- newImage.InputName = imageName[0]
- img, err := newImage.getLocalImage()
+ newImage, err := ir.NewFromLocal(imageName[0])
if err != nil {
return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name)
}
- newImage.image = img
- return &newImage, nil
+ return newImage, nil
}
// LoadFromArchiveReference creates a new image object for images pulled from a tar archive and the like (podman load)
@@ -194,16 +183,11 @@ func (ir *Runtime) LoadFromArchiveReference(ctx context.Context, srcRef types.Im
}
for _, name := range imageNames {
- newImage := Image{
- InputName: name,
- imageruntime: ir,
- }
- img, err := newImage.getLocalImage()
+ newImage, err := ir.NewFromLocal(name)
if err != nil {
return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name)
}
- newImage.image = img
- newImages = append(newImages, &newImage)
+ newImages = append(newImages, newImage)
}
ir.newImageEvent(events.LoadFromArchive, "")
return newImages, nil
@@ -234,7 +218,7 @@ func (i *Image) reloadImage() error {
if err != nil {
return errors.Wrapf(err, "unable to reload image")
}
- i.image = newImage.image
+ i.image = newImage
return nil
}
@@ -247,60 +231,60 @@ func stripSha256(name string) string {
}
// getLocalImage resolves an unknown input describing an image and
-// returns a storage.Image or an error. It is used by NewFromLocal.
-func (i *Image) getLocalImage() (*storage.Image, error) {
- imageError := fmt.Sprintf("unable to find '%s' in local storage", i.InputName)
- if i.InputName == "" {
- return nil, errors.Errorf("input name is blank")
+// returns an updated input name, and a storage.Image, or an error. It is used by NewFromLocal.
+func (ir *Runtime) getLocalImage(inputName string) (string, *storage.Image, error) {
+ imageError := fmt.Sprintf("unable to find '%s' in local storage", inputName)
+ if inputName == "" {
+ return "", nil, errors.Errorf("input name is blank")
}
// Check if the input name has a transport and if so strip it
- dest, err := alltransports.ParseImageName(i.InputName)
+ dest, err := alltransports.ParseImageName(inputName)
if err == nil && dest.DockerReference() != nil {
- i.InputName = dest.DockerReference().String()
+ inputName = dest.DockerReference().String()
}
- img, err := i.imageruntime.getImage(stripSha256(i.InputName))
+ img, err := ir.getImage(stripSha256(inputName))
if err == nil {
- return img.image, err
+ return inputName, img, err
}
// container-storage wasn't able to find it in its current form
// check if the input name has a tag, and if not, run it through
// again
- decomposedImage, err := decompose(i.InputName)
+ decomposedImage, err := decompose(inputName)
if err != nil {
- return nil, err
+ return "", nil, err
}
// The image has a registry name in it and we made sure we looked for it locally
// with a tag. It cannot be local.
if decomposedImage.hasRegistry {
- return nil, errors.Wrapf(ErrNoSuchImage, imageError)
+ return "", nil, errors.Wrapf(ErrNoSuchImage, imageError)
}
// if the image is saved with the repository localhost, searching with localhost prepended is necessary
// We don't need to strip the sha because we have already determined it is not an ID
ref, err := decomposedImage.referenceWithRegistry(DefaultLocalRegistry)
if err != nil {
- return nil, err
+ return "", nil, err
}
- img, err = i.imageruntime.getImage(ref.String())
+ img, err = ir.getImage(ref.String())
if err == nil {
- return img.image, err
+ return inputName, img, err
}
// grab all the local images
- images, err := i.imageruntime.GetImages()
+ images, err := ir.GetImages()
if err != nil {
- return nil, err
+ return "", nil, err
}
// check the repotags of all images for a match
repoImage, err := findImageInRepotags(decomposedImage, images)
if err == nil {
- return repoImage, nil
+ return inputName, repoImage, nil
}
- return nil, errors.Wrapf(ErrNoSuchImage, err.Error())
+ return "", nil, errors.Wrapf(ErrNoSuchImage, err.Error())
}
// ID returns the image ID as a string
@@ -460,7 +444,7 @@ func (i *Image) Remove(ctx context.Context, force bool) error {
// getImage retrieves an image matching the given name or hash from system
// storage
// If no matching image can be found, an error is returned
-func (ir *Runtime) getImage(image string) (*Image, error) {
+func (ir *Runtime) getImage(image string) (*storage.Image, error) {
var img *storage.Image
ref, err := is.Transport.ParseStoreReference(ir.store, image)
if err == nil {
@@ -476,8 +460,7 @@ func (ir *Runtime) getImage(image string) (*Image, error) {
}
img = img2
}
- newImage := ir.newFromStorage(img)
- return newImage, nil
+ return img, nil
}
// GetImages retrieves all images present in storage
@@ -702,18 +685,12 @@ func (i *Image) toImageSourceRef(ctx context.Context) (types.ImageSource, error)
//Size returns the size of the image
func (i *Image) Size(ctx context.Context) (*uint64, error) {
- if i.image == nil {
- localImage, err := i.getLocalImage()
- if err != nil {
- return nil, err
- }
- i.image = localImage
- }
- if sum, err := i.imageruntime.store.ImageSize(i.ID()); err == nil && sum >= 0 {
+ sum, err := i.imageruntime.store.ImageSize(i.ID())
+ if err == nil && sum >= 0 {
usum := uint64(sum)
return &usum, nil
}
- return nil, errors.Errorf("unable to determine size")
+ return nil, errors.Wrap(err, "unable to determine size")
}
// toImageRef returns an Image Reference type from an image
@@ -831,7 +808,8 @@ func (i *Image) History(ctx context.Context) ([]*History, error) {
id := "<missing>"
if x == numHistories {
id = i.ID()
- } else if layer != nil {
+ }
+ if layer != nil {
if !oci.History[x].EmptyLayer {
size = layer.UncompressedSize
}
@@ -938,12 +916,7 @@ func (i *Image) imageInspectInfo(ctx context.Context) (*types.ImageInspectInfo,
return i.inspectInfo, nil
}
-// Inspect returns an image's inspect data
-func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) {
- span, _ := opentracing.StartSpanFromContext(ctx, "imageInspect")
- span.SetTag("type", "image")
- defer span.Finish()
-
+func (i *Image) inspect(ctx context.Context, calculateSize bool) (*inspect.ImageData, error) {
ociv1Img, err := i.ociv1Image(ctx)
if err != nil {
ociv1Img = &ociv1.Image{}
@@ -958,8 +931,10 @@ func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) {
}
size := int64(-1)
- if usize, err := i.Size(ctx); err == nil {
- size = int64(*usize)
+ if calculateSize {
+ if usize, err := i.Size(ctx); err == nil {
+ size = int64(*usize)
+ }
}
repoTags, err := i.RepoTags()
@@ -1012,9 +987,38 @@ func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) {
History: ociv1Img.History,
NamesHistory: i.NamesHistory(),
}
+ if manifestType == manifest.DockerV2Schema2MediaType {
+ hc, err := i.GetHealthCheck(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if hc != nil {
+ data.HealthCheck = hc
+ }
+ }
return data, nil
}
+// Inspect returns an image's inspect data
+func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) {
+ span, _ := opentracing.StartSpanFromContext(ctx, "imageInspect")
+
+ span.SetTag("type", "image")
+ defer span.Finish()
+
+ return i.inspect(ctx, true)
+}
+
+// InspectNoSize returns an image's inspect data without calculating the size for the image
+func (i *Image) InspectNoSize(ctx context.Context) (*inspect.ImageData, error) {
+ span, _ := opentracing.StartSpanFromContext(ctx, "imageInspectNoSize")
+
+ span.SetTag("type", "image")
+ defer span.Finish()
+
+ return i.inspect(ctx, false)
+}
+
// Import imports and image into the store and returns an image
func (ir *Runtime) Import(ctx context.Context, path, reference string, writer io.Writer, signingOptions SigningOptions, imageConfig ociv1.Image) (*Image, error) {
src, err := tarball.Transport.ParseReference(path)
diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go
index 3ff6210d9..19f7eee1e 100644
--- a/libpod/image/image_test.go
+++ b/libpod/image/image_test.go
@@ -18,7 +18,6 @@ import (
var (
bbNames = []string{"docker.io/library/busybox:latest", "docker.io/library/busybox", "docker.io/busybox:latest", "docker.io/busybox", "busybox:latest", "busybox"}
bbGlibcNames = []string{"docker.io/library/busybox:glibc", "docker.io/busybox:glibc", "busybox:glibc"}
- fedoraNames = []string{"registry.fedoraproject.org/fedora-minimal:latest", "registry.fedoraproject.org/fedora-minimal", "fedora-minimal:latest", "fedora-minimal"}
)
type localImageTest struct {
@@ -139,7 +138,6 @@ func TestImage_New(t *testing.T) {
ir.Eventer = events.NewNullEventer()
// Build the list of pull names
names = append(names, bbNames...)
- names = append(names, fedoraNames...)
writer := os.Stdout
// Iterate over the names and delete the image
diff --git a/libpod/image/manifests.go b/libpod/image/manifests.go
new file mode 100644
index 000000000..9dbeb4cc5
--- /dev/null
+++ b/libpod/image/manifests.go
@@ -0,0 +1,154 @@
+package image
+
+import (
+ "context"
+
+ "github.com/containers/buildah/manifests"
+ "github.com/containers/image/v5/manifest"
+ "github.com/containers/image/v5/transports/alltransports"
+ "github.com/containers/image/v5/types"
+ "github.com/opencontainers/go-digest"
+ "github.com/pkg/errors"
+)
+
+// Options for adding a manifest
+// swagger:model ManifestAddOpts
+type ManifestAddOpts struct {
+ All bool `json:"all"`
+ Annotation map[string]string `json:"annotation"`
+ Arch string `json:"arch"`
+ Features []string `json:"features"`
+ Images []string `json:"images"`
+ OSVersion string `json:"os_version"`
+ Variant string `json:"variant"`
+}
+
+// InspectManifest returns a dockerized version of the manifest list
+func (i *Image) InspectManifest() (*manifest.Schema2List, error) {
+ list, err := i.getManifestList()
+ if err != nil {
+ return nil, err
+ }
+ return list.Docker(), nil
+}
+
+// RemoveManifest removes the given digest from the manifest list.
+func (i *Image) RemoveManifest(d digest.Digest) (string, error) {
+ list, err := i.getManifestList()
+ if err != nil {
+ return "", err
+ }
+ if err := list.Remove(d); err != nil {
+ return "", err
+ }
+ return list.SaveToImage(i.imageruntime.store, i.ID(), nil, "")
+}
+
+// getManifestList is a helper to obtain a manifest list
+func (i *Image) getManifestList() (manifests.List, error) {
+ _, list, err := manifests.LoadFromImage(i.imageruntime.store, i.ID())
+ return list, err
+}
+
+// CreateManifestList creates a new manifest list and can optionally add given images
+// to the list
+func CreateManifestList(rt *Runtime, systemContext types.SystemContext, names []string, imgs []string, all bool) (string, error) {
+ list := manifests.Create()
+ opts := ManifestAddOpts{Images: names, All: all}
+ for _, img := range imgs {
+ var ref types.ImageReference
+ newImage, err := rt.NewFromLocal(img)
+ if err == nil {
+ ir, err := newImage.toImageRef(context.Background())
+ if err != nil {
+ return "", err
+ }
+ if ir == nil {
+ return "", errors.New("unable to convert image to ImageReference")
+ }
+ ref = ir.Reference()
+ } else {
+ ref, err = alltransports.ParseImageName(img)
+ if err != nil {
+ return "", err
+ }
+ }
+ list, err = addManifestToList(ref, list, systemContext, opts)
+ if err != nil {
+ return "", err
+ }
+ }
+ return list.SaveToImage(rt.store, "", names, manifest.DockerV2ListMediaType)
+}
+
+func addManifestToList(ref types.ImageReference, list manifests.List, systemContext types.SystemContext, opts ManifestAddOpts) (manifests.List, error) {
+ d, err := list.Add(context.Background(), &systemContext, ref, opts.All)
+ if err != nil {
+ return nil, err
+ }
+ if len(opts.OSVersion) > 0 {
+ if err := list.SetOSVersion(d, opts.OSVersion); err != nil {
+ return nil, err
+ }
+ }
+ if len(opts.Features) > 0 {
+ if err := list.SetFeatures(d, opts.Features); err != nil {
+ return nil, err
+ }
+ }
+ if len(opts.Arch) > 0 {
+ if err := list.SetArchitecture(d, opts.Arch); err != nil {
+ return nil, err
+ }
+ }
+ if len(opts.Variant) > 0 {
+ if err := list.SetVariant(d, opts.Variant); err != nil {
+ return nil, err
+ }
+ }
+ if len(opts.Annotation) > 0 {
+ if err := list.SetAnnotations(&d, opts.Annotation); err != nil {
+ return nil, err
+ }
+ }
+ return list, err
+}
+
+// AddManifest adds a manifest to a given manifest list.
+func (i *Image) AddManifest(systemContext types.SystemContext, opts ManifestAddOpts) (string, error) {
+ var (
+ ref types.ImageReference
+ )
+ newImage, err := i.imageruntime.NewFromLocal(opts.Images[0])
+ if err == nil {
+ ir, err := newImage.toImageRef(context.Background())
+ if err != nil {
+ return "", err
+ }
+ ref = ir.Reference()
+ } else {
+ ref, err = alltransports.ParseImageName(opts.Images[0])
+ if err != nil {
+ return "", err
+ }
+ }
+ list, err := i.getManifestList()
+ if err != nil {
+ return "", err
+ }
+ list, err = addManifestToList(ref, list, systemContext, opts)
+ if err != nil {
+ return "", err
+ }
+ return list.SaveToImage(i.imageruntime.store, i.ID(), nil, "")
+}
+
+// PushManifest pushes a manifest to a destination
+func (i *Image) PushManifest(dest types.ImageReference, opts manifests.PushOptions) (digest.Digest, error) {
+ list, err := i.getManifestList()
+ if err != nil {
+ return "", err
+ }
+ _, d, err := list.Push(context.Background(), dest, opts)
+ return d, err
+}
diff --git a/libpod/image/parts.go b/libpod/image/parts.go
index d4677f935..d6c98783b 100644
--- a/libpod/image/parts.go
+++ b/libpod/image/parts.go
@@ -67,7 +67,7 @@ func (ip *imageParts) suspiciousRefNameTagValuesForSearch() (string, string, str
} else if _, hasDigest := ip.unnormalizedRef.(reference.Digested); hasDigest {
tag = "none"
} else {
- tag = "latest"
+ tag = LatestTag
}
return registry, imageName, tag
}
diff --git a/libpod/image/pull.go b/libpod/image/pull.go
index 76294ba06..fd359d593 100644
--- a/libpod/image/pull.go
+++ b/libpod/image/pull.go
@@ -126,6 +126,7 @@ func (ir *Runtime) pullGoalFromImageReference(ctx context.Context, srcRef types.
if err != nil {
return nil, err
}
+ defer tarSource.Close()
manifest, err := tarSource.LoadTarManifest()
if err != nil {
diff --git a/libpod/image/tree.go b/libpod/image/tree.go
new file mode 100644
index 000000000..c7c69462f
--- /dev/null
+++ b/libpod/image/tree.go
@@ -0,0 +1,138 @@
+package image
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/docker/go-units"
+ "github.com/pkg/errors"
+)
+
+const (
+ middleItem = "├── "
+ continueItem = "│ "
+ lastItem = "└── "
+)
+
+type tree struct {
+ img *Image
+ imageInfo *InfoImage
+ layerInfo map[string]*LayerInfo
+ sb *strings.Builder
+}
+
+// GenerateTree creates an image tree string representation for displaying it
+// to the user.
+func (i *Image) GenerateTree(whatRequires bool) (string, error) {
+ // Fetch map of image-layers, which is used for printing output.
+ layerInfo, err := GetLayersMapWithImageInfo(i.imageruntime)
+ if err != nil {
+ return "", errors.Wrapf(err, "error while retrieving layers of image %q", i.InputName)
+ }
+
+ // Create an imageInfo and fill the image and layer info
+ imageInfo := &InfoImage{
+ ID: i.ID(),
+ Tags: i.Names(),
+ }
+
+ if err := BuildImageHierarchyMap(imageInfo, layerInfo, i.TopLayer()); err != nil {
+ return "", err
+ }
+ sb := &strings.Builder{}
+ tree := &tree{i, imageInfo, layerInfo, sb}
+ if err := tree.print(whatRequires); err != nil {
+ return "", err
+ }
+ return tree.string(), nil
+}
+
+func (t *tree) string() string {
+ return t.sb.String()
+}
+
+func (t *tree) print(whatRequires bool) error {
+ size, err := t.img.Size(context.Background())
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(t.sb, "Image ID: %s\n", t.imageInfo.ID[:12])
+ fmt.Fprintf(t.sb, "Tags: %s\n", t.imageInfo.Tags)
+ fmt.Fprintf(t.sb, "Size: %v\n", units.HumanSizeWithPrecision(float64(*size), 4))
+ if t.img.TopLayer() != "" {
+ fmt.Fprintf(t.sb, "Image Layers\n")
+ } else {
+ fmt.Fprintf(t.sb, "No Image Layers\n")
+ }
+
+ if !whatRequires {
+ // fill imageInfo with layers associated with image.
+ // the layers will be filled such that
+ // (Start)RootLayer->...intermediate Parent Layer(s)-> TopLayer(End)
+ // Build output from imageInfo into buffer
+ t.printImageHierarchy(t.imageInfo)
+ } else {
+ // fill imageInfo with layers associated with image.
+ // the layers will be filled such that
+ // (Start)TopLayer->...intermediate Child Layer(s)-> Child TopLayer(End)
+ // (Forks)... intermediate Child Layer(s) -> Child Top Layer(End)
+ return t.printImageChildren(t.layerInfo, t.img.TopLayer(), "", true)
+ }
+ return nil
+}
+
+// Stores all children layers which are created using given Image.
+// Layers are stored as follows
+// (Start)TopLayer->...intermediate Child Layer(s)-> Child TopLayer(End)
+// (Forks)... intermediate Child Layer(s) -> Child Top Layer(End)
+func (t *tree) printImageChildren(layerMap map[string]*LayerInfo, layerID string, prefix string, last bool) error {
+ if layerID == "" {
+ return nil
+ }
+ ll, ok := layerMap[layerID]
+ if !ok {
+ return fmt.Errorf("lookup error: layerid %s, not found", layerID)
+ }
+ fmt.Fprint(t.sb, prefix)
+
+ //initialize intend with middleItem to reduce middleItem checks.
+ intend := middleItem
+ if !last {
+ // add continueItem i.e. '|' for next iteration prefix
+ prefix += continueItem
+ } else if len(ll.ChildID) > 1 || len(ll.ChildID) == 0 {
+ // The above condition ensure, alignment happens for node, which has more then 1 children.
+ // If node is last in printing hierarchy, it should not be printed as middleItem i.e. ├──
+ intend = lastItem
+ prefix += " "
+ }
+
+ var tags string
+ if len(ll.RepoTags) > 0 {
+ tags = fmt.Sprintf(" Top Layer of: %s", ll.RepoTags)
+ }
+ fmt.Fprintf(t.sb, "%sID: %s Size: %7v%s\n", intend, ll.ID[:12], units.HumanSizeWithPrecision(float64(ll.Size), 4), tags)
+ for count, childID := range ll.ChildID {
+ if err := t.printImageChildren(layerMap, childID, prefix, count == len(ll.ChildID)-1); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// prints the layers info of image
+func (t *tree) printImageHierarchy(imageInfo *InfoImage) {
+ for count, l := range imageInfo.Layers {
+ var tags string
+ intend := middleItem
+ if len(l.RepoTags) > 0 {
+ tags = fmt.Sprintf(" Top Layer of: %s", l.RepoTags)
+ }
+ if count == len(imageInfo.Layers)-1 {
+ intend = lastItem
+ }
+ fmt.Fprintf(t.sb, "%s ID: %s Size: %7v%s\n", intend, l.ID[:12], units.HumanSizeWithPrecision(float64(l.Size), 4), tags)
+ }
+}