Browse code

Idempotent service update --publish-add

This fix tries to address the issue raised in 25375 where
`service update --publish-add` returns an error if the exact
same value is repeated (idempotent).

This fix use a map to filter out repeated port configs so
that `--publish-add` does not error out.

An integration test has been added.

This fix fixes 25375.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/08/05 12:51:28
Showing 3 changed files
... ...
@@ -2,6 +2,7 @@ package service
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"sort"
5 6
 	"strings"
6 7
 	"time"
7 8
 
... ...
@@ -216,7 +217,9 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
216 216
 		if spec.EndpointSpec == nil {
217 217
 			spec.EndpointSpec = &swarm.EndpointSpec{}
218 218
 		}
219
-		updatePorts(flags, &spec.EndpointSpec.Ports)
219
+		if err := updatePorts(flags, &spec.EndpointSpec.Ports); err != nil {
220
+			return err
221
+		}
220 222
 	}
221 223
 
222 224
 	if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil {
... ...
@@ -369,23 +372,54 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
369 369
 	*mounts = newMounts
370 370
 }
371 371
 
372
-func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
372
+type byPortConfig []swarm.PortConfig
373
+
374
+func (r byPortConfig) Len() int      { return len(r) }
375
+func (r byPortConfig) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
376
+func (r byPortConfig) Less(i, j int) bool {
377
+	// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
378
+	// In updatePorts we already filter out with map so there is duplicate entries
379
+	return portConfigToString(&r[i]) < portConfigToString(&r[j])
380
+}
381
+
382
+func portConfigToString(portConfig *swarm.PortConfig) string {
383
+	protocol := portConfig.Protocol
384
+	if protocol == "" {
385
+		protocol = "tcp"
386
+	}
387
+	return fmt.Sprintf("%v/%s", portConfig.PublishedPort, protocol)
388
+}
389
+
390
+func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
391
+	// The key of the map is `port/protocol`, e.g., `80/tcp`
392
+	portSet := map[string]swarm.PortConfig{}
393
+	// Check to see if there are any conflict in flags.
373 394
 	if flags.Changed(flagPublishAdd) {
374 395
 		values := flags.Lookup(flagPublishAdd).Value.(*opts.ListOpts).GetAll()
375 396
 		ports, portBindings, _ := nat.ParsePortSpecs(values)
376 397
 
377 398
 		for port := range ports {
378
-			*portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...)
399
+			newConfigs := convertPortToPortConfig(port, portBindings)
400
+			for _, entry := range newConfigs {
401
+				if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry {
402
+					return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol)
403
+				}
404
+				portSet[portConfigToString(&entry)] = entry
405
+			}
379 406
 		}
380 407
 	}
381 408
 
382
-	if !flags.Changed(flagPublishRemove) {
383
-		return
409
+	// Override previous PortConfig in service if there is any duplicate
410
+	for _, entry := range *portConfig {
411
+		if _, ok := portSet[portConfigToString(&entry)]; !ok {
412
+			portSet[portConfigToString(&entry)] = entry
413
+		}
384 414
 	}
415
+
385 416
 	toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll()
386 417
 	newPorts := []swarm.PortConfig{}
387 418
 portLoop:
388
-	for _, port := range *portConfig {
419
+	for _, port := range portSet {
389 420
 		for _, rawTargetPort := range toRemove {
390 421
 			targetPort := nat.Port(rawTargetPort)
391 422
 			if equalPort(targetPort, port) {
... ...
@@ -394,7 +428,10 @@ portLoop:
394 394
 		}
395 395
 		newPorts = append(newPorts, port)
396 396
 	}
397
+	// Sort the PortConfig to avoid unnecessary updates
398
+	sort.Sort(byPortConfig(newPorts))
397 399
 	*portConfig = newPorts
400
+	return nil
398 401
 }
399 402
 
400 403
 func equalPort(targetPort nat.Port, port swarm.PortConfig) bool {
... ...
@@ -141,8 +141,56 @@ func TestUpdatePorts(t *testing.T) {
141 141
 		{TargetPort: 555},
142 142
 	}
143 143
 
144
-	updatePorts(flags, &portConfigs)
144
+	err := updatePorts(flags, &portConfigs)
145
+	assert.Equal(t, err, nil)
145 146
 	assert.Equal(t, len(portConfigs), 2)
146
-	assert.Equal(t, portConfigs[0].TargetPort, uint32(555))
147
-	assert.Equal(t, portConfigs[1].TargetPort, uint32(1000))
147
+	// Do a sort to have the order (might have changed by map)
148
+	targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)}
149
+	sort.Ints(targetPorts)
150
+	assert.Equal(t, targetPorts[0], 555)
151
+	assert.Equal(t, targetPorts[1], 1000)
152
+}
153
+
154
+func TestUpdatePortsDuplicateEntries(t *testing.T) {
155
+	// Test case for #25375
156
+	flags := newUpdateCommand(nil).Flags()
157
+	flags.Set("publish-add", "80:80")
158
+
159
+	portConfigs := []swarm.PortConfig{
160
+		{TargetPort: 80, PublishedPort: 80},
161
+	}
162
+
163
+	err := updatePorts(flags, &portConfigs)
164
+	assert.Equal(t, err, nil)
165
+	assert.Equal(t, len(portConfigs), 1)
166
+	assert.Equal(t, portConfigs[0].TargetPort, uint32(80))
167
+}
168
+
169
+func TestUpdatePortsDuplicateKeys(t *testing.T) {
170
+	// Test case for #25375
171
+	flags := newUpdateCommand(nil).Flags()
172
+	flags.Set("publish-add", "80:20")
173
+
174
+	portConfigs := []swarm.PortConfig{
175
+		{TargetPort: 80, PublishedPort: 80},
176
+	}
177
+
178
+	err := updatePorts(flags, &portConfigs)
179
+	assert.Equal(t, err, nil)
180
+	assert.Equal(t, len(portConfigs), 1)
181
+	assert.Equal(t, portConfigs[0].TargetPort, uint32(20))
182
+}
183
+
184
+func TestUpdatePortsConflictingFlags(t *testing.T) {
185
+	// Test case for #25375
186
+	flags := newUpdateCommand(nil).Flags()
187
+	flags.Set("publish-add", "80:80")
188
+	flags.Set("publish-add", "80:20")
189
+
190
+	portConfigs := []swarm.PortConfig{
191
+		{TargetPort: 80, PublishedPort: 80},
192
+	}
193
+
194
+	err := updatePorts(flags, &portConfigs)
195
+	assert.Error(t, err, "conflicting port mapping")
148 196
 }
... ...
@@ -194,3 +194,29 @@ func (s *DockerSwarmSuite) TestSwarmNodeTaskListFilter(c *check.C) {
194 194
 	c.Assert(out, checker.Not(checker.Contains), name+".2")
195 195
 	c.Assert(out, checker.Not(checker.Contains), name+".3")
196 196
 }
197
+
198
+// Test case for #25375
199
+func (s *DockerSwarmSuite) TestSwarmPublishAdd(c *check.C) {
200
+	d := s.AddDaemon(c, true, true)
201
+
202
+	name := "top"
203
+	out, err := d.Cmd("service", "create", "--name", name, "--label", "x=y", "busybox", "top")
204
+	c.Assert(err, checker.IsNil)
205
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
206
+
207
+	out, err = d.Cmd("service", "update", "--publish-add", "80:80", name)
208
+	c.Assert(err, checker.IsNil)
209
+
210
+	out, err = d.Cmd("service", "update", "--publish-add", "80:80", name)
211
+	c.Assert(err, checker.IsNil)
212
+
213
+	out, err = d.Cmd("service", "update", "--publish-add", "80:80", "--publish-add", "80:20", name)
214
+	c.Assert(err, checker.NotNil)
215
+
216
+	out, err = d.Cmd("service", "update", "--publish-add", "80:20", name)
217
+	c.Assert(err, checker.IsNil)
218
+
219
+	out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", name)
220
+	c.Assert(err, checker.IsNil)
221
+	c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 20 80}]")
222
+}