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