Browse code

Return error for incorrect argument of `service update --publish-rm <TargetPort>`

Currently `--publish-rm` only accepts `<TargetPort>` or `<TargetPort>[/Protocol]`
though there are some confusions.

Since `--publish-add` accepts `<PublishedPort>:<TargetPort>[/Protocol]`, some user
may provide `--publish-rm 80:80`. However, there is no error checking so the incorrect
provided argument is ignored silently.

This fix adds the check to make sure `--publish-rm` only accepts `<TargetPort>[/Protocol]`
and returns error if the format is invalid.

The `--publish-rm` itself may needs to be revisited to have a better UI/UX experience,
see discussions on:
https://github.com/docker/swarmkit/issues/1396
https://github.com/docker/docker/issues/25200#issuecomment-236213242
https://github.com/docker/docker/issues/25338#issuecomment-240787002

This fix is short term measure so that end users are not misled by the silently ignored error
of `--publish-rm`.

This fix is related to (but is not a complete fix):
https://github.com/docker/swarmkit/issues/1396

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

Yong Tang authored on 2016/08/19 10:09:07
Showing 3 changed files
... ...
@@ -47,7 +47,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
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(), flagPublishRemove, "Remove a published port by its target port")
50
+	flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port")
51 51
 	flags.MarkHidden(flagPublishRemove)
52 52
 	flags.Var(newListOptsVar(), flagPortRemove, "Remove a port(target-port mandatory)")
53 53
 	flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
... ...
@@ -645,6 +645,23 @@ 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
+// This validation is only used for `--publish-rm`.
649
+// The `--publish-rm` takes:
650
+// <TargetPort>[/<Protocol>] (e.g., 80, 80/tcp, 53/udp)
651
+func validatePublishRemove(val string) (string, error) {
652
+	proto, port := nat.SplitProtoPort(val)
653
+	if proto != "tcp" && proto != "udp" {
654
+		return "", fmt.Errorf("invalid protocol '%s' for %s", proto, val)
655
+	}
656
+	if strings.Contains(port, ":") {
657
+		return "", fmt.Errorf("invalid port format: '%s', should be <TargetPort>[/<Protocol>] (e.g., 80, 80/tcp, 53/udp)", port)
658
+	}
659
+	if _, err := nat.ParsePort(port); err != nil {
660
+		return "", err
661
+	}
662
+	return val, nil
663
+}
664
+
648 665
 func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
649 666
 	// The key of the map is `port/protocol`, e.g., `80/tcp`
650 667
 	portSet := map[string]swarm.PortConfig{}
... ...
@@ -345,3 +345,43 @@ func TestUpdateHosts(t *testing.T) {
345 345
 	assert.Equal(t, hosts[1], "2001:db8:abc8::1 ipv6.net")
346 346
 	assert.Equal(t, hosts[2], "4.3.2.1 example.org")
347 347
 }
348
+
349
+func TestUpdatePortsRmWithProtocol(t *testing.T) {
350
+	flags := newUpdateCommand(nil).Flags()
351
+	flags.Set("publish-add", "8081:81")
352
+	flags.Set("publish-add", "8082:82")
353
+	flags.Set("publish-rm", "80")
354
+	flags.Set("publish-rm", "81/tcp")
355
+	flags.Set("publish-rm", "82/udp")
356
+
357
+	portConfigs := []swarm.PortConfig{
358
+		{TargetPort: 80, PublishedPort: 8080, Protocol: swarm.PortConfigProtocolTCP},
359
+	}
360
+
361
+	err := updatePorts(flags, &portConfigs)
362
+	assert.Equal(t, err, nil)
363
+	assert.Equal(t, len(portConfigs), 1)
364
+	assert.Equal(t, portConfigs[0].TargetPort, uint32(82))
365
+}
366
+
367
+func TestValidatePort(t *testing.T) {
368
+	validPorts := []string{"80/tcp", "80", "80/udp"}
369
+	invalidPorts := map[string]string{
370
+		"9999999":   "out of range",
371
+		"80:80/tcp": "invalid port format",
372
+		"53:53/udp": "invalid port format",
373
+		"80:80":     "invalid port format",
374
+		"80/xyz":    "invalid protocol",
375
+		"tcp":       "invalid syntax",
376
+		"udp":       "invalid syntax",
377
+		"":          "invalid protocol",
378
+	}
379
+	for _, port := range validPorts {
380
+		_, err := validatePublishRemove(port)
381
+		assert.Equal(t, err, nil)
382
+	}
383
+	for port, e := range invalidPorts {
384
+		_, err := validatePublishRemove(port)
385
+		assert.Error(t, err, e)
386
+	}
387
+}
... ...
@@ -108,6 +108,12 @@ func (opts *ListOpts) Type() string {
108 108
 	return "list"
109 109
 }
110 110
 
111
+// WithValidator returns the ListOpts with validator set.
112
+func (opts *ListOpts) WithValidator(validator ValidatorFctType) *ListOpts {
113
+	opts.validator = validator
114
+	return opts
115
+}
116
+
111 117
 // NamedOption is an interface that list and map options
112 118
 // with names implement.
113 119
 type NamedOption interface {