Skip to content

Commit 7dd3050

Browse files
authored
Merge pull request #3319 from austinvazquez/feat-add-image-prune-filter
Add image prune --filter support
2 parents b54de25 + 7c9751e commit 7dd3050

File tree

10 files changed

+857
-124
lines changed

10 files changed

+857
-124
lines changed

cmd/nerdctl/image_list_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func TestImages(t *testing.T) {
7979

8080
func TestImagesFilter(t *testing.T) {
8181
testutil.RequiresBuild(t)
82+
testutil.RegisterBuildCacheCleanup(t)
8283
t.Parallel()
8384
base := testutil.NewBase(t)
8485
tempName := testutil.Identifier(base.T)
@@ -121,6 +122,7 @@ LABEL version=0.1`, testutil.CommonImage)
121122

122123
func TestImagesFilterDangling(t *testing.T) {
123124
testutil.RequiresBuild(t)
125+
testutil.RegisterBuildCacheCleanup(t)
124126
base := testutil.NewBase(t)
125127
base.Cmd("images", "prune", "--all").AssertOK()
126128

cmd/nerdctl/image_prune.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func newImagePruneCommand() *cobra.Command {
3838
}
3939

4040
imagePruneCommand.Flags().BoolP("all", "a", false, "Remove all unused images, not just dangling ones")
41+
imagePruneCommand.Flags().StringSlice("filter", []string{}, "Filter output based on conditions provided")
4142
imagePruneCommand.Flags().BoolP("force", "f", false, "Do not prompt for confirmation")
4243
return imagePruneCommand
4344
}
@@ -52,6 +53,14 @@ func processImagePruneOptions(cmd *cobra.Command) (types.ImagePruneOptions, erro
5253
return types.ImagePruneOptions{}, err
5354
}
5455

56+
var filters []string
57+
if cmd.Flags().Changed("filter") {
58+
filters, err = cmd.Flags().GetStringSlice("filter")
59+
if err != nil {
60+
return types.ImagePruneOptions{}, err
61+
}
62+
}
63+
5564
force, err := cmd.Flags().GetBool("force")
5665
if err != nil {
5766
return types.ImagePruneOptions{}, err
@@ -61,6 +70,7 @@ func processImagePruneOptions(cmd *cobra.Command) (types.ImagePruneOptions, erro
6170
Stdout: cmd.OutOrStdout(),
6271
GOptions: globalOptions,
6372
All: all,
73+
Filters: filters,
6474
Force: force,
6575
}, err
6676
}

cmd/nerdctl/image_prune_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"fmt"
2121
"testing"
22+
"time"
2223

2324
"github.com/containerd/nerdctl/v2/pkg/testutil"
2425
)
@@ -71,3 +72,57 @@ func TestImagePruneAll(t *testing.T) {
7172
base.Cmd("image", "prune", "--force", "--all").AssertOutContains(imageName)
7273
base.Cmd("images").AssertOutNotContains(imageName)
7374
}
75+
76+
func TestImagePruneFilterLabel(t *testing.T) {
77+
testutil.RequiresBuild(t)
78+
testutil.RegisterBuildCacheCleanup(t)
79+
80+
base := testutil.NewBase(t)
81+
imageName := testutil.Identifier(t)
82+
t.Cleanup(func() { base.Cmd("rmi", "--force", imageName) })
83+
84+
dockerfile := fmt.Sprintf(`FROM %s
85+
CMD ["echo", "nerdctl-test-image-prune-filter-label"]
86+
LABEL foo=bar
87+
LABEL version=0.1`, testutil.CommonImage)
88+
89+
buildCtx := createBuildContext(t, dockerfile)
90+
91+
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
92+
base.Cmd("images", "--all").AssertOutContains(imageName)
93+
94+
base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=baz").AssertOK()
95+
base.Cmd("images", "--all").AssertOutContains(imageName)
96+
97+
base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=bar").AssertOK()
98+
base.Cmd("images", "--all").AssertOutNotContains(imageName)
99+
}
100+
101+
func TestImagePruneFilterUntil(t *testing.T) {
102+
testutil.RequiresBuild(t)
103+
testutil.RegisterBuildCacheCleanup(t)
104+
105+
// Docker image's created timestamp is set based on base image creation time.
106+
testutil.DockerIncompatible(t)
107+
108+
base := testutil.NewBase(t)
109+
imageName := testutil.Identifier(t)
110+
t.Cleanup(func() { base.Cmd("rmi", "--force", imageName) })
111+
112+
dockerfile := fmt.Sprintf(`FROM %s
113+
CMD ["echo", "nerdctl-test-image-prune-filter-until"]`, testutil.CommonImage)
114+
115+
buildCtx := createBuildContext(t, dockerfile)
116+
117+
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
118+
base.Cmd("images", "--all").AssertOutContains(imageName)
119+
120+
base.Cmd("image", "prune", "--force", "--all", "--filter", "until=12h").AssertOK()
121+
base.Cmd("images", "--all").AssertOutContains(imageName)
122+
123+
// Pause to ensure enough time has passed for the image to be cleaned on next prune.
124+
time.Sleep(3 * time.Second)
125+
126+
base.Cmd("image", "prune", "--force", "--all", "--filter", "until=10ms").AssertOK()
127+
base.Cmd("images", "--all").AssertOutNotContains(imageName)
128+
}

docs/command-reference.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ Flags:
746746
- :nerd_face: `--format=wide`: Wide table
747747
- :nerd_face: `--format=json`: Alias of `--format='{{json .}}'`
748748
- :whale: `--digests`: Show digests (compatible with Docker, unlike ID)
749-
- :whale: `-f, --filter`: Filter the images. For now, only 'before=<image:tag>' and 'since=<image:tag>' is supported.
749+
- :whale: `-f, --filter`: Filter the images.
750750
- :whale: `--filter=before=<image:tag>`: Images created before given image (exclusive)
751751
- :whale: `--filter=since=<image:tag>`: Images created after given image (exclusive)
752752
- :whale: `--filter=label<key>=<value>`: Matches images based on the presence of a label alone or a label and a value
@@ -886,10 +886,11 @@ Usage: `nerdctl image prune [OPTIONS]`
886886
Flags:
887887

888888
- :whale: `-a, --all`: Remove all unused images, not just dangling ones
889+
- :whale: `-f, --filter`: Filter the images.
890+
- :whale: `--filter=until=<timestamp>`: Images created before given date formatted timestamps or Go duration strings. Currently does not support Unix timestamps.
891+
- :whale: `--filter=label<key>=<value>`: Matches images based on the presence of a label alone or a label and a value
889892
- :whale: `-f, --force`: Do not prompt for confirmation
890893

891-
Unimplemented `docker image prune` flags: `--filter`
892-
893894
### :nerd_face: nerdctl image convert
894895

895896
Convert an image format.

pkg/api/types/image_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ type ImagePruneOptions struct {
236236
GOptions GlobalCommandOptions
237237
// All Remove all unused images, not just dangling ones.
238238
All bool
239+
// Filters output based on conditions provided for the --filter argument
240+
Filters []string
239241
// Force will not prompt for confirmation.
240242
Force bool
241243
}

pkg/cmd/image/list.go

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,36 +80,29 @@ func List(ctx context.Context, client *containerd.Client, filters, nameAndRefFil
8080
return nil, err
8181
}
8282

83-
if f.Dangling != nil {
84-
imageList = imgutil.FilterDangling(imageList, *f.Dangling)
83+
filters := []imgutil.Filter{}
84+
if f.Dangling != nil && *f.Dangling {
85+
filters = append(filters, imgutil.FilterDanglingImages())
86+
} else if f.Dangling != nil {
87+
filters = append(filters, imgutil.FilterTaggedImages())
8588
}
8689

87-
imageList, err = imgutil.FilterByLabel(ctx, client, imageList, f.Labels)
88-
if err != nil {
89-
return nil, err
90+
if len(f.Labels) > 0 {
91+
filters = append(filters, imgutil.FilterByLabel(ctx, client, f.Labels))
9092
}
9193

92-
imageList, err = imgutil.FilterByReference(imageList, f.Reference)
93-
if err != nil {
94-
return nil, err
94+
if len(f.Reference) > 0 {
95+
filters = append(filters, imgutil.FilterByReference(f.Reference))
9596
}
9697

97-
var beforeImages []images.Image
98-
if len(f.Before) > 0 {
99-
beforeImages, err = imageStore.List(ctx, f.Before...)
100-
if err != nil {
101-
return nil, err
102-
}
103-
}
104-
var sinceImages []images.Image
105-
if len(f.Since) > 0 {
106-
sinceImages, err = imageStore.List(ctx, f.Since...)
107-
if err != nil {
108-
return nil, err
109-
}
98+
if len(f.Before) > 0 || len(f.Since) > 0 {
99+
filters = append(filters, imgutil.FilterByCreatedAt(ctx, client, f.Before, f.Since))
110100
}
111101

112-
imageList = imgutil.FilterImages(imageList, beforeImages, sinceImages)
102+
imageList, err = imgutil.ApplyFilters(imageList, filters...)
103+
if err != nil {
104+
return []images.Image{}, err
105+
}
113106
}
114107

115108
sort.Slice(imageList, func(i, j int) bool {

pkg/cmd/image/prune.go

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,43 @@ import (
3434
// Prune will remove all dangling images. If all is specified, will also remove all images not referenced by any container.
3535
func Prune(ctx context.Context, client *containerd.Client, options types.ImagePruneOptions) error {
3636
var (
37-
imageStore = client.ImageService()
38-
contentStore = client.ContentStore()
39-
containerStore = client.ContainerService()
37+
imageStore = client.ImageService()
38+
contentStore = client.ContentStore()
4039
)
4140

42-
imageList, err := imageStore.List(ctx)
43-
if err != nil {
44-
return err
45-
}
46-
47-
var filteredImages []images.Image
41+
var (
42+
imagesToBeRemoved []images.Image
43+
err error
44+
)
4845

49-
if options.All {
50-
containerList, err := containerStore.List(ctx)
46+
filters := []imgutil.Filter{}
47+
if len(options.Filters) > 0 {
48+
parsedFilters, err := imgutil.ParseFilters(options.Filters)
5149
if err != nil {
5250
return err
5351
}
54-
usedImages := make(map[string]struct{})
55-
for _, container := range containerList {
56-
usedImages[container.Image] = struct{}{}
52+
if len(parsedFilters.Labels) > 0 {
53+
filters = append(filters, imgutil.FilterByLabel(ctx, client, parsedFilters.Labels))
5754
}
58-
59-
for _, image := range imageList {
60-
if _, ok := usedImages[image.Name]; ok {
61-
continue
62-
}
63-
64-
filteredImages = append(filteredImages, image)
55+
if len(parsedFilters.Until) > 0 {
56+
filters = append(filters, imgutil.FilterUntil(parsedFilters.Until))
6557
}
58+
}
59+
60+
if options.All {
61+
// Remove all unused images; not just dangling ones
62+
imagesToBeRemoved, err = imgutil.GetUnusedImages(ctx, client, filters...)
6663
} else {
67-
filteredImages = imgutil.FilterDangling(imageList, true)
64+
// Remove dangling images only
65+
imagesToBeRemoved, err = imgutil.GetDanglingImages(ctx, client, filters...)
66+
}
67+
if err != nil {
68+
return err
6869
}
6970

7071
delOpts := []images.DeleteOpt{images.SynchronousDelete()}
7172
removedImages := make(map[string][]digest.Digest)
72-
for _, image := range filteredImages {
73+
for _, image := range imagesToBeRemoved {
7374
digests, err := image.RootFS(ctx, contentStore, platforms.DefaultStrict())
7475
if err != nil {
7576
log.G(ctx).WithError(err).Warnf("failed to enumerate rootfs")

0 commit comments

Comments
 (0)