Browse code

Change `docker service update` semantics

Change `docker service update` to replace attributes of the target
service rather than augment them. One particular occurrence where the
previous behavior proved problematic is when trying to update a port
mapping: the merge semantics provided no way of removing published
ports, but strictly of adding more.

The utility merge* functions where renamed accordingly to update*.

Signed-off-by: Arnaud Porterie (icecrime) <arnaud.porterie@docker.com>

Arnaud Porterie (icecrime) authored on 2016/06/18 08:24:07
Showing 3 changed files
... ...
@@ -46,7 +46,7 @@ func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID stri
46 46
 		return err
47 47
 	}
48 48
 
49
-	err = mergeService(&service.Spec, flags)
49
+	err = updateService(&service.Spec, flags)
50 50
 	if err != nil {
51 51
 		return err
52 52
 	}
... ...
@@ -59,52 +59,52 @@ func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID stri
59 59
 	return nil
60 60
 }
61 61
 
62
-func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
62
+func updateService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
63 63
 
64
-	mergeString := func(flag string, field *string) {
64
+	updateString := func(flag string, field *string) {
65 65
 		if flags.Changed(flag) {
66 66
 			*field, _ = flags.GetString(flag)
67 67
 		}
68 68
 	}
69 69
 
70
-	mergeListOpts := func(flag string, field *[]string) {
70
+	updateListOpts := func(flag string, field *[]string) {
71 71
 		if flags.Changed(flag) {
72 72
 			value := flags.Lookup(flag).Value.(*opts.ListOpts)
73 73
 			*field = value.GetAll()
74 74
 		}
75 75
 	}
76 76
 
77
-	mergeSlice := func(flag string, field *[]string) {
77
+	updateSlice := func(flag string, field *[]string) {
78 78
 		if flags.Changed(flag) {
79 79
 			*field, _ = flags.GetStringSlice(flag)
80 80
 		}
81 81
 	}
82 82
 
83
-	mergeInt64Value := func(flag string, field *int64) {
83
+	updateInt64Value := func(flag string, field *int64) {
84 84
 		if flags.Changed(flag) {
85 85
 			*field = flags.Lookup(flag).Value.(int64Value).Value()
86 86
 		}
87 87
 	}
88 88
 
89
-	mergeDuration := func(flag string, field *time.Duration) {
89
+	updateDuration := func(flag string, field *time.Duration) {
90 90
 		if flags.Changed(flag) {
91 91
 			*field, _ = flags.GetDuration(flag)
92 92
 		}
93 93
 	}
94 94
 
95
-	mergeDurationOpt := func(flag string, field *time.Duration) {
95
+	updateDurationOpt := func(flag string, field *time.Duration) {
96 96
 		if flags.Changed(flag) {
97 97
 			*field = *flags.Lookup(flag).Value.(*DurationOpt).Value()
98 98
 		}
99 99
 	}
100 100
 
101
-	mergeUint64 := func(flag string, field *uint64) {
101
+	updateUint64 := func(flag string, field *uint64) {
102 102
 		if flags.Changed(flag) {
103 103
 			*field, _ = flags.GetUint64(flag)
104 104
 		}
105 105
 	}
106 106
 
107
-	mergeUint64Opt := func(flag string, field *uint64) {
107
+	updateUint64Opt := func(flag string, field *uint64) {
108 108
 		if flags.Changed(flag) {
109 109
 			*field = *flags.Lookup(flag).Value.(*Uint64Opt).Value()
110 110
 		}
... ...
@@ -112,23 +112,23 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
112 112
 
113 113
 	cspec := &spec.TaskTemplate.ContainerSpec
114 114
 	task := &spec.TaskTemplate
115
-	mergeString(flagName, &spec.Name)
116
-	mergeLabels(flags, &spec.Labels)
117
-	mergeString("image", &cspec.Image)
118
-	mergeSlice("command", &cspec.Command)
119
-	mergeSlice("arg", &cspec.Command)
120
-	mergeListOpts("env", &cspec.Env)
121
-	mergeString("workdir", &cspec.Dir)
122
-	mergeString("user", &cspec.User)
123
-	mergeMounts(flags, &cspec.Mounts)
115
+	updateString(flagName, &spec.Name)
116
+	updateLabels(flags, &spec.Labels)
117
+	updateString("image", &cspec.Image)
118
+	updateSlice("command", &cspec.Command)
119
+	updateSlice("arg", &cspec.Command)
120
+	updateListOpts("env", &cspec.Env)
121
+	updateString("workdir", &cspec.Dir)
122
+	updateString("user", &cspec.User)
123
+	updateMounts(flags, &cspec.Mounts)
124 124
 
125 125
 	if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) {
126 126
 		if task.Resources == nil {
127 127
 			task.Resources = &swarm.ResourceRequirements{}
128 128
 		}
129 129
 		task.Resources.Limits = &swarm.Resources{}
130
-		mergeInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs)
131
-		mergeInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes)
130
+		updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs)
131
+		updateInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes)
132 132
 
133 133
 	}
134 134
 	if flags.Changed(flagReserveCPU) || flags.Changed(flagReserveMemory) {
... ...
@@ -136,11 +136,11 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
136 136
 			task.Resources = &swarm.ResourceRequirements{}
137 137
 		}
138 138
 		task.Resources.Reservations = &swarm.Resources{}
139
-		mergeInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs)
140
-		mergeInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
139
+		updateInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs)
140
+		updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
141 141
 	}
142 142
 
143
-	mergeDurationOpt("stop-grace-period", cspec.StopGracePeriod)
143
+	updateDurationOpt("stop-grace-period", cspec.StopGracePeriod)
144 144
 
145 145
 	if flags.Changed(flagRestartCondition) || flags.Changed(flagRestartDelay) || flags.Changed(flagRestartMaxAttempts) || flags.Changed(flagRestartWindow) {
146 146
 		if task.RestartPolicy == nil {
... ...
@@ -151,17 +151,17 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
151 151
 			value, _ := flags.GetString(flagRestartCondition)
152 152
 			task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value)
153 153
 		}
154
-		mergeDurationOpt(flagRestartDelay, task.RestartPolicy.Delay)
155
-		mergeUint64Opt(flagRestartMaxAttempts, task.RestartPolicy.MaxAttempts)
156
-		mergeDurationOpt((flagRestartWindow), task.RestartPolicy.Window)
154
+		updateDurationOpt(flagRestartDelay, task.RestartPolicy.Delay)
155
+		updateUint64Opt(flagRestartMaxAttempts, task.RestartPolicy.MaxAttempts)
156
+		updateDurationOpt((flagRestartWindow), task.RestartPolicy.Window)
157 157
 	}
158 158
 
159 159
 	if flags.Changed(flagConstraint) {
160 160
 		task.Placement = &swarm.Placement{}
161
-		mergeSlice(flagConstraint, &task.Placement.Constraints)
161
+		updateSlice(flagConstraint, &task.Placement.Constraints)
162 162
 	}
163 163
 
164
-	if err := mergeMode(flags, &spec.Mode); err != nil {
164
+	if err := updateMode(flags, &spec.Mode); err != nil {
165 165
 		return err
166 166
 	}
167 167
 
... ...
@@ -169,11 +169,11 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
169 169
 		if spec.UpdateConfig == nil {
170 170
 			spec.UpdateConfig = &swarm.UpdateConfig{}
171 171
 		}
172
-		mergeUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism)
173
-		mergeDuration(flagUpdateDelay, &spec.UpdateConfig.Delay)
172
+		updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism)
173
+		updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay)
174 174
 	}
175 175
 
176
-	mergeNetworks(flags, &spec.Networks)
176
+	updateNetworks(flags, &spec.Networks)
177 177
 	if flags.Changed(flagEndpointMode) {
178 178
 		value, _ := flags.GetString(flagEndpointMode)
179 179
 		spec.EndpointSpec.Mode = swarm.ResolutionMode(value)
... ...
@@ -183,38 +183,36 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
183 183
 		if spec.EndpointSpec == nil {
184 184
 			spec.EndpointSpec = &swarm.EndpointSpec{}
185 185
 		}
186
-		mergePorts(flags, &spec.EndpointSpec.Ports)
186
+		updatePorts(flags, &spec.EndpointSpec.Ports)
187 187
 	}
188 188
 	return nil
189 189
 }
190 190
 
191
-func mergeLabels(flags *pflag.FlagSet, field *map[string]string) {
191
+func updateLabels(flags *pflag.FlagSet, field *map[string]string) {
192 192
 	if !flags.Changed(flagLabel) {
193 193
 		return
194 194
 	}
195 195
 
196
-	if *field == nil {
197
-		*field = make(map[string]string)
198
-	}
199
-
200 196
 	values := flags.Lookup(flagLabel).Value.(*opts.ListOpts).GetAll()
197
+
198
+	localLabels := map[string]string{}
201 199
 	for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
202
-		(*field)[key] = value
200
+		localLabels[key] = value
203 201
 	}
202
+	*field = localLabels
204 203
 }
205 204
 
206 205
 // TODO: should this override by destination path, or does swarm handle that?
207
-func mergeMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
206
+func updateMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
208 207
 	if !flags.Changed(flagMount) {
209 208
 		return
210 209
 	}
211 210
 
212
-	values := flags.Lookup(flagMount).Value.(*MountOpt).Value()
213
-	*mounts = append(*mounts, values...)
211
+	*mounts = flags.Lookup(flagMount).Value.(*MountOpt).Value()
214 212
 }
215 213
 
216 214
 // TODO: should this override by name, or does swarm handle that?
217
-func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
215
+func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
218 216
 	if !flags.Changed(flagPublish) {
219 217
 		return
220 218
 	}
... ...
@@ -222,22 +220,27 @@ func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
222 222
 	values := flags.Lookup(flagPublish).Value.(*opts.ListOpts).GetAll()
223 223
 	ports, portBindings, _ := nat.ParsePortSpecs(values)
224 224
 
225
+	var localPortConfig []swarm.PortConfig
225 226
 	for port := range ports {
226
-		*portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...)
227
+		localPortConfig = append(localPortConfig, convertPortToPortConfig(port, portBindings)...)
227 228
 	}
229
+	*portConfig = localPortConfig
228 230
 }
229 231
 
230
-func mergeNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
232
+func updateNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
231 233
 	if !flags.Changed(flagNetwork) {
232 234
 		return
233 235
 	}
234 236
 	networks, _ := flags.GetStringSlice(flagNetwork)
237
+
238
+	var localAttachments []swarm.NetworkAttachmentConfig
235 239
 	for _, network := range networks {
236
-		*attachments = append(*attachments, swarm.NetworkAttachmentConfig{Target: network})
240
+		localAttachments = append(localAttachments, swarm.NetworkAttachmentConfig{Target: network})
237 241
 	}
242
+	*attachments = localAttachments
238 243
 }
239 244
 
240
-func mergeMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
245
+func updateMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
241 246
 	if !flags.Changed(flagMode) && !flags.Changed(flagReplicas) {
242 247
 		return nil
243 248
 	}
244 249
new file mode 100644
... ...
@@ -0,0 +1,39 @@
0
+// +build !windows
1
+
2
+package main
3
+
4
+import (
5
+	"github.com/docker/docker/pkg/integration/checker"
6
+	"github.com/docker/engine-api/types/swarm"
7
+	"github.com/go-check/check"
8
+)
9
+
10
+func setPortConfig(portConfig []swarm.PortConfig) serviceConstructor {
11
+	return func(s *swarm.Service) {
12
+		if s.Spec.EndpointSpec == nil {
13
+			s.Spec.EndpointSpec = &swarm.EndpointSpec{}
14
+		}
15
+		s.Spec.EndpointSpec.Ports = portConfig
16
+	}
17
+}
18
+
19
+func (s *DockerSwarmSuite) TestApiServiceUpdatePort(c *check.C) {
20
+	d := s.AddDaemon(c, true, true)
21
+
22
+	// Create a service with a port mapping of 8080:8081.
23
+	portConfig := []swarm.PortConfig{{TargetPort: 8081, PublishedPort: 8080}}
24
+	serviceID := d.createService(c, simpleTestService, setInstances(1), setPortConfig(portConfig))
25
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
26
+
27
+	// Update the service: changed the port mapping from 8080:8081 to 8082:8083.
28
+	updatedPortConfig := []swarm.PortConfig{{TargetPort: 8083, PublishedPort: 8082}}
29
+	remoteService := d.getService(c, serviceID)
30
+	d.updateService(c, remoteService, setPortConfig(updatedPortConfig))
31
+
32
+	// Inspect the service and verify port mapping.
33
+	updatedService := d.getService(c, serviceID)
34
+	c.Assert(updatedService.Spec.EndpointSpec, check.NotNil)
35
+	c.Assert(len(updatedService.Spec.EndpointSpec.Ports), check.Equals, 1)
36
+	c.Assert(updatedService.Spec.EndpointSpec.Ports[0].TargetPort, check.Equals, uint32(8083))
37
+	c.Assert(updatedService.Spec.EndpointSpec.Ports[0].PublishedPort, check.Equals, uint32(8082))
38
+}
0 39
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+// +build !windows
1
+
2
+package main
3
+
4
+import (
5
+	"encoding/json"
6
+
7
+	"github.com/docker/docker/pkg/integration/checker"
8
+	"github.com/docker/engine-api/types/swarm"
9
+	"github.com/go-check/check"
10
+)
11
+
12
+func (s *DockerSwarmSuite) TestServiceUpdatePort(c *check.C) {
13
+	d := s.AddDaemon(c, true, true)
14
+
15
+	serviceName := "TestServiceUpdatePort"
16
+	serviceArgs := append([]string{"create", "--name", serviceName, "-p", "8080:8081", defaultSleepImage}, defaultSleepCommand...)
17
+
18
+	// Create a service with a port mapping of 8080:8081.
19
+	out, err := d.Cmd("service", serviceArgs...)
20
+	c.Assert(err, checker.IsNil)
21
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
22
+
23
+	// Update the service: changed the port mapping from 8080:8081 to 8082:8083.
24
+	_, err = d.Cmd("service", "update", "-p", "8082:8083", serviceName)
25
+	c.Assert(err, checker.IsNil)
26
+
27
+	// Inspect the service and verify port mapping
28
+	expected := []swarm.PortConfig{
29
+		{
30
+			Protocol:      "tcp",
31
+			PublishedPort: 8082,
32
+			TargetPort:    8083,
33
+		},
34
+	}
35
+
36
+	out, err = d.Cmd("service", "inspect", "--format", "{{ json .Spec.EndpointSpec.Ports }}", serviceName)
37
+	c.Assert(err, checker.IsNil)
38
+
39
+	var portConfig []swarm.PortConfig
40
+	if err := json.Unmarshal([]byte(out), &portConfig); err != nil {
41
+		c.Fatalf("invalid JSON in inspect result: %v (%s)", err, out)
42
+	}
43
+	c.Assert(portConfig, checker.DeepEquals, expected)
44
+}