Browse code

cli: Allow service's networks to be updated

Resolve networks IDs on the client side.

Avoid filling in deprecated Spec.Networks field.

Sort networks in the TaskSpec for update stability.

Add an integration test for changing service networks.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2017/03/24 09:51:57
Showing 7 changed files
... ...
@@ -63,7 +63,9 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
63 63
 	apiClient := dockerCli.Client()
64 64
 	createOpts := types.ServiceCreateOptions{}
65 65
 
66
-	service, err := opts.ToService()
66
+	ctx := context.Background()
67
+
68
+	service, err := opts.ToService(ctx, apiClient)
67 69
 	if err != nil {
68 70
 		return err
69 71
 	}
... ...
@@ -79,8 +81,6 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
79 79
 
80 80
 	}
81 81
 
82
-	ctx := context.Background()
83
-
84 82
 	if err := resolveServiceImageDigest(dockerCli, &service); err != nil {
85 83
 		return err
86 84
 	}
... ...
@@ -2,17 +2,20 @@ package service
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"sort"
5 6
 	"strconv"
6 7
 	"strings"
7 8
 	"time"
8 9
 
9 10
 	"github.com/docker/docker/api/types/container"
10 11
 	"github.com/docker/docker/api/types/swarm"
12
+	"github.com/docker/docker/client"
11 13
 	"github.com/docker/docker/opts"
12 14
 	runconfigopts "github.com/docker/docker/runconfig/opts"
13 15
 	shlex "github.com/flynn-archive/go-shlex"
14 16
 	"github.com/pkg/errors"
15 17
 	"github.com/spf13/pflag"
18
+	"golang.org/x/net/context"
16 19
 )
17 20
 
18 21
 type int64Value interface {
... ...
@@ -270,12 +273,17 @@ func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
270 270
 	return c.value
271 271
 }
272 272
 
273
-func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
273
+func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks []string) ([]swarm.NetworkAttachmentConfig, error) {
274 274
 	nets := []swarm.NetworkAttachmentConfig{}
275
-	for _, network := range networks {
276
-		nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
275
+	for _, networkIDOrName := range networks {
276
+		network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false)
277
+		if err != nil {
278
+			return nil, err
279
+		}
280
+		nets = append(nets, swarm.NetworkAttachmentConfig{Target: network.ID})
277 281
 	}
278
-	return nets
282
+	sort.Sort(byNetworkTarget(nets))
283
+	return nets, nil
279 284
 }
280 285
 
281 286
 type endpointOptions struct {
... ...
@@ -455,7 +463,7 @@ func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) {
455 455
 	return serviceMode, nil
456 456
 }
457 457
 
458
-func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
458
+func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.APIClient) (swarm.ServiceSpec, error) {
459 459
 	var service swarm.ServiceSpec
460 460
 
461 461
 	envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll())
... ...
@@ -487,6 +495,11 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
487 487
 		return service, err
488 488
 	}
489 489
 
490
+	networks, err := convertNetworks(ctx, apiClient, opts.networks.GetAll())
491
+	if err != nil {
492
+		return service, err
493
+	}
494
+
490 495
 	service = swarm.ServiceSpec{
491 496
 		Annotations: swarm.Annotations{
492 497
 			Name:   opts.name,
... ...
@@ -517,7 +530,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
517 517
 				Secrets:         nil,
518 518
 				Healthcheck:     healthConfig,
519 519
 			},
520
-			Networks:      convertNetworks(opts.networks.GetAll()),
520
+			Networks:      networks,
521 521
 			Resources:     opts.resources.ToResourceRequirements(),
522 522
 			RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
523 523
 			Placement: &swarm.Placement{
... ...
@@ -526,7 +539,6 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
526 526
 			},
527 527
 			LogDriver: opts.logDriver.toLogDriver(),
528 528
 		},
529
-		Networks:       convertNetworks(opts.networks.GetAll()),
530 529
 		Mode:           serviceMode,
531 530
 		UpdateConfig:   opts.update.config(),
532 531
 		RollbackConfig: opts.rollback.config(),
... ...
@@ -666,6 +678,8 @@ const (
666 666
 	flagMountAdd                = "mount-add"
667 667
 	flagName                    = "name"
668 668
 	flagNetwork                 = "network"
669
+	flagNetworkAdd              = "network-add"
670
+	flagNetworkRemove           = "network-rm"
669 671
 	flagPublish                 = "publish"
670 672
 	flagPublishRemove           = "publish-rm"
671 673
 	flagPublishAdd              = "publish-add"
... ...
@@ -74,6 +74,10 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
74 74
 	flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"})
75 75
 	flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference")
76 76
 	flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"})
77
+	flags.Var(&serviceOpts.networks, flagNetworkAdd, "Add a network")
78
+	flags.SetAnnotation(flagNetworkAdd, "version", []string{"1.29"})
79
+	flags.Var(newListOptsVar(), flagNetworkRemove, "Remove a network")
80
+	flags.SetAnnotation(flagNetworkRemove, "version", []string{"1.29"})
77 81
 	flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port")
78 82
 	flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container")
79 83
 	flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"})
... ...
@@ -147,7 +151,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
147 147
 		updateOpts.Rollback = "previous"
148 148
 	}
149 149
 
150
-	err = updateService(flags, spec)
150
+	err = updateService(ctx, apiClient, flags, spec)
151 151
 	if err != nil {
152 152
 		return err
153 153
 	}
... ...
@@ -207,7 +211,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
207 207
 	return waitOnService(ctx, dockerCli, serviceID, opts)
208 208
 }
209 209
 
210
-func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
210
+func updateService(ctx context.Context, apiClient client.APIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
211 211
 	updateString := func(flag string, field *string) {
212 212
 		if flags.Changed(flag) {
213 213
 			*field, _ = flags.GetString(flag)
... ...
@@ -316,6 +320,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
316 316
 		updatePlacementPreferences(flags, task.Placement)
317 317
 	}
318 318
 
319
+	if anyChanged(flags, flagNetworkAdd, flagNetworkRemove) {
320
+		if err := updateNetworks(ctx, apiClient, flags, spec); err != nil {
321
+			return err
322
+		}
323
+	}
324
+
319 325
 	if err := updateReplicas(flags, &spec.Mode); err != nil {
320 326
 		return err
321 327
 	}
... ...
@@ -623,7 +633,6 @@ func (m byMountSource) Less(i, j int) bool {
623 623
 }
624 624
 
625 625
 func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error {
626
-
627 626
 	mountsByTarget := map[string]mounttypes.Mount{}
628 627
 
629 628
 	if flags.Changed(flagMountAdd) {
... ...
@@ -947,3 +956,63 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec)
947 947
 	}
948 948
 	return nil
949 949
 }
950
+
951
+type byNetworkTarget []swarm.NetworkAttachmentConfig
952
+
953
+func (m byNetworkTarget) Len() int      { return len(m) }
954
+func (m byNetworkTarget) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
955
+func (m byNetworkTarget) Less(i, j int) bool {
956
+	return m[i].Target < m[j].Target
957
+}
958
+
959
+func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
960
+	// spec.TaskTemplate.Networks takes precedence over the deprecated
961
+	// spec.Networks field. If spec.Network is in use, we'll migrate those
962
+	// values to spec.TaskTemplate.Networks.
963
+	specNetworks := spec.TaskTemplate.Networks
964
+	if len(specNetworks) == 0 {
965
+		specNetworks = spec.Networks
966
+	}
967
+	spec.Networks = nil
968
+
969
+	toRemove := buildToRemoveSet(flags, flagNetworkRemove)
970
+	idsToRemove := make(map[string]struct{})
971
+	for networkIDOrName := range toRemove {
972
+		network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false)
973
+		if err != nil {
974
+			return err
975
+		}
976
+		idsToRemove[network.ID] = struct{}{}
977
+	}
978
+
979
+	existingNetworks := make(map[string]struct{})
980
+	var newNetworks []swarm.NetworkAttachmentConfig
981
+	for _, network := range specNetworks {
982
+		if _, exists := idsToRemove[network.Target]; exists {
983
+			continue
984
+		}
985
+
986
+		newNetworks = append(newNetworks, network)
987
+		existingNetworks[network.Target] = struct{}{}
988
+	}
989
+
990
+	if flags.Changed(flagNetworkAdd) {
991
+		values := flags.Lookup(flagNetworkAdd).Value.(*opts.ListOpts).GetAll()
992
+		networks, err := convertNetworks(ctx, apiClient, values)
993
+		if err != nil {
994
+			return err
995
+		}
996
+		for _, network := range networks {
997
+			if _, exists := existingNetworks[network.Target]; exists {
998
+				return errors.Errorf("service is already attached to network %s", network.Target)
999
+			}
1000
+			newNetworks = append(newNetworks, network)
1001
+			existingNetworks[network.Target] = struct{}{}
1002
+		}
1003
+	}
1004
+
1005
+	sort.Sort(byNetworkTarget(newNetworks))
1006
+
1007
+	spec.TaskTemplate.Networks = newNetworks
1008
+	return nil
1009
+}
... ...
@@ -22,7 +22,7 @@ func TestUpdateServiceArgs(t *testing.T) {
22 22
 	cspec := &spec.TaskTemplate.ContainerSpec
23 23
 	cspec.Args = []string{"old", "args"}
24 24
 
25
-	updateService(flags, spec)
25
+	updateService(nil, nil, flags, spec)
26 26
 	assert.EqualStringSlice(t, cspec.Args, []string{"the", "new args"})
27 27
 }
28 28
 
... ...
@@ -458,18 +458,18 @@ func TestUpdateReadOnly(t *testing.T) {
458 458
 	// Update with --read-only=true, changed to true
459 459
 	flags := newUpdateCommand(nil).Flags()
460 460
 	flags.Set("read-only", "true")
461
-	updateService(flags, spec)
461
+	updateService(nil, nil, flags, spec)
462 462
 	assert.Equal(t, cspec.ReadOnly, true)
463 463
 
464 464
 	// Update without --read-only, no change
465 465
 	flags = newUpdateCommand(nil).Flags()
466
-	updateService(flags, spec)
466
+	updateService(nil, nil, flags, spec)
467 467
 	assert.Equal(t, cspec.ReadOnly, true)
468 468
 
469 469
 	// Update with --read-only=false, changed to false
470 470
 	flags = newUpdateCommand(nil).Flags()
471 471
 	flags.Set("read-only", "false")
472
-	updateService(flags, spec)
472
+	updateService(nil, nil, flags, spec)
473 473
 	assert.Equal(t, cspec.ReadOnly, false)
474 474
 }
475 475
 
... ...
@@ -480,17 +480,17 @@ func TestUpdateStopSignal(t *testing.T) {
480 480
 	// Update with --stop-signal=SIGUSR1
481 481
 	flags := newUpdateCommand(nil).Flags()
482 482
 	flags.Set("stop-signal", "SIGUSR1")
483
-	updateService(flags, spec)
483
+	updateService(nil, nil, flags, spec)
484 484
 	assert.Equal(t, cspec.StopSignal, "SIGUSR1")
485 485
 
486 486
 	// Update without --stop-signal, no change
487 487
 	flags = newUpdateCommand(nil).Flags()
488
-	updateService(flags, spec)
488
+	updateService(nil, nil, flags, spec)
489 489
 	assert.Equal(t, cspec.StopSignal, "SIGUSR1")
490 490
 
491 491
 	// Update with --stop-signal=SIGWINCH
492 492
 	flags = newUpdateCommand(nil).Flags()
493 493
 	flags.Set("stop-signal", "SIGWINCH")
494
-	updateService(flags, spec)
494
+	updateService(nil, nil, flags, spec)
495 495
 	assert.Equal(t, cspec.StopSignal, "SIGWINCH")
496 496
 }
... ...
@@ -58,6 +58,8 @@ Options:
58 58
       --log-opt list                       Logging driver options (default [])
59 59
       --mount-add mount                    Add or update a mount on a service
60 60
       --mount-rm list                      Remove a mount by its target path (default [])
61
+      --network-add list                   Add a network
62
+      --network-rm list                    Remove a network
61 63
       --no-healthcheck                     Disable any container-specified HEALTHCHECK
62 64
       --placement-pref-add pref            Add a placement preference
63 65
       --placement-pref-rm pref             Remove a placement preference
... ...
@@ -205,7 +205,30 @@ func (d *Swarm) CheckServiceTasks(service string) func(*check.C) (interface{}, c
205 205
 	}
206 206
 }
207 207
 
208
-// CheckRunningTaskImages returns the number of different images attached to a running task
208
+// CheckRunningTaskNetworks returns the number of times each network is referenced from a task.
209
+func (d *Swarm) CheckRunningTaskNetworks(c *check.C) (interface{}, check.CommentInterface) {
210
+	var tasks []swarm.Task
211
+
212
+	filterArgs := filters.NewArgs()
213
+	filterArgs.Add("desired-state", "running")
214
+	filters, err := filters.ToParam(filterArgs)
215
+	c.Assert(err, checker.IsNil)
216
+
217
+	status, out, err := d.SockRequest("GET", "/tasks?filters="+filters, nil)
218
+	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
219
+	c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out)))
220
+	c.Assert(json.Unmarshal(out, &tasks), checker.IsNil)
221
+
222
+	result := make(map[string]int)
223
+	for _, task := range tasks {
224
+		for _, network := range task.Spec.Networks {
225
+			result[network.Target]++
226
+		}
227
+	}
228
+	return result, nil
229
+}
230
+
231
+// CheckRunningTaskImages returns the times each image is running as a task.
209 232
 func (d *Swarm) CheckRunningTaskImages(c *check.C) (interface{}, check.CommentInterface) {
210 233
 	var tasks []swarm.Task
211 234
 
... ...
@@ -855,6 +855,45 @@ func (s *DockerSwarmSuite) TestSwarmServiceTTYUpdate(c *check.C) {
855 855
 	c.Assert(strings.TrimSpace(out), checker.Equals, "true")
856 856
 }
857 857
 
858
+func (s *DockerSwarmSuite) TestSwarmServiceNetworkUpdate(c *check.C) {
859
+	d := s.AddDaemon(c, true, true)
860
+
861
+	result := icmd.RunCmd(d.Command("network", "create", "-d", "overlay", "foo"))
862
+	result.Assert(c, icmd.Success)
863
+	fooNetwork := strings.TrimSpace(string(result.Combined()))
864
+
865
+	result = icmd.RunCmd(d.Command("network", "create", "-d", "overlay", "bar"))
866
+	result.Assert(c, icmd.Success)
867
+	barNetwork := strings.TrimSpace(string(result.Combined()))
868
+
869
+	result = icmd.RunCmd(d.Command("network", "create", "-d", "overlay", "baz"))
870
+	result.Assert(c, icmd.Success)
871
+	bazNetwork := strings.TrimSpace(string(result.Combined()))
872
+
873
+	// Create a service
874
+	name := "top"
875
+	result = icmd.RunCmd(d.Command("service", "create", "--network", "foo", "--network", "bar", "--name", name, "busybox", "top"))
876
+	result.Assert(c, icmd.Success)
877
+
878
+	// Make sure task has been deployed.
879
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckRunningTaskNetworks, checker.DeepEquals,
880
+		map[string]int{fooNetwork: 1, barNetwork: 1})
881
+
882
+	// Remove a network
883
+	result = icmd.RunCmd(d.Command("service", "update", "--network-rm", "foo", name))
884
+	result.Assert(c, icmd.Success)
885
+
886
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckRunningTaskNetworks, checker.DeepEquals,
887
+		map[string]int{barNetwork: 1})
888
+
889
+	// Add a network
890
+	result = icmd.RunCmd(d.Command("service", "update", "--network-add", "baz", name))
891
+	result.Assert(c, icmd.Success)
892
+
893
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckRunningTaskNetworks, checker.DeepEquals,
894
+		map[string]int{barNetwork: 1, bazNetwork: 1})
895
+}
896
+
858 897
 func (s *DockerSwarmSuite) TestDNSConfig(c *check.C) {
859 898
 	d := s.AddDaemon(c, true, true)
860 899