Merge pull request #4512 from kunalkushwaha/prune-filter

image prune command fixed as per docker image prune.
This commit is contained in:
OpenShift Merge Robot
2019-11-22 21:56:12 +01:00
committed by GitHub
14 changed files with 383 additions and 23 deletions

4
API.md
View File

@ -95,7 +95,7 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[func ImageSave(options: ImageSaveOptions) MoreResponse](#ImageSave)
[func ImagesPrune(all: bool) []string](#ImagesPrune)
[func ImagesPrune(all: bool, filter: []string) []string](#ImagesPrune)
[func ImportImage(source: string, reference: string, message: string, changes: []string, delete: bool) string](#ImportImage)
@ -766,7 +766,7 @@ ImageSave allows you to save an image from the local image storage to a tarball
### <a name="ImagesPrune"></a>func ImagesPrune
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
method ImagesPrune(all: [bool](https://godoc.org/builtin#bool)) [[]string](#[]string)</div>
method ImagesPrune(all: [bool](https://godoc.org/builtin#bool), filter: [[]string](#[]string)) [[]string](#[]string)</div>
ImagesPrune removes all unused images from the local store. Upon successful pruning,
the IDs of the removed images are returned.
### <a name="ImportImage"></a>func ImportImage

View File

@ -175,7 +175,9 @@ type HistoryValues struct {
}
type PruneImagesValues struct {
PodmanCommand
All bool
All bool
Force bool
Filter []string
}
type PruneContainersValues struct {

View File

@ -1,7 +1,10 @@
package main
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/pkg/adapter"
@ -34,9 +37,24 @@ func init() {
pruneImagesCommand.SetUsageTemplate(UsageTemplate())
flags := pruneImagesCommand.Flags()
flags.BoolVarP(&pruneImagesCommand.All, "all", "a", false, "Remove all unused images, not just dangling ones")
flags.BoolVarP(&pruneImagesCommand.Force, "force", "f", false, "Do not prompt for confirmation")
flags.StringArrayVar(&pruneImagesCommand.Filter, "filter", []string{}, "Provide filter values (e.g. 'label=<key>=<value>')")
}
func pruneImagesCmd(c *cliconfig.PruneImagesValues) error {
if !c.Force {
reader := bufio.NewReader(os.Stdin)
fmt.Printf(`
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] `)
ans, err := reader.ReadString('\n')
if err != nil {
return errors.Wrapf(err, "error reading input")
}
if strings.ToLower(ans)[0] != 'y' {
return nil
}
}
runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand)
if err != nil {
return errors.Wrapf(err, "could not get runtime")
@ -45,7 +63,7 @@ func pruneImagesCmd(c *cliconfig.PruneImagesValues) error {
// Call prune; if any cids are returned, print them and then
// return err in case an error also came up
pruneCids, err := runtime.PruneImages(getContext(), c.All)
pruneCids, err := runtime.PruneImages(getContext(), c.All, c.Filter)
if len(pruneCids) > 0 {
for _, cid := range pruneCids {
fmt.Println(cid)

View File

@ -117,7 +117,8 @@ Are you sure you want to continue? [y/N] `, volumeString)
// Call prune; if any cids are returned, print them and then
// return err in case an error also came up
pruneCids, err := runtime.PruneImages(ctx, c.All)
// TODO: support for filters in system prune
pruneCids, err := runtime.PruneImages(ctx, c.All, []string{})
if len(pruneCids) > 0 {
fmt.Println("Deleted Images")
for _, cid := range pruneCids {

View File

@ -1217,7 +1217,7 @@ method UnmountContainer(name: string, force: bool) -> ()
# ImagesPrune removes all unused images from the local store. Upon successful pruning,
# the IDs of the removed images are returned.
method ImagesPrune(all: bool) -> (pruned: []string)
method ImagesPrune(all: bool, filter: []string) -> (pruned: []string)
# This function is not implemented yet.
# method ListContainerPorts(name: string) -> (notimplemented: NotImplemented)

View File

@ -25,13 +25,16 @@ Print usage statement
Remove all dangling images from local storage
```
$ sudo podman image prune
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
f3e20dc537fb04cb51672a5cb6fdf2292e61d411315549391a0d1f64e4e3097e
324a7a3b2e0135f4226ffdd473e4099fd9e477a74230cdc35de69e84c0f9d907
```
Remove all unused images from local storage
Remove all unused images from local storage without confirming
```
$ sudo podman image prune -a
$ sudo podman image prune -a -f
f3e20dc537fb04cb51672a5cb6fdf2292e61d411315549391a0d1f64e4e3097e
324a7a3b2e0135f4226ffdd473e4099fd9e477a74230cdc35de69e84c0f9d907
6125002719feb1ddf3030acab1df6156da7ce0e78e571e9b6e9c250424d6220c
@ -41,6 +44,40 @@ e4e5109420323221f170627c138817770fb64832da7d8fe2babd863148287fca
```
Remove all unused images from local storage since given time/hours.
```
$ sudo podman image prune -a --filter until=2019-11-14T06:15:42.937792374Z
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
e813d2135f17fadeffeea8159a34cfdd4c30b98d8111364b913a91fd930643e9
5e6572320437022e2746467ddf5b3561bf06e099e8e6361df27e0b2a7ed0b17b
58fda2abf5042b35dfe04e5f8ee458a3cc26375bf309efb42c078b551a2055c7
6d2bd30fe924d3414b64bd3920760617e6ced872364bc3bc6959a623252da002
33d1c829be64a1e1d379caf4feec1f05a892c3ef7aa82c0be53d3c08a96c59c5
f9f0a8a58c9e02a2b3250b88cc5c95b1e10245ca2c4161d19376580aaa90f55c
1ef14d5ede80db78978b25ad677fd3e897a578c3af614e1fda608d40c8809707
45e1482040e441a521953a6da2eca9bafc769e15667a07c23720d6e0cafc3ab2
$ sudo podman image prune -f --filter until=10h
f3e20dc537fb04cb51672a5cb6fdf2292e61d411315549391a0d1f64e4e3097e
324a7a3b2e0135f4226ffdd473e4099fd9e477a74230cdc35de69e84c0f9d907
```
Remove all unused images from local storage with label version 1.0
```
$ sudo podman image prune -a -f --filter label=version=1.0
e813d2135f17fadeffeea8159a34cfdd4c30b98d8111364b913a91fd930643e9
5e6572320437022e2746467ddf5b3561bf06e099e8e6361df27e0b2a7ed0b17b
58fda2abf5042b35dfe04e5f8ee458a3cc26375bf309efb42c078b551a2055c7
6d2bd30fe924d3414b64bd3920760617e6ced872364bc3bc6959a623252da002
33d1c829be64a1e1d379caf4feec1f05a892c3ef7aa82c0be53d3c08a96c59c5
f9f0a8a58c9e02a2b3250b88cc5c95b1e10245ca2c4161d19376580aaa90f55c
1ef14d5ede80db78978b25ad677fd3e897a578c3af614e1fda608d40c8809707
45e1482040e441a521953a6da2eca9bafc769e15667a07c23720d6e0cafc3ab2
```
## SEE ALSO
podman(1), podman-images

View File

@ -74,6 +74,11 @@ type InfoImage struct {
Layers []LayerInfo
}
// ImageFilter is a function to determine whether a image is included
// in command output. Images to be outputted are tested using the function.
// A true return will include the image, a false return will exclude it.
type ImageFilter func(*Image) bool //nolint
// ErrRepoTagNotFound is the error returned when the image id given doesn't match a rep tag in store
var ErrRepoTagNotFound = stderrors.New("unable to match user input to any specific repotag")

View File

@ -2,23 +2,78 @@ package image
import (
"context"
"strings"
"time"
"github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/pkg/timetype"
"github.com/containers/storage"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func generatePruneFilterFuncs(filter, filterValue string) (ImageFilter, error) {
switch filter {
case "label":
var filterArray = strings.SplitN(filterValue, "=", 2)
var filterKey = filterArray[0]
if len(filterArray) > 1 {
filterValue = filterArray[1]
} else {
filterValue = ""
}
return func(i *Image) bool {
labels, err := i.Labels(context.Background())
if err != nil {
return false
}
for labelKey, labelValue := range labels {
if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) {
return true
}
}
return false
}, nil
case "until":
ts, err := timetype.GetTimestamp(filterValue, time.Now())
if err != nil {
return nil, err
}
seconds, nanoseconds, err := timetype.ParseTimestamps(ts, 0)
if err != nil {
return nil, err
}
until := time.Unix(seconds, nanoseconds)
return func(i *Image) bool {
if !until.IsZero() && i.Created().After((until)) {
return true
}
return false
}, nil
}
return nil, nil
}
// GetPruneImages returns a slice of images that have no names/unused
func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) {
func (ir *Runtime) GetPruneImages(all bool, filterFuncs []ImageFilter) ([]*Image, error) {
var (
pruneImages []*Image
)
allImages, err := ir.GetRWImages()
if err != nil {
return nil, err
}
for _, i := range allImages {
// filter the images based on this.
for _, filterFunc := range filterFuncs {
if !filterFunc(i) {
continue
}
}
if len(i.Names()) == 0 {
pruneImages = append(pruneImages, i)
continue
@ -38,9 +93,25 @@ func (ir *Runtime) GetPruneImages(all bool) ([]*Image, error) {
// PruneImages prunes dangling and optionally all unused images from the local
// image store
func (ir *Runtime) PruneImages(ctx context.Context, all bool) ([]string, error) {
var prunedCids []string
pruneImages, err := ir.GetPruneImages(all)
func (ir *Runtime) PruneImages(ctx context.Context, all bool, filter []string) ([]string, error) {
var (
prunedCids []string
filterFuncs []ImageFilter
)
for _, f := range filter {
filterSplit := strings.SplitN(f, "=", 2)
if len(filterSplit) < 2 {
return nil, errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
}
generatedFunc, err := generatePruneFilterFuncs(filterSplit[0], filterSplit[1])
if err != nil {
return nil, errors.Wrapf(err, "invalid filter")
}
filterFuncs = append(filterFuncs, generatedFunc)
}
pruneImages, err := ir.GetPruneImages(all, filterFuncs)
if err != nil {
return nil, errors.Wrap(err, "unable to get images to prune")
}

View File

@ -27,7 +27,7 @@ import (
"github.com/containers/libpod/pkg/util"
"github.com/containers/storage/pkg/archive"
"github.com/pkg/errors"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
)
// LocalRuntime describes a typical libpod runtime
@ -147,8 +147,8 @@ func (r *LocalRuntime) RemoveImage(ctx context.Context, img *ContainerImage, for
}
// PruneImages is wrapper into PruneImages within the image pkg
func (r *LocalRuntime) PruneImages(ctx context.Context, all bool) ([]string, error) {
return r.ImageRuntime().PruneImages(ctx, all)
func (r *LocalRuntime) PruneImages(ctx context.Context, all bool, filter []string) ([]string, error) {
return r.ImageRuntime().PruneImages(ctx, all, filter)
}
// Export is a wrapper to container export to a tarfile

View File

@ -415,8 +415,8 @@ func (ci *ContainerImage) History(ctx context.Context) ([]*image.History, error)
}
// PruneImages is the wrapper call for a remote-client to prune images
func (r *LocalRuntime) PruneImages(ctx context.Context, all bool) ([]string, error) {
return iopodman.ImagesPrune().Call(r.Conn, all)
func (r *LocalRuntime) PruneImages(ctx context.Context, all bool, filter []string) ([]string, error) {
return iopodman.ImagesPrune().Call(r.Conn, all, filter)
}
// Export is a wrapper to container export to a tarfile

131
pkg/timetype/timestamp.go Normal file
View File

@ -0,0 +1,131 @@
package timetype
// code adapted from https://github.com/moby/moby/blob/master/api/types/time/timestamp.go
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)
// These are additional predefined layouts for use in Time.Format and Time.Parse
// with --since and --until parameters for `docker logs` and `docker events`
const (
rFC3339Local = "2006-01-02T15:04:05" // RFC3339 with local timezone
rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone
dateWithZone = "2006-01-02Z07:00" // RFC3339 with time at 00:00:00
dateLocal = "2006-01-02" // RFC3339 with local timezone and time at 00:00:00
)
// GetTimestamp tries to parse given string as golang duration,
// then RFC3339 time and finally as a Unix timestamp. If
// any of these were successful, it returns a Unix timestamp
// as string otherwise returns the given value back.
// In case of duration input, the returned timestamp is computed
// as the given reference time minus the amount of the duration.
func GetTimestamp(value string, reference time.Time) (string, error) {
if d, err := time.ParseDuration(value); value != "0" && err == nil {
return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil
}
var format string
// if the string has a Z or a + or three dashes use parse otherwise use parseinlocation
parseInLocation := !(strings.ContainsAny(value, "zZ+") || strings.Count(value, "-") == 3)
if strings.Contains(value, ".") {
if parseInLocation {
format = rFC3339NanoLocal
} else {
format = time.RFC3339Nano
}
} else if strings.Contains(value, "T") {
// we want the number of colons in the T portion of the timestamp
tcolons := strings.Count(value, ":")
// if parseInLocation is off and we have a +/- zone offset (not Z) then
// there will be an extra colon in the input for the tz offset subtract that
// colon from the tcolons count
if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 {
tcolons--
}
if parseInLocation {
switch tcolons {
case 0:
format = "2006-01-02T15"
case 1:
format = "2006-01-02T15:04"
default:
format = rFC3339Local
}
} else {
switch tcolons {
case 0:
format = "2006-01-02T15Z07:00"
case 1:
format = "2006-01-02T15:04Z07:00"
default:
format = time.RFC3339
}
}
} else if parseInLocation {
format = dateLocal
} else {
format = dateWithZone
}
var t time.Time
var err error
if parseInLocation {
t, err = time.ParseInLocation(format, value, time.FixedZone(reference.Zone()))
} else {
t, err = time.Parse(format, value)
}
if err != nil {
// if there is a `-` then it's an RFC3339 like timestamp
if strings.Contains(value, "-") {
return "", err // was probably an RFC3339 like timestamp but the parser failed with an error
}
if _, _, err := parseTimestamp(value); err != nil {
return "", fmt.Errorf("failed to parse value as time or duration: %q", value)
}
return value, nil // unix timestamp in and out case (meaning: the value passed at the command line is already in the right format for passing to the server)
}
return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
}
// ParseTimestamps returns seconds and nanoseconds from a timestamp that has the
// format "%d.%09d", time.Unix(), int64(time.Nanosecond()))
// if the incoming nanosecond portion is longer or shorter than 9 digits it is
// converted to nanoseconds. The expectation is that the seconds and
// seconds will be used to create a time variable. For example:
// seconds, nanoseconds, err := ParseTimestamp("1136073600.000000001",0)
// if err == nil since := time.Unix(seconds, nanoseconds)
// returns seconds as def(aultSeconds) if value == ""
func ParseTimestamps(value string, def int64) (int64, int64, error) {
if value == "" {
return def, 0, nil
}
return parseTimestamp(value)
}
func parseTimestamp(value string) (int64, int64, error) {
sa := strings.SplitN(value, ".", 2)
s, err := strconv.ParseInt(sa[0], 10, 64)
if err != nil {
return s, 0, err
}
if len(sa) != 2 {
return s, 0, nil
}
n, err := strconv.ParseInt(sa[1], 10, 64)
if err != nil {
return s, n, err
}
// should already be in nanoseconds but just in case convert n to nanoseconds
n = int64(float64(n) * math.Pow(float64(10), float64(9-len(sa[1]))))
return s, n, nil
}

View File

@ -0,0 +1,95 @@
package timetype
// code adapted from https://github.com/moby/moby/blob/master/api/types/time/timestamp.go
import (
"fmt"
"testing"
"time"
)
func TestGetTimestamp(t *testing.T) {
now := time.Now().In(time.UTC)
cases := []struct {
in, expected string
expectedErr bool
}{
// Partial RFC3339 strings get parsed with second precision
{"2006-01-02T15:04:05.999999999+07:00", "1136189045.999999999", false},
{"2006-01-02T15:04:05.999999999Z", "1136214245.999999999", false},
{"2006-01-02T15:04:05.999999999", "1136214245.999999999", false},
{"2006-01-02T15:04:05Z", "1136214245.000000000", false},
{"2006-01-02T15:04:05", "1136214245.000000000", false},
{"2006-01-02T15:04:0Z", "", true},
{"2006-01-02T15:04:0", "", true},
{"2006-01-02T15:04Z", "1136214240.000000000", false},
{"2006-01-02T15:04+00:00", "1136214240.000000000", false},
{"2006-01-02T15:04-00:00", "1136214240.000000000", false},
{"2006-01-02T15:04", "1136214240.000000000", false},
{"2006-01-02T15:0Z", "", true},
{"2006-01-02T15:0", "", true},
{"2006-01-02T15Z", "1136214000.000000000", false},
{"2006-01-02T15+00:00", "1136214000.000000000", false},
{"2006-01-02T15-00:00", "1136214000.000000000", false},
{"2006-01-02T15", "1136214000.000000000", false},
{"2006-01-02T1Z", "1136163600.000000000", false},
{"2006-01-02T1", "1136163600.000000000", false},
{"2006-01-02TZ", "", true},
{"2006-01-02T", "", true},
{"2006-01-02+00:00", "1136160000.000000000", false},
{"2006-01-02-00:00", "1136160000.000000000", false},
{"2006-01-02-00:01", "1136160060.000000000", false},
{"2006-01-02Z", "1136160000.000000000", false},
{"2006-01-02", "1136160000.000000000", false},
{"2015-05-13T20:39:09Z", "1431549549.000000000", false},
// unix timestamps returned as is
{"1136073600", "1136073600", false},
{"1136073600.000000001", "1136073600.000000001", false},
// Durations
{"1m", fmt.Sprintf("%d", now.Add(-1*time.Minute).Unix()), false},
{"1.5h", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
{"1h30m", fmt.Sprintf("%d", now.Add(-90*time.Minute).Unix()), false},
{"invalid", "", true},
{"", "", true},
}
for _, c := range cases {
o, err := GetTimestamp(c.in, now)
if o != c.expected ||
(err == nil && c.expectedErr) ||
(err != nil && !c.expectedErr) {
t.Errorf("wrong value for '%s'. expected:'%s' got:'%s' with error: `%s`", c.in, c.expected, o, err)
t.Fail()
}
}
}
func TestParseTimestamps(t *testing.T) {
cases := []struct {
in string
def, expectedS, expectedN int64
expectedErr bool
}{
// unix timestamps
{"1136073600", 0, 1136073600, 0, false},
{"1136073600.000000001", 0, 1136073600, 1, false},
{"1136073600.0000000010", 0, 1136073600, 1, false},
{"1136073600.00000001", 0, 1136073600, 10, false},
{"foo.bar", 0, 0, 0, true},
{"1136073600.bar", 0, 1136073600, 0, true},
{"", -1, -1, 0, false},
}
for _, c := range cases {
s, n, err := ParseTimestamps(c.in, c.def)
if s != c.expectedS ||
n != c.expectedN ||
(err == nil && c.expectedErr) ||
(err != nil && !c.expectedErr) {
t.Errorf("wrong values for input `%s` with default `%d` expected:'%d'seconds and `%d`nanosecond got:'%d'seconds and `%d`nanoseconds with error: `%s`", c.in, c.def, c.expectedS, c.expectedN, s, n, err)
t.Fail()
}
}
}

View File

@ -21,7 +21,7 @@ import (
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/cmd/podman/varlink"
iopodman "github.com/containers/libpod/cmd/podman/varlink"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/libpod/image"
@ -29,7 +29,7 @@ import (
"github.com/containers/libpod/pkg/util"
"github.com/containers/libpod/utils"
"github.com/containers/storage/pkg/archive"
"github.com/opencontainers/image-spec/specs-go/v1"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -740,8 +740,8 @@ func (i *LibpodAPI) ContainerRunlabel(call iopodman.VarlinkCall, input iopodman.
}
// ImagesPrune ....
func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall, all bool) error {
prunedImages, err := i.Runtime.ImageRuntime().PruneImages(context.TODO(), all)
func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall, all bool, filter []string) error {
prunedImages, err := i.Runtime.ImageRuntime().PruneImages(context.TODO(), all, []string{})
if err != nil {
return call.ReplyErrorOccurred(err.Error())
}

View File

@ -64,7 +64,7 @@ var _ = Describe("Podman prune", func() {
hasNone, _ := none.GrepString("<none>")
Expect(hasNone).To(BeTrue())
prune := podmanTest.Podman([]string{"image", "prune"})
prune := podmanTest.Podman([]string{"image", "prune", "-f"})
prune.WaitWithDefaultTimeout()
Expect(prune.ExitCode()).To(Equal(0))
@ -78,7 +78,7 @@ var _ = Describe("Podman prune", func() {
It("podman image prune unused images", func() {
podmanTest.RestoreAllArtifacts()
prune := podmanTest.PodmanNoCache([]string{"image", "prune", "-a"})
prune := podmanTest.PodmanNoCache([]string{"image", "prune", "-af"})
prune.WaitWithDefaultTimeout()
Expect(prune.ExitCode()).To(Equal(0))