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>
... | ... |
@@ -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 |
|