From 4e71f4409e53eadea0aa39383fba3e249072a932 Mon Sep 17 00:00:00 2001 From: meilier Date: Tue, 2 Feb 2021 00:46:23 +0800 Subject: [PATCH 07/10] fix images command when only give repository --- daemon/images.go | 145 +++++++++++++++++++++------------- daemon/images_test.go | 178 ++++++++++++++++++++++++++++++++++++++++++ image/image.go | 9 ++- 3 files changed, 277 insertions(+), 55 deletions(-) create mode 100644 daemon/images_test.go diff --git a/daemon/images.go b/daemon/images.go index 5560d18c..e61817cc 100644 --- a/daemon/images.go +++ b/daemon/images.go @@ -15,9 +15,11 @@ package daemon import ( "context" + "fmt" "sort" "strings" + "github.com/containers/storage" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -29,79 +31,114 @@ import ( ) const ( - none = "" - decimalPrefixBase = 1000 + none = "" + decimalPrefixBase = 1000 + minImageFieldLenWithTag = 2 ) +type listOptions struct { + localStore *store.Store + logEntry *logrus.Entry + imageName string +} + +func (b *Backend) getListOptions(req *pb.ListRequest) listOptions { + return listOptions{ + localStore: b.daemon.localStore, + logEntry: logrus.WithFields(logrus.Fields{"ImageName": req.GetImageName()}), + imageName: req.GetImageName(), + } +} + // List lists all images func (b *Backend) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) { - logEntry := logrus.WithFields(logrus.Fields{"ImageName": req.GetImageName()}) - logEntry.Info("ListRequest received") - - var reqRepository, reqTag string - const minImageFieldLenWithTag = 2 - if req.ImageName != "" { - imageName := req.ImageName - _, img, err := image.FindImage(b.daemon.localStore, imageName) - if err != nil { - logEntry.Error(err) - return nil, errors.Wrapf(err, "find local image %v error", imageName) - } + logrus.WithFields(logrus.Fields{ + "ImageName": req.GetImageName(), + }).Info("ListRequest received") - parts := strings.Split(imageName, ":") - if len(parts) >= minImageFieldLenWithTag { - reqRepository, reqTag = strings.Join(parts[0:len(parts)-1], ":"), parts[len(parts)-1] - } + opts := b.getListOptions(req) - imageInfo := &pb.ListResponse_ImageInfo{ - Repository: reqRepository, - Tag: reqTag, - Id: img.ID, - Created: img.Created.Format(constant.LayoutTime), - Size_: getImageSize(b.daemon.localStore, img.ID), - } + slashLastIndex := strings.LastIndex(opts.imageName, "/") + colonLastIndex := strings.LastIndex(opts.imageName, ":") + if opts.imageName != "" && strings.Contains(opts.imageName, ":") && colonLastIndex > slashLastIndex { + return listOneImage(opts) + } + return listImages(opts) +} - return &pb.ListResponse{Images: []*pb.ListResponse_ImageInfo{imageInfo}}, nil +func listOneImage(opts listOptions) (*pb.ListResponse, error) { + _, image, err := image.FindImage(opts.localStore, opts.imageName) + if err != nil { + opts.logEntry.Error(err) + return nil, errors.Wrapf(err, "find local image %v error", opts.imageName) } - images, err := b.daemon.localStore.Images() + result := make([]*pb.ListResponse_ImageInfo, 0, len(image.Names)) + appendImageToResult(&result, image, opts.localStore) + + for _, info := range result { + if opts.imageName == fmt.Sprintf("%s:%s", info.Repository, info.Tag) { + result = []*pb.ListResponse_ImageInfo{info} + } + } + + return &pb.ListResponse{Images: result}, nil +} + +func listImages(opts listOptions) (*pb.ListResponse, error) { + images, err := opts.localStore.Images() if err != nil { - logEntry.Error(err) + opts.logEntry.Error(err) return &pb.ListResponse{}, errors.Wrap(err, "failed list images from local storage") } + sort.Slice(images, func(i, j int) bool { return images[i].Created.After(images[j].Created) }) result := make([]*pb.ListResponse_ImageInfo, 0, len(images)) - for _, image := range images { - names := image.Names - if len(names) == 0 { - names = []string{none} + for i := range images { + appendImageToResult(&result, &images[i], opts.localStore) + } + + if opts.imageName == "" { + return &pb.ListResponse{Images: result}, nil + } + + sameRepositoryResult := make([]*pb.ListResponse_ImageInfo, 0, len(images)) + for _, info := range result { + if opts.imageName == info.Repository || strings.HasPrefix(info.Id, opts.imageName) { + sameRepositoryResult = append(sameRepositoryResult, info) + } + } + + if len(sameRepositoryResult) == 0 { + return &pb.ListResponse{}, errors.Errorf("failed to list images with repository %q in local storage", opts.imageName) + } + return &pb.ListResponse{Images: sameRepositoryResult}, nil +} + +func appendImageToResult(result *[]*pb.ListResponse_ImageInfo, image *storage.Image, store *store.Store) { + names := image.Names + if len(names) == 0 { + names = []string{none} + } + + for _, name := range names { + repository, tag := name, none + parts := strings.Split(name, ":") + if len(parts) >= minImageFieldLenWithTag { + repository, tag = strings.Join(parts[0:len(parts)-1], ":"), parts[len(parts)-1] } - for _, name := range names { - repository, tag := name, none - parts := strings.Split(name, ":") - if len(parts) >= minImageFieldLenWithTag { - repository, tag = strings.Join(parts[0:len(parts)-1], ":"), parts[len(parts)-1] - } - if reqRepository != "" && reqRepository != repository { - continue - } - if reqTag != "" && reqTag != tag { - continue - } - - imageInfo := &pb.ListResponse_ImageInfo{ - Repository: repository, - Tag: tag, - Id: image.ID, - Created: image.Created.Format(constant.LayoutTime), - Size_: getImageSize(b.daemon.localStore, image.ID), - } - result = append(result, imageInfo) + + imageInfo := &pb.ListResponse_ImageInfo{ + Repository: repository, + Tag: tag, + Id: image.ID, + Created: image.Created.Format(constant.LayoutTime), + Size_: getImageSize(store, image.ID), } + *result = append(*result, imageInfo) } - return &pb.ListResponse{Images: result}, nil } func getImageSize(store *store.Store, id string) string { diff --git a/daemon/images_test.go b/daemon/images_test.go new file mode 100644 index 00000000..a970ce0b --- /dev/null +++ b/daemon/images_test.go @@ -0,0 +1,178 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved. +// isula-build licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// Author: Weizheng Xing +// Create: 2021-02-03 +// Description: This file tests List interface + +package daemon + +import ( + "context" + "fmt" + "testing" + + "github.com/bndr/gotabulate" + "github.com/containers/storage" + "github.com/containers/storage/pkg/stringid" + "gotest.tools/v3/assert" + + constant "isula.org/isula-build" + pb "isula.org/isula-build/api/services" +) + +func TestList(t *testing.T) { + d := prepare(t) + defer tmpClean(d) + + options := &storage.ImageOptions{} + img, err := d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image:test1"}, "", "", options) + if err != nil { + t.Fatalf("create image with error: %v", err) + } + _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image:test2"}, "", "", options) + if err != nil { + t.Fatalf("create image with error: %v", err) + } + _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"egami:test"}, "", "", options) + if err != nil { + t.Fatalf("create image with error: %v", err) + } + // image with no name and tag + _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{}, "", "", options) + if err != nil { + t.Fatalf("create image with error: %v", err) + } + d.Daemon.localStore.SetNames(img.ID, append(img.Names, "image:test1-backup")) + // image who's repo contains port + _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"hub.example.com:8080/image:test"}, "", "", options) + if err != nil { + t.Fatalf("create image with error: %v", err) + } + + testcases := []struct { + name string + req *pb.ListRequest + wantErr bool + errString string + }{ + { + name: "normal case list specific image with repository[:tag]", + req: &pb.ListRequest{ + ImageName: "image:test1", + }, + wantErr: false, + }, + { + name: "normal case list specific image with image id", + req: &pb.ListRequest{ + ImageName: img.ID, + }, + wantErr: false, + }, + { + name: "normal case list all images", + req: &pb.ListRequest{ + ImageName: "", + }, + wantErr: false, + }, + { + name: "normal case list all images with repository", + req: &pb.ListRequest{ + ImageName: "image", + }, + wantErr: false, + }, + { + name: "abnormal case no image found in local store", + req: &pb.ListRequest{ + ImageName: "coffee:costa", + }, + wantErr: true, + errString: "failed to parse image", + }, + { + name: "abnormal case no repository", + req: &pb.ListRequest{ + ImageName: "coffee", + }, + wantErr: true, + errString: "failed to list images with repository", + }, + { + name: "abnormal case ImageName only contains latest tag", + req: &pb.ListRequest{ + ImageName: ":latest", + }, + wantErr: true, + errString: "invalid reference format", + }, + { + name: "normal case ImageName contains port number and tag", + req: &pb.ListRequest{ + ImageName: "hub.example.com:8080/image:test", + }, + wantErr: false, + }, + { + name: "normal case ImageName contains port number", + req: &pb.ListRequest{ + ImageName: "hub.example.com:8080/image", + }, + wantErr: false, + }, + { + name: "abnormal case wrong ImageName", + req: &pb.ListRequest{ + ImageName: "hub.example.com:8080/", + }, + wantErr: true, + errString: "failed to list images with repository", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.TODO() + resp, err := d.Daemon.backend.List(ctx, tc.req) + + if tc.wantErr == true { + assert.ErrorContains(t, err, tc.errString) + } + if tc.wantErr == false { + assert.NilError(t, err) + formatAndPrint(resp.Images) + } + }) + } +} + +func formatAndPrint(images []*pb.ListResponse_ImageInfo) { + emptyStr := `----------- ---- --------- -------- + REPOSITORY TAG IMAGE ID CREATED + ----------- ---- --------- --------` + lines := make([][]string, 0, len(images)) + title := []string{"REPOSITORY", "TAG", "IMAGE ID", "CREATED", "SIZE"} + for _, image := range images { + if image == nil { + continue + } + line := []string{image.Repository, image.Tag, image.Id[:constant.DefaultIDLen], image.Created, image.Size_} + lines = append(lines, line) + } + if len(lines) == 0 { + fmt.Println(emptyStr) + return + } + tabulate := gotabulate.Create(lines) + tabulate.SetHeaders(title) + tabulate.SetAlign("left") + fmt.Print(tabulate.Render("simple")) +} diff --git a/image/image.go b/image/image.go index bbbc7b94..36785bdf 100644 --- a/image/image.go +++ b/image/image.go @@ -590,12 +590,19 @@ func ResolveName(name string, sc *types.SystemContext, store *store.Store) ([]st } func tryResolveNameInStore(name string, store *store.Store) string { + defaultTag := "latest" + logrus.Infof("Try to find image: %s in local storage", name) img, err := store.Image(name) + if err == nil { + return img.ID + } + + logrus.Infof("Try to find image: %s:%s in local storage", name, defaultTag) + img, err = store.Image(fmt.Sprintf("%s:%s", name, defaultTag)) if err != nil { return "" } - return img.ID } -- 2.27.0