Browse code

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

Add support for simple and complex syntax to `--publish` through the
use of `PortOpt`.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>

Vincent Demeester authored on 2016/12/09 06:32:10
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
 		}
... ...
@@ -645,6 +642,7 @@ func portConfigToString(portConfig *swarm.PortConfig) string {
645 645
 	return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode)
646 646
 }
647 647
 
648
+// FIXME(vdemeester) port to opts.PortOpt
648 649
 // This validation is only used for `--publish-rm`.
649 650
 // The `--publish-rm` takes:
650 651
 // <TargetPort>[/<Protocol>] (e.g., 80, 80/tcp, 53/udp)
... ...
@@ -667,26 +665,13 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
667 667
 	portSet := map[string]swarm.PortConfig{}
668 668
 	// Check to see if there are any conflict in flags.
669 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
-	}
670
+		ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value()
683 671
 
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)
672
+		for _, port := range ports {
673
+			if v, ok := portSet[portConfigToString(&port)]; ok && v != port {
674
+				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)
688 675
 			}
689
-			portSet[portConfigToString(&entry)] = entry
676
+			portSet[portConfigToString(&port)] = port
690 677
 		}
691 678
 	}
692 679
 
... ...
@@ -697,26 +682,12 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
697 697
 		}
698 698
 	}
699 699
 
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
-	}
700
+	toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value()
708 701
 
709 702
 	newPorts := []swarm.PortConfig{}
710 703
 portLoop:
711 704
 	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() {
705
+		for _, pConfig := range toRemove {
720 706
 			if equalProtocol(port.Protocol, pConfig.Protocol) &&
721 707
 				port.TargetPort == pConfig.TargetPort &&
722 708
 				equalPublishMode(port.PublishMode, pConfig.PublishMode) {
... ...
@@ -364,6 +364,7 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) {
364 364
 	assert.Equal(t, portConfigs[0].TargetPort, uint32(82))
365 365
 }
366 366
 
367
+// FIXME(vdemeester) port to opts.PortOpt
367 368
 func TestValidatePort(t *testing.T) {
368 369
 	validPorts := []string{"80/tcp", "80", "80/udp"}
369 370
 	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
... ...
@@ -2752,7 +2752,7 @@ _docker_service_update() {
2752 2752
 			--host
2753 2753
 			--mode
2754 2754
 			--name
2755
-			--port
2755
+			--publish
2756 2756
 			--secret
2757 2757
 		"
2758 2758
 
... ...
@@ -2799,8 +2799,8 @@ _docker_service_update() {
2799 2799
 			--host-add
2800 2800
 			--host-rm
2801 2801
 			--image
2802
-			--port-add
2803
-			--port-rm
2802
+			--publish-add
2803
+			--publish-rm
2804 2804
 			--secret-add
2805 2805
 			--secret-rm
2806 2806
 		"
... ...
@@ -1795,7 +1795,7 @@ __docker_service_subcommand() {
1795 1795
                 "($help)*--env-file=[Read environment variables from a file]:environment file:_files" \
1796 1796
                 "($help)--mode=[Service Mode]:mode:(global replicated)" \
1797 1797
                 "($help)--name=[Service name]:name: " \
1798
-                "($help)*--port=[Publish a port]:port: " \
1798
+                "($help)*--publish=[Publish a port]:port: " \
1799 1799
                 "($help -): :__docker_complete_images" \
1800 1800
                 "($help -):command: _command_names -e" \
1801 1801
                 "($help -)*::arguments: _normal" && ret=0
... ...
@@ -1868,8 +1868,8 @@ __docker_service_subcommand() {
1868 1868
                 "($help)*--group-add=[Add additional supplementary user groups to the container]:group:_groups" \
1869 1869
                 "($help)*--group-rm=[Remove previously added supplementary user groups from the container]:group:_groups" \
1870 1870
                 "($help)--image=[Service image tag]:image:__docker_complete_repositories" \
1871
-                "($help)*--port-add=[Add or update a port]:port: " \
1872
-                "($help)*--port-rm=[Remove a port(target-port mandatory)]:port: " \
1871
+                "($help)*--publish-add=[Add or update a port]:port: " \
1872
+                "($help)*--publish-rm=[Remove a port(target-port mandatory)]:port: " \
1873 1873
                 "($help)--rollback[Rollback to previous specification]" \
1874 1874
                 "($help -)1:service:__docker_complete_services" && ret=0
1875 1875
             ;;
... ...
@@ -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,75 @@ 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
+		p.ports = append(p.ports, pConfig)
84
+	} else {
85
+		// short syntax
86
+		portConfigs := []swarm.PortConfig{}
87
+		// We can ignore errors because the format was already validated by ValidatePort
88
+		ports, portBindings, _ := nat.ParsePortSpecs([]string{value})
67 89
 
68
-			pConfig.PublishedPort = uint32(pPort)
69
-		default:
70
-			return fmt.Errorf("invalid field key %s", key)
90
+		for port := range ports {
91
+			portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...)
71 92
 		}
93
+		p.ports = append(p.ports, portConfigs...)
72 94
 	}
73
-
74
-	if pConfig.TargetPort == 0 {
75
-		return fmt.Errorf("missing mandatory field %q", portOptTargetPort)
76
-	}
77
-
78
-	p.ports = append(p.ports, pConfig)
79 95
 	return nil
80 96
 }
81 97
 
... ...
@@ -98,3 +116,22 @@ 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
+		})
117
+	}
118
+	return ports
119
+}
101 120
new file mode 100644
... ...
@@ -0,0 +1,243 @@
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
+				},
21
+			},
22
+		},
23
+		{
24
+			value: "80:8080",
25
+			expected: []swarm.PortConfig{
26
+				{
27
+					Protocol:      "tcp",
28
+					TargetPort:    8080,
29
+					PublishedPort: 80,
30
+				},
31
+			},
32
+		},
33
+		{
34
+			value: "8080:80/tcp",
35
+			expected: []swarm.PortConfig{
36
+				{
37
+					Protocol:      "tcp",
38
+					TargetPort:    80,
39
+					PublishedPort: 8080,
40
+				},
41
+			},
42
+		},
43
+		{
44
+			value: "80:8080/udp",
45
+			expected: []swarm.PortConfig{
46
+				{
47
+					Protocol:      "udp",
48
+					TargetPort:    8080,
49
+					PublishedPort: 80,
50
+				},
51
+			},
52
+		},
53
+		{
54
+			value: "80-81:8080-8081/tcp",
55
+			expected: []swarm.PortConfig{
56
+				{
57
+					Protocol:      "tcp",
58
+					TargetPort:    8080,
59
+					PublishedPort: 80,
60
+				},
61
+				{
62
+					Protocol:      "tcp",
63
+					TargetPort:    8081,
64
+					PublishedPort: 81,
65
+				},
66
+			},
67
+		},
68
+		{
69
+			value: "80-82:8080-8082/udp",
70
+			expected: []swarm.PortConfig{
71
+				{
72
+					Protocol:      "udp",
73
+					TargetPort:    8080,
74
+					PublishedPort: 80,
75
+				},
76
+				{
77
+					Protocol:      "udp",
78
+					TargetPort:    8081,
79
+					PublishedPort: 81,
80
+				},
81
+				{
82
+					Protocol:      "udp",
83
+					TargetPort:    8082,
84
+					PublishedPort: 82,
85
+				},
86
+			},
87
+		},
88
+	}
89
+	for _, tc := range testCases {
90
+		var port PortOpt
91
+		assert.NilError(t, port.Set(tc.value))
92
+		assert.Equal(t, len(port.Value()), len(tc.expected))
93
+		for _, expectedPortConfig := range tc.expected {
94
+			assertContains(t, port.Value(), expectedPortConfig)
95
+		}
96
+	}
97
+}
98
+
99
+func TestPortOptValidComplexSyntax(t *testing.T) {
100
+	testCases := []struct {
101
+		value    string
102
+		expected []swarm.PortConfig
103
+	}{
104
+		{
105
+			value: "target=80",
106
+			expected: []swarm.PortConfig{
107
+				{
108
+					TargetPort: 80,
109
+				},
110
+			},
111
+		},
112
+		{
113
+			value: "target=80,protocol=tcp",
114
+			expected: []swarm.PortConfig{
115
+				{
116
+					Protocol:   "tcp",
117
+					TargetPort: 80,
118
+				},
119
+			},
120
+		},
121
+		{
122
+			value: "target=80,published=8080,protocol=tcp",
123
+			expected: []swarm.PortConfig{
124
+				{
125
+					Protocol:      "tcp",
126
+					TargetPort:    80,
127
+					PublishedPort: 8080,
128
+				},
129
+			},
130
+		},
131
+		{
132
+			value: "published=80,target=8080,protocol=tcp",
133
+			expected: []swarm.PortConfig{
134
+				{
135
+					Protocol:      "tcp",
136
+					TargetPort:    8080,
137
+					PublishedPort: 80,
138
+				},
139
+			},
140
+		},
141
+		{
142
+			value: "target=80,published=8080,protocol=tcp,mode=host",
143
+			expected: []swarm.PortConfig{
144
+				{
145
+					Protocol:      "tcp",
146
+					TargetPort:    80,
147
+					PublishedPort: 8080,
148
+					PublishMode:   "host",
149
+				},
150
+			},
151
+		},
152
+		{
153
+			value: "target=80,published=8080,mode=host",
154
+			expected: []swarm.PortConfig{
155
+				{
156
+					TargetPort:    80,
157
+					PublishedPort: 8080,
158
+					PublishMode:   "host",
159
+				},
160
+			},
161
+		},
162
+		{
163
+			value: "target=80,published=8080,mode=ingress",
164
+			expected: []swarm.PortConfig{
165
+				{
166
+					TargetPort:    80,
167
+					PublishedPort: 8080,
168
+					PublishMode:   "ingress",
169
+				},
170
+			},
171
+		},
172
+	}
173
+	for _, tc := range testCases {
174
+		var port PortOpt
175
+		assert.NilError(t, port.Set(tc.value))
176
+		assert.Equal(t, len(port.Value()), len(tc.expected))
177
+		for _, expectedPortConfig := range tc.expected {
178
+			assertContains(t, port.Value(), expectedPortConfig)
179
+		}
180
+	}
181
+}
182
+
183
+func TestPortOptInvalidComplexSyntax(t *testing.T) {
184
+	testCases := []struct {
185
+		value         string
186
+		expectedError string
187
+	}{
188
+		{
189
+			value:         "invalid,target=80",
190
+			expectedError: "invalid field",
191
+		},
192
+		{
193
+			value:         "invalid=field",
194
+			expectedError: "invalid field",
195
+		},
196
+		{
197
+			value:         "protocol=invalid",
198
+			expectedError: "invalid protocol value",
199
+		},
200
+		{
201
+			value:         "target=invalid",
202
+			expectedError: "invalid syntax",
203
+		},
204
+		{
205
+			value:         "published=invalid",
206
+			expectedError: "invalid syntax",
207
+		},
208
+		{
209
+			value:         "mode=invalid",
210
+			expectedError: "invalid publish mode value",
211
+		},
212
+		{
213
+			value:         "published=8080,protocol=tcp,mode=ingress",
214
+			expectedError: "missing mandatory field",
215
+		},
216
+		{
217
+			value:         `target=80,protocol="tcp,mode=ingress"`,
218
+			expectedError: "non-quoted-field",
219
+		},
220
+		{
221
+			value:         `target=80,"protocol=tcp,mode=ingress"`,
222
+			expectedError: "invalid protocol value",
223
+		},
224
+	}
225
+	for _, tc := range testCases {
226
+		var port PortOpt
227
+		assert.Error(t, port.Set(tc.value), tc.expectedError)
228
+	}
229
+}
230
+
231
+func assertContains(t *testing.T, portConfigs []swarm.PortConfig, expected swarm.PortConfig) {
232
+	var contains = false
233
+	for _, portConfig := range portConfigs {
234
+		if portConfig == expected {
235
+			contains = true
236
+			break
237
+		}
238
+	}
239
+	if !contains {
240
+		t.Errorf("expected %v to contain %v, did not", portConfigs, expected)
241
+	}
242
+}