410 lines
12 KiB
Diff
410 lines
12 KiB
Diff
|
|
From 4e71f4409e53eadea0aa39383fba3e249072a932 Mon Sep 17 00:00:00 2001
|
||
|
|
From: meilier <xingweizheng@huawei.com>
|
||
|
|
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 = "<none>"
|
||
|
|
- decimalPrefixBase = 1000
|
||
|
|
+ none = "<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
|
||
|
|
|