From 93358ef915f639e52088b0f6aec52e77d3da0af7 Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Wed, 5 Feb 2020 12:07:44 +0100 Subject: [PATCH] Refactor image tree for API usage Signed-off-by: Sascha Grunert --- API.md | 14 +++ cmd/podman/tree.go | 97 +------------------ cmd/podman/varlink/io.podman.varlink | 10 ++ libpod/image/tree.go | 138 +++++++++++++++++++++++++++ pkg/adapter/images.go | 33 ------- pkg/adapter/images_remote.go | 31 ------ pkg/adapter/runtime.go | 9 ++ pkg/adapter/runtime_remote.go | 4 + pkg/api/handlers/libpod/images.go | 41 +++++--- pkg/api/handlers/swagger.go | 9 ++ pkg/api/server/register_images.go | 25 +++++ pkg/varlinkapi/images.go | 14 +++ 12 files changed, 254 insertions(+), 171 deletions(-) create mode 100644 libpod/image/tree.go delete mode 100644 pkg/adapter/images.go diff --git a/API.md b/API.md index cfbe394de2..2460de09fd 100644 --- a/API.md +++ b/API.md @@ -95,6 +95,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in [func ImageSave(options: ImageSaveOptions) MoreResponse](#ImageSave) +[func ImageTree(name: string, whatRequires: bool) string](#ImageTree) + [func ImagesPrune(all: bool, filter: []string) []string](#ImagesPrune) [func ImportImage(source: string, reference: string, message: string, changes: []string, delete: bool) string](#ImportImage) @@ -775,6 +777,18 @@ $ varlink call -m unix:/run/podman/io.podman/io.podman.ImageExists '{"name": "im method ImageSave(options: [ImageSaveOptions](#ImageSaveOptions)) [MoreResponse](#MoreResponse) ImageSave allows you to save an image from the local image storage to a tarball +### func ImageTree +
+ +method ImageTree(name: [string](https://godoc.org/builtin#string), whatRequires: [bool](https://godoc.org/builtin#bool)) [string](https://godoc.org/builtin#string)
+ImageTree returns the image tree for the provided image name or ID +#### Example +~~~ +$ varlink call -m unix:/run/podman/io.podman/io.podman.ImageTree '{"name": "alpine"}' +{ + "tree": "Image ID: e7d92cdc71fe\nTags: [docker.io/library/alpine:latest]\nSize: 5.861MB\nImage Layers\n└── ID: 5216338b40a7 Size: 5.857MB Top Layer of: [docker.io/library/alpine:latest]\n" +} +~~~ ### func ImagesPrune
diff --git a/cmd/podman/tree.go b/cmd/podman/tree.go index 69b42639de..28c770f0ce 100644 --- a/cmd/podman/tree.go +++ b/cmd/podman/tree.go @@ -1,23 +1,14 @@ package main import ( - "context" "fmt" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/adapter" - "github.com/docker/go-units" "github.com/pkg/errors" "github.com/spf13/cobra" ) -const ( - middleItem = "├── " - continueItem = "│ " - lastItem = "└── " -) - var ( treeCommand cliconfig.TreeValues @@ -56,95 +47,11 @@ func treeCmd(c *cliconfig.TreeValues) error { return errors.Wrapf(err, "error creating libpod runtime") } defer runtime.DeferredShutdown(false) - imageInfo, layerInfoMap, img, err := runtime.Tree(c.InputArgs[0]) - if err != nil { - return err - } - return printTree(imageInfo, layerInfoMap, img, c.WhatRequires) -} -func printTree(imageInfo *image.InfoImage, layerInfoMap map[string]*image.LayerInfo, img *adapter.ContainerImage, whatRequires bool) error { - size, err := img.Size(context.Background()) + tree, err := runtime.ImageTree(c.InputArgs[0], c.WhatRequires) if err != nil { return err } - - 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)) - if img.TopLayer() != "" { - fmt.Printf("Image Layers\n") - } else { - fmt.Printf("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 - 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) - return printImageChildren(layerInfoMap, 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 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.Print(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.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 - } - } + fmt.Print(tree) return nil } - -// prints the layers info of image -func printImageHierarchy(imageInfo *image.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/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index a0227c48c8..e9792fa8f0 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -1188,6 +1188,16 @@ method GetPodsByStatus(statuses: []string) -> (pods: []string) # ~~~ method ImageExists(name: string) -> (exists: int) +# ImageTree returns the image tree for the provided image name or ID +# #### Example +# ~~~ +# $ varlink call -m unix:/run/podman/io.podman/io.podman.ImageTree '{"name": "alpine"}' +# { +# "tree": "Image ID: e7d92cdc71fe\nTags: [docker.io/library/alpine:latest]\nSize: 5.861MB\nImage Layers\n└── ID: 5216338b40a7 Size: 5.857MB Top Layer of: [docker.io/library/alpine:latest]\n" +# } +# ~~~ +method ImageTree(name: string, whatRequires: bool) -> (tree: string) + # ContainerExists takes a full or partial container ID or name and returns an int as to # whether the container exists in local storage. A result of 0 means the container does # exists; whereas a result of 1 means it could not be found. diff --git a/libpod/image/tree.go b/libpod/image/tree.go new file mode 100644 index 0000000000..c7c69462ff --- /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) + } +} diff --git a/pkg/adapter/images.go b/pkg/adapter/images.go deleted file mode 100644 index 762f1a6565..0000000000 --- a/pkg/adapter/images.go +++ /dev/null @@ -1,33 +0,0 @@ -// +build !remoteclient - -package adapter - -import ( - "github.com/containers/libpod/libpod/image" - "github.com/pkg/errors" -) - -// Tree ... -func (r *LocalRuntime) Tree(imageOrID string) (*image.InfoImage, map[string]*image.LayerInfo, *ContainerImage, error) { - img, err := r.NewImageFromLocal(imageOrID) - if err != nil { - return nil, nil, nil, err - } - - // Fetch map of image-layers, which is used for printing output. - layerInfoMap, err := image.GetLayersMapWithImageInfo(r.Runtime.ImageRuntime()) - if err != nil { - return nil, nil, nil, errors.Wrapf(err, "error while retrieving layers of image %q", img.InputName) - } - - // Create an imageInfo and fill the image and layer info - imageInfo := &image.InfoImage{ - ID: img.ID(), - Tags: img.Names(), - } - - if err := image.BuildImageHierarchyMap(imageInfo, layerInfoMap, img.TopLayer()); err != nil { - return nil, nil, nil, err - } - return imageInfo, layerInfoMap, img, nil -} diff --git a/pkg/adapter/images_remote.go b/pkg/adapter/images_remote.go index 1d4997d9a8..e7b38dccc8 100644 --- a/pkg/adapter/images_remote.go +++ b/pkg/adapter/images_remote.go @@ -7,9 +7,7 @@ import ( "encoding/json" iopodman "github.com/containers/libpod/cmd/podman/varlink" - "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/inspect" - "github.com/pkg/errors" ) // Inspect returns returns an ImageData struct from over a varlink connection @@ -24,32 +22,3 @@ func (i *ContainerImage) Inspect(ctx context.Context) (*inspect.ImageData, error } return &data, nil } - -// Tree ... -func (r *LocalRuntime) Tree(imageOrID string) (*image.InfoImage, map[string]*image.LayerInfo, *ContainerImage, error) { - layerInfoMap := make(map[string]*image.LayerInfo) - imageInfo := &image.InfoImage{} - - img, err := r.NewImageFromLocal(imageOrID) - if err != nil { - return nil, nil, nil, err - } - - reply, err := iopodman.GetLayersMapWithImageInfo().Call(r.Conn) - if err != nil { - return nil, nil, nil, errors.Wrap(err, "failed to obtain image layers") - } - if err := json.Unmarshal([]byte(reply), &layerInfoMap); err != nil { - return nil, nil, nil, errors.Wrap(err, "failed to unmarshal image layers") - } - - reply, err = iopodman.BuildImageHierarchyMap().Call(r.Conn, imageOrID) - if err != nil { - return nil, nil, nil, errors.Wrap(err, "failed to get build image map") - } - if err := json.Unmarshal([]byte(reply), imageInfo); err != nil { - return nil, nil, nil, errors.Wrap(err, "failed to unmarshal build image map") - } - - return imageInfo, layerInfoMap, img, nil -} diff --git a/pkg/adapter/runtime.go b/pkg/adapter/runtime.go index 40089797dd..dfe6b7f071 100644 --- a/pkg/adapter/runtime.go +++ b/pkg/adapter/runtime.go @@ -133,6 +133,15 @@ func (r *LocalRuntime) NewImageFromLocal(name string) (*ContainerImage, error) { return &ContainerImage{img}, nil } +// ImageTree reutnrs an new image.Tree for the provided `imageOrID` and `whatrequires` flag +func (r *LocalRuntime) ImageTree(imageOrID string, whatRequires bool) (string, error) { + img, err := r.Runtime.ImageRuntime().NewFromLocal(imageOrID) + if err != nil { + return "", err + } + return img.GenerateTree(whatRequires) +} + // LoadFromArchiveReference calls into local storage to load an image from an archive func (r *LocalRuntime) LoadFromArchiveReference(ctx context.Context, srcRef types.ImageReference, signaturePolicyPath string, writer io.Writer) ([]*ContainerImage, error) { var containerImages []*ContainerImage diff --git a/pkg/adapter/runtime_remote.go b/pkg/adapter/runtime_remote.go index c908358ff2..220d4cf75e 100644 --- a/pkg/adapter/runtime_remote.go +++ b/pkg/adapter/runtime_remote.go @@ -344,6 +344,10 @@ func (r *LocalRuntime) New(ctx context.Context, name, signaturePolicyPath, authf return newImage, nil } +func (r *LocalRuntime) ImageTree(imageOrID string, whatRequires bool) (string, error) { + return iopodman.ImageTree().Call(r.Conn, imageOrID, whatRequires) +} + // IsParent goes through the layers in the store and checks if i.TopLayer is // the parent of any other layer in store. Double check that image with that // layer exists as well. diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index bcbe4977e7..eac0e4dad8 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -46,17 +46,34 @@ func ImageExists(w http.ResponseWriter, r *http.Request) { } func ImageTree(w http.ResponseWriter, r *http.Request) { - // tree is a bit of a mess ... logic is in adapter and therefore not callable from here. needs rework - - // name := utils.GetName(r) - // _, layerInfoMap, _, err := s.Runtime.Tree(name) - // if err != nil { - // Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to find image information for %q", name)) - // return - // } - // it is not clear to me how to deal with this given all the processing of the image - // is in main. we need to discuss how that really should be and return something useful. - handlers.UnsupportedHandler(w, r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + name := utils.GetName(r) + + img, err := runtime.ImageRuntime().NewFromLocal(name) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusNotFound, errors.Wrapf(err, "Failed to find image %s", name)) + return + } + + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + WhatRequires bool `schema:"whatrequires"` + }{ + WhatRequires: false, + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + tree, err := img.GenerateTree(query.WhatRequires) + if err != nil { + utils.Error(w, "Server error", http.StatusInternalServerError, errors.Wrapf(err, "failed to generate image tree for %s", name)) + return + } + + utils.WriteResponse(w, http.StatusOK, tree) } func GetImage(w http.ResponseWriter, r *http.Request) { @@ -72,8 +89,8 @@ func GetImage(w http.ResponseWriter, r *http.Request) { return } utils.WriteResponse(w, http.StatusOK, inspect) - } + func GetImages(w http.ResponseWriter, r *http.Request) { images, err := utils.GetImages(w, r) if err != nil { diff --git a/pkg/api/handlers/swagger.go b/pkg/api/handlers/swagger.go index 10525bfc70..4ba123ba9d 100644 --- a/pkg/api/handlers/swagger.go +++ b/pkg/api/handlers/swagger.go @@ -136,3 +136,12 @@ type swagInspectVolumeResponse struct { libpod.InspectVolumeData } } + +// Image tree response +// swagger:response LibpodImageTreeResponse +type swagImageTreeResponse struct { + // in:body + Body struct { + ImageTreeResponse + } +} diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index f082c5fec7..4706a7c69f 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -578,6 +578,31 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/{name}/exists"), APIHandler(s.Context, libpod.ImageExists)) + // swagger:operation POST /libpod/images/{name}/tree libpod libpodImageTree + // --- + // tags: + // - images + // summary: Image tree + // description: Retrieve the image tree for the provided image name or ID + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // - in: query + // name: whatrequires + // type: boolean + // description: show all child images and layers of the specified image + // produces: + // - application/json + // responses: + // 200: + // $ref: '#/responses/LibpodImageTreeResponse' + // 401: + // $ref: '#/responses/NoSuchImage' + // 500: + // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/{name}/tree"), APIHandler(s.Context, libpod.ImageTree)) // swagger:operation GET /libpod/images/{name}/history libpod libpodImageHistory // --- diff --git a/pkg/varlinkapi/images.go b/pkg/varlinkapi/images.go index b144bfa5ef..c4809f16b8 100644 --- a/pkg/varlinkapi/images.go +++ b/pkg/varlinkapi/images.go @@ -1016,3 +1016,17 @@ func (i *LibpodAPI) BuildImageHierarchyMap(call iopodman.VarlinkCall, name strin } return call.ReplyBuildImageHierarchyMap(string(b)) } + +// ImageTree returns the image tree string for the provided image name or ID +func (i *LibpodAPI) ImageTree(call iopodman.VarlinkCall, nameOrID string, whatRequires bool) error { + img, err := i.Runtime.ImageRuntime().NewFromLocal(nameOrID) + if err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + + tree, err := img.GenerateTree(whatRequires) + if err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + return call.ReplyImageTree(tree) +}