Browse code

Convert DanglingOnly to Filters for `docker image prune`

This fix convert DanglingOnly in ImagesPruneConfig to Filters,
so that it is possible to maintain API compatibility in the future.

Several integration tests have been added to cover changes.

This fix is related to 28497.

A follow up to this PR will be done once this PR is merged.

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

Yong Tang authored on 2016/11/17 14:46:37
Showing 22 changed files
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"github.com/docker/docker/api/types"
10 10
 	"github.com/docker/docker/api/types/backend"
11 11
 	"github.com/docker/docker/api/types/container"
12
+	"github.com/docker/docker/api/types/filters"
12 13
 	"github.com/docker/docker/pkg/archive"
13 14
 )
14 15
 
... ...
@@ -64,7 +65,7 @@ type attachBackend interface {
64 64
 
65 65
 // systemBackend includes functions to implement to provide system wide containers functionality
66 66
 type systemBackend interface {
67
-	ContainersPrune(config *types.ContainersPruneConfig) (*types.ContainersPruneReport, error)
67
+	ContainersPrune(pruneFilters filters.Args) (*types.ContainersPruneReport, error)
68 68
 }
69 69
 
70 70
 // Backend is all the methods that need to be implemented to provide container specific functionality.
... ...
@@ -541,16 +541,12 @@ func (s *containerRouter) postContainersPrune(ctx context.Context, w http.Respon
541 541
 		return err
542 542
 	}
543 543
 
544
-	if err := httputils.CheckForJSON(r); err != nil {
545
-		return err
546
-	}
547
-
548
-	var cfg types.ContainersPruneConfig
549
-	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
544
+	pruneFilters, err := filters.FromParam(r.Form.Get("filters"))
545
+	if err != nil {
550 546
 		return err
551 547
 	}
552 548
 
553
-	pruneReport, err := s.backend.ContainersPrune(&cfg)
549
+	pruneReport, err := s.backend.ContainersPrune(pruneFilters)
554 550
 	if err != nil {
555 551
 		return err
556 552
 	}
... ...
@@ -29,7 +29,7 @@ type imageBackend interface {
29 29
 	Images(imageFilters filters.Args, all bool, withExtraAttrs bool) ([]*types.ImageSummary, error)
30 30
 	LookupImage(name string) (*types.ImageInspect, error)
31 31
 	TagImage(imageName, repository, tag string) error
32
-	ImagesPrune(config *types.ImagesPruneConfig) (*types.ImagesPruneReport, error)
32
+	ImagesPrune(pruneFilters filters.Args) (*types.ImagesPruneReport, error)
33 33
 }
34 34
 
35 35
 type importExportBackend interface {
... ...
@@ -331,16 +331,12 @@ func (s *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter
331 331
 		return err
332 332
 	}
333 333
 
334
-	if err := httputils.CheckForJSON(r); err != nil {
335
-		return err
336
-	}
337
-
338
-	var cfg types.ImagesPruneConfig
339
-	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
334
+	pruneFilters, err := filters.FromParam(r.Form.Get("filters"))
335
+	if err != nil {
340 336
 		return err
341 337
 	}
342 338
 
343
-	pruneReport, err := s.backend.ImagesPrune(&cfg)
339
+	pruneReport, err := s.backend.ImagesPrune(pruneFilters)
344 340
 	if err != nil {
345 341
 		return err
346 342
 	}
... ...
@@ -2,6 +2,7 @@ package network
2 2
 
3 3
 import (
4 4
 	"github.com/docker/docker/api/types"
5
+	"github.com/docker/docker/api/types/filters"
5 6
 	"github.com/docker/docker/api/types/network"
6 7
 	"github.com/docker/libnetwork"
7 8
 )
... ...
@@ -17,5 +18,5 @@ type Backend interface {
17 17
 	ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error
18 18
 	DisconnectContainerFromNetwork(containerName string, networkName string, force bool) error
19 19
 	DeleteNetwork(name string) error
20
-	NetworksPrune(config *types.NetworksPruneConfig) (*types.NetworksPruneReport, error)
20
+	NetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error)
21 21
 }
... ...
@@ -297,16 +297,7 @@ func (n *networkRouter) postNetworksPrune(ctx context.Context, w http.ResponseWr
297 297
 		return err
298 298
 	}
299 299
 
300
-	if err := httputils.CheckForJSON(r); err != nil {
301
-		return err
302
-	}
303
-
304
-	var cfg types.NetworksPruneConfig
305
-	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
306
-		return err
307
-	}
308
-
309
-	pruneReport, err := n.backend.NetworksPrune(&cfg)
300
+	pruneReport, err := n.backend.NetworksPrune(filters.Args{})
310 301
 	if err != nil {
311 302
 		return err
312 303
 	}
... ...
@@ -3,6 +3,7 @@ package volume
3 3
 import (
4 4
 	// TODO return types need to be refactored into pkg
5 5
 	"github.com/docker/docker/api/types"
6
+	"github.com/docker/docker/api/types/filters"
6 7
 )
7 8
 
8 9
 // Backend is the methods that need to be implemented to provide
... ...
@@ -12,5 +13,5 @@ type Backend interface {
12 12
 	VolumeInspect(name string) (*types.Volume, error)
13 13
 	VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
14 14
 	VolumeRm(name string, force bool) error
15
-	VolumesPrune(config *types.VolumesPruneConfig) (*types.VolumesPruneReport, error)
15
+	VolumesPrune(pruneFilters filters.Args) (*types.VolumesPruneReport, error)
16 16
 }
... ...
@@ -5,7 +5,7 @@ import (
5 5
 	"net/http"
6 6
 
7 7
 	"github.com/docker/docker/api/server/httputils"
8
-	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
9 9
 	volumetypes "github.com/docker/docker/api/types/volume"
10 10
 	"golang.org/x/net/context"
11 11
 )
... ...
@@ -72,16 +72,7 @@ func (v *volumeRouter) postVolumesPrune(ctx context.Context, w http.ResponseWrit
72 72
 		return err
73 73
 	}
74 74
 
75
-	if err := httputils.CheckForJSON(r); err != nil {
76
-		return err
77
-	}
78
-
79
-	var cfg types.VolumesPruneConfig
80
-	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
81
-		return err
82
-	}
83
-
84
-	pruneReport, err := v.backend.VolumesPrune(&cfg)
75
+	pruneReport, err := v.backend.VolumesPrune(filters.Args{})
85 76
 	if err != nil {
86 77
 		return err
87 78
 	}
... ...
@@ -4187,11 +4187,17 @@ paths:
4187 4187
   /containers/prune:
4188 4188
     post:
4189 4189
       summary: "Delete stopped containers"
4190
-      consumes:
4191
-        - "application/json"
4192 4190
       produces:
4193 4191
         - "application/json"
4194 4192
       operationId: "ContainerPrune"
4193
+      parameters:
4194
+        - name: "filters"
4195
+          in: "query"
4196
+          description: |
4197
+            Filters to process on the prune list, encoded as JSON (a `map[string][]string`).
4198
+
4199
+            Available filters:
4200
+          type: "string"
4195 4201
       responses:
4196 4202
         200:
4197 4203
           description: "No error"
... ...
@@ -4849,21 +4855,20 @@ paths:
4849 4849
   /images/prune:
4850 4850
     post:
4851 4851
       summary: "Delete unused images"
4852
-      consumes:
4853
-        - "application/json"
4854 4852
       produces:
4855 4853
         - "application/json"
4856 4854
       operationId: "ImagePrune"
4857 4855
       parameters:
4858
-        - name: "body"
4859
-          in: "body"
4860
-          schema:
4861
-            type: "object"
4862
-            properties:
4863
-              DanglingOnly:
4864
-                description: "Only delete unused *and* untagged images"
4865
-                type: "boolean"
4866
-                default: false
4856
+        - name: "filters"
4857
+          in: "query"
4858
+          description: |
4859
+            Filters to process on the prune list, encoded as JSON (a `map[string][]string`).
4860
+
4861
+            Available filters:
4862
+            - `dangling=<boolean>` When set to `true` (or `1`), prune only
4863
+               unused *and* untagged images. When set to `false`
4864
+               (or `0`), all unused images are pruned.
4865
+          type: "string"
4867 4866
       responses:
4868 4867
         200:
4869 4868
           description: "No error"
... ...
@@ -5945,11 +5950,17 @@ paths:
5945 5945
   /volumes/prune:
5946 5946
     post:
5947 5947
       summary: "Delete unused volumes"
5948
-      consumes:
5949
-        - "application/json"
5950 5948
       produces:
5951 5949
         - "application/json"
5952 5950
       operationId: "VolumePrune"
5951
+      parameters:
5952
+        - name: "filters"
5953
+          in: "query"
5954
+          description: |
5955
+            Filters to process on the prune list, encoded as JSON (a `map[string][]string`).
5956
+
5957
+            Available filters:
5958
+          type: "string"
5953 5959
       responses:
5954 5960
         200:
5955 5961
           description: "No error"
... ...
@@ -6287,6 +6298,14 @@ paths:
6287 6287
       produces:
6288 6288
         - "application/json"
6289 6289
       operationId: "NetworkPrune"
6290
+      parameters:
6291
+        - name: "filters"
6292
+          in: "query"
6293
+          description: |
6294
+            Filters to process on the prune list, encoded as JSON (a `map[string][]string`).
6295
+
6296
+            Available filters:
6297
+          type: "string"
6290 6298
       responses:
6291 6299
         200:
6292 6300
           description: "No error"
... ...
@@ -509,27 +509,6 @@ type DiskUsage struct {
509 509
 	Volumes    []*Volume
510 510
 }
511 511
 
512
-// ImagesPruneConfig contains the configuration for Engine API:
513
-// POST "/images/prune"
514
-type ImagesPruneConfig struct {
515
-	DanglingOnly bool
516
-}
517
-
518
-// ContainersPruneConfig contains the configuration for Engine API:
519
-// POST "/images/prune"
520
-type ContainersPruneConfig struct {
521
-}
522
-
523
-// VolumesPruneConfig contains the configuration for Engine API:
524
-// POST "/images/prune"
525
-type VolumesPruneConfig struct {
526
-}
527
-
528
-// NetworksPruneConfig contains the configuration for Engine API:
529
-// POST "/networks/prune"
530
-type NetworksPruneConfig struct {
531
-}
532
-
533 512
 // ContainersPruneReport contains the response for Engine API:
534 513
 // POST "/containers/prune"
535 514
 type ContainersPruneReport struct {
... ...
@@ -5,7 +5,7 @@ import (
5 5
 
6 6
 	"golang.org/x/net/context"
7 7
 
8
-	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
9 9
 	"github.com/docker/docker/cli"
10 10
 	"github.com/docker/docker/cli/command"
11 11
 	units "github.com/docker/go-units"
... ...
@@ -52,7 +52,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u
52 52
 		return
53 53
 	}
54 54
 
55
-	report, err := dockerCli.Client().ContainersPrune(context.Background(), types.ContainersPruneConfig{})
55
+	report, err := dockerCli.Client().ContainersPrune(context.Background(), filters.Args{})
56 56
 	if err != nil {
57 57
 		return
58 58
 	}
... ...
@@ -5,7 +5,7 @@ import (
5 5
 
6 6
 	"golang.org/x/net/context"
7 7
 
8
-	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
9 9
 	"github.com/docker/docker/cli"
10 10
 	"github.com/docker/docker/cli/command"
11 11
 	units "github.com/docker/go-units"
... ...
@@ -54,6 +54,9 @@ 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()
58
+	pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all))
59
+
57 60
 	warning := danglingWarning
58 61
 	if opts.all {
59 62
 		warning = allImageWarning
... ...
@@ -62,9 +65,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u
62 62
 		return
63 63
 	}
64 64
 
65
-	report, err := dockerCli.Client().ImagesPrune(context.Background(), types.ImagesPruneConfig{
66
-		DanglingOnly: !opts.all,
67
-	})
65
+	report, err := dockerCli.Client().ImagesPrune(context.Background(), pruneFilters)
68 66
 	if err != nil {
69 67
 		return
70 68
 	}
... ...
@@ -5,7 +5,7 @@ import (
5 5
 
6 6
 	"golang.org/x/net/context"
7 7
 
8
-	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
9 9
 	"github.com/docker/docker/cli"
10 10
 	"github.com/docker/docker/cli/command"
11 11
 	"github.com/spf13/cobra"
... ...
@@ -50,7 +50,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, e
50 50
 		return
51 51
 	}
52 52
 
53
-	report, err := dockerCli.Client().NetworksPrune(context.Background(), types.NetworksPruneConfig{})
53
+	report, err := dockerCli.Client().NetworksPrune(context.Background(), filters.Args{})
54 54
 	if err != nil {
55 55
 		return
56 56
 	}
... ...
@@ -5,7 +5,7 @@ import (
5 5
 
6 6
 	"golang.org/x/net/context"
7 7
 
8
-	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
9 9
 	"github.com/docker/docker/cli"
10 10
 	"github.com/docker/docker/cli/command"
11 11
 	units "github.com/docker/go-units"
... ...
@@ -52,7 +52,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u
52 52
 		return
53 53
 	}
54 54
 
55
-	report, err := dockerCli.Client().VolumesPrune(context.Background(), types.VolumesPruneConfig{})
55
+	report, err := dockerCli.Client().VolumesPrune(context.Background(), filters.Args{})
56 56
 	if err != nil {
57 57
 		return
58 58
 	}
... ...
@@ -5,18 +5,24 @@ import (
5 5
 	"fmt"
6 6
 
7 7
 	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
8 9
 	"golang.org/x/net/context"
9 10
 )
10 11
 
11 12
 // ContainersPrune requests the daemon to delete unused data
12
-func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) {
13
+func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
13 14
 	var report types.ContainersPruneReport
14 15
 
15 16
 	if err := cli.NewVersionError("1.25", "container prune"); err != nil {
16 17
 		return report, err
17 18
 	}
18 19
 
19
-	serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil)
20
+	query, err := getFiltersQuery(pruneFilters)
21
+	if err != nil {
22
+		return report, err
23
+	}
24
+
25
+	serverResp, err := cli.post(ctx, "/containers/prune", query, nil, nil)
20 26
 	if err != nil {
21 27
 		return report, err
22 28
 	}
... ...
@@ -5,18 +5,24 @@ import (
5 5
 	"fmt"
6 6
 
7 7
 	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
8 9
 	"golang.org/x/net/context"
9 10
 )
10 11
 
11 12
 // ImagesPrune requests the daemon to delete unused data
12
-func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) {
13
+func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (types.ImagesPruneReport, error) {
13 14
 	var report types.ImagesPruneReport
14 15
 
15 16
 	if err := cli.NewVersionError("1.25", "image prune"); err != nil {
16 17
 		return report, err
17 18
 	}
18 19
 
19
-	serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil)
20
+	query, err := getFiltersQuery(pruneFilters)
21
+	if err != nil {
22
+		return report, err
23
+	}
24
+
25
+	serverResp, err := cli.post(ctx, "/images/prune", query, nil, nil)
20 26
 	if err != nil {
21 27
 		return report, err
22 28
 	}
... ...
@@ -64,7 +64,7 @@ type ContainerAPIClient interface {
64 64
 	ContainerWait(ctx context.Context, container string) (int64, error)
65 65
 	CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
66 66
 	CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error
67
-	ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error)
67
+	ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
68 68
 }
69 69
 
70 70
 // ImageAPIClient defines API client methods for the images
... ...
@@ -82,7 +82,7 @@ type ImageAPIClient interface {
82 82
 	ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error)
83 83
 	ImageSave(ctx context.Context, images []string) (io.ReadCloser, error)
84 84
 	ImageTag(ctx context.Context, image, ref string) error
85
-	ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error)
85
+	ImagesPrune(ctx context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error)
86 86
 }
87 87
 
88 88
 // NetworkAPIClient defines API client methods for the networks
... ...
@@ -94,7 +94,7 @@ type NetworkAPIClient interface {
94 94
 	NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error)
95 95
 	NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
96 96
 	NetworkRemove(ctx context.Context, networkID string) error
97
-	NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error)
97
+	NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error)
98 98
 }
99 99
 
100 100
 // NodeAPIClient defines API client methods for the nodes
... ...
@@ -157,7 +157,7 @@ type VolumeAPIClient interface {
157 157
 	VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error)
158 158
 	VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error)
159 159
 	VolumeRemove(ctx context.Context, volumeID string, force bool) error
160
-	VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error)
160
+	VolumesPrune(ctx context.Context, pruneFilter filters.Args) (types.VolumesPruneReport, error)
161 161
 }
162 162
 
163 163
 // SecretAPIClient defines API client methods for secrets
... ...
@@ -5,14 +5,24 @@ import (
5 5
 	"fmt"
6 6
 
7 7
 	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
8 9
 	"golang.org/x/net/context"
9 10
 )
10 11
 
11 12
 // NetworksPrune requests the daemon to delete unused networks
12
-func (cli *Client) NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error) {
13
+func (cli *Client) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) {
13 14
 	var report types.NetworksPruneReport
14 15
 
15
-	serverResp, err := cli.post(ctx, "/networks/prune", nil, cfg, nil)
16
+	if err := cli.NewVersionError("1.25", "network prune"); err != nil {
17
+		return report, err
18
+	}
19
+
20
+	query, err := getFiltersQuery(pruneFilters)
21
+	if err != nil {
22
+		return report, err
23
+	}
24
+
25
+	serverResp, err := cli.post(ctx, "/networks/prune", query, nil, nil)
16 26
 	if err != nil {
17 27
 		return report, err
18 28
 	}
... ...
@@ -1,6 +1,10 @@
1 1
 package client
2 2
 
3
-import "regexp"
3
+import (
4
+	"github.com/docker/docker/api/types/filters"
5
+	"net/url"
6
+	"regexp"
7
+)
4 8
 
5 9
 var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)
6 10
 
... ...
@@ -13,3 +17,17 @@ func getDockerOS(serverHeader string) string {
13 13
 	}
14 14
 	return osType
15 15
 }
16
+
17
+// getFiltersQuery returns a url query with "filters" query term, based on the
18
+// filters provided.
19
+func getFiltersQuery(f filters.Args) (url.Values, error) {
20
+	query := url.Values{}
21
+	if f.Len() > 0 {
22
+		filterJSON, err := filters.ToParam(f)
23
+		if err != nil {
24
+			return query, err
25
+		}
26
+		query.Set("filters", filterJSON)
27
+	}
28
+	return query, nil
29
+}
... ...
@@ -5,18 +5,24 @@ import (
5 5
 	"fmt"
6 6
 
7 7
 	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
8 9
 	"golang.org/x/net/context"
9 10
 )
10 11
 
11 12
 // VolumesPrune requests the daemon to delete unused data
12
-func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) {
13
+func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) (types.VolumesPruneReport, error) {
13 14
 	var report types.VolumesPruneReport
14 15
 
15 16
 	if err := cli.NewVersionError("1.25", "volume prune"); err != nil {
16 17
 		return report, err
17 18
 	}
18 19
 
19
-	serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil)
20
+	query, err := getFiltersQuery(pruneFilters)
21
+	if err != nil {
22
+		return report, err
23
+	}
24
+
25
+	serverResp, err := cli.post(ctx, "/volumes/prune", query, nil, nil)
20 26
 	if err != nil {
21 27
 		return report, err
22 28
 	}
... ...
@@ -1,11 +1,13 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"regexp"
5 6
 
6 7
 	"github.com/Sirupsen/logrus"
7 8
 	"github.com/docker/distribution/digest"
8 9
 	"github.com/docker/docker/api/types"
10
+	"github.com/docker/docker/api/types/filters"
9 11
 	"github.com/docker/docker/image"
10 12
 	"github.com/docker/docker/layer"
11 13
 	"github.com/docker/docker/pkg/directory"
... ...
@@ -16,7 +18,7 @@ import (
16 16
 )
17 17
 
18 18
 // ContainersPrune removes unused containers
19
-func (daemon *Daemon) ContainersPrune(config *types.ContainersPruneConfig) (*types.ContainersPruneReport, error) {
19
+func (daemon *Daemon) ContainersPrune(pruneFilters filters.Args) (*types.ContainersPruneReport, error) {
20 20
 	rep := &types.ContainersPruneReport{}
21 21
 
22 22
 	allContainers := daemon.List()
... ...
@@ -40,7 +42,7 @@ func (daemon *Daemon) ContainersPrune(config *types.ContainersPruneConfig) (*typ
40 40
 }
41 41
 
42 42
 // VolumesPrune removes unused local volumes
43
-func (daemon *Daemon) VolumesPrune(config *types.VolumesPruneConfig) (*types.VolumesPruneReport, error) {
43
+func (daemon *Daemon) VolumesPrune(pruneFilters filters.Args) (*types.VolumesPruneReport, error) {
44 44
 	rep := &types.VolumesPruneReport{}
45 45
 
46 46
 	pruneVols := func(v volume.Volume) error {
... ...
@@ -70,11 +72,20 @@ func (daemon *Daemon) VolumesPrune(config *types.VolumesPruneConfig) (*types.Vol
70 70
 }
71 71
 
72 72
 // ImagesPrune removes unused images
73
-func (daemon *Daemon) ImagesPrune(config *types.ImagesPruneConfig) (*types.ImagesPruneReport, error) {
73
+func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPruneReport, error) {
74 74
 	rep := &types.ImagesPruneReport{}
75 75
 
76
+	danglingOnly := true
77
+	if pruneFilters.Include("dangling") {
78
+		if pruneFilters.ExactMatch("dangling", "false") || pruneFilters.ExactMatch("dangling", "0") {
79
+			danglingOnly = false
80
+		} else if !pruneFilters.ExactMatch("dangling", "true") && !pruneFilters.ExactMatch("dangling", "1") {
81
+			return nil, fmt.Errorf("Invalid filter 'dangling=%s'", pruneFilters.Get("dangling"))
82
+		}
83
+	}
84
+
76 85
 	var allImages map[image.ID]*image.Image
77
-	if config.DanglingOnly {
86
+	if danglingOnly {
78 87
 		allImages = daemon.imageStore.Heads()
79 88
 	} else {
80 89
 		allImages = daemon.imageStore.Map()
... ...
@@ -106,7 +117,7 @@ func (daemon *Daemon) ImagesPrune(config *types.ImagesPruneConfig) (*types.Image
106 106
 		deletedImages := []types.ImageDelete{}
107 107
 		refs := daemon.referenceStore.References(dgst)
108 108
 		if len(refs) > 0 {
109
-			if config.DanglingOnly {
109
+			if danglingOnly {
110 110
 				// Not a dangling image
111 111
 				continue
112 112
 			}
... ...
@@ -156,7 +167,7 @@ func (daemon *Daemon) ImagesPrune(config *types.ImagesPruneConfig) (*types.Image
156 156
 }
157 157
 
158 158
 // localNetworksPrune removes unused local networks
159
-func (daemon *Daemon) localNetworksPrune(config *types.NetworksPruneConfig) (*types.NetworksPruneReport, error) {
159
+func (daemon *Daemon) localNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
160 160
 	rep := &types.NetworksPruneReport{}
161 161
 	var err error
162 162
 	// When the function returns true, the walk will stop.
... ...
@@ -177,7 +188,7 @@ func (daemon *Daemon) localNetworksPrune(config *types.NetworksPruneConfig) (*ty
177 177
 }
178 178
 
179 179
 // clusterNetworksPrune removes unused cluster networks
180
-func (daemon *Daemon) clusterNetworksPrune(config *types.NetworksPruneConfig) (*types.NetworksPruneReport, error) {
180
+func (daemon *Daemon) clusterNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
181 181
 	rep := &types.NetworksPruneReport{}
182 182
 	cluster := daemon.GetCluster()
183 183
 	networks, err := cluster.GetNetworks()
... ...
@@ -207,15 +218,15 @@ func (daemon *Daemon) clusterNetworksPrune(config *types.NetworksPruneConfig) (*
207 207
 }
208 208
 
209 209
 // NetworksPrune removes unused networks
210
-func (daemon *Daemon) NetworksPrune(config *types.NetworksPruneConfig) (*types.NetworksPruneReport, error) {
210
+func (daemon *Daemon) NetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
211 211
 	rep := &types.NetworksPruneReport{}
212
-	clusterRep, err := daemon.clusterNetworksPrune(config)
212
+	clusterRep, err := daemon.clusterNetworksPrune(pruneFilters)
213 213
 	if err != nil {
214 214
 		logrus.Warnf("could not remove cluster networks: %v", err)
215 215
 	} else {
216 216
 		rep.NetworksDeleted = append(rep.NetworksDeleted, clusterRep.NetworksDeleted...)
217 217
 	}
218
-	localRep, err := daemon.localNetworksPrune(config)
218
+	localRep, err := daemon.localNetworksPrune(pruneFilters)
219 219
 	if err != nil {
220 220
 		logrus.Warnf("could not remove local networks: %v", err)
221 221
 	} else {
... ...
@@ -59,3 +59,33 @@ func (s *DockerSwarmSuite) TestPruneNetwork(c *check.C) {
59 59
 	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 0)
60 60
 	pruneNetworkAndVerify(c, d, []string{}, []string{"n1", "n3"})
61 61
 }
62
+
63
+func (s *DockerDaemonSuite) TestPruneImageDangling(c *check.C) {
64
+	c.Assert(s.d.StartWithBusybox(), checker.IsNil)
65
+
66
+	out, _, err := s.d.buildImageWithOut("test",
67
+		`FROM busybox
68
+                 LABEL foo=bar`, true, "-q")
69
+	c.Assert(err, checker.IsNil)
70
+	id := strings.TrimSpace(out)
71
+
72
+	out, err = s.d.Cmd("images", "-q", "--no-trunc")
73
+	c.Assert(err, checker.IsNil)
74
+	c.Assert(strings.TrimSpace(out), checker.Contains, id)
75
+
76
+	out, err = s.d.Cmd("image", "prune", "--force")
77
+	c.Assert(err, checker.IsNil)
78
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id)
79
+
80
+	out, err = s.d.Cmd("images", "-q", "--no-trunc")
81
+	c.Assert(err, checker.IsNil)
82
+	c.Assert(strings.TrimSpace(out), checker.Contains, id)
83
+
84
+	out, err = s.d.Cmd("image", "prune", "--force", "--all")
85
+	c.Assert(err, checker.IsNil)
86
+	c.Assert(strings.TrimSpace(out), checker.Contains, id)
87
+
88
+	out, err = s.d.Cmd("images", "-q", "--no-trunc")
89
+	c.Assert(err, checker.IsNil)
90
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id)
91
+}