Skip to content

Commit

Permalink
Merge pull request #3319 from austinvazquez/feat-add-image-prune-filter
Browse files Browse the repository at this point in the history
Add image prune --filter support
  • Loading branch information
AkihiroSuda authored Aug 17, 2024
2 parents b54de25 + 7c9751e commit 7dd3050
Show file tree
Hide file tree
Showing 10 changed files with 857 additions and 124 deletions.
2 changes: 2 additions & 0 deletions cmd/nerdctl/image_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func TestImages(t *testing.T) {

func TestImagesFilter(t *testing.T) {
testutil.RequiresBuild(t)
testutil.RegisterBuildCacheCleanup(t)
t.Parallel()
base := testutil.NewBase(t)
tempName := testutil.Identifier(base.T)
Expand Down Expand Up @@ -121,6 +122,7 @@ LABEL version=0.1`, testutil.CommonImage)

func TestImagesFilterDangling(t *testing.T) {
testutil.RequiresBuild(t)
testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
base.Cmd("images", "prune", "--all").AssertOK()

Expand Down
10 changes: 10 additions & 0 deletions cmd/nerdctl/image_prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func newImagePruneCommand() *cobra.Command {
}

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

var filters []string
if cmd.Flags().Changed("filter") {
filters, err = cmd.Flags().GetStringSlice("filter")
if err != nil {
return types.ImagePruneOptions{}, err
}
}

force, err := cmd.Flags().GetBool("force")
if err != nil {
return types.ImagePruneOptions{}, err
Expand All @@ -61,6 +70,7 @@ func processImagePruneOptions(cmd *cobra.Command) (types.ImagePruneOptions, erro
Stdout: cmd.OutOrStdout(),
GOptions: globalOptions,
All: all,
Filters: filters,
Force: force,
}, err
}
Expand Down
55 changes: 55 additions & 0 deletions cmd/nerdctl/image_prune_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"fmt"
"testing"
"time"

"github.com/containerd/nerdctl/v2/pkg/testutil"
)
Expand Down Expand Up @@ -71,3 +72,57 @@ func TestImagePruneAll(t *testing.T) {
base.Cmd("image", "prune", "--force", "--all").AssertOutContains(imageName)
base.Cmd("images").AssertOutNotContains(imageName)
}

func TestImagePruneFilterLabel(t *testing.T) {
testutil.RequiresBuild(t)
testutil.RegisterBuildCacheCleanup(t)

base := testutil.NewBase(t)
imageName := testutil.Identifier(t)
t.Cleanup(func() { base.Cmd("rmi", "--force", imageName) })

dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "nerdctl-test-image-prune-filter-label"]
LABEL foo=bar
LABEL version=0.1`, testutil.CommonImage)

buildCtx := createBuildContext(t, dockerfile)

base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
base.Cmd("images", "--all").AssertOutContains(imageName)

base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=baz").AssertOK()
base.Cmd("images", "--all").AssertOutContains(imageName)

base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=bar").AssertOK()
base.Cmd("images", "--all").AssertOutNotContains(imageName)
}

func TestImagePruneFilterUntil(t *testing.T) {
testutil.RequiresBuild(t)
testutil.RegisterBuildCacheCleanup(t)

// Docker image's created timestamp is set based on base image creation time.
testutil.DockerIncompatible(t)

base := testutil.NewBase(t)
imageName := testutil.Identifier(t)
t.Cleanup(func() { base.Cmd("rmi", "--force", imageName) })

dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "nerdctl-test-image-prune-filter-until"]`, testutil.CommonImage)

buildCtx := createBuildContext(t, dockerfile)

base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
base.Cmd("images", "--all").AssertOutContains(imageName)

base.Cmd("image", "prune", "--force", "--all", "--filter", "until=12h").AssertOK()
base.Cmd("images", "--all").AssertOutContains(imageName)

// Pause to ensure enough time has passed for the image to be cleaned on next prune.
time.Sleep(3 * time.Second)

base.Cmd("image", "prune", "--force", "--all", "--filter", "until=10ms").AssertOK()
base.Cmd("images", "--all").AssertOutNotContains(imageName)
}
7 changes: 4 additions & 3 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,7 @@ 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=<image:tag>' and 'since=<image:tag>' is supported.
- :whale: `-f, --filter`: Filter the images.
- :whale: `--filter=before=<image:tag>`: Images created before given image (exclusive)
- :whale: `--filter=since=<image:tag>`: Images created after given image (exclusive)
- :whale: `--filter=label<key>=<value>`: Matches images based on the presence of a label alone or a label and a value
Expand Down Expand Up @@ -886,10 +886,11 @@ Usage: `nerdctl image prune [OPTIONS]`
Flags:

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

Unimplemented `docker image prune` flags: `--filter`

### :nerd_face: nerdctl image convert

Convert an image format.
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/types/image_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ type ImagePruneOptions struct {
GOptions GlobalCommandOptions
// All Remove all unused images, not just dangling ones.
All bool
// Filters output based on conditions provided for the --filter argument
Filters []string
// Force will not prompt for confirmation.
Force bool
}
Expand Down
37 changes: 15 additions & 22 deletions pkg/cmd/image/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,36 +80,29 @@ func List(ctx context.Context, client *containerd.Client, filters, nameAndRefFil
return nil, err
}

if f.Dangling != nil {
imageList = imgutil.FilterDangling(imageList, *f.Dangling)
filters := []imgutil.Filter{}
if f.Dangling != nil && *f.Dangling {
filters = append(filters, imgutil.FilterDanglingImages())
} else if f.Dangling != nil {
filters = append(filters, imgutil.FilterTaggedImages())
}

imageList, err = imgutil.FilterByLabel(ctx, client, imageList, f.Labels)
if err != nil {
return nil, err
if len(f.Labels) > 0 {
filters = append(filters, imgutil.FilterByLabel(ctx, client, f.Labels))
}

imageList, err = imgutil.FilterByReference(imageList, f.Reference)
if err != nil {
return nil, err
if len(f.Reference) > 0 {
filters = append(filters, imgutil.FilterByReference(f.Reference))
}

var beforeImages []images.Image
if len(f.Before) > 0 {
beforeImages, err = imageStore.List(ctx, f.Before...)
if err != nil {
return nil, err
}
}
var sinceImages []images.Image
if len(f.Since) > 0 {
sinceImages, err = imageStore.List(ctx, f.Since...)
if err != nil {
return nil, err
}
if len(f.Before) > 0 || len(f.Since) > 0 {
filters = append(filters, imgutil.FilterByCreatedAt(ctx, client, f.Before, f.Since))
}

imageList = imgutil.FilterImages(imageList, beforeImages, sinceImages)
imageList, err = imgutil.ApplyFilters(imageList, filters...)
if err != nil {
return []images.Image{}, err
}
}

sort.Slice(imageList, func(i, j int) bool {
Expand Down
47 changes: 24 additions & 23 deletions pkg/cmd/image/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,42 +34,43 @@ import (
// Prune will remove all dangling images. If all is specified, will also remove all images not referenced by any container.
func Prune(ctx context.Context, client *containerd.Client, options types.ImagePruneOptions) error {
var (
imageStore = client.ImageService()
contentStore = client.ContentStore()
containerStore = client.ContainerService()
imageStore = client.ImageService()
contentStore = client.ContentStore()
)

imageList, err := imageStore.List(ctx)
if err != nil {
return err
}

var filteredImages []images.Image
var (
imagesToBeRemoved []images.Image
err error
)

if options.All {
containerList, err := containerStore.List(ctx)
filters := []imgutil.Filter{}
if len(options.Filters) > 0 {
parsedFilters, err := imgutil.ParseFilters(options.Filters)
if err != nil {
return err
}
usedImages := make(map[string]struct{})
for _, container := range containerList {
usedImages[container.Image] = struct{}{}
if len(parsedFilters.Labels) > 0 {
filters = append(filters, imgutil.FilterByLabel(ctx, client, parsedFilters.Labels))
}

for _, image := range imageList {
if _, ok := usedImages[image.Name]; ok {
continue
}

filteredImages = append(filteredImages, image)
if len(parsedFilters.Until) > 0 {
filters = append(filters, imgutil.FilterUntil(parsedFilters.Until))
}
}

if options.All {
// Remove all unused images; not just dangling ones
imagesToBeRemoved, err = imgutil.GetUnusedImages(ctx, client, filters...)
} else {
filteredImages = imgutil.FilterDangling(imageList, true)
// Remove dangling images only
imagesToBeRemoved, err = imgutil.GetDanglingImages(ctx, client, filters...)
}
if err != nil {
return err
}

delOpts := []images.DeleteOpt{images.SynchronousDelete()}
removedImages := make(map[string][]digest.Digest)
for _, image := range filteredImages {
for _, image := range imagesToBeRemoved {
digests, err := image.RootFS(ctx, contentStore, platforms.DefaultStrict())
if err != nil {
log.G(ctx).WithError(err).Warnf("failed to enumerate rootfs")
Expand Down
Loading

0 comments on commit 7dd3050

Please sign in to comment.