Browse code

Add `--filter until=<timestamp>` for `docker container/image prune`

This fix is a follow up for comment
https://github.com/docker/docker/pull/28535#issuecomment-263215225

This fix provides `--filter until=<timestamp>` for `docker container/image prune`.

This fix adds `--filter until=<timestamp>` to `docker container/image prune`
so that it is possible to specify a timestamp and prune those containers/images
that are earlier than the timestamp.

Related docs has been updated

Several integration tests have been added to cover changes.

This fix fixes #28497.

This fix is related to #28535.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/12/08 07:02:13
Showing 16 changed files
... ...
@@ -312,7 +312,12 @@ func (n *networkRouter) postNetworksPrune(ctx context.Context, w http.ResponseWr
312 312
 		return err
313 313
 	}
314 314
 
315
-	pruneReport, err := n.backend.NetworksPrune(filters.Args{})
315
+	pruneFilters, err := filters.FromParam(r.Form.Get("filters"))
316
+	if err != nil {
317
+		return err
318
+	}
319
+
320
+	pruneReport, err := n.backend.NetworksPrune(pruneFilters)
316 321
 	if err != nil {
317 322
 		return err
318 323
 	}
... ...
@@ -4191,6 +4191,7 @@ paths:
4191 4191
             Filters to process on the prune list, encoded as JSON (a `map[string][]string`).
4192 4192
 
4193 4193
             Available filters:
4194
+            - `until=<timestamp>` Prune containers created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time.
4194 4195
           type: "string"
4195 4196
       responses:
4196 4197
         200:
... ...
@@ -4866,6 +4867,7 @@ paths:
4866 4866
             - `dangling=<boolean>` When set to `true` (or `1`), prune only
4867 4867
                unused *and* untagged images. When set to `false`
4868 4868
                (or `0`), all unused images are pruned.
4869
+            - `until=<string>` Prune images created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time.
4869 4870
           type: "string"
4870 4871
       responses:
4871 4872
         200:
... ...
@@ -6295,8 +6297,6 @@ paths:
6295 6295
   /networks/prune:
6296 6296
     post:
6297 6297
       summary: "Delete unused networks"
6298
-      consumes:
6299
-        - "application/json"
6300 6298
       produces:
6301 6299
         - "application/json"
6302 6300
       operationId: "NetworkPrune"
... ...
@@ -6307,6 +6307,7 @@ paths:
6307 6307
             Filters to process on the prune list, encoded as JSON (a `map[string][]string`).
6308 6308
 
6309 6309
             Available filters:
6310
+            - `until=<timestamp>` Prune networks created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time.
6310 6311
           type: "string"
6311 6312
       responses:
6312 6313
         200:
... ...
@@ -3,21 +3,22 @@ package container
3 3
 import (
4 4
 	"fmt"
5 5
 
6
-	"github.com/docker/docker/api/types/filters"
7 6
 	"github.com/docker/docker/cli"
8 7
 	"github.com/docker/docker/cli/command"
8
+	"github.com/docker/docker/opts"
9 9
 	units "github.com/docker/go-units"
10 10
 	"github.com/spf13/cobra"
11 11
 	"golang.org/x/net/context"
12 12
 )
13 13
 
14 14
 type pruneOptions struct {
15
-	force bool
15
+	force  bool
16
+	filter opts.FilterOpt
16 17
 }
17 18
 
18 19
 // NewPruneCommand returns a new cobra prune command for containers
19 20
 func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
20
-	var opts pruneOptions
21
+	opts := pruneOptions{filter: opts.NewFilterOpt()}
21 22
 
22 23
 	cmd := &cobra.Command{
23 24
 		Use:   "prune [OPTIONS]",
... ...
@@ -39,6 +40,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
39 39
 
40 40
 	flags := cmd.Flags()
41 41
 	flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation")
42
+	flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')")
42 43
 
43 44
 	return cmd
44 45
 }
... ...
@@ -47,11 +49,13 @@ const warning = `WARNING! This will remove all stopped containers.
47 47
 Are you sure you want to continue?`
48 48
 
49 49
 func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
50
+	pruneFilters := opts.filter.Value()
51
+
50 52
 	if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
51 53
 		return
52 54
 	}
53 55
 
54
-	report, err := dockerCli.Client().ContainersPrune(context.Background(), filters.Args{})
56
+	report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters)
55 57
 	if err != nil {
56 58
 		return
57 59
 	}
... ...
@@ -69,6 +73,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u
69 69
 
70 70
 // RunPrune calls the Container Prune API
71 71
 // This returns the amount of space reclaimed and a detailed output string
72
-func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) {
73
-	return runPrune(dockerCli, pruneOptions{force: true})
72
+func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
73
+	return runPrune(dockerCli, pruneOptions{force: true, filter: filter})
74 74
 }
... ...
@@ -5,21 +5,22 @@ import (
5 5
 
6 6
 	"golang.org/x/net/context"
7 7
 
8
-	"github.com/docker/docker/api/types/filters"
9 8
 	"github.com/docker/docker/cli"
10 9
 	"github.com/docker/docker/cli/command"
10
+	"github.com/docker/docker/opts"
11 11
 	units "github.com/docker/go-units"
12 12
 	"github.com/spf13/cobra"
13 13
 )
14 14
 
15 15
 type pruneOptions struct {
16
-	force bool
17
-	all   bool
16
+	force  bool
17
+	all    bool
18
+	filter opts.FilterOpt
18 19
 }
19 20
 
20 21
 // NewPruneCommand returns a new cobra prune command for images
21 22
 func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
22
-	var opts pruneOptions
23
+	opts := pruneOptions{filter: opts.NewFilterOpt()}
23 24
 
24 25
 	cmd := &cobra.Command{
25 26
 		Use:   "prune [OPTIONS]",
... ...
@@ -42,6 +43,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
42 42
 	flags := cmd.Flags()
43 43
 	flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation")
44 44
 	flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones")
45
+	flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')")
45 46
 
46 47
 	return cmd
47 48
 }
... ...
@@ -54,7 +56,7 @@ Are you sure you want to continue?`
54 54
 )
55 55
 
56 56
 func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
57
-	pruneFilters := filters.NewArgs()
57
+	pruneFilters := opts.filter.Value()
58 58
 	pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all))
59 59
 
60 60
 	warning := danglingWarning
... ...
@@ -87,6 +89,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u
87 87
 
88 88
 // RunPrune calls the Image Prune API
89 89
 // This returns the amount of space reclaimed and a detailed output string
90
-func RunPrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) {
91
-	return runPrune(dockerCli, pruneOptions{force: true, all: all})
90
+func RunPrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) {
91
+	return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
92 92
 }
... ...
@@ -5,19 +5,20 @@ import (
5 5
 
6 6
 	"golang.org/x/net/context"
7 7
 
8
-	"github.com/docker/docker/api/types/filters"
9 8
 	"github.com/docker/docker/cli"
10 9
 	"github.com/docker/docker/cli/command"
10
+	"github.com/docker/docker/opts"
11 11
 	"github.com/spf13/cobra"
12 12
 )
13 13
 
14 14
 type pruneOptions struct {
15
-	force bool
15
+	force  bool
16
+	filter opts.FilterOpt
16 17
 }
17 18
 
18 19
 // NewPruneCommand returns a new cobra prune command for networks
19 20
 func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
20
-	var opts pruneOptions
21
+	opts := pruneOptions{filter: opts.NewFilterOpt()}
21 22
 
22 23
 	cmd := &cobra.Command{
23 24
 		Use:   "prune [OPTIONS]",
... ...
@@ -38,6 +39,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
38 38
 
39 39
 	flags := cmd.Flags()
40 40
 	flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation")
41
+	flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')")
41 42
 
42 43
 	return cmd
43 44
 }
... ...
@@ -46,11 +48,13 @@ const warning = `WARNING! This will remove all networks not used by at least one
46 46
 Are you sure you want to continue?`
47 47
 
48 48
 func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) {
49
+	pruneFilters := opts.filter.Value()
50
+
49 51
 	if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
50 52
 		return
51 53
 	}
52 54
 
53
-	report, err := dockerCli.Client().NetworksPrune(context.Background(), filters.Args{})
55
+	report, err := dockerCli.Client().NetworksPrune(context.Background(), pruneFilters)
54 56
 	if err != nil {
55 57
 		return
56 58
 	}
... ...
@@ -67,7 +71,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, e
67 67
 
68 68
 // RunPrune calls the Network Prune API
69 69
 // This returns the amount of space reclaimed and a detailed output string
70
-func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) {
71
-	output, err := runPrune(dockerCli, pruneOptions{force: true})
70
+func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
71
+	output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter})
72 72
 	return 0, output, err
73 73
 }
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"github.com/docker/docker/cli/command/image"
7 7
 	"github.com/docker/docker/cli/command/network"
8 8
 	"github.com/docker/docker/cli/command/volume"
9
+	"github.com/docker/docker/opts"
9 10
 	"github.com/spf13/cobra"
10 11
 )
11 12
 
... ...
@@ -30,21 +31,21 @@ func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
30 30
 }
31 31
 
32 32
 // RunContainerPrune executes a prune command for containers
33
-func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) {
34
-	return container.RunPrune(dockerCli)
33
+func RunContainerPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
34
+	return container.RunPrune(dockerCli, filter)
35 35
 }
36 36
 
37 37
 // RunVolumePrune executes a prune command for volumes
38
-func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) {
38
+func RunVolumePrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
39 39
 	return volume.RunPrune(dockerCli)
40 40
 }
41 41
 
42 42
 // RunImagePrune executes a prune command for images
43
-func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) {
44
-	return image.RunPrune(dockerCli, all)
43
+func RunImagePrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) {
44
+	return image.RunPrune(dockerCli, all, filter)
45 45
 }
46 46
 
47 47
 // RunNetworkPrune executes a prune command for networks
48
-func RunNetworkPrune(dockerCli *command.DockerCli) (uint64, string, error) {
49
-	return network.RunPrune(dockerCli)
48
+func RunNetworkPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
49
+	return network.RunPrune(dockerCli, filter)
50 50
 }
... ...
@@ -6,18 +6,20 @@ import (
6 6
 	"github.com/docker/docker/cli"
7 7
 	"github.com/docker/docker/cli/command"
8 8
 	"github.com/docker/docker/cli/command/prune"
9
+	"github.com/docker/docker/opts"
9 10
 	units "github.com/docker/go-units"
10 11
 	"github.com/spf13/cobra"
11 12
 )
12 13
 
13 14
 type pruneOptions struct {
14
-	force bool
15
-	all   bool
15
+	force  bool
16
+	all    bool
17
+	filter opts.FilterOpt
16 18
 }
17 19
 
18 20
 // NewPruneCommand creates a new cobra.Command for `docker prune`
19 21
 func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
20
-	var opts pruneOptions
22
+	opts := pruneOptions{filter: opts.NewFilterOpt()}
21 23
 
22 24
 	cmd := &cobra.Command{
23 25
 		Use:   "prune [OPTIONS]",
... ...
@@ -32,6 +34,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
32 32
 	flags := cmd.Flags()
33 33
 	flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation")
34 34
 	flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones")
35
+	flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')")
35 36
 
36 37
 	return cmd
37 38
 }
... ...
@@ -48,27 +51,27 @@ Are you sure you want to continue?`
48 48
 	allImageDesc      = `- all images without at least one container associated to them`
49 49
 )
50 50
 
51
-func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error {
51
+func runPrune(dockerCli *command.DockerCli, options pruneOptions) error {
52 52
 	var message string
53 53
 
54
-	if opts.all {
54
+	if options.all {
55 55
 		message = fmt.Sprintf(warning, allImageDesc)
56 56
 	} else {
57 57
 		message = fmt.Sprintf(warning, danglingImageDesc)
58 58
 	}
59 59
 
60
-	if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) {
60
+	if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) {
61 61
 		return nil
62 62
 	}
63 63
 
64 64
 	var spaceReclaimed uint64
65 65
 
66
-	for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){
66
+	for _, pruneFn := range []func(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error){
67 67
 		prune.RunContainerPrune,
68 68
 		prune.RunVolumePrune,
69 69
 		prune.RunNetworkPrune,
70 70
 	} {
71
-		spc, output, err := pruneFn(dockerCli)
71
+		spc, output, err := pruneFn(dockerCli, options.filter)
72 72
 		if err != nil {
73 73
 			return err
74 74
 		}
... ...
@@ -78,7 +81,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error {
78 78
 		}
79 79
 	}
80 80
 
81
-	spc, output, err := prune.RunImagePrune(dockerCli, opts.all)
81
+	spc, output, err := prune.RunImagePrune(dockerCli, options.all, options.filter)
82 82
 	if err != nil {
83 83
 		return err
84 84
 	}
85 85
new file mode 100644
... ...
@@ -0,0 +1,111 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/filters"
13
+	"github.com/docker/docker/pkg/testutil/assert"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestContainersPruneError(t *testing.T) {
18
+	client := &Client{
19
+		client:  newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
20
+		version: "1.25",
21
+	}
22
+
23
+	filters := filters.NewArgs()
24
+
25
+	_, err := client.ContainersPrune(context.Background(), filters)
26
+	assert.Error(t, err, "Error response from daemon: Server error")
27
+}
28
+
29
+func TestContainersPrune(t *testing.T) {
30
+	expectedURL := "/v1.25/containers/prune"
31
+
32
+	danglingFilters := filters.NewArgs()
33
+	danglingFilters.Add("dangling", "true")
34
+
35
+	noDanglingFilters := filters.NewArgs()
36
+	noDanglingFilters.Add("dangling", "false")
37
+
38
+	danglingUntilFilters := filters.NewArgs()
39
+	danglingUntilFilters.Add("dangling", "true")
40
+	danglingUntilFilters.Add("until", "2016-12-15T14:00")
41
+
42
+	listCases := []struct {
43
+		filters             filters.Args
44
+		expectedQueryParams map[string]string
45
+	}{
46
+		{
47
+			filters: filters.Args{},
48
+			expectedQueryParams: map[string]string{
49
+				"until":   "",
50
+				"filter":  "",
51
+				"filters": "",
52
+			},
53
+		},
54
+		{
55
+			filters: danglingFilters,
56
+			expectedQueryParams: map[string]string{
57
+				"until":   "",
58
+				"filter":  "",
59
+				"filters": `{"dangling":{"true":true}}`,
60
+			},
61
+		},
62
+		{
63
+			filters: danglingUntilFilters,
64
+			expectedQueryParams: map[string]string{
65
+				"until":   "",
66
+				"filter":  "",
67
+				"filters": `{"dangling":{"true":true},"until":{"2016-12-15T14:00":true}}`,
68
+			},
69
+		},
70
+		{
71
+			filters: noDanglingFilters,
72
+			expectedQueryParams: map[string]string{
73
+				"until":   "",
74
+				"filter":  "",
75
+				"filters": `{"dangling":{"false":true}}`,
76
+			},
77
+		},
78
+	}
79
+	for _, listCase := range listCases {
80
+		client := &Client{
81
+			client: newMockClient(func(req *http.Request) (*http.Response, error) {
82
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
83
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
84
+				}
85
+				query := req.URL.Query()
86
+				for key, expected := range listCase.expectedQueryParams {
87
+					actual := query.Get(key)
88
+					assert.Equal(t, actual, expected)
89
+				}
90
+				content, err := json.Marshal(types.ContainersPruneReport{
91
+					ContainersDeleted: []string{"container_id1", "container_id2"},
92
+					SpaceReclaimed:    9999,
93
+				})
94
+				if err != nil {
95
+					return nil, err
96
+				}
97
+				return &http.Response{
98
+					StatusCode: http.StatusOK,
99
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
100
+				}, nil
101
+			}),
102
+			version: "1.25",
103
+		}
104
+
105
+		report, err := client.ContainersPrune(context.Background(), listCase.filters)
106
+		assert.NilError(t, err)
107
+		assert.Equal(t, len(report.ContainersDeleted), 2)
108
+		assert.Equal(t, report.SpaceReclaimed, uint64(9999))
109
+	}
110
+}
0 111
new file mode 100644
... ...
@@ -0,0 +1,106 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/filters"
13
+	"github.com/docker/docker/pkg/testutil/assert"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestImagesPruneError(t *testing.T) {
18
+	client := &Client{
19
+		client:  newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
20
+		version: "1.25",
21
+	}
22
+
23
+	filters := filters.NewArgs()
24
+
25
+	_, err := client.ImagesPrune(context.Background(), filters)
26
+	assert.Error(t, err, "Error response from daemon: Server error")
27
+}
28
+
29
+func TestImagesPrune(t *testing.T) {
30
+	expectedURL := "/v1.25/images/prune"
31
+
32
+	danglingFilters := filters.NewArgs()
33
+	danglingFilters.Add("dangling", "true")
34
+
35
+	noDanglingFilters := filters.NewArgs()
36
+	noDanglingFilters.Add("dangling", "false")
37
+
38
+	listCases := []struct {
39
+		filters             filters.Args
40
+		expectedQueryParams map[string]string
41
+	}{
42
+		{
43
+			filters: filters.Args{},
44
+			expectedQueryParams: map[string]string{
45
+				"until":   "",
46
+				"filter":  "",
47
+				"filters": "",
48
+			},
49
+		},
50
+		{
51
+			filters: danglingFilters,
52
+			expectedQueryParams: map[string]string{
53
+				"until":   "",
54
+				"filter":  "",
55
+				"filters": `{"dangling":{"true":true}}`,
56
+			},
57
+		},
58
+		{
59
+			filters: noDanglingFilters,
60
+			expectedQueryParams: map[string]string{
61
+				"until":   "",
62
+				"filter":  "",
63
+				"filters": `{"dangling":{"false":true}}`,
64
+			},
65
+		},
66
+	}
67
+	for _, listCase := range listCases {
68
+		client := &Client{
69
+			client: newMockClient(func(req *http.Request) (*http.Response, error) {
70
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
71
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
72
+				}
73
+				query := req.URL.Query()
74
+				for key, expected := range listCase.expectedQueryParams {
75
+					actual := query.Get(key)
76
+					assert.Equal(t, actual, expected)
77
+				}
78
+				content, err := json.Marshal(types.ImagesPruneReport{
79
+					ImagesDeleted: []types.ImageDelete{
80
+						{
81
+							Deleted: "image_id1",
82
+						},
83
+						{
84
+							Deleted: "image_id2",
85
+						},
86
+					},
87
+					SpaceReclaimed: 9999,
88
+				})
89
+				if err != nil {
90
+					return nil, err
91
+				}
92
+				return &http.Response{
93
+					StatusCode: http.StatusOK,
94
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
95
+				}, nil
96
+			}),
97
+			version: "1.25",
98
+		}
99
+
100
+		report, err := client.ImagesPrune(context.Background(), listCase.filters)
101
+		assert.NilError(t, err)
102
+		assert.Equal(t, len(report.ImagesDeleted), 2)
103
+		assert.Equal(t, report.SpaceReclaimed, uint64(9999))
104
+	}
105
+}
0 106
new file mode 100644
... ...
@@ -0,0 +1,99 @@
0
+package client
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/api/types/filters"
13
+	"github.com/docker/docker/pkg/testutil/assert"
14
+	"golang.org/x/net/context"
15
+)
16
+
17
+func TestNetworksPruneError(t *testing.T) {
18
+	client := &Client{
19
+		client:  newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
20
+		version: "1.25",
21
+	}
22
+
23
+	filters := filters.NewArgs()
24
+
25
+	_, err := client.NetworksPrune(context.Background(), filters)
26
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
27
+		t.Fatalf("expected a Server Error, got %v", err)
28
+	}
29
+}
30
+
31
+func TestNetworksPrune(t *testing.T) {
32
+	expectedURL := "/v1.25/networks/prune"
33
+
34
+	danglingFilters := filters.NewArgs()
35
+	danglingFilters.Add("dangling", "true")
36
+
37
+	noDanglingFilters := filters.NewArgs()
38
+	noDanglingFilters.Add("dangling", "false")
39
+
40
+	listCases := []struct {
41
+		filters             filters.Args
42
+		expectedQueryParams map[string]string
43
+	}{
44
+		{
45
+			filters: filters.Args{},
46
+			expectedQueryParams: map[string]string{
47
+				"until":   "",
48
+				"filter":  "",
49
+				"filters": "",
50
+			},
51
+		},
52
+		{
53
+			filters: danglingFilters,
54
+			expectedQueryParams: map[string]string{
55
+				"until":   "",
56
+				"filter":  "",
57
+				"filters": `{"dangling":{"true":true}}`,
58
+			},
59
+		},
60
+		{
61
+			filters: noDanglingFilters,
62
+			expectedQueryParams: map[string]string{
63
+				"until":   "",
64
+				"filter":  "",
65
+				"filters": `{"dangling":{"false":true}}`,
66
+			},
67
+		},
68
+	}
69
+	for _, listCase := range listCases {
70
+		client := &Client{
71
+			client: newMockClient(func(req *http.Request) (*http.Response, error) {
72
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
73
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
74
+				}
75
+				query := req.URL.Query()
76
+				for key, expected := range listCase.expectedQueryParams {
77
+					actual := query.Get(key)
78
+					assert.Equal(t, actual, expected)
79
+				}
80
+				content, err := json.Marshal(types.NetworksPruneReport{
81
+					NetworksDeleted: []string{"network_id1", "network_id2"},
82
+				})
83
+				if err != nil {
84
+					return nil, err
85
+				}
86
+				return &http.Response{
87
+					StatusCode: http.StatusOK,
88
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
89
+				}, nil
90
+			}),
91
+			version: "1.25",
92
+		}
93
+
94
+		report, err := client.NetworksPrune(context.Background(), listCase.filters)
95
+		assert.NilError(t, err)
96
+		assert.Equal(t, len(report.NetworksDeleted), 2)
97
+	}
98
+}
... ...
@@ -3,11 +3,13 @@ package daemon
3 3
 import (
4 4
 	"fmt"
5 5
 	"regexp"
6
+	"time"
6 7
 
7 8
 	"github.com/Sirupsen/logrus"
8 9
 	"github.com/docker/distribution/digest"
9 10
 	"github.com/docker/docker/api/types"
10 11
 	"github.com/docker/docker/api/types/filters"
12
+	timetypes "github.com/docker/docker/api/types/time"
11 13
 	"github.com/docker/docker/image"
12 14
 	"github.com/docker/docker/layer"
13 15
 	"github.com/docker/docker/pkg/directory"
... ...
@@ -21,9 +23,17 @@ import (
21 21
 func (daemon *Daemon) ContainersPrune(pruneFilters filters.Args) (*types.ContainersPruneReport, error) {
22 22
 	rep := &types.ContainersPruneReport{}
23 23
 
24
+	until, err := getUntilFromPruneFilters(pruneFilters)
25
+	if err != nil {
26
+		return nil, err
27
+	}
28
+
24 29
 	allContainers := daemon.List()
25 30
 	for _, c := range allContainers {
26 31
 		if !c.IsRunning() {
32
+			if !until.IsZero() && c.Created.After(until) {
33
+				continue
34
+			}
27 35
 			cSize, _ := daemon.getSize(c)
28 36
 			// TODO: sets RmLink to true?
29 37
 			err := daemon.ContainerRm(c.ID, &types.ContainerRmConfig{})
... ...
@@ -84,6 +94,11 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune
84 84
 		}
85 85
 	}
86 86
 
87
+	until, err := getUntilFromPruneFilters(pruneFilters)
88
+	if err != nil {
89
+		return nil, err
90
+	}
91
+
87 92
 	var allImages map[image.ID]*image.Image
88 93
 	if danglingOnly {
89 94
 		allImages = daemon.imageStore.Heads()
... ...
@@ -104,6 +119,9 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune
104 104
 		if len(daemon.referenceStore.References(dgst)) == 0 && len(daemon.imageStore.Children(id)) != 0 {
105 105
 			continue
106 106
 		}
107
+		if !until.IsZero() && img.Created.After(until) {
108
+			continue
109
+		}
107 110
 		topImages[id] = img
108 111
 	}
109 112
 
... ...
@@ -169,9 +187,17 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune
169 169
 // localNetworksPrune removes unused local networks
170 170
 func (daemon *Daemon) localNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
171 171
 	rep := &types.NetworksPruneReport{}
172
-	var err error
172
+
173
+	until, err := getUntilFromPruneFilters(pruneFilters)
174
+	if err != nil {
175
+		return rep, err
176
+	}
177
+
173 178
 	// When the function returns true, the walk will stop.
174 179
 	l := func(nw libnetwork.Network) bool {
180
+		if !until.IsZero() && nw.Info().Created().After(until) {
181
+			return false
182
+		}
175 183
 		nwName := nw.Name()
176 184
 		predefined := runconfig.IsPreDefinedNetwork(nwName)
177 185
 		if !predefined && len(nw.Endpoints()) == 0 {
... ...
@@ -190,6 +216,12 @@ func (daemon *Daemon) localNetworksPrune(pruneFilters filters.Args) (*types.Netw
190 190
 // clusterNetworksPrune removes unused cluster networks
191 191
 func (daemon *Daemon) clusterNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
192 192
 	rep := &types.NetworksPruneReport{}
193
+
194
+	until, err := getUntilFromPruneFilters(pruneFilters)
195
+	if err != nil {
196
+		return nil, err
197
+	}
198
+
193 199
 	cluster := daemon.GetCluster()
194 200
 	networks, err := cluster.GetNetworks()
195 201
 	if err != nil {
... ...
@@ -200,6 +232,9 @@ func (daemon *Daemon) clusterNetworksPrune(pruneFilters filters.Args) (*types.Ne
200 200
 		if nw.Name == "ingress" {
201 201
 			continue
202 202
 		}
203
+		if !until.IsZero() && nw.Created.After(until) {
204
+			continue
205
+		}
203 206
 		// https://github.com/docker/docker/issues/24186
204 207
 		// `docker network inspect` unfortunately displays ONLY those containers that are local to that node.
205 208
 		// So we try to remove it anyway and check the error
... ...
@@ -234,3 +269,24 @@ func (daemon *Daemon) NetworksPrune(pruneFilters filters.Args) (*types.NetworksP
234 234
 	}
235 235
 	return rep, err
236 236
 }
237
+
238
+func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) {
239
+	until := time.Time{}
240
+	if !pruneFilters.Include("until") {
241
+		return until, nil
242
+	}
243
+	untilFilters := pruneFilters.Get("until")
244
+	if len(untilFilters) > 1 {
245
+		return until, fmt.Errorf("more than one until filter specified")
246
+	}
247
+	ts, err := timetypes.GetTimestamp(untilFilters[0], time.Now())
248
+	if err != nil {
249
+		return until, err
250
+	}
251
+	seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0)
252
+	if err != nil {
253
+		return until, err
254
+	}
255
+	until = time.Unix(seconds, nanoseconds)
256
+	return until, nil
257
+}
... ...
@@ -21,8 +21,10 @@ Usage:	docker container prune [OPTIONS]
21 21
 Remove all stopped containers
22 22
 
23 23
 Options:
24
-  -f, --force   Do not prompt for confirmation
25
-      --help    Print usage
24
+Options:
25
+      --filter filter   Provide filter values (e.g. 'until=<timestamp>')
26
+  -f, --force           Do not prompt for confirmation
27
+      --help            Print usage
26 28
 ```
27 29
 
28 30
 ## Examples
... ...
@@ -38,6 +40,63 @@ f98f9c2aa1eaf727e4ec9c0283bc7d4aa4762fbdba7f26191f26c97f64090360
38 38
 Total reclaimed space: 212 B
39 39
 ```
40 40
 
41
+## Filtering
42
+
43
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
44
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`)
45
+
46
+The currently supported filters are:
47
+
48
+* until (`<timestamp>`) - only remove containers created before given timestamp
49
+
50
+The `until` filter can be Unix timestamps, date formatted
51
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed
52
+relative to the daemon machine’s time. Supported formats for date
53
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`,
54
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local
55
+timezone on the daemon will be used if you do not provide either a `Z` or a
56
+`+-00:00` timezone offset at the end of the timestamp.  When providing Unix
57
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds
58
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap
59
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a
60
+fraction of a second no more than nine digits long.
61
+
62
+The following removes containers created more than 5 minutes ago:
63
+```bash
64
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
65
+CONTAINER ID        IMAGE               COMMAND             CREATED AT                      STATUS
66
+61b9efa71024        busybox             "sh"                2017-01-04 13:23:33 -0800 PST   Exited (0) 41 seconds ago
67
+53a9bc23a516        busybox             "sh"                2017-01-04 13:11:59 -0800 PST   Exited (0) 12 minutes ago
68
+
69
+$ docker container prune --force --filter "until=5m"
70
+Deleted Containers:
71
+53a9bc23a5168b6caa2bfbefddf1b30f93c7ad57f3dec271fd32707497cb9369
72
+
73
+Total reclaimed space: 25 B
74
+
75
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
76
+CONTAINER ID        IMAGE               COMMAND             CREATED AT                      STATUS
77
+61b9efa71024        busybox             "sh"                2017-01-04 13:23:33 -0800 PST   Exited (0) 44 seconds ago
78
+```
79
+
80
+The following removes containers created before `2017-01-04T13:10:00`:
81
+```bash
82
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
83
+CONTAINER ID        IMAGE               COMMAND             CREATED AT                      STATUS
84
+53a9bc23a516        busybox             "sh"                2017-01-04 13:11:59 -0800 PST   Exited (0) 7 minutes ago
85
+4a75091a6d61        busybox             "sh"                2017-01-04 13:09:53 -0800 PST   Exited (0) 9 minutes ago
86
+
87
+$ docker container prune --force --filter "until=2017-01-04T13:10:00"
88
+Deleted Containers:
89
+4a75091a6d618526fcd8b33ccd6e5928ca2a64415466f768a6180004b0c72c6c
90
+
91
+Total reclaimed space: 27 B
92
+
93
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
94
+CONTAINER ID        IMAGE               COMMAND             CREATED AT                      STATUS
95
+53a9bc23a516        busybox             "sh"                2017-01-04 13:11:59 -0800 PST   Exited (0) 9 minutes ago
96
+```
97
+
41 98
 ## Related information
42 99
 
43 100
 * [system df](system_df.md)
... ...
@@ -21,9 +21,10 @@ Usage:	docker image prune [OPTIONS]
21 21
 Remove unused images
22 22
 
23 23
 Options:
24
-  -a, --all     Remove all unused images, not just dangling ones
25
-  -f, --force   Do not prompt for confirmation
26
-      --help    Print usage
24
+  -a, --all             Remove all unused images, not just dangling ones
25
+      --filter filter   Provide filter values (e.g. 'until=<timestamp>')
26
+  -f, --force           Do not prompt for confirmation
27
+      --help            Print usage
27 28
 ```
28 29
 
29 30
 Remove all dangling images. If `-a` is specified, will also remove all images not referenced by any container.
... ...
@@ -62,6 +63,87 @@ deleted: sha256:2c675ee9ed53425e31a13e3390bf3f539bf8637000e4bcfbb85ee03ef4d910a1
62 62
 Total reclaimed space: 16.43 MB
63 63
 ```
64 64
 
65
+## Filtering
66
+
67
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
68
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`)
69
+
70
+The currently supported filters are:
71
+
72
+* until (`<timestamp>`) - only remove images created before given timestamp
73
+
74
+The `until` filter can be Unix timestamps, date formatted
75
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed
76
+relative to the daemon machine’s time. Supported formats for date
77
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`,
78
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local
79
+timezone on the daemon will be used if you do not provide either a `Z` or a
80
+`+-00:00` timezone offset at the end of the timestamp.  When providing Unix
81
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds
82
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap
83
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a
84
+fraction of a second no more than nine digits long.
85
+
86
+The following removes images created before `2017-01-04T00:00:00`:
87
+```bash
88
+$ docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}'
89
+REPOSITORY          TAG                 IMAGE ID            CREATED AT                      SIZE
90
+foo                 latest              2f287ac753da        2017-01-04 13:42:23 -0800 PST   3.98 MB
91
+alpine              latest              88e169ea8f46        2016-12-27 10:17:25 -0800 PST   3.98 MB
92
+busybox             latest              e02e811dd08f        2016-10-07 14:03:58 -0700 PDT   1.09 MB
93
+
94
+$ docker image prune -a --force --filter "until=2017-01-04T00:00:00"
95
+Deleted Images:
96
+untagged: alpine:latest
97
+untagged: alpine@sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8
98
+untagged: busybox:latest
99
+untagged: busybox@sha256:29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912
100
+deleted: sha256:e02e811dd08fd49e7f6032625495118e63f597eb150403d02e3238af1df240ba
101
+deleted: sha256:e88b3f82283bc59d5e0df427c824e9f95557e661fcb0ea15fb0fb6f97760f9d9
102
+
103
+Total reclaimed space: 1.093 MB
104
+
105
+$ docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}'
106
+REPOSITORY          TAG                 IMAGE ID            CREATED AT                      SIZE
107
+foo                 latest              2f287ac753da        2017-01-04 13:42:23 -0800 PST   3.98 MB
108
+```
109
+
110
+The following removes images created more than 10 days (`240h`) ago:
111
+```bash
112
+$ docker images
113
+REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
114
+foo                 latest              2f287ac753da        14 seconds ago      3.98 MB
115
+alpine              latest              88e169ea8f46        8 days ago          3.98 MB
116
+debian              jessie              7b0a06c805e8        2 months ago        123 MB
117
+busybox             latest              e02e811dd08f        2 months ago        1.09 MB
118
+golang              1.7.0               138c2e655421        4 months ago        670 MB
119
+
120
+$ docker image prune -a --force --filter "until=240h"
121
+Deleted Images:
122
+untagged: golang:1.7.0
123
+untagged: golang@sha256:6765038c2b8f407fd6e3ecea043b44580c229ccfa2a13f6d85866cf2b4a9628e
124
+deleted: sha256:138c2e6554219de65614d88c15521bfb2da674cbb0bf840de161f89ff4264b96
125
+deleted: sha256:ec353c2e1a673f456c4b78906d0d77f9d9456cfb5229b78c6a960bfb7496b76a
126
+deleted: sha256:fe22765feaf3907526b4921c73ea6643ff9e334497c9b7e177972cf22f68ee93
127
+deleted: sha256:ff845959c80148421a5c3ae11cc0e6c115f950c89bc949646be55ed18d6a2912
128
+deleted: sha256:a4320831346648c03db64149eafc83092e2b34ab50ca6e8c13112388f25899a7
129
+deleted: sha256:4c76020202ee1d9709e703b7c6de367b325139e74eebd6b55b30a63c196abaf3
130
+deleted: sha256:d7afd92fb07236c8a2045715a86b7d5f0066cef025018cd3ca9a45498c51d1d6
131
+deleted: sha256:9e63c5bce4585dd7038d830a1f1f4e44cb1a1515b00e620ac718e934b484c938
132
+untagged: debian:jessie
133
+untagged: debian@sha256:c1af755d300d0c65bb1194d24bce561d70c98a54fb5ce5b1693beb4f7988272f
134
+deleted: sha256:7b0a06c805e8f23807fb8856621c60851727e85c7bcb751012c813f122734c8d
135
+deleted: sha256:f96222d75c5563900bc4dd852179b720a0885de8f7a0619ba0ac76e92542bbc8
136
+
137
+Total reclaimed space: 792.6 MB
138
+
139
+$ docker images
140
+REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
141
+foo                 latest              2f287ac753da        About a minute ago   3.98 MB
142
+alpine              latest              88e169ea8f46        8 days ago           3.98 MB
143
+busybox             latest              e02e811dd08f        2 months ago         1.09 MB
144
+```
145
+
65 146
 ## Related information
66 147
 
67 148
 * [system df](system_df.md)
... ...
@@ -12,8 +12,9 @@ Usage:	docker network prune [OPTIONS]
12 12
 Remove all unused networks
13 13
 
14 14
 Options:
15
-  -f, --force   Do not prompt for confirmation
16
-      --help    Print usage
15
+      --filter filter   Provide filter values (e.g. 'until=<timestamp>')
16
+  -f, --force           Do not prompt for confirmation
17
+      --help            Print usage
17 18
 ```
18 19
 
19 20
 Remove all unused networks. Unused networks are those which are not referenced by any containers.
... ...
@@ -29,6 +30,51 @@ n1
29 29
 n2
30 30
 ```
31 31
 
32
+## Filtering
33
+
34
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
35
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`)
36
+
37
+The currently supported filters are:
38
+
39
+* until (`<timestamp>`) - only remove networks created before given timestamp
40
+
41
+The `until` filter can be Unix timestamps, date formatted
42
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed
43
+relative to the daemon machine’s time. Supported formats for date
44
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`,
45
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local
46
+timezone on the daemon will be used if you do not provide either a `Z` or a
47
+`+-00:00` timezone offset at the end of the timestamp.  When providing Unix
48
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds
49
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap
50
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a
51
+fraction of a second no more than nine digits long.
52
+
53
+The following removes networks created more than 5 minutes ago. Note that
54
+system networks such as `bridge`, `host`, and `none` will never be pruned:
55
+
56
+```bash
57
+$ docker network ls
58
+NETWORK ID          NAME                DRIVER              SCOPE
59
+7430df902d7a        bridge              bridge              local
60
+ea92373fd499        foo-1-day-ago       bridge              local
61
+ab53663ed3c7        foo-1-min-ago       bridge              local
62
+97b91972bc3b        host                host                local
63
+f949d337b1f5        none                null                local
64
+
65
+$ docker network prune --force --filter until=5m
66
+Deleted Networks:
67
+foo-1-day-ago
68
+
69
+$ docker network ls
70
+NETWORK ID          NAME                DRIVER              SCOPE
71
+7430df902d7a        bridge              bridge              local
72
+ab53663ed3c7        foo-1-min-ago       bridge              local
73
+97b91972bc3b        host                host                local
74
+f949d337b1f5        none                null                local
75
+```
76
+
32 77
 ## Related information
33 78
 
34 79
 * [network disconnect ](network_disconnect.md)
... ...
@@ -21,9 +21,10 @@ Usage:	docker system prune [OPTIONS]
21 21
 Delete unused data
22 22
 
23 23
 Options:
24
-  -a, --all     Remove all unused data not just dangling ones
25
-  -f, --force   Do not prompt for confirmation
26
-      --help    Print usage
24
+  -a, --all             Remove all unused images not just dangling ones
25
+      --filter filter   Provide filter values (e.g. 'until=<timestamp>')
26
+  -f, --force           Do not prompt for confirmation
27
+      --help            Print usage
27 28
 ```
28 29
 
29 30
 Remove all unused containers, volumes, networks and images (both dangling and unreferenced).
... ...
@@ -64,6 +65,27 @@ deleted: sha256:3a88a5c81eb5c283e72db2dbc6d65cbfd8e80b6c89bb6e714cfaaa0eed99c548
64 64
 Total reclaimed space: 13.5 MB
65 65
 ```
66 66
 
67
+## Filtering
68
+
69
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
70
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`)
71
+
72
+The currently supported filters are:
73
+
74
+* until (`<timestamp>`) - only remove containers, images, and networks created before given timestamp
75
+
76
+The `until` filter can be Unix timestamps, date formatted
77
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed
78
+relative to the daemon machine’s time. Supported formats for date
79
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`,
80
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local
81
+timezone on the daemon will be used if you do not provide either a `Z` or a
82
+`+-00:00` timezone offset at the end of the timestamp.  When providing Unix
83
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds
84
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap
85
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a
86
+fraction of a second no more than nine digits long.
87
+
67 88
 ## Related information
68 89
 
69 90
 * [volume create](volume_create.md)
... ...
@@ -5,6 +5,7 @@ package main
5 5
 import (
6 6
 	"strconv"
7 7
 	"strings"
8
+	"time"
8 9
 
9 10
 	"github.com/docker/docker/integration-cli/checker"
10 11
 	"github.com/docker/docker/integration-cli/daemon"
... ...
@@ -90,3 +91,23 @@ func (s *DockerDaemonSuite) TestPruneImageDangling(c *check.C) {
90 90
 	c.Assert(err, checker.IsNil)
91 91
 	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id)
92 92
 }
93
+
94
+func (s *DockerSuite) TestPruneContainerUntil(c *check.C) {
95
+	out, _ := dockerCmd(c, "run", "-d", "busybox")
96
+	id1 := strings.TrimSpace(out)
97
+	c.Assert(waitExited(id1, 5*time.Second), checker.IsNil)
98
+
99
+	until := daemonUnixTime(c)
100
+
101
+	out, _ = dockerCmd(c, "run", "-d", "busybox")
102
+	id2 := strings.TrimSpace(out)
103
+	c.Assert(waitExited(id2, 5*time.Second), checker.IsNil)
104
+
105
+	out, _ = dockerCmd(c, "container", "prune", "--force", "--filter", "until="+until)
106
+	c.Assert(strings.TrimSpace(out), checker.Contains, id1)
107
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id2)
108
+
109
+	out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc")
110
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id1)
111
+	c.Assert(strings.TrimSpace(out), checker.Contains, id2)
112
+}