Browse code

API/CLI discrepancy on hostname validation (#21595).

This fix tries to fix the discrepancy between API and CLI on hostname
validation. Previously, the hostname validation was handled at the
CLI interface in runconfig/opts/parse.go and return an error if the
hostname is invalid. However, if an end user use the remote API to
pass the hostname, the error will not be returned immediately.
Instead the error will only be thrown out when the container creation
fails. This creates behavior discrepancy between API and CLI.

In this fix, the hostname validation was moved to
verifyContainerSettings so the behavior will be the same for API and
CLI.

After the change, since CLI does not handle the hostname validation
any more, the previous unit tests about hostname validation on CLI
in runconfig/opts/parse_test.go has to be updated as well because
there is no validation at this stage. All those unit tests are moved
to integration test TestRunTooLongHostname so that the hostname
validation is still properly covered as before.

Note: Since the hostname validation moved to API, the error message
changes from `invalid hostname format for --hostname:` to
`invalid hostname format:` as well because `--hostname` is passed
to CLI only.

This fix fixes #21595.

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

Yong Tang authored on 2016/03/30 01:37:43
Showing 4 changed files
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"os"
14 14
 	"path"
15 15
 	"path/filepath"
16
+	"regexp"
16 17
 	"runtime"
17 18
 	"strings"
18 19
 	"sync"
... ...
@@ -1338,6 +1339,18 @@ func (daemon *Daemon) verifyContainerSettings(hostConfig *containertypes.HostCon
1338 1338
 				return nil, err
1339 1339
 			}
1340 1340
 		}
1341
+
1342
+		// Validate if the given hostname is RFC 1123 (https://tools.ietf.org/html/rfc1123) compliant.
1343
+		if len(config.Hostname) > 0 {
1344
+			// RFC1123 specifies that 63 bytes is the maximium length
1345
+			// Windows has the limitation of 63 bytes in length
1346
+			// Linux hostname is limited to HOST_NAME_MAX=64, not not including the terminating null byte.
1347
+			// We limit the length to 63 bytes here to match RFC1035 and RFC1123.
1348
+			matched, _ := regexp.MatchString("^(([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])\\.)*([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])$", config.Hostname)
1349
+			if len(config.Hostname) > 63 || !matched {
1350
+				return nil, fmt.Errorf("invalid hostname format: %s", config.Hostname)
1351
+			}
1352
+		}
1341 1353
 	}
1342 1354
 
1343 1355
 	if hostConfig == nil {
... ...
@@ -4295,15 +4295,33 @@ func (s *DockerSuite) TestRunTooLongHostname(c *check.C) {
4295 4295
 	hostname1 := "this-is-a-way-too-long-hostname-but-it-should-give-a-nice-error.local"
4296 4296
 	out, _, err := dockerCmdWithError("run", "--hostname", hostname1, "busybox", "echo", "test")
4297 4297
 	c.Assert(err, checker.NotNil, check.Commentf("Expected docker run to fail!"))
4298
-	c.Assert(out, checker.Contains, "invalid hostname format for --hostname:", check.Commentf("Expected to have 'invalid hostname format for --hostname:' in the output, get: %s!", out))
4298
+	c.Assert(out, checker.Contains, "invalid hostname format:", check.Commentf("Expected to have 'invalid hostname format:' in the output, get: %s!", out))
4299 4299
 
4300
-	// HOST_NAME_MAX=64 so 65 bytes will fail
4301
-	hostname2 := "this-is-a-hostname-with-65-bytes-so-it-should-give-an-error.local"
4302
-	out, _, err = dockerCmdWithError("run", "--hostname", hostname2, "busybox", "echo", "test")
4303
-	c.Assert(err, checker.NotNil, check.Commentf("Expected docker run to fail!"))
4304
-	c.Assert(out, checker.Contains, "invalid hostname format for --hostname:", check.Commentf("Expected to have 'invalid hostname format for --hostname:' in the output, get: %s!", out))
4300
+	// Addtional test cases
4301
+	validHostnames := map[string]string{
4302
+		"hostname":    "hostname",
4303
+		"host-name":   "host-name",
4304
+		"hostname123": "hostname123",
4305
+		"123hostname": "123hostname",
4306
+		"hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error",
4307
+	}
4308
+	for hostname := range validHostnames {
4309
+		dockerCmd(c, "run", "--hostname", hostname, "busybox", "echo", "test")
4310
+	}
4305 4311
 
4306
-	// 64 bytes will be OK
4307
-	hostname3 := "this-is-a-hostname-with-64-bytes-so-will-not-give-an-error.local"
4308
-	dockerCmd(c, "run", "--hostname", hostname3, "busybox", "echo", "test")
4312
+	invalidHostnames := map[string]string{
4313
+		"^hostname": "invalid hostname format: ^hostname",
4314
+		"hostname%": "invalid hostname format: hostname%",
4315
+		"host&name": "invalid hostname format: host&name",
4316
+		"-hostname": "invalid hostname format: -hostname",
4317
+		"host_name": "invalid hostname format: host_name",
4318
+		"hostname-of-64-bytes-long-should-be-invalid-and-be-with-an-error": "invalid hostname format: hostname-of-64-bytes-long-should-be-invalid-and-be-with-an-error",
4319
+	}
4320
+
4321
+	for hostname, expectedError := range invalidHostnames {
4322
+		out, _, err = dockerCmdWithError("run", "--hostname", hostname, "busybox", "echo", "test")
4323
+		c.Assert(err, checker.NotNil, check.Commentf("Expected docker run to fail!"))
4324
+		c.Assert(out, checker.Contains, expectedError, check.Commentf("Expected to have '%s' in the output, get: %s!", expectedError, out))
4325
+
4326
+	}
4309 4327
 }
... ...
@@ -6,7 +6,6 @@ import (
6 6
 	"fmt"
7 7
 	"io/ioutil"
8 8
 	"path"
9
-	"regexp"
10 9
 	"strconv"
11 10
 	"strings"
12 11
 
... ...
@@ -243,15 +242,6 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host
243 243
 	if *flEntrypoint != "" {
244 244
 		entrypoint = strslice.StrSlice{*flEntrypoint}
245 245
 	}
246
-	// Validate if the given hostname is RFC 1123 (https://tools.ietf.org/html/rfc1123) compliant.
247
-	hostname := *flHostname
248
-	if hostname != "" {
249
-		// Linux hostname is limited to HOST_NAME_MAX=64, not including the terminating null byte.
250
-		matched, _ := regexp.MatchString("^(([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])\\.)*([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])$", hostname)
251
-		if len(hostname) > 64 || !matched {
252
-			return nil, nil, nil, cmd, fmt.Errorf("invalid hostname format for --hostname: %s", hostname)
253
-		}
254
-	}
255 246
 
256 247
 	ports, portBindings, err := nat.ParsePortSpecs(flPublish.GetAll())
257 248
 	if err != nil {
... ...
@@ -390,15 +390,7 @@ func TestParseHostname(t *testing.T) {
390 390
 		"host-name":   "host-name",
391 391
 		"hostname123": "hostname123",
392 392
 		"123hostname": "123hostname",
393
-		"hostname-of-64-bytes-long-should-be-valid-and-without-any-errors": "hostname-of-64-bytes-long-should-be-valid-and-without-any-errors",
394
-	}
395
-	invalidHostnames := map[string]string{
396
-		"^hostname": "invalid hostname format for --hostname: ^hostname",
397
-		"hostname%": "invalid hostname format for --hostname: hostname%",
398
-		"host&name": "invalid hostname format for --hostname: host&name",
399
-		"-hostname": "invalid hostname format for --hostname: -hostname",
400
-		"host_name": "invalid hostname format for --hostname: host_name",
401
-		"hostname-of-65-bytes-long-should-be-invalid-and-be-given-an-error": "invalid hostname format for --hostname: hostname-of-65-bytes-long-should-be-invalid-and-be-given-an-error",
393
+		"hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error",
402 394
 	}
403 395
 	hostnameWithDomain := "--hostname=hostname.domainname"
404 396
 	hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
... ...
@@ -407,11 +399,6 @@ func TestParseHostname(t *testing.T) {
407 407
 			t.Fatalf("Expected the config to have 'hostname' as hostname, got '%v'", config.Hostname)
408 408
 		}
409 409
 	}
410
-	for hostname, expectedError := range invalidHostnames {
411
-		if _, _, err := parse(t, fmt.Sprintf("--hostname=%s", hostname)); err == nil || err.Error() != expectedError {
412
-			t.Fatalf("Expected error '%v' with '--hostname=%s', got '%s'", expectedError, hostname, err)
413
-		}
414
-	}
415 410
 	if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" && config.Domainname != "" {
416 411
 		t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got '%v'", config.Hostname)
417 412
 	}