From 2338a9cf5a1ba5576b92e49065335a9c9251ade0 Mon Sep 17 00:00:00 2001 From: Srini Brahmaroutu Date: Mon, 3 Nov 2014 18:15:55 +0000 Subject: [PATCH] add ability to publish range of ports Closes #8899 Signed-off-by: Srini Brahmaroutu --- docs/man/docker-create.1.md | 4 +- docs/man/docker-run.1.md | 6 +- docs/sources/reference/commandline/cli.md | 6 +- docs/sources/reference/run.md | 9 ++- integration-cli/docker_cli_create_test.go | 99 +++++++++++++++++++++++ integration-cli/docker_cli_run_test.go | 24 ++++++ nat/nat.go | 47 +++++++---- nat/nat_test.go | 98 +++++++++++++++++++++- pkg/parsers/parsers.go | 25 ++++++ pkg/parsers/parsers_test.go | 33 ++++++++ runconfig/config_test.go | 4 +- runconfig/parse.go | 9 ++- 12 files changed, 332 insertions(+), 32 deletions(-) diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index a83873794a..96a049672c 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -121,8 +121,10 @@ IMAGE [COMMAND] [ARG...] Publish all exposed ports to the host interfaces. The default is *false*. **-p**, **--publish**=[] - Publish a container's port to the host + Publish a container's port, or a range of ports, to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) **--privileged**=*true*|*false* diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 659d3d3215..b9571dbe25 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -146,7 +146,7 @@ ENTRYPOINT. Read in a line delimited file of environment variables **--expose**=[] - Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host + Expose a port, or a range of ports (e.g. --expose=3300-3310), from the container without publishing it to your host **-h**, **--hostname**="" Container host name @@ -224,8 +224,10 @@ ports to a random port on the host between 49153 and 65535. To find the mapping between the host ports and the exposed ports, use **docker port**. **-p**, **--publish**=[] - Publish a container's port to the host + Publish a container's port, or range of ports, to the host. format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) **--privileged**=*true*|*false* diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index aab3d6af47..e48a393b79 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -686,8 +686,10 @@ Creates a new container. 'container:': reuses another container network stack 'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. -P, --publish-all=false Publish all exposed ports to the host interfaces - -p, --publish=[] Publish a container's port to the host + -p, --publish=[] Publish a container's port, or a range of ports (e.g., `-p 3300-3310`), to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) --privileged=false Give extended privileges to this container --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) @@ -1514,6 +1516,8 @@ removed before the image is removed. -P, --publish-all=false Publish all exposed ports to the host interfaces -p, --publish=[] Publish a container's port to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) --privileged=false Give extended privileges to this container --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index d13284b5d0..1cd6231751 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -487,10 +487,11 @@ or override the Dockerfile's exposed defaults: --expose=[]: Expose a port or a range of ports from the container without publishing it to your host -P=false : Publish all exposed ports to the host interfaces - -p=[] : Publish a container᾿s port to the host (format: - ip:hostPort:containerPort | ip::containerPort | - hostPort:containerPort | containerPort) - (use 'docker port' to see the actual mapping) + -p=[] : Publish a container᾿s port or a range of ports to the host + format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) + (use 'docker port' to see the actual mapping) --link="" : Add link to another container (name:alias) As mentioned previously, `EXPOSE` (and `--expose`) makes ports available diff --git a/integration-cli/docker_cli_create_test.go b/integration-cli/docker_cli_create_test.go index 0dc7993798..1192f3647d 100644 --- a/integration-cli/docker_cli_create_test.go +++ b/integration-cli/docker_cli_create_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "github.com/docker/docker/nat" "os" "os/exec" "testing" @@ -102,6 +103,104 @@ func TestCreateHostConfig(t *testing.T) { logDone("create - hostconfig") } +func TestCreateWithPortRange(t *testing.T) { + runCmd := exec.Command(dockerBinary, "create", "-p", "3300-3303:3300-3303/tcp", "busybox", "echo") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + if err != nil { + t.Fatal(out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + + inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID) + out, _, err = runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("out should've been a container id: %s, %v", out, err) + } + + containers := []struct { + HostConfig *struct { + PortBindings map[nat.Port][]nat.PortBinding + } + }{} + if err := json.Unmarshal([]byte(out), &containers); err != nil { + t.Fatalf("Error inspecting the container: %s", err) + } + if len(containers) != 1 { + t.Fatalf("Unexpected container count. Expected 0, received: %d", len(containers)) + } + + c := containers[0] + if c.HostConfig == nil { + t.Fatalf("Expected HostConfig, got none") + } + + if len(c.HostConfig.PortBindings) != 4 { + t.Fatalf("Expected 4 ports bindings, got %d", len(c.HostConfig.PortBindings)) + } + for k, v := range c.HostConfig.PortBindings { + if len(v) != 1 { + t.Fatalf("Expected 1 ports binding, for the port %s but found %s", k, v) + } + if k.Port() != v[0].HostPort { + t.Fatalf("Expected host port %d to match published port %d", k.Port(), v[0].HostPort) + } + } + + deleteAllContainers() + + logDone("create - port range") +} + +func TestCreateWithiLargePortRange(t *testing.T) { + runCmd := exec.Command(dockerBinary, "create", "-p", "1-65535:1-65535/tcp", "busybox", "echo") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + if err != nil { + t.Fatal(out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + + inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID) + out, _, err = runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("out should've been a container id: %s, %v", out, err) + } + + containers := []struct { + HostConfig *struct { + PortBindings map[nat.Port][]nat.PortBinding + } + }{} + if err := json.Unmarshal([]byte(out), &containers); err != nil { + t.Fatalf("Error inspecting the container: %s", err) + } + if len(containers) != 1 { + t.Fatalf("Unexpected container count. Expected 0, received: %d", len(containers)) + } + + c := containers[0] + if c.HostConfig == nil { + t.Fatalf("Expected HostConfig, got none") + } + + if len(c.HostConfig.PortBindings) != 65535 { + t.Fatalf("Expected 65535 ports bindings, got %d", len(c.HostConfig.PortBindings)) + } + for k, v := range c.HostConfig.PortBindings { + if len(v) != 1 { + t.Fatalf("Expected 1 ports binding, for the port %s but found %s", k, v) + } + if k.Port() != v[0].HostPort { + t.Fatalf("Expected host port %d to match published port %d", k.Port(), v[0].HostPort) + } + } + + deleteAllContainers() + + logDone("create - large port range") +} + // "test123" should be printed by docker create + start func TestCreateEchoStdout(t *testing.T) { runCmd := exec.Command(dockerBinary, "create", "busybox", "echo", "test123") diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 2a75b27d8c..748c4d6815 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2737,3 +2737,27 @@ func TestRunNetHost(t *testing.T) { logDone("run - net host mode") } + +func TestRunAllowPortRangeThroughPublish(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "-d", "--expose", "3000-3003", "-p", "3000-3003", "busybox", "top") + out, _, err := runCommandWithOutput(cmd) + defer deleteAllContainers() + + id := strings.TrimSpace(out) + portstr, err := inspectFieldJSON(id, "NetworkSettings.Ports") + if err != nil { + t.Fatal(err) + } + var ports nat.PortMap + err = unmarshalJSON([]byte(portstr), &ports) + for port, binding := range ports { + portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0]) + if portnum < 3000 || portnum > 3003 { + t.Fatalf("Port is out of range ", portnum, binding, out) + } + if binding == nil || len(binding) != 1 || len(binding[0].HostPort) == 0 { + t.Fatal("Port is not mapped for the port "+port, out) + } + } + logDone("run - allow port range through --expose flag") +} diff --git a/nat/nat.go b/nat/nat.go index 1246626b0d..8f2e90e668 100644 --- a/nat/nat.go +++ b/nat/nat.go @@ -122,31 +122,48 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, if containerPort == "" { return nil, nil, fmt.Errorf("No port specified: %s", rawPort) } - if _, err := strconv.ParseUint(containerPort, 10, 16); err != nil { + + startPort, endPort, err := parsers.ParsePortRange(containerPort) + if err != nil { return nil, nil, fmt.Errorf("Invalid containerPort: %s", containerPort) } - if _, err := strconv.ParseUint(hostPort, 10, 16); hostPort != "" && err != nil { - return nil, nil, fmt.Errorf("Invalid hostPort: %s", hostPort) + + var startHostPort, endHostPort uint64 = 0, 0 + if len(hostPort) > 0 { + startHostPort, endHostPort, err = parsers.ParsePortRange(hostPort) + if err != nil { + return nil, nil, fmt.Errorf("Invalid hostPort: %s", hostPort) + } + } + + if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { + return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) } if !validateProto(proto) { return nil, nil, fmt.Errorf("Invalid proto: %s", proto) } - port := NewPort(proto, containerPort) - if _, exists := exposedPorts[port]; !exists { - exposedPorts[port] = struct{}{} - } + for i := uint64(0); i <= (endPort - startPort); i++ { + containerPort = strconv.FormatUint(startPort+i, 10) + if len(hostPort) > 0 { + hostPort = strconv.FormatUint(startHostPort+i, 10) + } + port := NewPort(proto, containerPort) + if _, exists := exposedPorts[port]; !exists { + exposedPorts[port] = struct{}{} + } - binding := PortBinding{ - HostIp: rawIp, - HostPort: hostPort, + binding := PortBinding{ + HostIp: rawIp, + HostPort: hostPort, + } + bslice, exists := bindings[port] + if !exists { + bslice = []PortBinding{} + } + bindings[port] = append(bslice, binding) } - bslice, exists := bindings[port] - if !exists { - bslice = []PortBinding{} - } - bindings[port] = append(bslice, binding) } return exposedPorts, bindings, nil } diff --git a/nat/nat_test.go b/nat/nat_test.go index 4ae9f4ece5..34c210e6e2 100644 --- a/nat/nat_test.go +++ b/nat/nat_test.go @@ -108,7 +108,7 @@ func TestParsePortSpecs(t *testing.T) { portMap, bindingMap, err = ParsePortSpecs([]string{"1234/tcp", "2345/udp"}) if err != nil { - t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) + t.Fatalf("Error while processing ParsePortSpecs: %s", err) } if _, ok := portMap[Port("1234/tcp")]; !ok { @@ -136,7 +136,7 @@ func TestParsePortSpecs(t *testing.T) { portMap, bindingMap, err = ParsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp"}) if err != nil { - t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) + t.Fatalf("Error while processing ParsePortSpecs: %s", err) } if _, ok := portMap[Port("1234/tcp")]; !ok { @@ -166,7 +166,7 @@ func TestParsePortSpecs(t *testing.T) { portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp"}) if err != nil { - t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) + t.Fatalf("Error while processing ParsePortSpecs: %s", err) } if _, ok := portMap[Port("1234/tcp")]; !ok { @@ -199,3 +199,95 @@ func TestParsePortSpecs(t *testing.T) { t.Fatal("Received no error while trying to parse a hostname instead of ip") } } + +func TestParsePortSpecsWithRange(t *testing.T) { + var ( + portMap map[Port]struct{} + bindingMap map[Port][]PortBinding + err error + ) + + portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp"}) + + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[Port("1235/tcp")]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[Port("2346/udp")]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + for portspec, bindings := range bindingMap { + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portspec) + } + + if bindings[0].HostIp != "" { + t.Fatalf("HostIp should not be set for %s", portspec) + } + + if bindings[0].HostPort != "" { + t.Fatalf("HostPort should not be set for %s", portspec) + } + } + + portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp"}) + + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[Port("1235/tcp")]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[Port("2346/udp")]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + for portspec, bindings := range bindingMap { + _, port := SplitProtoPort(string(portspec)) + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portspec) + } + + if bindings[0].HostIp != "" { + t.Fatalf("HostIp should not be set for %s", portspec) + } + + if bindings[0].HostPort != port { + t.Fatalf("HostPort should be %s for %s", port, portspec) + } + } + + portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234-1236:1234-1236/tcp", "0.0.0.0:2345-2347:2345-2347/udp"}) + + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[Port("1235/tcp")]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[Port("2346/udp")]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + for portspec, bindings := range bindingMap { + _, port := SplitProtoPort(string(portspec)) + if len(bindings) != 1 || bindings[0].HostIp != "0.0.0.0" || bindings[0].HostPort != port { + t.Fatalf("Expect single binding to port %d but found %s", port, bindings) + } + } + + _, _, err = ParsePortSpecs([]string{"localhost:1234-1236:1234-1236/tcp"}) + + if err == nil { + t.Fatal("Received no error while trying to parse a hostname instead of ip") + } +} diff --git a/pkg/parsers/parsers.go b/pkg/parsers/parsers.go index 2851fe163a..6563190410 100644 --- a/pkg/parsers/parsers.go +++ b/pkg/parsers/parsers.go @@ -104,3 +104,28 @@ func ParseKeyValueOpt(opt string) (string, string, error) { } return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil } + +func ParsePortRange(ports string) (uint64, uint64, error) { + if ports == "" { + return 0, 0, fmt.Errorf("Empty string specified for ports.") + } + if !strings.Contains(ports, "-") { + start, err := strconv.ParseUint(ports, 10, 16) + end := start + return start, end, err + } + + parts := strings.Split(ports, "-") + start, err := strconv.ParseUint(parts[0], 10, 16) + if err != nil { + return 0, 0, err + } + end, err := strconv.ParseUint(parts[1], 10, 16) + if err != nil { + return 0, 0, err + } + if end < start { + return 0, 0, fmt.Errorf("Invalid range specified for the Port: %s", ports) + } + return start, end, nil +} diff --git a/pkg/parsers/parsers_test.go b/pkg/parsers/parsers_test.go index 12b8df5708..aac1e33e35 100644 --- a/pkg/parsers/parsers_test.go +++ b/pkg/parsers/parsers_test.go @@ -1,6 +1,7 @@ package parsers import ( + "strings" "testing" ) @@ -81,3 +82,35 @@ func TestParsePortMapping(t *testing.T) { t.Fail() } } + +func TestParsePortRange(t *testing.T) { + if start, end, err := ParsePortRange("8000-8080"); err != nil || start != 8000 || end != 8080 { + t.Fatalf("Error: %s or Expecting {start,end} values {8000,8080} but found {%d,%d}.", err, start, end) + } +} + +func TestParsePortRangeIncorrectRange(t *testing.T) { + if _, _, err := ParsePortRange("9000-8080"); err == nil || !strings.Contains(err.Error(), "Invalid range specified for the Port") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } +} + +func TestParsePortRangeIncorrectEndRange(t *testing.T) { + if _, _, err := ParsePortRange("8000-a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } + + if _, _, err := ParsePortRange("8000-30a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } +} + +func TestParsePortRangeIncorrectStartRange(t *testing.T) { + if _, _, err := ParsePortRange("a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } + + if _, _, err := ParsePortRange("30a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } +} diff --git a/runconfig/config_test.go b/runconfig/config_test.go index f856c87f54..accbd9107e 100644 --- a/runconfig/config_test.go +++ b/runconfig/config_test.go @@ -256,8 +256,8 @@ func TestMerge(t *testing.T) { t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts)) } for portSpecs := range configUser.ExposedPorts { - if portSpecs.Port() != "0000" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { - t.Fatalf("Expected 0000 or 1111 or 2222 or 3333, found %s", portSpecs) + if portSpecs.Port() != "0" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { + t.Fatalf("Expected %q or %q or %q or %q, found %s", 0, 1111, 2222, 3333, portSpecs) } } diff --git a/runconfig/parse.go b/runconfig/parse.go index 0d682f35d3..5c684e3467 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -197,11 +197,12 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe if strings.Contains(e, "-") { proto, port := nat.SplitProtoPort(e) //parse the start and end port and create a sequence of ports to expose - parts := strings.Split(port, "-") - start, _ := strconv.Atoi(parts[0]) - end, _ := strconv.Atoi(parts[1]) + start, end, err := parsers.ParsePortRange(port) + if err != nil { + return nil, nil, cmd, fmt.Errorf("Invalid range format for --expose: %s, error: %s", e, err) + } for i := start; i <= end; i++ { - p := nat.NewPort(proto, strconv.Itoa(i)) + p := nat.NewPort(proto, strconv.FormatUint(i, 10)) if _, exists := ports[p]; !exists { ports[p] = struct{}{} }