Browse code

add `docker network prune`

`docker network prune` prunes unused networks, including overlay ones.
`docker system prune` also prunes unused networks.

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>

Akihiro Suda authored on 2016/10/18 13:36:52
Showing 29 changed files
... ...
@@ -17,4 +17,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 21
 }
... ...
@@ -37,6 +37,7 @@ func (r *networkRouter) initRoutes() {
37 37
 		router.NewPostRoute("/networks/create", r.postNetworkCreate),
38 38
 		router.NewPostRoute("/networks/{id:.*}/connect", r.postNetworkConnect),
39 39
 		router.NewPostRoute("/networks/{id:.*}/disconnect", r.postNetworkDisconnect),
40
+		router.NewPostRoute("/networks/prune", r.postNetworksPrune),
40 41
 		// DELETE
41 42
 		router.NewDeleteRoute("/networks/{id:.*}", r.deleteNetwork),
42 43
 	}
... ...
@@ -274,3 +274,24 @@ func buildEndpointResource(id string, name string, info libnetwork.EndpointInfo)
274 274
 	}
275 275
 	return er
276 276
 }
277
+
278
+func (n *networkRouter) postNetworksPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
279
+	if err := httputils.ParseForm(r); err != nil {
280
+		return err
281
+	}
282
+
283
+	if err := httputils.CheckForJSON(r); err != nil {
284
+		return err
285
+	}
286
+
287
+	var cfg types.NetworksPruneConfig
288
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
289
+		return err
290
+	}
291
+
292
+	pruneReport, err := n.backend.NetworksPrune(&cfg)
293
+	if err != nil {
294
+		return err
295
+	}
296
+	return httputils.WriteJSON(w, http.StatusOK, pruneReport)
297
+}
... ...
@@ -522,6 +522,11 @@ type ContainersPruneConfig struct {
522 522
 type VolumesPruneConfig struct {
523 523
 }
524 524
 
525
+// NetworksPruneConfig contains the configuration for Remote API:
526
+// POST "/networks/prune"
527
+type NetworksPruneConfig struct {
528
+}
529
+
525 530
 // ContainersPruneReport contains the response for Remote API:
526 531
 // POST "/containers/prune"
527 532
 type ContainersPruneReport struct {
... ...
@@ -542,3 +547,9 @@ type ImagesPruneReport struct {
542 542
 	ImagesDeleted  []ImageDelete
543 543
 	SpaceReclaimed uint64
544 544
 }
545
+
546
+// NetworksPruneReport contains the response for Remote API:
547
+// POST "/networks/prune"
548
+type NetworksPruneReport struct {
549
+	NetworksDeleted []string
550
+}
... ...
@@ -26,6 +26,7 @@ func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command {
26 26
 		newInspectCommand(dockerCli),
27 27
 		newListCommand(dockerCli),
28 28
 		newRemoveCommand(dockerCli),
29
+		NewPruneCommand(dockerCli),
29 30
 	)
30 31
 	return cmd
31 32
 }
32 33
new file mode 100644
... ...
@@ -0,0 +1,72 @@
0
+package network
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/docker/docker/cli/command"
10
+	"github.com/spf13/cobra"
11
+)
12
+
13
+type pruneOptions struct {
14
+	force bool
15
+}
16
+
17
+// NewPruneCommand returns a new cobra prune command for networks
18
+func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
19
+	var opts pruneOptions
20
+
21
+	cmd := &cobra.Command{
22
+		Use:   "prune [OPTIONS]",
23
+		Short: "Remove all unused networks",
24
+		Args:  cli.NoArgs,
25
+		RunE: func(cmd *cobra.Command, args []string) error {
26
+			output, err := runPrune(dockerCli, opts)
27
+			if err != nil {
28
+				return err
29
+			}
30
+			if output != "" {
31
+				fmt.Fprintln(dockerCli.Out(), output)
32
+			}
33
+			return nil
34
+		},
35
+	}
36
+
37
+	flags := cmd.Flags()
38
+	flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation")
39
+
40
+	return cmd
41
+}
42
+
43
+const warning = `WARNING! This will remove all networks not used by at least one container.
44
+Are you sure you want to continue?`
45
+
46
+func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) {
47
+	if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
48
+		return
49
+	}
50
+
51
+	report, err := dockerCli.Client().NetworksPrune(context.Background(), types.NetworksPruneConfig{})
52
+	if err != nil {
53
+		return
54
+	}
55
+
56
+	if len(report.NetworksDeleted) > 0 {
57
+		output = "Deleted Networks:\n"
58
+		for _, id := range report.NetworksDeleted {
59
+			output += id + "\n"
60
+		}
61
+	}
62
+
63
+	return
64
+}
65
+
66
+// RunPrune calls the Network Prune API
67
+// This returns the amount of space reclaimed and a detailed output string
68
+func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) {
69
+	output, err := runPrune(dockerCli, pruneOptions{force: true})
70
+	return 0, output, err
71
+}
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"github.com/docker/docker/cli/command"
5 5
 	"github.com/docker/docker/cli/command/container"
6 6
 	"github.com/docker/docker/cli/command/image"
7
+	"github.com/docker/docker/cli/command/network"
7 8
 	"github.com/docker/docker/cli/command/volume"
8 9
 	"github.com/spf13/cobra"
9 10
 )
... ...
@@ -23,6 +24,11 @@ func NewImagePruneCommand(dockerCli *command.DockerCli) *cobra.Command {
23 23
 	return image.NewPruneCommand(dockerCli)
24 24
 }
25 25
 
26
+// NewNetworkPruneCommand returns a cobra prune command for Networks
27
+func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
28
+	return network.NewPruneCommand(dockerCli)
29
+}
30
+
26 31
 // RunContainerPrune executes a prune command for containers
27 32
 func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) {
28 33
 	return container.RunPrune(dockerCli)
... ...
@@ -37,3 +43,8 @@ func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) {
37 37
 func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) {
38 38
 	return image.RunPrune(dockerCli, all)
39 39
 }
40
+
41
+// RunNetworkPrune executes a prune command for networks
42
+func RunNetworkPrune(dockerCli *command.DockerCli) (uint64, string, error) {
43
+	return network.RunPrune(dockerCli)
44
+}
... ...
@@ -39,6 +39,7 @@ const (
39 39
 	warning = `WARNING! This will remove:
40 40
 	- all stopped containers
41 41
 	- all volumes not used by at least one container
42
+	- all networks not used by at least one container
42 43
 	%s
43 44
 Are you sure you want to continue?`
44 45
 
... ...
@@ -64,13 +65,14 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error {
64 64
 	for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){
65 65
 		prune.RunContainerPrune,
66 66
 		prune.RunVolumePrune,
67
+		prune.RunNetworkPrune,
67 68
 	} {
68 69
 		spc, output, err := pruneFn(dockerCli)
69 70
 		if err != nil {
70 71
 			return err
71 72
 		}
72
-		if spc > 0 {
73
-			spaceReclaimed += spc
73
+		spaceReclaimed += spc
74
+		if output != "" {
74 75
 			fmt.Fprintln(dockerCli.Out(), output)
75 76
 		}
76 77
 	}
... ...
@@ -91,6 +91,7 @@ type NetworkAPIClient interface {
91 91
 	NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error)
92 92
 	NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
93 93
 	NetworkRemove(ctx context.Context, networkID string) error
94
+	NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error)
94 95
 }
95 96
 
96 97
 // NodeAPIClient defines API client methods for the nodes
97 98
new file mode 100644
... ...
@@ -0,0 +1,26 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// NetworksPrune requests the daemon to delete unused networks
11
+func (cli *Client) NetworksPrune(ctx context.Context, cfg types.NetworksPruneConfig) (types.NetworksPruneReport, error) {
12
+	var report types.NetworksPruneReport
13
+
14
+	serverResp, err := cli.post(ctx, "/networks/prune", nil, cfg, nil)
15
+	if err != nil {
16
+		return report, err
17
+	}
18
+	defer ensureReaderClosed(serverResp)
19
+
20
+	if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil {
21
+		return report, fmt.Errorf("Error retrieving network prune report: %v", err)
22
+	}
23
+
24
+	return report, nil
25
+}
... ...
@@ -279,6 +279,7 @@ func (cli *DaemonCli) start(opts daemonOptions) (err error) {
279 279
 
280 280
 	// initMiddlewares needs cli.d to be populated. Dont change this init order.
281 281
 	cli.initMiddlewares(api, serverConfig)
282
+	d.SetCluster(c)
282 283
 	initRouter(api, d, c)
283 284
 
284 285
 	cli.setupConfigReloadTrap()
285 286
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package daemon
1
+
2
+import (
3
+	apitypes "github.com/docker/docker/api/types"
4
+)
5
+
6
+// Cluster is the interface for github.com/docker/docker/daemon/cluster.(*Cluster).
7
+type Cluster interface {
8
+	GetNetwork(input string) (apitypes.NetworkResource, error)
9
+	GetNetworks() ([]apitypes.NetworkResource, error)
10
+	RemoveNetwork(input string) error
11
+}
... ...
@@ -102,6 +102,7 @@ type Daemon struct {
102 102
 	containerdRemote          libcontainerd.Remote
103 103
 	defaultIsolation          containertypes.Isolation // Default isolation mode on Windows
104 104
 	clusterProvider           cluster.Provider
105
+	cluster                   Cluster
105 106
 }
106 107
 
107 108
 // HasExperimental returns whether the experimental features of the daemon are enabled or not
... ...
@@ -1234,3 +1235,13 @@ func copyBlkioEntry(entries []*containerd.BlkioStatsEntry) []types.BlkioStatEntr
1234 1234
 	}
1235 1235
 	return out
1236 1236
 }
1237
+
1238
+// GetCluster returns the cluster
1239
+func (daemon *Daemon) GetCluster() Cluster {
1240
+	return daemon.cluster
1241
+}
1242
+
1243
+// SetCluster sets the cluster
1244
+func (daemon *Daemon) SetCluster(cluster Cluster) {
1245
+	daemon.cluster = cluster
1246
+}
... ...
@@ -1,6 +1,8 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"regexp"
5
+
4 6
 	"github.com/Sirupsen/logrus"
5 7
 	"github.com/docker/distribution/digest"
6 8
 	"github.com/docker/docker/api/types"
... ...
@@ -8,7 +10,9 @@ import (
8 8
 	"github.com/docker/docker/layer"
9 9
 	"github.com/docker/docker/pkg/directory"
10 10
 	"github.com/docker/docker/reference"
11
+	"github.com/docker/docker/runconfig"
11 12
 	"github.com/docker/docker/volume"
13
+	"github.com/docker/libnetwork"
12 14
 )
13 15
 
14 16
 // ContainersPrune removes unused containers
... ...
@@ -150,3 +154,72 @@ func (daemon *Daemon) ImagesPrune(config *types.ImagesPruneConfig) (*types.Image
150 150
 
151 151
 	return rep, nil
152 152
 }
153
+
154
+// localNetworksPrune removes unused local networks
155
+func (daemon *Daemon) localNetworksPrune(config *types.NetworksPruneConfig) (*types.NetworksPruneReport, error) {
156
+	rep := &types.NetworksPruneReport{}
157
+	var err error
158
+	// When the function returns true, the walk will stop.
159
+	l := func(nw libnetwork.Network) bool {
160
+		nwName := nw.Name()
161
+		predefined := runconfig.IsPreDefinedNetwork(nwName)
162
+		if !predefined && len(nw.Endpoints()) == 0 {
163
+			if err = daemon.DeleteNetwork(nw.ID()); err != nil {
164
+				logrus.Warnf("could not remove network %s: %v", nwName, err)
165
+				return false
166
+			}
167
+			rep.NetworksDeleted = append(rep.NetworksDeleted, nwName)
168
+		}
169
+		return false
170
+	}
171
+	daemon.netController.WalkNetworks(l)
172
+	return rep, err
173
+}
174
+
175
+// clusterNetworksPrune removes unused cluster networks
176
+func (daemon *Daemon) clusterNetworksPrune(config *types.NetworksPruneConfig) (*types.NetworksPruneReport, error) {
177
+	rep := &types.NetworksPruneReport{}
178
+	cluster := daemon.GetCluster()
179
+	networks, err := cluster.GetNetworks()
180
+	if err != nil {
181
+		return rep, err
182
+	}
183
+	networkIsInUse := regexp.MustCompile(`network ([[:alnum:]]+) is in use`)
184
+	for _, nw := range networks {
185
+		if nw.Name == "ingress" {
186
+			continue
187
+		}
188
+		// https://github.com/docker/docker/issues/24186
189
+		// `docker network inspect` unfortunately displays ONLY those containers that are local to that node.
190
+		// So we try to remove it anyway and check the error
191
+		err = cluster.RemoveNetwork(nw.ID)
192
+		if err != nil {
193
+			// we can safely ignore the "network .. is in use" error
194
+			match := networkIsInUse.FindStringSubmatch(err.Error())
195
+			if len(match) != 2 || match[1] != nw.ID {
196
+				logrus.Warnf("could not remove network %s: %v", nw.Name, err)
197
+			}
198
+			continue
199
+		}
200
+		rep.NetworksDeleted = append(rep.NetworksDeleted, nw.Name)
201
+	}
202
+	return rep, nil
203
+}
204
+
205
+// NetworksPrune removes unused networks
206
+func (daemon *Daemon) NetworksPrune(config *types.NetworksPruneConfig) (*types.NetworksPruneReport, error) {
207
+	rep := &types.NetworksPruneReport{}
208
+	clusterRep, err := daemon.clusterNetworksPrune(config)
209
+	if err != nil {
210
+		logrus.Warnf("could not remove cluster networks: %v", err)
211
+	} else {
212
+		rep.NetworksDeleted = append(rep.NetworksDeleted, clusterRep.NetworksDeleted...)
213
+	}
214
+	localRep, err := daemon.localNetworksPrune(config)
215
+	if err != nil {
216
+		logrus.Warnf("could not remove local networks: %v", err)
217
+	} else {
218
+		rep.NetworksDeleted = append(rep.NetworksDeleted, localRep.NetworksDeleted...)
219
+	}
220
+	return rep, err
221
+}
... ...
@@ -157,8 +157,10 @@ This section lists each version from latest to oldest.  Each listing includes a
157 157
 * `POST /containers/prune` prunes stopped containers.
158 158
 * `POST /images/prune` prunes unused images.
159 159
 * `POST /volumes/prune` prunes unused volumes.
160
+* `POST /networks/prune` prunes unused networks.
160 161
 * Every API response now includes a `Docker-Experimental` header specifying if experimental features are enabled (value can be `true` or `false`).
161 162
 
163
+
162 164
 ### v1.24 API changes
163 165
 
164 166
 [Docker Remote API v1.24](docker_remote_api_v1.24.md) documentation
... ...
@@ -3881,6 +3881,36 @@ Instruct the driver to remove the network (`id`).
3881 3881
 -   **404** - no such network
3882 3882
 -   **500** - server error
3883 3883
 
3884
+### Prune unused networks
3885
+
3886
+`POST /networks/prune`
3887
+
3888
+Delete unused networks
3889
+
3890
+**Example request**:
3891
+
3892
+    POST /networks/prune HTTP/1.1
3893
+    Content-Type: application/json
3894
+
3895
+    {
3896
+    }
3897
+
3898
+**Example response**:
3899
+
3900
+    HTTP/1.1 200 OK
3901
+    Content-Type: application/json
3902
+
3903
+    {
3904
+        "NetworksDeleted": [
3905
+            "n1"
3906
+        ],
3907
+    }
3908
+
3909
+**Status codes**:
3910
+
3911
+-   **200** – no error
3912
+-   **500** – server error
3913
+
3884 3914
 ## 3.6 Plugins
3885 3915
 
3886 3916
 ### List plugins
... ...
@@ -43,4 +43,5 @@ Total reclaimed space: 212 B
43 43
 * [system df](system_df.md)
44 44
 * [volume prune](volume_prune.md)
45 45
 * [image prune](image_prune.md)
46
+* [network prune](network_prune.md)
46 47
 * [system prune](system_prune.md)
... ...
@@ -67,4 +67,5 @@ Total reclaimed space: 16.43 MB
67 67
 * [system df](system_df.md)
68 68
 * [container prune](container_prune.md)
69 69
 * [volume prune](volume_prune.md)
70
+* [network prune](network_prune.md)
70 71
 * [system prune](system_prune.md)
... ...
@@ -98,5 +98,6 @@ You can connect a container to one or more networks. The networks need not be th
98 98
 * [network disconnect](network_disconnect.md)
99 99
 * [network ls](network_ls.md)
100 100
 * [network rm](network_rm.md)
101
+* [network prune](network_prune.md)
101 102
 * [Understand Docker container networks](https://docs.docker.com/engine/userguide/networking/)
102 103
 * [Work with networks](https://docs.docker.com/engine/userguide/networking/work-with-networks/)
... ...
@@ -197,4 +197,5 @@ to create an externally isolated `overlay` network, you can specify the
197 197
 * [network disconnect](network_disconnect.md)
198 198
 * [network ls](network_ls.md)
199 199
 * [network rm](network_rm.md)
200
+* [network prune](network_prune.md)
200 201
 * [Understand Docker container networks](https://docs.docker.com/engine/userguide/networking/)
... ...
@@ -39,4 +39,5 @@ Disconnects a container from a network. The container must be running to disconn
39 39
 * [network create](network_create.md)
40 40
 * [network ls](network_ls.md)
41 41
 * [network rm](network_rm.md)
42
+* [network prune](network_prune.md)
42 43
 * [Understand Docker container networks](https://docs.docker.com/engine/userguide/networking/)
... ...
@@ -128,4 +128,5 @@ $ docker network inspect simple-network
128 128
 * [network create](network_create.md)
129 129
 * [network ls](network_ls.md)
130 130
 * [network rm](network_rm.md)
131
+* [network prune](network_prune.md)
131 132
 * [Understand Docker container networks](https://docs.docker.com/engine/userguide/networking/)
... ...
@@ -214,4 +214,5 @@ d1584f8dc718: host
214 214
 * [network create](network_create.md)
215 215
 * [network inspect](network_inspect.md)
216 216
 * [network rm](network_rm.md)
217
+* [network prune](network_prune.md)
217 218
 * [Understand Docker container networks](https://docs.docker.com/engine/userguide/networking/)
218 219
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+---
1
+title: "network prune"
2
+description: "Remove unused networks"
3
+keywords: [network, prune, delete]
4
+---
5
+
6
+# network prune
7
+
8
+```markdown
9
+Usage:	docker network prune [OPTIONS]
10
+
11
+Remove all unused networks
12
+
13
+Options:
14
+  -f, --force   Do not prompt for confirmation
15
+      --help    Print usage
16
+```
17
+
18
+Remove all unused networks. Unused networks are those which are not referenced by any containers.
19
+
20
+Example output:
21
+
22
+```bash
23
+$ docker network prune
24
+WARNING! This will remove all networks not used by at least one container.
25
+Are you sure you want to continue? [y/N] y
26
+Deleted Networks:
27
+n1
28
+n2
29
+```
30
+
31
+## Related information
32
+
33
+* [network disconnect ](network_disconnect.md)
34
+* [network connect](network_connect.md)
35
+* [network create](network_create.md)
36
+* [network ls](network_ls.md)
37
+* [network inspect](network_inspect.md)
38
+* [network rm](network_rm.md)
39
+* [Understand Docker container networks](../../userguide/networking/index.md)
40
+* [system df](system_df.md)
41
+* [container prune](container_prune.md)
42
+* [image prune](image_prune.md)
43
+* [volume prune](volume_prune.md)
44
+* [system prune](system_prune.md)
... ...
@@ -55,4 +55,5 @@ deletion.
55 55
 * [network create](network_create.md)
56 56
 * [network ls](network_ls.md)
57 57
 * [network inspect](network_inspect.md)
58
+* [network prune](network_prune.md)
58 59
 * [Understand Docker container networks](https://docs.docker.com/engine/userguide/networking/)
... ...
@@ -66,8 +66,11 @@ my-named-vol                                                       0
66 66
 * `UNIQUE SIZE` is the amount of space that is only used by a given image
67 67
 * `SIZE` is the virtual size of the image, it is the sum of `SHARED SIZE` and `UNIQUE SIZE`
68 68
 
69
+Note that network information is not shown because it doesn't consume the disk space.
70
+
69 71
 ## Related Information
70 72
 * [system prune](system_prune.md)
71 73
 * [container prune](container_prune.md)
72 74
 * [volume prune](volume_prune.md)
73 75
 * [image prune](image_prune.md)
76
+* [network prune](network_prune.md)
... ...
@@ -26,7 +26,7 @@ Options:
26 26
       --help    Print usage
27 27
 ```
28 28
 
29
-Remove all unused containers, volumes and images (both dangling and unreferenced).
29
+Remove all unused containers, volumes, networks and images (both dangling and unreferenced).
30 30
 
31 31
 Example output:
32 32
 
... ...
@@ -35,6 +35,7 @@ $ docker system prune -a
35 35
 WARNING! This will remove:
36 36
 	- all stopped containers
37 37
 	- all volumes not used by at least one container
38
+	- all networks not used by at least one container
38 39
 	- all images without at least one container associated to them
39 40
 Are you sure you want to continue? [y/N] y
40 41
 Deleted Containers:
... ...
@@ -74,4 +75,5 @@ Total reclaimed space: 13.5 MB
74 74
 * [system df](system_df.md)
75 75
 * [container prune](container_prune.md)
76 76
 * [image prune](image_prune.md)
77
+* [network prune](network_prune.md)
77 78
 * [system prune](system_prune.md)
... ...
@@ -50,4 +50,5 @@ Total reclaimed space: 36 B
50 50
 * [system df](system_df.md)
51 51
 * [container prune](container_prune.md)
52 52
 * [image prune](image_prune.md)
53
+* [network prune](network_prune.md)
53 54
 * [system prune](system_prune.md)
54 55
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+// +build !windows
1
+
2
+package main
3
+
4
+import (
5
+	"strconv"
6
+	"strings"
7
+
8
+	"github.com/docker/docker/pkg/integration/checker"
9
+	"github.com/go-check/check"
10
+)
11
+
12
+func pruneNetworkAndVerify(c *check.C, d *SwarmDaemon, kept, pruned []string) {
13
+	_, err := d.Cmd("network", "prune", "--force")
14
+	c.Assert(err, checker.IsNil)
15
+	out, err := d.Cmd("network", "ls", "--format", "{{.Name}}")
16
+	c.Assert(err, checker.IsNil)
17
+	for _, s := range kept {
18
+		c.Assert(out, checker.Contains, s)
19
+	}
20
+	for _, s := range pruned {
21
+		c.Assert(out, checker.Not(checker.Contains), s)
22
+	}
23
+}
24
+
25
+func (s *DockerSwarmSuite) TestPruneNetwork(c *check.C) {
26
+	d := s.AddDaemon(c, true, true)
27
+	_, err := d.Cmd("network", "create", "n1") // used by container (testprune)
28
+	c.Assert(err, checker.IsNil)
29
+	_, err = d.Cmd("network", "create", "n2")
30
+	c.Assert(err, checker.IsNil)
31
+	_, err = d.Cmd("network", "create", "n3", "--driver", "overlay") // used by service (testprunesvc)
32
+	c.Assert(err, checker.IsNil)
33
+	_, err = d.Cmd("network", "create", "n4", "--driver", "overlay")
34
+	c.Assert(err, checker.IsNil)
35
+
36
+	cName := "testprune"
37
+	_, err = d.Cmd("run", "-d", "--name", cName, "--net", "n1", "busybox", "top")
38
+	c.Assert(err, checker.IsNil)
39
+
40
+	serviceName := "testprunesvc"
41
+	replicas := 1
42
+	out, err := d.Cmd("service", "create", "--name", serviceName,
43
+		"--replicas", strconv.Itoa(replicas),
44
+		"--network", "n3",
45
+		"busybox", "top")
46
+	c.Assert(err, checker.IsNil)
47
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
48
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, replicas+1)
49
+
50
+	// prune and verify
51
+	pruneNetworkAndVerify(c, d, []string{"n1", "n3"}, []string{"n2", "n4"})
52
+
53
+	// remove containers, then prune and verify again
54
+	_, err = d.Cmd("rm", "-f", cName)
55
+	c.Assert(err, checker.IsNil)
56
+	_, err = d.Cmd("service", "rm", serviceName)
57
+	c.Assert(err, checker.IsNil)
58
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 0)
59
+	pruneNetworkAndVerify(c, d, []string{}, []string{"n1", "n3"})
60
+}