Skip to content

Commit

Permalink
Merge pull request kubernetes-sigs#1359 from roman-kiselenko/feature/…
Browse files Browse the repository at this point in the history
…support-image-filters

Implemented the `--filter` flag for `images` command.
  • Loading branch information
k8s-ci-robot authored Feb 22, 2024
2 parents cb400b8 + 98843b1 commit 7c196c0
Show file tree
Hide file tree
Showing 4 changed files with 545 additions and 1 deletion.
99 changes: 98 additions & 1 deletion cmd/crictl/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"context"
"errors"
"fmt"
"regexp"
"slices"
"sort"
"strings"
"syscall"
Expand Down Expand Up @@ -142,6 +144,11 @@ var listImageCommand = &cli.Command{
Aliases: []string{"q"},
Usage: "Only show image IDs",
},
&cli.StringSliceFlag{
Name: "filter",
Aliases: []string{"f"},
Usage: "The filtering flag format is of 'dangling=(true/false)', 'reference=regex', '(before|since)=<image-name>[:<tag>]|<image id>|<image@digest>'",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Expand Down Expand Up @@ -174,8 +181,16 @@ var listImageCommand = &cli.Command{
if err != nil {
return fmt.Errorf("listing images: %w", err)
}

sort.Sort(imageByRef(r.Images))

if len(c.StringSlice("filter")) > 0 && len(r.Images) > 0 {
r.Images, err = filterImagesList(r.Images, c.StringSlice("filter"))
if err != nil {
return fmt.Errorf("listing images: %w", err)
}
}

switch c.String("output") {
case "json":
return outputProtobufObjAsJSON(r)
Expand Down Expand Up @@ -524,7 +539,6 @@ var imageFsInfoCommand = &cli.Command{
tablePrintFileSystem("Image", r.ImageFilesystems)

return nil

},
}

Expand Down Expand Up @@ -650,6 +664,89 @@ func ListImages(client internalapi.ImageManagerService, image string) (*pb.ListI
return resp, nil
}

// filterImagesList filter images based on --filter flag
func filterImagesList(imageList []*pb.Image, filters []string) ([]*pb.Image, error) {
filtered := []*pb.Image{}
filtered = append(filtered, imageList...)
for _, filter := range filters {
switch {
case strings.HasPrefix(filter, "before="):
reversedList := filtered
slices.Reverse(reversedList)
filtered = filterByBeforeSince(strings.TrimPrefix(filter, "before="), reversedList)
slices.Reverse(filtered)
case strings.HasPrefix(filter, "dangling="):
filtered = filterByDangling(strings.TrimPrefix(filter, "dangling="), filtered)
case strings.HasPrefix(filter, "reference="):
filtered = filterByReference(strings.TrimPrefix(filter, "reference="), filtered)
case strings.HasPrefix(filter, "since="):
filtered = filterByBeforeSince(strings.TrimPrefix(filter, "since="), filtered)
default:
return []*pb.Image{}, fmt.Errorf("Unknown filter flag: %v", filter)
}
}
return filtered, nil
}

func filterByBeforeSince(filterValue string, imageList []*pb.Image) []*pb.Image {
filtered := []*pb.Image{}
for _, img := range imageList {
// Filter by <image-name>[:<tag>]
if strings.Contains(filterValue, ":") && !strings.Contains(filterValue, "@") {
imageName, _ := normalizeRepoDigest(img.RepoDigests)
repoTagPairs := normalizeRepoTagPair(img.RepoTags, imageName)
if strings.Join(repoTagPairs[0], ":") == filterValue {
break
}
filtered = append(filtered, img)
}
// Filter by <image id>
if !strings.Contains(filterValue, ":") && !strings.Contains(filterValue, "@") {
if strings.HasPrefix(img.Id, filterValue) {
break
}
filtered = append(filtered, img)
}
// Filter by <image@sha>
if strings.Contains(filterValue, ":") && strings.Contains(filterValue, "@") {
if len(img.RepoDigests) > 0 {
if strings.HasPrefix(img.RepoDigests[0], filterValue) {
break
}
filtered = append(filtered, img)
}
}
}
return filtered
}

func filterByReference(filterValue string, imageList []*pb.Image) []*pb.Image {
filtered := []*pb.Image{}
re, _ := regexp.Compile(filterValue)
for _, img := range imageList {
imgName, _ := normalizeRepoDigest(img.RepoDigests)
if re.MatchString(imgName) || imgName == filterValue {
filtered = append(filtered, img)
}
}

return filtered
}

func filterByDangling(filterValue string, imageList []*pb.Image) []*pb.Image {
filtered := []*pb.Image{}
for _, img := range imageList {
if filterValue == "true" && len(img.RepoTags) == 0 {
filtered = append(filtered, img)
}
if filterValue == "false" && len(img.RepoTags) > 0 {
filtered = append(filtered, img)
}
}

return filtered
}

// ImageStatus sends an ImageStatusRequest to the server, and parses
// the returned ImageStatusResponse.
func ImageStatus(client internalapi.ImageManagerService, image string, verbose bool) (*pb.ImageStatusResponse, error) {
Expand Down
181 changes: 181 additions & 0 deletions cmd/crictl/image_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
pb "k8s.io/cri-api/pkg/apis/runtime/v1"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func fakeImage(id string, digest []string, tags []string) *pb.Image {
return &pb.Image{Id: id, RepoDigests: digest, RepoTags: tags}
}

func assert(input []*pb.Image, options []string, images []string) {
actual, _ := filterImagesList(input, options)
expected := []string{}
for _, img := range actual {
expected = append(expected, img.Id)
}
Expect(images).To(Equal(expected))
}

var _ = DescribeTable("filterImagesListByDangling", assert,
Entry("returns filtered images with dangling --filter=dangling=true",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/busybox@sha256:1"}, []string{}),
fakeImage("2", []string{"docker.io/library/nginx@sha256:2"}, []string{"latest"}),
},
[]string{"dangling=true"},
[]string{"1"},
),
Entry("returns filtered images with dangling --filter=dangling=false",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"1.2.2"}),
},
[]string{"dangling=false"},
[]string{"2", "4"},
),
)

var _ = DescribeTable("filterImagesListByReference", assert,
Entry("returns filtered images with one reference --filter=reference=busybox",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/busybox@sha256:1"}, []string{"latest"}),
fakeImage("2", []string{"docker.io/library/nginx@sha256:2"}, []string{"latest"}),
},
[]string{"reference=busybox"},
[]string{"1"},
),
Entry("returns filtered images with many reference --filter=reference=busybox, --filter=reference=k8s",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"1.2.2"}),
fakeImage("5", []string{"registry.k8s.io/e2e-test-images/busybox@sha256:5"}, []string{"1.2.2"}),
},
[]string{"reference=busybox", "reference=k8s"},
[]string{"5"},
),
)

var _ = DescribeTable("filterImagesListByBefore", assert,
Entry("returns filtered images with --filter=before=<image-name>[:<tag>]",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"docker.io/library/server:0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"docker.io/library/busybox:1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"docker.io/library/nginx:1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"docker.io/library/app:1.2.2"}),
},
[]string{"before=docker.io/library/nginx:1.0.0"},
[]string{"4"},
),
Entry("returns filtered images with --filter=before=<image id>",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"docker.io/library/server:0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"docker.io/library/busybox:1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"docker.io/library/nginx:1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"docker.io/library/app:1.2.2"}),
},
[]string{"before=1"},
[]string{"2", "3", "4"},
),
Entry("returns filtered images with --filter=before=<image@digest>",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"docker.io/library/server:0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"docker.io/library/busybox:1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"docker.io/library/nginx:1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"docker.io/library/app:1.2.2"}),
},
[]string{"before=docker.io/library/busybox@sha256:2"},
[]string{"3", "4"},
),
)

var _ = DescribeTable("filterImagesListBySince", assert,
Entry("returns filtered images with --filter=since=<image-name>[:<tag>]",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"docker.io/library/server:0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"docker.io/library/busybox:1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"docker.io/library/nginx:1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"docker.io/library/app:1.2.2"}),
},
[]string{"since=docker.io/library/busybox:1.2.0"},
[]string{"1"},
),
Entry("returns filtered images with --filter=since=<image id>",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"docker.io/library/server:0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"docker.io/library/busybox:1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"docker.io/library/nginx:1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"docker.io/library/app:1.2.2"}),
},
[]string{"since=3"},
[]string{"1", "2"},
),
Entry("returns filtered images with --filter=since=<image@digest>",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"docker.io/library/server:0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"docker.io/library/busybox:1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"docker.io/library/nginx:1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"docker.io/library/app:1.2.2"}),
},
[]string{"since=docker.io/library/nginx@sha256:3"},
[]string{"1", "2"},
),
)

var _ = DescribeTable("filterImagesListByChainable", assert,
Entry("returns filtered images with --filter=since=<image-id> and --filter=reference=<ref>",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"docker.io/library/server:0.0.0"}),
fakeImage("2", []string{"docker.io/library/busybox@sha256:2"}, []string{"docker.io/library/busybox:1.2.0"}),
fakeImage("3", []string{"docker.io/library/nginx@sha256:3"}, []string{"docker.io/library/nginx:1.0.0"}),
fakeImage("4", []string{"docker.io/library/app@sha256:4"}, []string{"docker.io/library/app:1.2.2"}),
},
[]string{"since=3", "reference=busybox"},
[]string{"2"},
),
Entry("returns filtered images with --filter=since=<image-id> and --filter=reference=<ref>",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"0.0.0"}),
fakeImage("2", []string{"registry.k8s.io/e2e-test-images/busybox@sha256:5"}, []string{"1.2.2"}),
fakeImage("3", []string{"docker.io/library/busybox@sha256:2"}, []string{"1.2.0"}),
fakeImage("4", []string{"docker.io/library/nginx@sha256:3"}, []string{"1.0.0"}),
fakeImage("5", []string{"docker.io/library/app@sha256:4"}, []string{"1.2.2"}),
},
[]string{"since=5", "reference=busybox"},
[]string{"2", "3"},
),
Entry("returns empty images list --filter=since=<image-id> and --filter=reference=<ref>",
[]*pb.Image{
fakeImage("1", []string{"docker.io/library/server@sha256:1"}, []string{"0.0.0"}),
fakeImage("2", []string{"registry.k8s.io/e2e-test-images/busybox@sha256:5"}, []string{"1.2.2"}),
fakeImage("3", []string{"docker.io/library/busybox@sha256:2"}, []string{"1.2.0"}),
fakeImage("4", []string{"docker.io/library/nginx@sha256:3"}, []string{"1.0.0"}),
fakeImage("5", []string{"docker.io/library/app@sha256:4"}, []string{"1.2.2"}),
},
[]string{"since=5", "reference=kubefun"},
[]string{},
),
)
Loading

0 comments on commit 7c196c0

Please sign in to comment.