Browse code

Add --prune to stack deploy.

Add to command line reference.

Signed-off-by: Daniel Nephin <dnephin@docker.com>

Daniel Nephin authored on 2017/02/23 05:43:13
Showing 9 changed files
... ...
@@ -3,8 +3,10 @@ package stack
3 3
 import (
4 4
 	"fmt"
5 5
 
6
+	"github.com/docker/docker/api/types/swarm"
6 7
 	"github.com/docker/docker/cli"
7 8
 	"github.com/docker/docker/cli/command"
9
+	"github.com/docker/docker/cli/compose/convert"
8 10
 	"github.com/pkg/errors"
9 11
 	"github.com/spf13/cobra"
10 12
 	"golang.org/x/net/context"
... ...
@@ -19,6 +21,7 @@ type deployOptions struct {
19 19
 	composefile      string
20 20
 	namespace        string
21 21
 	sendRegistryAuth bool
22
+	prune            bool
22 23
 }
23 24
 
24 25
 func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
... ...
@@ -39,6 +42,8 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
39 39
 	addBundlefileFlag(&opts.bundlefile, flags)
40 40
 	addComposefileFlag(&opts.composefile, flags)
41 41
 	addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
42
+	flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
43
+	flags.SetAnnotation("prune", "version", []string{"1.27"})
42 44
 	return cmd
43 45
 }
44 46
 
... ...
@@ -71,3 +76,22 @@ func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli
71 71
 	}
72 72
 	return nil
73 73
 }
74
+
75
+// pruneServices removes services that are no longer referenced in the source
76
+func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) bool {
77
+	client := dockerCli.Client()
78
+
79
+	oldServices, err := getServices(ctx, client, namespace.Name())
80
+	if err != nil {
81
+		fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
82
+		return true
83
+	}
84
+
85
+	pruneServices := []swarm.Service{}
86
+	for _, service := range oldServices {
87
+		if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
88
+			pruneServices = append(pruneServices, service)
89
+		}
90
+	}
91
+	return removeServices(ctx, dockerCli, pruneServices)
92
+}
... ...
@@ -21,6 +21,14 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
21 21
 
22 22
 	namespace := convert.NewNamespace(opts.namespace)
23 23
 
24
+	if opts.prune {
25
+		services := map[string]struct{}{}
26
+		for service := range bundle.Services {
27
+			services[service] = struct{}{}
28
+		}
29
+		pruneServices(ctx, dockerCli, namespace, services)
30
+	}
31
+
24 32
 	networks := make(map[string]types.NetworkCreate)
25 33
 	for _, service := range bundle.Services {
26 34
 		for _, networkName := range service.Networks {
... ...
@@ -52,8 +52,15 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo
52 52
 
53 53
 	namespace := convert.NewNamespace(opts.namespace)
54 54
 
55
-	serviceNetworks := getServicesDeclaredNetworks(config.Services)
55
+	if opts.prune {
56
+		services := map[string]struct{}{}
57
+		for _, service := range config.Services {
58
+			services[service.Name] = struct{}{}
59
+		}
60
+		pruneServices(ctx, dockerCli, namespace, services)
61
+	}
56 62
 
63
+	serviceNetworks := getServicesDeclaredNetworks(config.Services)
57 64
 	networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks)
58 65
 	if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
59 66
 		return err
60 67
new file mode 100644
... ...
@@ -0,0 +1,54 @@
0
+package stack
1
+
2
+import (
3
+	"bytes"
4
+	"testing"
5
+
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/swarm"
8
+	"github.com/docker/docker/cli/compose/convert"
9
+	"github.com/docker/docker/cli/internal/test"
10
+	"github.com/docker/docker/client"
11
+	"github.com/docker/docker/pkg/testutil/assert"
12
+	"golang.org/x/net/context"
13
+)
14
+
15
+type fakeClient struct {
16
+	client.Client
17
+	serviceList []string
18
+	removedIDs  []string
19
+}
20
+
21
+func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
22
+	services := []swarm.Service{}
23
+	for _, name := range cli.serviceList {
24
+		services = append(services, swarm.Service{
25
+			ID: name,
26
+			Spec: swarm.ServiceSpec{
27
+				Annotations: swarm.Annotations{Name: name},
28
+			},
29
+		})
30
+	}
31
+	return services, nil
32
+}
33
+
34
+func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
35
+	cli.removedIDs = append(cli.removedIDs, serviceID)
36
+	return nil
37
+}
38
+
39
+func TestPruneServices(t *testing.T) {
40
+	ctx := context.Background()
41
+	namespace := convert.NewNamespace("foo")
42
+	services := map[string]struct{}{
43
+		"new":  {},
44
+		"keep": {},
45
+	}
46
+	client := &fakeClient{serviceList: []string{"foo_keep", "foo_remove"}}
47
+	dockerCli := test.NewFakeCli(client, &bytes.Buffer{})
48
+	dockerCli.SetErr(&bytes.Buffer{})
49
+
50
+	pruneServices(ctx, dockerCli, namespace, services)
51
+
52
+	assert.DeepEqual(t, client.removedIDs, []string{"foo_remove"})
53
+}
... ...
@@ -68,7 +68,7 @@ func runRemove(dockerCli *command.DockerCli, opts removeOptions) error {
68 68
 
69 69
 func removeServices(
70 70
 	ctx context.Context,
71
-	dockerCli *command.DockerCli,
71
+	dockerCli command.Cli,
72 72
 	services []swarm.Service,
73 73
 ) bool {
74 74
 	var err error
... ...
@@ -83,7 +83,7 @@ func removeServices(
83 83
 
84 84
 func removeNetworks(
85 85
 	ctx context.Context,
86
-	dockerCli *command.DockerCli,
86
+	dockerCli command.Cli,
87 87
 	networks []types.NetworkResource,
88 88
 ) bool {
89 89
 	var err error
... ...
@@ -98,7 +98,7 @@ func removeNetworks(
98 98
 
99 99
 func removeSecrets(
100 100
 	ctx context.Context,
101
-	dockerCli *command.DockerCli,
101
+	dockerCli command.Cli,
102 102
 	secrets []swarm.Secret,
103 103
 ) bool {
104 104
 	var err error
... ...
@@ -2,6 +2,7 @@ package convert
2 2
 
3 3
 import (
4 4
 	"io/ioutil"
5
+	"strings"
5 6
 
6 7
 	"github.com/docker/docker/api/types"
7 8
 	networktypes "github.com/docker/docker/api/types/network"
... ...
@@ -24,6 +25,11 @@ func (n Namespace) Scope(name string) string {
24 24
 	return n.name + "_" + name
25 25
 }
26 26
 
27
+// Descope returns the name without the namespace prefix
28
+func (n Namespace) Descope(name string) string {
29
+	return strings.TrimPrefix(name, n.name+"_")
30
+}
31
+
27 32
 // Name returns the name of the namespace
28 33
 func (n Namespace) Name() string {
29 34
 	return n.name
... ...
@@ -35,7 +35,7 @@ func (c *FakeCli) SetIn(in io.ReadCloser) {
35 35
 	c.in = in
36 36
 }
37 37
 
38
-// SetErr sets the standard error stream th cli should write on
38
+// SetErr sets the stderr stream for the cli to the specified io.Writer
39 39
 func (c *FakeCli) SetErr(err io.Writer) {
40 40
 	c.err = err
41 41
 }
... ...
@@ -30,6 +30,7 @@ Options:
30 30
       --bundle-file string    Path to a Distributed Application Bundle file
31 31
       --compose-file string   Path to a Compose file
32 32
       --help                  Print usage
33
+      --prune                 Prune services that are no longer referenced
33 34
       --with-registry-auth    Send registry authentication details to Swarm agents
34 35
 ```
35 36
 
... ...
@@ -27,6 +27,7 @@ Options:
27 27
       --bundle-file string    Path to a Distributed Application Bundle file
28 28
   -c, --compose-file string   Path to a Compose file
29 29
       --help                  Print usage
30
+      --prune                 Prune services that are no longer referenced
30 31
       --with-registry-auth    Send registry authentication details to Swarm agents
31 32
 ```
32 33