mirror of
https://github.com/containers/podman.git
synced 2025-06-28 22:53:21 +08:00
Merge pull request #1642 from kunalkushwaha/image-tree
Tree implementation for podman images
This commit is contained in:
@ -66,6 +66,11 @@ type TagValues struct {
|
||||
PodmanCommand
|
||||
}
|
||||
|
||||
type TreeValues struct {
|
||||
PodmanCommand
|
||||
WhatRequires bool
|
||||
}
|
||||
|
||||
type WaitValues struct {
|
||||
PodmanCommand
|
||||
Interval uint
|
||||
|
@ -73,6 +73,7 @@ var imageSubCommands = []*cobra.Command{
|
||||
_rmSubCommand,
|
||||
_saveCommand,
|
||||
_tagCommand,
|
||||
_treeCommand,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
190
cmd/podman/tree.go
Normal file
190
cmd/podman/tree.go
Normal 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)
|
||||
}
|
||||
}
|
88
docs/podman-image-tree.1.md
Normal file
88
docs/podman-image-tree.1.md
Normal 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>
|
@ -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
|
||||
|
@ -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
64
test/e2e/tree_test.go
Normal 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))
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user