From 2b43c9a2e53a32072668b88983d7afae4a8f6d27 Mon Sep 17 00:00:00 2001 From: "Zheao.Li" Date: Tue, 9 Aug 2022 23:08:31 +0800 Subject: [PATCH] Support filter argument for `nerdctl images` command Signed-off-by: Zheao.Li --- README.md | 9 ++++- cmd/nerdctl/images.go | 41 +++++++++++++++++++--- cmd/nerdctl/images_test.go | 21 +++++++++++ pkg/imgutil/imgutil.go | 71 +++++++++++++++++++++++++++++++++++++- 4 files changed, 135 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0906f1d8390..17b9f6d971d 100644 --- a/README.md +++ b/README.md @@ -782,9 +782,16 @@ Flags: - :nerd_face: `--format=wide`: Wide table - :nerd_face: `--format=json`: Alias of `--format='{{json .}}'` - :whale: `--digests`: Show digests (compatible with Docker, unlike ID) +- :whale: `-f, --filter`: Filter the images. For now, only 'before=' and 'since=' is supported. + - :whale: `--filter=before=`: Images created before given image (exclusive) + - :whale: `--filter=since=`: Images created after given image (exclusive) - :nerd_face: `--names`: Show image names -Unimplemented `docker images` flags: `--filter` +Following arguments for `--filter` are not supported yet: + +1. `--filter=label==`: Filter images by label +2. `--filter=reference=`: Filter images by reference +3. `--filter=dangling=true`: Filter images by dangling ### :whale: :blue_square: nerdctl pull Pull an image from a registry. diff --git a/cmd/nerdctl/images.go b/cmd/nerdctl/images.go index 07f49a98b7f..5ae6b449f69 100644 --- a/cmd/nerdctl/images.go +++ b/cmd/nerdctl/images.go @@ -73,6 +73,7 @@ Properties: imagesCommand.Flags().Bool("no-trunc", false, "Don't truncate output") // Alias "-f" is reserved for "--filter" imagesCommand.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}', 'wide'") + imagesCommand.Flags().StringSliceP("filter", "f", []string{}, "Filter output based on conditions provided") imagesCommand.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp }) @@ -103,13 +104,43 @@ func imagesAction(cmd *cobra.Command, args []string) error { var ( imageStore = client.ImageService() ) + var imageList []images.Image - // To-do: Add support for --filter. - imageList, err := imageStore.List(ctx, filters...) - if err != nil { - return err - } + if !cmd.Flags().Changed("filter") { + imageList, err = imageStore.List(ctx, filters...) + if err != nil { + return err + } + } else { + inputFilters, err := cmd.Flags().GetStringSlice("filter") + if err != nil { + return err + } + beforeFilters, sinceFilters, err := imgutil.ParseFilters(inputFilters) + if err != nil { + return err + } + imageList, err = imageStore.List(ctx, filters...) + if err != nil { + return err + } + var beforeImages []images.Image + if len(beforeFilters) > 0 { + beforeImages, err = imageStore.List(ctx, beforeFilters...) + if err != nil { + return err + } + } + var afterImages []images.Image + if len(sinceFilters) > 0 { + afterImages, err = imageStore.List(ctx, sinceFilters...) + if err != nil { + return err + } + } + imageList = imgutil.FilterImages(imageList, beforeImages, afterImages) + } return printImages(ctx, cmd, client, imageList) } diff --git a/cmd/nerdctl/images_test.go b/cmd/nerdctl/images_test.go index 670e20caf3f..9cfa5152858 100644 --- a/cmd/nerdctl/images_test.go +++ b/cmd/nerdctl/images_test.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "os" "strings" "testing" @@ -74,3 +75,23 @@ func TestImages(t *testing.T) { return nil }) } + +func TestImagesFilter(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + tempName := testutil.Identifier(base.T) + tempName2 := testutil.Identifier(base.T) + base.Cmd("pull", testutil.CommonImage).AssertOK() + + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) + + buildCtx, err := createBuildContext(dockerfile) + assert.NilError(t, err) + defer os.RemoveAll(buildCtx) + base.Cmd("build", "-t", tempName, "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK() + base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName, "latest")).AssertOutContains(strings.Split(testutil.CommonImage, ":")[0]) + base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName2, "latest")).AssertOutNotContains(tempName) + base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutContains(tempName) + base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutNotContains(strings.Split(testutil.CommonImage, ":")[0]) +} diff --git a/pkg/imgutil/imgutil.go b/pkg/imgutil/imgutil.go index 61ccbf56db8..0ff43b26f22 100644 --- a/pkg/imgutil/imgutil.go +++ b/pkg/imgutil/imgutil.go @@ -22,6 +22,8 @@ import ( "fmt" "io" "reflect" + "strings" + "time" "github.com/containerd/containerd" "github.com/containerd/containerd/content" @@ -35,9 +37,9 @@ import ( "github.com/containerd/nerdctl/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/pkg/imgutil/pull" + "github.com/containerd/nerdctl/pkg/referenceutil" "github.com/docker/docker/errdefs" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/sirupsen/logrus" ) @@ -49,6 +51,11 @@ type EnsuredImage struct { Remote bool // true for stargz or overlaybd } +var ( + FilterBeforeType = "before" + FilterSinceType = "since" +) + // PullMode is either one of "always", "missing", "never" type PullMode = string @@ -373,3 +380,65 @@ func ParseRepoTag(imgName string) (string, string) { return repository, tag } + +func ParseFilters(filters []string) ([]string, []string, error) { + var beforeFilters []string + var sinceFilters []string + for _, filter := range filters { + tempFilterToken := strings.Split(filter, "=") + switch len(tempFilterToken) { + case 1: + return nil, nil, fmt.Errorf("invalid filter %q", filter) + case 2: + if tempFilterToken[0] == FilterBeforeType { + canonicalRef, err := referenceutil.ParseAny(tempFilterToken[1]) + if err != nil { + return nil, nil, err + } + beforeFilters = append(beforeFilters, fmt.Sprintf("name==%s", canonicalRef.String())) + beforeFilters = append(beforeFilters, fmt.Sprintf("name==%s", tempFilterToken[1])) + } else if tempFilterToken[0] == FilterSinceType { + canonicalRef, err := referenceutil.ParseAny(tempFilterToken[1]) + if err != nil { + return nil, nil, err + } + sinceFilters = append(sinceFilters, fmt.Sprintf("name==%s", canonicalRef.String())) + sinceFilters = append(sinceFilters, fmt.Sprintf("name==%s", tempFilterToken[1])) + } else { + return nil, nil, fmt.Errorf("invalid filter %q", filter) + } + default: + return nil, nil, fmt.Errorf("invalid filter %q", filter) + } + } + return beforeFilters, sinceFilters, nil +} + +func FilterImages(labelImages []images.Image, beforeImages []images.Image, sinceImages []images.Image) []images.Image { + + var filteredImages []images.Image + maxTime := time.Now() + minTime := time.Date(1970, time.Month(1), 1, 0, 0, 0, 0, time.UTC) + if len(beforeImages) > 0 { + maxTime = beforeImages[0].CreatedAt + for _, value := range beforeImages { + if value.CreatedAt.After(maxTime) { + maxTime = value.CreatedAt + } + } + } + if len(sinceImages) > 0 { + minTime = sinceImages[0].CreatedAt + for _, value := range sinceImages { + if value.CreatedAt.Before(minTime) { + minTime = value.CreatedAt + } + } + } + for _, image := range labelImages { + if image.CreatedAt.After(minTime) && image.CreatedAt.Before(maxTime) { + filteredImages = append(filteredImages, image) + } + } + return filteredImages +}