Browse code

Merge pull request #28943 from vdemeester/publish-long-short-syntax

Remove --port and update --publish for services to support syntaxes

Sebastiaan van Stijn authored on 2016/12/15 01:55:48
Showing 11 changed files
... ...
@@ -72,7 +72,7 @@ To manually remove all plugins and resolve this problem, take the following step
72 72
 ### Networking
73 73
 
74 74
 + Add `--attachable` network support to enable `docker run` to work in swarm-mode overlay network [#25962](https://github.com/docker/docker/pull/25962)
75
-+ Add support for host port PublishMode in services using the `--port` option in `docker service create` [#27917](https://github.com/docker/docker/pull/27917)
75
++ Add support for host port PublishMode in services using the `--publish` option in `docker service create` [#27917](https://github.com/docker/docker/pull/27917) and [#28943](https://github.com/docker/docker/pull/28943)
76 76
 + Add support for Windows server 2016 overlay network driver (requires upcoming ws2016 update) [#28182](https://github.com/docker/docker/pull/28182)
77 77
 * Change the default `FORWARD` policy to `DROP` [#28257](https://github.com/docker/docker/pull/28257)
78 78
 + Add support for specifying static IP addresses for predefined network on windows [#22208](https://github.com/docker/docker/pull/22208)
... ...
@@ -40,13 +40,11 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
40 40
 	flags.Var(&opts.networks, flagNetwork, "Network attachments")
41 41
 	flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
42 42
 	flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port")
43
-	flags.MarkHidden(flagPublish)
44 43
 	flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
45 44
 	flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
46 45
 	flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options")
47 46
 	flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
48 47
 	flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
49
-	flags.Var(&opts.endpoint.expandedPorts, flagPort, "Publish a port")
50 48
 
51 49
 	flags.SetInterspersed(false)
52 50
 	return cmd
... ...
@@ -287,45 +287,17 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
287 287
 }
288 288
 
289 289
 type endpointOptions struct {
290
-	mode          string
291
-	publishPorts  opts.ListOpts
292
-	expandedPorts opts.PortOpt
290
+	mode         string
291
+	publishPorts opts.PortOpt
293 292
 }
294 293
 
295 294
 func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
296
-	portConfigs := []swarm.PortConfig{}
297
-	// We can ignore errors because the format was already validated by ValidatePort
298
-	ports, portBindings, _ := nat.ParsePortSpecs(e.publishPorts.GetAll())
299
-
300
-	for port := range ports {
301
-		portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...)
302
-	}
303
-
304 295
 	return &swarm.EndpointSpec{
305 296
 		Mode:  swarm.ResolutionMode(strings.ToLower(e.mode)),
306
-		Ports: append(portConfigs, e.expandedPorts.Value()...),
297
+		Ports: e.publishPorts.Value(),
307 298
 	}
308 299
 }
309 300
 
310
-// ConvertPortToPortConfig converts ports to the swarm type
311
-func ConvertPortToPortConfig(
312
-	port nat.Port,
313
-	portBindings map[nat.Port][]nat.PortBinding,
314
-) []swarm.PortConfig {
315
-	ports := []swarm.PortConfig{}
316
-
317
-	for _, binding := range portBindings[port] {
318
-		hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
319
-		ports = append(ports, swarm.PortConfig{
320
-			//TODO Name: ?
321
-			Protocol:      swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
322
-			TargetPort:    uint32(port.Int()),
323
-			PublishedPort: uint32(hostPort),
324
-		})
325
-	}
326
-	return ports
327
-}
328
-
329 301
 type logDriverOptions struct {
330 302
 	name string
331 303
 	opts opts.ListOpts
... ...
@@ -459,16 +431,13 @@ func newServiceOptions() *serviceOptions {
459 459
 		containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv),
460 460
 		env:             opts.NewListOpts(runconfigopts.ValidateEnv),
461 461
 		envFile:         opts.NewListOpts(nil),
462
-		endpoint: endpointOptions{
463
-			publishPorts: opts.NewListOpts(ValidatePort),
464
-		},
465
-		groups:    opts.NewListOpts(nil),
466
-		logDriver: newLogDriverOptions(),
467
-		dns:       opts.NewListOpts(opts.ValidateIPAddress),
468
-		dnsOption: opts.NewListOpts(nil),
469
-		dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
470
-		hosts:     opts.NewListOpts(runconfigopts.ValidateExtraHost),
471
-		networks:  opts.NewListOpts(nil),
462
+		groups:          opts.NewListOpts(nil),
463
+		logDriver:       newLogDriverOptions(),
464
+		dns:             opts.NewListOpts(opts.ValidateIPAddress),
465
+		dnsOption:       opts.NewListOpts(nil),
466
+		dnsSearch:       opts.NewListOpts(opts.ValidateDNSSearch),
467
+		hosts:           opts.NewListOpts(runconfigopts.ValidateExtraHost),
468
+		networks:        opts.NewListOpts(nil),
472 469
 	}
473 470
 }
474 471
 
... ...
@@ -649,9 +618,6 @@ const (
649 649
 	flagPublish               = "publish"
650 650
 	flagPublishRemove         = "publish-rm"
651 651
 	flagPublishAdd            = "publish-add"
652
-	flagPort                  = "port"
653
-	flagPortAdd               = "port-add"
654
-	flagPortRemove            = "port-rm"
655 652
 	flagReplicas              = "replicas"
656 653
 	flagReserveCPU            = "reserve-cpu"
657 654
 	flagReserveMemory         = "reserve-memory"
... ...
@@ -24,7 +24,7 @@ import (
24 24
 )
25 25
 
26 26
 func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
27
-	opts := newServiceOptions()
27
+	serviceOpts := newServiceOptions()
28 28
 
29 29
 	cmd := &cobra.Command{
30 30
 		Use:   "update [OPTIONS] SERVICE",
... ...
@@ -40,36 +40,33 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
40 40
 	flags.String("args", "", "Service command args")
41 41
 	flags.Bool("rollback", false, "Rollback to previous specification")
42 42
 	flags.Bool("force", false, "Force update even if no changes require it")
43
-	addServiceFlags(cmd, opts)
43
+	addServiceFlags(cmd, serviceOpts)
44 44
 
45 45
 	flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable")
46 46
 	flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container")
47 47
 	flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key")
48 48
 	flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key")
49 49
 	flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path")
50
-	flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port")
51
-	flags.MarkHidden(flagPublishRemove)
52
-	flags.Var(newListOptsVar(), flagPortRemove, "Remove a port(target-port mandatory)")
50
+	// flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port")
51
+	flags.Var(&opts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port")
53 52
 	flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
54 53
 	flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server")
55 54
 	flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option")
56 55
 	flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain")
57 56
 	flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)")
58
-	flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label")
59
-	flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label")
60
-	flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable")
57
+	flags.Var(&serviceOpts.labels, flagLabelAdd, "Add or update a service label")
58
+	flags.Var(&serviceOpts.containerLabels, flagContainerLabelAdd, "Add or update a container label")
59
+	flags.Var(&serviceOpts.env, flagEnvAdd, "Add or update an environment variable")
61 60
 	flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret")
62
-	flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service")
63
-	flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service")
64
-	flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint")
65
-	flags.Var(&opts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port")
66
-	flags.MarkHidden(flagPublishAdd)
67
-	flags.Var(&opts.endpoint.expandedPorts, flagPortAdd, "Add or update a port")
68
-	flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container")
69
-	flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server")
70
-	flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option")
71
-	flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain")
72
-	flags.Var(&opts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)")
61
+	flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service")
62
+	flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service")
63
+	flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint")
64
+	flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port")
65
+	flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container")
66
+	flags.Var(&serviceOpts.dns, flagDNSAdd, "Add or update a custom DNS server")
67
+	flags.Var(&serviceOpts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option")
68
+	flags.Var(&serviceOpts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain")
69
+	flags.Var(&serviceOpts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)")
73 70
 
74 71
 	return cmd
75 72
 }
... ...
@@ -276,7 +273,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
276 276
 		}
277 277
 	}
278 278
 
279
-	if anyChanged(flags, flagPublishAdd, flagPublishRemove, flagPortAdd, flagPortRemove) {
279
+	if anyChanged(flags, flagPublishAdd, flagPublishRemove) {
280 280
 		if spec.EndpointSpec == nil {
281 281
 			spec.EndpointSpec = &swarm.EndpointSpec{}
282 282
 		}
... ...
@@ -633,18 +630,11 @@ func (r byPortConfig) Less(i, j int) bool {
633 633
 
634 634
 func portConfigToString(portConfig *swarm.PortConfig) string {
635 635
 	protocol := portConfig.Protocol
636
-	if protocol == "" {
637
-		protocol = "tcp"
638
-	}
639
-
640 636
 	mode := portConfig.PublishMode
641
-	if mode == "" {
642
-		mode = "ingress"
643
-	}
644
-
645 637
 	return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode)
646 638
 }
647 639
 
640
+// FIXME(vdemeester) port to opts.PortOpt
648 641
 // This validation is only used for `--publish-rm`.
649 642
 // The `--publish-rm` takes:
650 643
 // <TargetPort>[/<Protocol>] (e.g., 80, 80/tcp, 53/udp)
... ...
@@ -665,58 +655,21 @@ func validatePublishRemove(val string) (string, error) {
665 665
 func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
666 666
 	// The key of the map is `port/protocol`, e.g., `80/tcp`
667 667
 	portSet := map[string]swarm.PortConfig{}
668
-	// Check to see if there are any conflict in flags.
669
-	if flags.Changed(flagPublishAdd) {
670
-		values := flags.Lookup(flagPublishAdd).Value.(*opts.ListOpts).GetAll()
671
-		ports, portBindings, _ := nat.ParsePortSpecs(values)
672
-
673
-		for port := range ports {
674
-			newConfigs := ConvertPortToPortConfig(port, portBindings)
675
-			for _, entry := range newConfigs {
676
-				if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry {
677
-					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)
678
-				}
679
-				portSet[portConfigToString(&entry)] = entry
680
-			}
681
-		}
682
-	}
683
-
684
-	if flags.Changed(flagPortAdd) {
685
-		for _, entry := range flags.Lookup(flagPortAdd).Value.(*opts.PortOpt).Value() {
686
-			if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry {
687
-				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)
688
-			}
689
-			portSet[portConfigToString(&entry)] = entry
690
-		}
691
-	}
692 668
 
693
-	// Override previous PortConfig in service if there is any duplicate
669
+	// Build the current list of portConfig
694 670
 	for _, entry := range *portConfig {
695 671
 		if _, ok := portSet[portConfigToString(&entry)]; !ok {
696 672
 			portSet[portConfigToString(&entry)] = entry
697 673
 		}
698 674
 	}
699 675
 
700
-	toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll()
701
-	removePortCSV := flags.Lookup(flagPortRemove).Value.(*opts.ListOpts).GetAll()
702
-	removePortOpts := &opts.PortOpt{}
703
-	for _, portCSV := range removePortCSV {
704
-		if err := removePortOpts.Set(portCSV); err != nil {
705
-			return err
706
-		}
707
-	}
708
-
709 676
 	newPorts := []swarm.PortConfig{}
677
+
678
+	// Clean current ports
679
+	toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value()
710 680
 portLoop:
711 681
 	for _, port := range portSet {
712
-		for _, rawTargetPort := range toRemove {
713
-			targetPort := nat.Port(rawTargetPort)
714
-			if equalPort(targetPort, port) {
715
-				continue portLoop
716
-			}
717
-		}
718
-
719
-		for _, pConfig := range removePortOpts.Value() {
682
+		for _, pConfig := range toRemove {
720 683
 			if equalProtocol(port.Protocol, pConfig.Protocol) &&
721 684
 				port.TargetPort == pConfig.TargetPort &&
722 685
 				equalPublishMode(port.PublishMode, pConfig.PublishMode) {
... ...
@@ -727,6 +680,23 @@ portLoop:
727 727
 		newPorts = append(newPorts, port)
728 728
 	}
729 729
 
730
+	// Check to see if there are any conflict in flags.
731
+	if flags.Changed(flagPublishAdd) {
732
+		ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value()
733
+
734
+		for _, port := range ports {
735
+			if v, ok := portSet[portConfigToString(&port)]; ok {
736
+				if v != port {
737
+					fmt.Println("v", v)
738
+					return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", port.PublishedPort, port.TargetPort, port.Protocol, v.PublishedPort, v.TargetPort, v.Protocol)
739
+				}
740
+				continue
741
+			}
742
+			//portSet[portConfigToString(&port)] = port
743
+			newPorts = append(newPorts, port)
744
+		}
745
+	}
746
+
730 747
 	// Sort the PortConfig to avoid unnecessary updates
731 748
 	sort.Sort(byPortConfig(newPorts))
732 749
 	*portConfig = newPorts
... ...
@@ -220,28 +220,18 @@ func TestUpdatePorts(t *testing.T) {
220 220
 	assert.Equal(t, targetPorts[1], 1000)
221 221
 }
222 222
 
223
-func TestUpdatePortsDuplicateEntries(t *testing.T) {
223
+func TestUpdatePortsDuplicate(t *testing.T) {
224 224
 	// Test case for #25375
225 225
 	flags := newUpdateCommand(nil).Flags()
226 226
 	flags.Set("publish-add", "80:80")
227 227
 
228 228
 	portConfigs := []swarm.PortConfig{
229
-		{TargetPort: 80, PublishedPort: 80},
230
-	}
231
-
232
-	err := updatePorts(flags, &portConfigs)
233
-	assert.Equal(t, err, nil)
234
-	assert.Equal(t, len(portConfigs), 1)
235
-	assert.Equal(t, portConfigs[0].TargetPort, uint32(80))
236
-}
237
-
238
-func TestUpdatePortsDuplicateKeys(t *testing.T) {
239
-	// Test case for #25375
240
-	flags := newUpdateCommand(nil).Flags()
241
-	flags.Set("publish-add", "80:80")
242
-
243
-	portConfigs := []swarm.PortConfig{
244
-		{TargetPort: 80, PublishedPort: 80},
229
+		{
230
+			TargetPort:    80,
231
+			PublishedPort: 80,
232
+			Protocol:      swarm.PortConfigProtocolTCP,
233
+			PublishMode:   swarm.PortConfigPublishModeIngress,
234
+		},
245 235
 	}
246 236
 
247 237
 	err := updatePorts(flags, &portConfigs)
... ...
@@ -355,15 +345,22 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) {
355 355
 	flags.Set("publish-rm", "82/udp")
356 356
 
357 357
 	portConfigs := []swarm.PortConfig{
358
-		{TargetPort: 80, PublishedPort: 8080, Protocol: swarm.PortConfigProtocolTCP},
358
+		{
359
+			TargetPort:    80,
360
+			PublishedPort: 8080,
361
+			Protocol:      swarm.PortConfigProtocolTCP,
362
+			PublishMode:   swarm.PortConfigPublishModeIngress,
363
+		},
359 364
 	}
360 365
 
361 366
 	err := updatePorts(flags, &portConfigs)
362 367
 	assert.Equal(t, err, nil)
363
-	assert.Equal(t, len(portConfigs), 1)
364
-	assert.Equal(t, portConfigs[0].TargetPort, uint32(82))
368
+	assert.Equal(t, len(portConfigs), 2)
369
+	assert.Equal(t, portConfigs[0].TargetPort, uint32(81))
370
+	assert.Equal(t, portConfigs[1].TargetPort, uint32(82))
365 371
 }
366 372
 
373
+// FIXME(vdemeester) port to opts.PortOpt
367 374
 func TestValidatePort(t *testing.T) {
368 375
 	validPorts := []string{"80/tcp", "80", "80/udp"}
369 376
 	invalidPorts := map[string]string{
... ...
@@ -21,7 +21,6 @@ import (
21 21
 	"github.com/docker/docker/api/types/swarm"
22 22
 	"github.com/docker/docker/cli"
23 23
 	"github.com/docker/docker/cli/command"
24
-	servicecmd "github.com/docker/docker/cli/command/service"
25 24
 	dockerclient "github.com/docker/docker/client"
26 25
 	"github.com/docker/docker/opts"
27 26
 	runconfigopts "github.com/docker/docker/runconfig/opts"
... ...
@@ -745,7 +744,7 @@ func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
745 745
 	for port := range ports {
746 746
 		portConfigs = append(
747 747
 			portConfigs,
748
-			servicecmd.ConvertPortToPortConfig(port, portBindings)...)
748
+			opts.ConvertPortToPortConfig(port, portBindings)...)
749 749
 	}
750 750
 
751 751
 	return &swarm.EndpointSpec{Ports: portConfigs}, nil
... ...
@@ -2756,7 +2756,7 @@ _docker_service_update() {
2756 2756
 			--host
2757 2757
 			--mode
2758 2758
 			--name
2759
-			--port
2759
+			--publish
2760 2760
 			--secret
2761 2761
 		"
2762 2762
 
... ...
@@ -2803,8 +2803,8 @@ _docker_service_update() {
2803 2803
 			--host-add
2804 2804
 			--host-rm
2805 2805
 			--image
2806
-			--port-add
2807
-			--port-rm
2806
+			--publish-add
2807
+			--publish-rm
2808 2808
 			--secret-add
2809 2809
 			--secret-rm
2810 2810
 		"
... ...
@@ -1797,7 +1797,7 @@ __docker_service_subcommand() {
1797 1797
                 "($help)*--env-file=[Read environment variables from a file]:environment file:_files" \
1798 1798
                 "($help)--mode=[Service Mode]:mode:(global replicated)" \
1799 1799
                 "($help)--name=[Service name]:name: " \
1800
-                "($help)*--port=[Publish a port]:port: " \
1800
+                "($help)*--publish=[Publish a port]:port: " \
1801 1801
                 "($help -): :__docker_complete_images" \
1802 1802
                 "($help -):command: _command_names -e" \
1803 1803
                 "($help -)*::arguments: _normal" && ret=0
... ...
@@ -1870,8 +1870,8 @@ __docker_service_subcommand() {
1870 1870
                 "($help)*--group-add=[Add additional supplementary user groups to the container]:group:_groups" \
1871 1871
                 "($help)*--group-rm=[Remove previously added supplementary user groups from the container]:group:_groups" \
1872 1872
                 "($help)--image=[Service image tag]:image:__docker_complete_repositories" \
1873
-                "($help)*--port-add=[Add or update a port]:port: " \
1874
-                "($help)*--port-rm=[Remove a port(target-port mandatory)]:port: " \
1873
+                "($help)*--publish-add=[Add or update a port]:port: " \
1874
+                "($help)*--publish-rm=[Remove a port(target-port mandatory)]:port: " \
1875 1875
                 "($help)--rollback[Rollback to previous specification]" \
1876 1876
                 "($help -)1:service:__docker_complete_services" && ret=0
1877 1877
             ;;
... ...
@@ -235,23 +235,51 @@ func (s *DockerSwarmSuite) TestSwarmNodeTaskListFilter(c *check.C) {
235 235
 func (s *DockerSwarmSuite) TestSwarmPublishAdd(c *check.C) {
236 236
 	d := s.AddDaemon(c, true, true)
237 237
 
238
-	name := "top"
239
-	out, err := d.Cmd("service", "create", "--name", name, "--label", "x=y", "busybox", "top")
240
-	c.Assert(err, checker.IsNil)
241
-	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
238
+	testCases := []struct {
239
+		name       string
240
+		publishAdd []string
241
+		ports      string
242
+	}{
243
+		{
244
+			name: "simple-syntax",
245
+			publishAdd: []string{
246
+				"80:80",
247
+				"80:80",
248
+				"80:80",
249
+				"80:20",
250
+			},
251
+			ports: "[{ tcp 80 80 ingress}]",
252
+		},
253
+		{
254
+			name: "complex-syntax",
255
+			publishAdd: []string{
256
+				"target=90,published=90,protocol=tcp,mode=ingress",
257
+				"target=90,published=90,protocol=tcp,mode=ingress",
258
+				"target=90,published=90,protocol=tcp,mode=ingress",
259
+				"target=30,published=90,protocol=tcp,mode=ingress",
260
+			},
261
+			ports: "[{ tcp 90 90 ingress}]",
262
+		},
263
+	}
242 264
 
243
-	out, err = d.Cmd("service", "update", "--publish-add", "80:80", name)
244
-	c.Assert(err, checker.IsNil)
265
+	for _, tc := range testCases {
266
+		out, err := d.Cmd("service", "create", "--name", tc.name, "--label", "x=y", "busybox", "top")
267
+		c.Assert(err, checker.IsNil, check.Commentf(out))
268
+		c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
245 269
 
246
-	out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", "80:80", name)
247
-	c.Assert(err, checker.IsNil)
270
+		out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", tc.publishAdd[0], tc.name)
271
+		c.Assert(err, checker.IsNil, check.Commentf(out))
248 272
 
249
-	out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", "80:80", "--publish-add", "80:20", name)
250
-	c.Assert(err, checker.NotNil)
273
+		out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", tc.publishAdd[1], tc.name)
274
+		c.Assert(err, checker.IsNil, check.Commentf(out))
251 275
 
252
-	out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", name)
253
-	c.Assert(err, checker.IsNil)
254
-	c.Assert(strings.TrimSpace(out), checker.Equals, "[{ tcp 80 80 ingress}]")
276
+		out, err = d.CmdRetryOutOfSequence("service", "update", "--publish-add", tc.publishAdd[2], "--publish-add", tc.publishAdd[3], tc.name)
277
+		c.Assert(err, checker.NotNil, check.Commentf(out))
278
+
279
+		out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.EndpointSpec.Ports }}", tc.name)
280
+		c.Assert(err, checker.IsNil)
281
+		c.Assert(strings.TrimSpace(out), checker.Equals, tc.ports)
282
+	}
255 283
 }
256 284
 
257 285
 func (s *DockerSwarmSuite) TestSwarmServiceWithGroup(c *check.C) {
... ...
@@ -3,10 +3,12 @@ package opts
3 3
 import (
4 4
 	"encoding/csv"
5 5
 	"fmt"
6
+	"regexp"
6 7
 	"strconv"
7 8
 	"strings"
8 9
 
9 10
 	"github.com/docker/docker/api/types/swarm"
11
+	"github.com/docker/go-connections/nat"
10 12
 )
11 13
 
12 14
 const (
... ...
@@ -23,59 +25,83 @@ type PortOpt struct {
23 23
 
24 24
 // Set a new port value
25 25
 func (p *PortOpt) Set(value string) error {
26
-	csvReader := csv.NewReader(strings.NewReader(value))
27
-	fields, err := csvReader.Read()
26
+	longSyntax, err := regexp.MatchString(`\w+=\w+(,\w+=\w+)*`, value)
28 27
 	if err != nil {
29 28
 		return err
30 29
 	}
31
-
32
-	pConfig := swarm.PortConfig{}
33
-	for _, field := range fields {
34
-		parts := strings.SplitN(field, "=", 2)
35
-		if len(parts) != 2 {
36
-			return fmt.Errorf("invalid field %s", field)
30
+	if longSyntax {
31
+		csvReader := csv.NewReader(strings.NewReader(value))
32
+		fields, err := csvReader.Read()
33
+		if err != nil {
34
+			return err
37 35
 		}
38 36
 
39
-		key := strings.ToLower(parts[0])
40
-		value := strings.ToLower(parts[1])
41
-
42
-		switch key {
43
-		case portOptProtocol:
44
-			if value != string(swarm.PortConfigProtocolTCP) && value != string(swarm.PortConfigProtocolUDP) {
45
-				return fmt.Errorf("invalid protocol value %s", value)
37
+		pConfig := swarm.PortConfig{}
38
+		for _, field := range fields {
39
+			parts := strings.SplitN(field, "=", 2)
40
+			if len(parts) != 2 {
41
+				return fmt.Errorf("invalid field %s", field)
46 42
 			}
47 43
 
48
-			pConfig.Protocol = swarm.PortConfigProtocol(value)
49
-		case portOptMode:
50
-			if value != string(swarm.PortConfigPublishModeIngress) && value != string(swarm.PortConfigPublishModeHost) {
51
-				return fmt.Errorf("invalid publish mode value %s", value)
44
+			key := strings.ToLower(parts[0])
45
+			value := strings.ToLower(parts[1])
46
+
47
+			switch key {
48
+			case portOptProtocol:
49
+				if value != string(swarm.PortConfigProtocolTCP) && value != string(swarm.PortConfigProtocolUDP) {
50
+					return fmt.Errorf("invalid protocol value %s", value)
51
+				}
52
+
53
+				pConfig.Protocol = swarm.PortConfigProtocol(value)
54
+			case portOptMode:
55
+				if value != string(swarm.PortConfigPublishModeIngress) && value != string(swarm.PortConfigPublishModeHost) {
56
+					return fmt.Errorf("invalid publish mode value %s", value)
57
+				}
58
+
59
+				pConfig.PublishMode = swarm.PortConfigPublishMode(value)
60
+			case portOptTargetPort:
61
+				tPort, err := strconv.ParseUint(value, 10, 16)
62
+				if err != nil {
63
+					return err
64
+				}
65
+
66
+				pConfig.TargetPort = uint32(tPort)
67
+			case portOptPublishedPort:
68
+				pPort, err := strconv.ParseUint(value, 10, 16)
69
+				if err != nil {
70
+					return err
71
+				}
72
+
73
+				pConfig.PublishedPort = uint32(pPort)
74
+			default:
75
+				return fmt.Errorf("invalid field key %s", key)
52 76
 			}
77
+		}
53 78
 
54
-			pConfig.PublishMode = swarm.PortConfigPublishMode(value)
55
-		case portOptTargetPort:
56
-			tPort, err := strconv.ParseUint(value, 10, 16)
57
-			if err != nil {
58
-				return err
59
-			}
79
+		if pConfig.TargetPort == 0 {
80
+			return fmt.Errorf("missing mandatory field %q", portOptTargetPort)
81
+		}
60 82
 
61
-			pConfig.TargetPort = uint32(tPort)
62
-		case portOptPublishedPort:
63
-			pPort, err := strconv.ParseUint(value, 10, 16)
64
-			if err != nil {
65
-				return err
66
-			}
83
+		if pConfig.PublishMode == "" {
84
+			pConfig.PublishMode = swarm.PortConfigPublishModeIngress
85
+		}
67 86
 
68
-			pConfig.PublishedPort = uint32(pPort)
69
-		default:
70
-			return fmt.Errorf("invalid field key %s", key)
87
+		if pConfig.Protocol == "" {
88
+			pConfig.Protocol = swarm.PortConfigProtocolTCP
71 89
 		}
72
-	}
73 90
 
74
-	if pConfig.TargetPort == 0 {
75
-		return fmt.Errorf("missing mandatory field %q", portOptTargetPort)
76
-	}
91
+		p.ports = append(p.ports, pConfig)
92
+	} else {
93
+		// short syntax
94
+		portConfigs := []swarm.PortConfig{}
95
+		// We can ignore errors because the format was already validated by ValidatePort
96
+		ports, portBindings, _ := nat.ParsePortSpecs([]string{value})
77 97
 
78
-	p.ports = append(p.ports, pConfig)
98
+		for port := range ports {
99
+			portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...)
100
+		}
101
+		p.ports = append(p.ports, portConfigs...)
102
+	}
79 103
 	return nil
80 104
 }
81 105
 
... ...
@@ -98,3 +124,23 @@ func (p *PortOpt) String() string {
98 98
 func (p *PortOpt) Value() []swarm.PortConfig {
99 99
 	return p.ports
100 100
 }
101
+
102
+// ConvertPortToPortConfig converts ports to the swarm type
103
+func ConvertPortToPortConfig(
104
+	port nat.Port,
105
+	portBindings map[nat.Port][]nat.PortBinding,
106
+) []swarm.PortConfig {
107
+	ports := []swarm.PortConfig{}
108
+
109
+	for _, binding := range portBindings[port] {
110
+		hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
111
+		ports = append(ports, swarm.PortConfig{
112
+			//TODO Name: ?
113
+			Protocol:      swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
114
+			TargetPort:    uint32(port.Int()),
115
+			PublishedPort: uint32(hostPort),
116
+			PublishMode:   swarm.PortConfigPublishModeIngress,
117
+		})
118
+	}
119
+	return ports
120
+}
101 121
new file mode 100644
... ...
@@ -0,0 +1,259 @@
0
+package opts
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/docker/docker/api/types/swarm"
6
+	"github.com/docker/docker/pkg/testutil/assert"
7
+)
8
+
9
+func TestPortOptValidSimpleSyntax(t *testing.T) {
10
+	testCases := []struct {
11
+		value    string
12
+		expected []swarm.PortConfig
13
+	}{
14
+		{
15
+			value: "80",
16
+			expected: []swarm.PortConfig{
17
+				{
18
+					Protocol:    "tcp",
19
+					TargetPort:  80,
20
+					PublishMode: swarm.PortConfigPublishModeIngress,
21
+				},
22
+			},
23
+		},
24
+		{
25
+			value: "80:8080",
26
+			expected: []swarm.PortConfig{
27
+				{
28
+					Protocol:      "tcp",
29
+					TargetPort:    8080,
30
+					PublishedPort: 80,
31
+					PublishMode:   swarm.PortConfigPublishModeIngress,
32
+				},
33
+			},
34
+		},
35
+		{
36
+			value: "8080:80/tcp",
37
+			expected: []swarm.PortConfig{
38
+				{
39
+					Protocol:      "tcp",
40
+					TargetPort:    80,
41
+					PublishedPort: 8080,
42
+					PublishMode:   swarm.PortConfigPublishModeIngress,
43
+				},
44
+			},
45
+		},
46
+		{
47
+			value: "80:8080/udp",
48
+			expected: []swarm.PortConfig{
49
+				{
50
+					Protocol:      "udp",
51
+					TargetPort:    8080,
52
+					PublishedPort: 80,
53
+					PublishMode:   swarm.PortConfigPublishModeIngress,
54
+				},
55
+			},
56
+		},
57
+		{
58
+			value: "80-81:8080-8081/tcp",
59
+			expected: []swarm.PortConfig{
60
+				{
61
+					Protocol:      "tcp",
62
+					TargetPort:    8080,
63
+					PublishedPort: 80,
64
+					PublishMode:   swarm.PortConfigPublishModeIngress,
65
+				},
66
+				{
67
+					Protocol:      "tcp",
68
+					TargetPort:    8081,
69
+					PublishedPort: 81,
70
+					PublishMode:   swarm.PortConfigPublishModeIngress,
71
+				},
72
+			},
73
+		},
74
+		{
75
+			value: "80-82:8080-8082/udp",
76
+			expected: []swarm.PortConfig{
77
+				{
78
+					Protocol:      "udp",
79
+					TargetPort:    8080,
80
+					PublishedPort: 80,
81
+					PublishMode:   swarm.PortConfigPublishModeIngress,
82
+				},
83
+				{
84
+					Protocol:      "udp",
85
+					TargetPort:    8081,
86
+					PublishedPort: 81,
87
+					PublishMode:   swarm.PortConfigPublishModeIngress,
88
+				},
89
+				{
90
+					Protocol:      "udp",
91
+					TargetPort:    8082,
92
+					PublishedPort: 82,
93
+					PublishMode:   swarm.PortConfigPublishModeIngress,
94
+				},
95
+			},
96
+		},
97
+	}
98
+	for _, tc := range testCases {
99
+		var port PortOpt
100
+		assert.NilError(t, port.Set(tc.value))
101
+		assert.Equal(t, len(port.Value()), len(tc.expected))
102
+		for _, expectedPortConfig := range tc.expected {
103
+			assertContains(t, port.Value(), expectedPortConfig)
104
+		}
105
+	}
106
+}
107
+
108
+func TestPortOptValidComplexSyntax(t *testing.T) {
109
+	testCases := []struct {
110
+		value    string
111
+		expected []swarm.PortConfig
112
+	}{
113
+		{
114
+			value: "target=80",
115
+			expected: []swarm.PortConfig{
116
+				{
117
+					TargetPort:  80,
118
+					Protocol:    "tcp",
119
+					PublishMode: swarm.PortConfigPublishModeIngress,
120
+				},
121
+			},
122
+		},
123
+		{
124
+			value: "target=80,protocol=tcp",
125
+			expected: []swarm.PortConfig{
126
+				{
127
+					Protocol:    "tcp",
128
+					TargetPort:  80,
129
+					PublishMode: swarm.PortConfigPublishModeIngress,
130
+				},
131
+			},
132
+		},
133
+		{
134
+			value: "target=80,published=8080,protocol=tcp",
135
+			expected: []swarm.PortConfig{
136
+				{
137
+					Protocol:      "tcp",
138
+					TargetPort:    80,
139
+					PublishedPort: 8080,
140
+					PublishMode:   swarm.PortConfigPublishModeIngress,
141
+				},
142
+			},
143
+		},
144
+		{
145
+			value: "published=80,target=8080,protocol=tcp",
146
+			expected: []swarm.PortConfig{
147
+				{
148
+					Protocol:      "tcp",
149
+					TargetPort:    8080,
150
+					PublishedPort: 80,
151
+					PublishMode:   swarm.PortConfigPublishModeIngress,
152
+				},
153
+			},
154
+		},
155
+		{
156
+			value: "target=80,published=8080,protocol=tcp,mode=host",
157
+			expected: []swarm.PortConfig{
158
+				{
159
+					Protocol:      "tcp",
160
+					TargetPort:    80,
161
+					PublishedPort: 8080,
162
+					PublishMode:   "host",
163
+				},
164
+			},
165
+		},
166
+		{
167
+			value: "target=80,published=8080,mode=host",
168
+			expected: []swarm.PortConfig{
169
+				{
170
+					TargetPort:    80,
171
+					PublishedPort: 8080,
172
+					PublishMode:   "host",
173
+					Protocol:      "tcp",
174
+				},
175
+			},
176
+		},
177
+		{
178
+			value: "target=80,published=8080,mode=ingress",
179
+			expected: []swarm.PortConfig{
180
+				{
181
+					TargetPort:    80,
182
+					PublishedPort: 8080,
183
+					PublishMode:   "ingress",
184
+					Protocol:      "tcp",
185
+				},
186
+			},
187
+		},
188
+	}
189
+	for _, tc := range testCases {
190
+		var port PortOpt
191
+		assert.NilError(t, port.Set(tc.value))
192
+		assert.Equal(t, len(port.Value()), len(tc.expected))
193
+		for _, expectedPortConfig := range tc.expected {
194
+			assertContains(t, port.Value(), expectedPortConfig)
195
+		}
196
+	}
197
+}
198
+
199
+func TestPortOptInvalidComplexSyntax(t *testing.T) {
200
+	testCases := []struct {
201
+		value         string
202
+		expectedError string
203
+	}{
204
+		{
205
+			value:         "invalid,target=80",
206
+			expectedError: "invalid field",
207
+		},
208
+		{
209
+			value:         "invalid=field",
210
+			expectedError: "invalid field",
211
+		},
212
+		{
213
+			value:         "protocol=invalid",
214
+			expectedError: "invalid protocol value",
215
+		},
216
+		{
217
+			value:         "target=invalid",
218
+			expectedError: "invalid syntax",
219
+		},
220
+		{
221
+			value:         "published=invalid",
222
+			expectedError: "invalid syntax",
223
+		},
224
+		{
225
+			value:         "mode=invalid",
226
+			expectedError: "invalid publish mode value",
227
+		},
228
+		{
229
+			value:         "published=8080,protocol=tcp,mode=ingress",
230
+			expectedError: "missing mandatory field",
231
+		},
232
+		{
233
+			value:         `target=80,protocol="tcp,mode=ingress"`,
234
+			expectedError: "non-quoted-field",
235
+		},
236
+		{
237
+			value:         `target=80,"protocol=tcp,mode=ingress"`,
238
+			expectedError: "invalid protocol value",
239
+		},
240
+	}
241
+	for _, tc := range testCases {
242
+		var port PortOpt
243
+		assert.Error(t, port.Set(tc.value), tc.expectedError)
244
+	}
245
+}
246
+
247
+func assertContains(t *testing.T, portConfigs []swarm.PortConfig, expected swarm.PortConfig) {
248
+	var contains = false
249
+	for _, portConfig := range portConfigs {
250
+		if portConfig == expected {
251
+			contains = true
252
+			break
253
+		}
254
+	}
255
+	if !contains {
256
+		t.Errorf("expected %v to contain %v, did not", portConfigs, expected)
257
+	}
258
+}