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