aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/cliconfig/config.go5
-rw-r--r--cmd/podman/image.go1
-rw-r--r--cmd/podman/tree.go190
-rw-r--r--docs/podman-image-tree.1.md88
-rw-r--r--docs/podman-image.1.md1
-rw-r--r--libpod/image/image.go67
-rw-r--r--test/e2e/tree_test.go64
7 files changed, 416 insertions, 0 deletions
diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go
index ec08eedb5..cb9d9a338 100644
--- a/cmd/podman/cliconfig/config.go
+++ b/cmd/podman/cliconfig/config.go
@@ -66,6 +66,11 @@ type TagValues struct {
PodmanCommand
}
+type TreeValues struct {
+ PodmanCommand
+ WhatRequires bool
+}
+
type WaitValues struct {
PodmanCommand
Interval uint
diff --git a/cmd/podman/image.go b/cmd/podman/image.go
index 52bac6ecb..fb295b8a1 100644
--- a/cmd/podman/image.go
+++ b/cmd/podman/image.go
@@ -60,6 +60,7 @@ var imageSubCommands = []*cobra.Command{
_rmSubCommand,
_saveCommand,
_tagCommand,
+ _treeCommand,
}
func init() {
diff --git a/cmd/podman/tree.go b/cmd/podman/tree.go
new file mode 100644
index 000000000..ebda18cdb
--- /dev/null
+++ b/cmd/podman/tree.go
@@ -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)
+ }
+}
diff --git a/docs/podman-image-tree.1.md b/docs/podman-image-tree.1.md
new file mode 100644
index 000000000..014499d6a
--- /dev/null
+++ b/docs/podman-image-tree.1.md
@@ -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>
diff --git a/docs/podman-image.1.md b/docs/podman-image.1.md
index b4ae752f6..54960045f 100644
--- a/docs/podman-image.1.md
+++ b/docs/podman-image.1.md
@@ -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
diff --git a/libpod/image/image.go b/libpod/image/image.go
index 72f07dad1..c5939e055 100644
--- a/libpod/image/image.go
+++ b/libpod/image/image.go
@@ -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
+}
diff --git a/test/e2e/tree_test.go b/test/e2e/tree_test.go
new file mode 100644
index 000000000..9740adada
--- /dev/null
+++ b/test/e2e/tree_test.go
@@ -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))
+ })
+})