diff options
-rw-r--r-- | cmd/podman/cliconfig/config.go | 5 | ||||
-rw-r--r-- | cmd/podman/image.go | 1 | ||||
-rw-r--r-- | cmd/podman/tree.go | 190 | ||||
-rw-r--r-- | docs/podman-image-tree.1.md | 88 | ||||
-rw-r--r-- | docs/podman-image.1.md | 1 | ||||
-rw-r--r-- | libpod/image/image.go | 67 | ||||
-rw-r--r-- | test/e2e/tree_test.go | 64 |
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)) + }) +}) |