Tree implementation for podman images

Signed-off-by: Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
This commit is contained in:
Kunal Kushwaha
2018-10-15 10:40:30 +09:00
parent 883566fbc0
commit a4b3b9ffbb
7 changed files with 416 additions and 0 deletions

View File

@ -66,6 +66,11 @@ type TagValues struct {
PodmanCommand
}
type TreeValues struct {
PodmanCommand
WhatRequires bool
}
type WaitValues struct {
PodmanCommand
Interval uint

View File

@ -60,6 +60,7 @@ var imageSubCommands = []*cobra.Command{
_rmSubCommand,
_saveCommand,
_tagCommand,
_treeCommand,
}
func init() {

190
cmd/podman/tree.go Normal file
View File

@ -0,0 +1,190 @@
package main
import (
"context"
"fmt"
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/libpod/image"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
const (
middleItem = "├── "
continueItem = "│ "
lastItem = "└── "
)
var (
treeCommand cliconfig.TreeValues
treeDescription = "Prints layer hierarchy of an image in a tree format"
_treeCommand = &cobra.Command{
Use: "tree",
Short: treeDescription,
Long: treeDescription,
RunE: func(cmd *cobra.Command, args []string) error {
treeCommand.InputArgs = args
treeCommand.GlobalFlags = MainGlobalOpts
return treeCmd(&treeCommand)
},
Example: "podman image tree alpine:latest",
}
)
func init() {
treeCommand.Command = _treeCommand
treeCommand.SetUsageTemplate(UsageTemplate())
treeCommand.Flags().BoolVar(&treeCommand.WhatRequires, "whatrequires", false, "Show all child images and layers of the specified image")
}
// infoImage keep information of Image along with all associated layers
type infoImage struct {
// id of image
id string
// tags of image
tags []string
// layers stores all layers of image.
layers []image.LayerInfo
}
func treeCmd(c *cliconfig.TreeValues) error {
args := c.InputArgs
if len(args) == 0 {
return errors.Errorf("an image name must be specified")
}
if len(args) > 1 {
return errors.Errorf("you must provide at most 1 argument")
}
runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand)
if err != nil {
return errors.Wrapf(err, "error creating libpod runtime")
}
defer runtime.Shutdown(false)
img, err := runtime.ImageRuntime().NewFromLocal(args[0])
if err != nil {
return err
}
// Fetch map of image-layers, which is used for printing output.
layerInfoMap, err := image.GetLayersMapWithImageInfo(runtime.ImageRuntime())
if err != nil {
return errors.Wrapf(err, "error while retriving layers of image %q", img.InputName)
}
// Create an imageInfo and fill the image and layer info
imageInfo := &infoImage{
id: img.ID(),
tags: img.Names(),
}
size, err := img.Size(context.Background())
if err != nil {
return errors.Wrapf(err, "error while retriving image size")
}
fmt.Printf("Image ID: %s\n", imageInfo.id[:12])
fmt.Printf("Tags:\t %s\n", imageInfo.tags)
fmt.Printf("Size:\t %v\n", units.HumanSizeWithPrecision(float64(*size), 4))
fmt.Printf(fmt.Sprintf("Image Layers\n"))
if !c.WhatRequires {
// fill imageInfo with layers associated with image.
// the layers will be filled such that
// (Start)RootLayer->...intermediate Parent Layer(s)-> TopLayer(End)
err := buildImageHierarchyMap(imageInfo, layerInfoMap, img.TopLayer())
if err != nil {
return err
}
// Build output from imageInfo into buffer
printImageHierarchy(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)
err := printImageChildren(layerInfoMap, img.TopLayer(), "", true)
if err != nil {
return err
}
}
return nil
}
// Stores hierarchy of images such that all parent layers using which image is built are stored in imageInfo
// Layers are added such that (Start)RootLayer->...intermediate Parent Layer(s)-> TopLayer(End)
func buildImageHierarchyMap(imageInfo *infoImage, layerMap map[string]*image.LayerInfo, layerID string) error {
if layerID == "" {
return nil
}
ll, ok := layerMap[layerID]
if !ok {
return fmt.Errorf("lookup error: layerid %s not found", layerID)
}
if err := buildImageHierarchyMap(imageInfo, layerMap, ll.ParentID); err != nil {
return err
}
imageInfo.layers = append(imageInfo.layers, *ll)
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 printImageChildren(layerMap map[string]*image.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.Printf(prefix)
//initialize intend with middleItem to reduce middleItem checks.
intend := middleItem
if !last {
// add continueItem i.e. '|' for next iteration prefix
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 childern.
// If node is last in printing hierarchy, it should not be printed as middleItem i.e. ├──
intend = lastItem
prefix = prefix + " "
}
var tags string
if len(ll.RepoTags) > 0 {
tags = fmt.Sprintf(" Top Layer of: %s", ll.RepoTags)
}
fmt.Printf("%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 := printImageChildren(layerMap, childID, prefix, (count == len(ll.ChildID)-1)); err != nil {
return err
}
}
return nil
}
// prints the layers info of image
func 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.Printf("%s ID: %s Size: %7v%s\n", intend, l.ID[:12], units.HumanSizeWithPrecision(float64(l.Size), 4), tags)
}
}

View File

@ -0,0 +1,88 @@
% podman-image-tree(1)
## NAME
podman\-image\-tree - Prints layer hierarchy of an image in a tree format
## SYNOPSIS
**podman image tree** [*image*:*tag*]**|**[*image-id*]
[**--help**|**-h**]
## DESCRIPTION
Prints layer hierarchy of an image in a tree format.
If you do not provide a *tag*, podman will default to `latest` for the *image*.
Layers are indicated with image tags as `Top Layer of`, when the tag is known locally.
## OPTIONS
**--help**, **-h**
Print usage statement
**--whatrequires**
Show all child images and layers of the specified image
## EXAMPLES
```
$ podman pull docker.io/library/wordpress
$ podman pull docker.io/library/php:7.2-apache
$ podman image tree docker.io/library/wordpress
Image ID: 6e880d17852f
Tags: [docker.io/library/wordpress:latest]
Size: 429.9MB
Image Layers
├── ID: 3c816b4ead84 Size: 58.47MB
├── ID: e39dad2af72e Size: 3.584kB
├── ID: b2d6a702383c Size: 213.6MB
├── ID: 94609408badd Size: 3.584kB
├── ID: f4dddbf86725 Size: 43.04MB
├── ID: 8f695df43a4c Size: 11.78kB
├── ID: c29d67bf8461 Size: 9.728kB
├── ID: 23f4315918f8 Size: 7.68kB
├── ID: d082f93a18b3 Size: 13.51MB
├── ID: 7ea8bedcac69 Size: 4.096kB
├── ID: dc3bbf7b3dc0 Size: 57.53MB
├── ID: fdbbc6404531 Size: 11.78kB
├── ID: 8d24785437c6 Size: 4.608kB
├── ID: 80715f9e8880 Size: 4.608kB Top Layer of: [docker.io/library/php:7.2-apache]
├── ID: c93cbcd6437e Size: 3.573MB
├── ID: dece674f3cd1 Size: 4.608kB
├── ID: 834f4497afda Size: 7.168kB
├── ID: bfe2ce1263f8 Size: 40.06MB
└── ID: 748e99b214cf Size: 11.78kB Top Layer of: [docker.io/library/wordpress:latest]
$ podman pull docker.io/circleci/ruby:latest
$ podman pull docker.io/library/ruby:latest
$ podman image tree ae96a4ad4f3f --whatrequires
Image ID: ae96a4ad4f3f
Tags: [docker.io/library/ruby:latest]
Size: 894.2MB
Image Layers
└── ID: 9c92106221c7 Size: 2.56kB Top Layer of: [docker.io/library/ruby:latest]
├── ID: 1b90f2b80ba0 Size: 3.584kB
│ ├── ID: 42b7d43ae61c Size: 169.5MB
│ ├── ID: 26dc8ba99ec3 Size: 2.048kB
│ ├── ID: b4f822db8d95 Size: 3.957MB
│ ├── ID: 044e9616ef8a Size: 164.7MB
│ ├── ID: bf94b940200d Size: 11.75MB
│ ├── ID: 4938e71bfb3b Size: 8.532MB
│ └── ID: f513034bf553 Size: 1.141MB
├── ID: 1e55901c3ea9 Size: 3.584kB
├── ID: b62835a63f51 Size: 169.5MB
├── ID: 9f4e8857f3fd Size: 2.048kB
├── ID: c3b392020e8f Size: 3.957MB
├── ID: 880163026a0a Size: 164.8MB
├── ID: 8c78b2b14643 Size: 11.75MB
├── ID: 830370cfa182 Size: 8.532MB
└── ID: 567fd7b7bd38 Size: 1.141MB Top Layer of: [docker.io/circleci/ruby:latest]
```
## SEE ALSO
podman(1), crio(8)
## HISTORY
Feb 2019, Originally compiled by Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>

View File

@ -28,6 +28,7 @@ The image command allows you to manage images
| sign | [podman-image-sign(1)](podman-image-sign.1.md) | Sign an image. |
| tag | [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. |
| trust | [podman-image-trust(1)](podman-image-trust.1.md)| Manage container image trust policy. |
| tree | [podman-image-tree(1)](podman-image-tree.1.md) | Prints layer hierarchy of an image in a tree format |
## SEE ALSO
podman

View File

@ -1212,3 +1212,70 @@ func (i *Image) newImageEvent(status events.Status) {
logrus.Infof("unable to write event to %s", i.imageruntime.EventsLogFilePath)
}
}
// LayerInfo keeps information of single layer
type LayerInfo struct {
// Layer ID
ID string
// Parent ID of current layer.
ParentID string
// ChildID of current layer.
// there can be multiple children in case of fork
ChildID []string
// RepoTag will have image repo names, if layer is top layer of image
RepoTags []string
// Size stores Uncompressed size of layer.
Size int64
}
// GetLayersMapWithImageInfo returns map of image-layers, with associated information like RepoTags, parent and list of child layers.
func GetLayersMapWithImageInfo(imageruntime *Runtime) (map[string]*LayerInfo, error) {
// Memory allocated to store map of layers with key LayerID.
// Map will build dependency chain with ParentID and ChildID(s)
layerInfoMap := make(map[string]*LayerInfo)
// scan all layers & fill size and parent id for each layer in layerInfoMap
layers, err := imageruntime.store.Layers()
if err != nil {
return nil, err
}
for _, layer := range layers {
_, ok := layerInfoMap[layer.ID]
if !ok {
layerInfoMap[layer.ID] = &LayerInfo{
ID: layer.ID,
Size: layer.UncompressedSize,
ParentID: layer.Parent,
}
} else {
return nil, fmt.Errorf("detected multiple layers with the same ID %q", layer.ID)
}
}
// scan all layers & add all childs for each layers to layerInfo
for _, layer := range layers {
_, ok := layerInfoMap[layer.ID]
if ok {
if layer.Parent != "" {
layerInfoMap[layer.Parent].ChildID = append(layerInfoMap[layer.Parent].ChildID, layer.ID)
}
} else {
return nil, fmt.Errorf("lookup error: layer-id %s, not found", layer.ID)
}
}
// Add the Repo Tags to Top layer of each image.
imgs, err := imageruntime.store.Images()
if err != nil {
return nil, err
}
for _, img := range imgs {
e, ok := layerInfoMap[img.TopLayer]
if !ok {
return nil, fmt.Errorf("top-layer for image %s not found local store", img.ID)
}
e.RepoTags = append(e.RepoTags, img.Names...)
}
return layerInfoMap, nil
}

64
test/e2e/tree_test.go Normal file
View File

@ -0,0 +1,64 @@
package integration
import (
"fmt"
"os"
. "github.com/containers/libpod/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Podman image tree", func() {
var (
tempdir string
err error
podmanTest *PodmanTestIntegration
)
BeforeEach(func() {
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
}
podmanTest = PodmanTestCreate(tempdir)
podmanTest.RestoreAllArtifacts()
})
AfterEach(func() {
podmanTest.Cleanup()
f := CurrentGinkgoTestDescription()
timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
GinkgoWriter.Write([]byte(timedResult))
})
It("podman image tree", func() {
if podmanTest.RemoteTest {
Skip("Does not work on remote client")
}
session := podmanTest.Podman([]string{"pull", "docker.io/library/busybox:latest"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
dockerfile := `FROM docker.io/library/busybox:latest
RUN mkdir hello
RUN touch test.txt
ENV foo=bar
`
podmanTest.BuildImage(dockerfile, "test:latest", "true")
session = podmanTest.Podman([]string{"image", "tree", "test:latest"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
session = podmanTest.Podman([]string{"image", "tree", "--whatrequires", "docker.io/library/busybox:latest"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
session = podmanTest.Podman([]string{"rmi", "test:latest"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
session = podmanTest.Podman([]string{"rmi", "docker.io/library/busybox:latest"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
})
})