From 47272f9cc563ea90d1f86df044f5429d15a37e4f Mon Sep 17 00:00:00 2001 From: Don Kjer Date: Fri, 1 May 2015 19:35:26 +0000 Subject: [PATCH] Adding support to publish on custom host port ranges Signed-off-by: Don Kjer Changing vendor/src/github.com/docker/libnetwork to match lindenlab/libnetwork custom-host-port-ranges-1.7 branch --- daemon/container_unix.go | 7 ++- docs/reference/run.md | 1 + docs/userguide/dockerlinks.md | 8 +++ integration-cli/docker_cli_port_test.go | 75 +++++++++++++++++++++++++ pkg/nat/nat.go | 38 +++++++++++-- pkg/nat/nat_test.go | 64 +++++++++++++++++++++ pkg/nat/sort.go | 7 ++- 7 files changed, 192 insertions(+), 8 deletions(-) diff --git a/daemon/container_unix.go b/daemon/container_unix.go index b52f14c5de..321464ca9d 100644 --- a/daemon/container_unix.go +++ b/daemon/container_unix.go @@ -729,10 +729,15 @@ func (container *Container) buildCreateEndpointOptions() ([]libnetwork.EndpointO for i := 0; i < len(binding); i++ { pbCopy := pb.GetCopy() newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort)) + var portStart, portEnd int + if err == nil { + portStart, portEnd, err = newP.Range() + } if err != nil { return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err) } - pbCopy.HostPort = uint16(newP.Int()) + pbCopy.HostPort = uint16(portStart) + pbCopy.HostPortEnd = uint16(portEnd) pbCopy.HostIP = net.ParseIP(binding[i].HostIP) pbList = append(pbList, pbCopy) } diff --git a/docs/reference/run.md b/docs/reference/run.md index 79bde75af1..54f17ff4a8 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -984,6 +984,7 @@ or override the Dockerfile's exposed defaults: 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`) + When specifying a range for hostPort only, the containerPort must not be a range. In this case the container port is published somewhere within the specified hostPort range. (e.g., `-p 1234-1236:1234/tcp`) (use 'docker port' to see the actual mapping) --link="" : Add link to another container (:alias or ) diff --git a/docs/userguide/dockerlinks.md b/docs/userguide/dockerlinks.md index 6a41db38f0..119c43cf79 100644 --- a/docs/userguide/dockerlinks.md +++ b/docs/userguide/dockerlinks.md @@ -50,6 +50,14 @@ container: And you saw why this isn't such a great idea because it constrains you to only one container on that specific port. +Instead, you may specify a range of host ports to bind a container port to +that is different than the default *ephemeral port range*: + + $ docker run -d -p 8000-9000:5000 training/webapp python app.py + +This would bind port 5000 in the container to a randomly available port +between 8000 and 9000 on the host. + There are also a few other ways you can configure the `-p` flag. By default the `-p` flag will bind the specified port to all interfaces on the host machine. But you can also specify a binding to a specific diff --git a/integration-cli/docker_cli_port_test.go b/integration-cli/docker_cli_port_test.go index 63bfc9a0db..c8a9e6a0a5 100644 --- a/integration-cli/docker_cli_port_test.go +++ b/integration-cli/docker_cli_port_test.go @@ -78,6 +78,81 @@ func (s *DockerSuite) TestPortList(c *check.C) { } dockerCmd(c, "rm", "-f", ID) + testRange := func() { + // host port ranges used + IDs := make([]string, 3) + for i := 0; i < 3; i++ { + out, _ = dockerCmd(c, "run", "-d", + "-p", "9090-9092:80", + "busybox", "top") + IDs[i] = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", IDs[i]) + + if !assertPortList(c, out, []string{ + fmt.Sprintf("80/tcp -> 0.0.0.0:%d", 9090+i)}) { + c.Error("Port list is not correct\n", out) + } + } + + // test port range exhaustion + out, _, err := dockerCmdWithError("run", "-d", + "-p", "9090-9092:80", + "busybox", "top") + if err == nil { + c.Errorf("Exhausted port range did not return an error. Out: %s", out) + } + + for i := 0; i < 3; i++ { + dockerCmd(c, "rm", "-f", IDs[i]) + } + } + testRange() + // Verify we ran re-use port ranges after they are no longer in use. + testRange() + + // test invalid port ranges + for _, invalidRange := range []string{"9090-9089:80", "9090-:80", "-9090:80"} { + out, _, err := dockerCmdWithError("run", "-d", + "-p", invalidRange, + "busybox", "top") + if err == nil { + c.Errorf("Port range should have returned an error. Out: %s", out) + } + } + + // test host range:container range spec. + out, _ = dockerCmd(c, "run", "-d", + "-p", "9800-9803:80-83", + "busybox", "top") + ID = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", ID) + + if !assertPortList(c, out, []string{ + "80/tcp -> 0.0.0.0:9800", + "81/tcp -> 0.0.0.0:9801", + "82/tcp -> 0.0.0.0:9802", + "83/tcp -> 0.0.0.0:9803"}) { + c.Error("Port list is not correct\n", out) + } + dockerCmd(c, "rm", "-f", ID) + + // test mixing protocols in same port range + out, _ = dockerCmd(c, "run", "-d", + "-p", "8000-8080:80", + "-p", "8000-8080:80/udp", + "busybox", "top") + ID = strings.TrimSpace(out) + + out, _ = dockerCmd(c, "port", ID) + + if !assertPortList(c, out, []string{ + "80/tcp -> 0.0.0.0:8000", + "80/udp -> 0.0.0.0:8000"}) { + c.Error("Port list is not correct\n", out) + } + dockerCmd(c, "rm", "-f", ID) } func assertPortList(c *check.C, out string, expected []string) bool { diff --git a/pkg/nat/nat.go b/pkg/nat/nat.go index 1fbb13e6e9..6595feb055 100644 --- a/pkg/nat/nat.go +++ b/pkg/nat/nat.go @@ -34,17 +34,20 @@ type PortSet map[Port]struct{} // Port is a string containing port number and protocol in the format "80/tcp" type Port string -// NewPort creates a new instance of a Port given a protocol and port number +// NewPort creates a new instance of a Port given a protocol and port number or port range func NewPort(proto, port string) (Port, error) { // Check for parsing issues on "port" now so we can avoid having // to check it later on. - portInt, err := ParsePort(port) + portStartInt, portEndInt, err := ParsePortRange(port) if err != nil { return "", err } - return Port(fmt.Sprintf("%d/%s", portInt, proto)), nil + if portStartInt == portEndInt { + return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil + } + return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil } // ParsePort parses the port number string and returns an int @@ -59,6 +62,18 @@ func ParsePort(rawPort string) (int, error) { return int(port), nil } +// ParsePortRange parses the port range string and returns start/end ints +func ParsePortRange(rawPort string) (int, int, error) { + if len(rawPort) == 0 { + return 0, 0, nil + } + start, end, err := parsers.ParsePortRange(rawPort) + if err != nil { + return 0, 0, err + } + return int(start), int(end), nil +} + // Proto returns the protocol of a Port func (p Port) Proto() string { proto, _ := SplitProtoPort(string(p)) @@ -84,6 +99,11 @@ func (p Port) Int() int { return int(port) } +// Range returns the start/end port numbers of a Port range as ints +func (p Port) Range() (int, int, error) { + return ParsePortRange(p.Port()) +} + // SplitProtoPort splits a port in the format of proto/port func SplitProtoPort(rawPort string) (string, string) { parts := strings.Split(rawPort, "/") @@ -162,7 +182,12 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, } if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { - return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + // Allow host port range iff containerPort is not a range. + // In this case, use the host port range as the dynamic + // host port range to allocate into. + if endPort != startPort { + return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + } } if !validateProto(strings.ToLower(proto)) { @@ -174,6 +199,11 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, if len(hostPort) > 0 { hostPort = strconv.FormatUint(startHostPort+i, 10) } + // Set hostPort to a range only if there is a single container port + // and a dynamic host port. + if startPort == endPort && startHostPort != endHostPort { + hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10)) + } port, err := NewPort(strings.ToLower(proto), containerPort) if err != nil { return nil, nil, err diff --git a/pkg/nat/nat_test.go b/pkg/nat/nat_test.go index d9472cc7d9..2c71142bad 100644 --- a/pkg/nat/nat_test.go +++ b/pkg/nat/nat_test.go @@ -41,6 +41,56 @@ func TestParsePort(t *testing.T) { } } +func TestParsePortRange(t *testing.T) { + var ( + begin int + end int + err error + ) + + type TestRange struct { + Range string + Begin int + End int + } + validRanges := []TestRange{ + {"1234", 1234, 1234}, + {"1234-1234", 1234, 1234}, + {"1234-1235", 1234, 1235}, + {"8000-9000", 8000, 9000}, + {"0", 0, 0}, + {"0-0", 0, 0}, + } + + for _, r := range validRanges { + begin, end, err = ParsePortRange(r.Range) + + if err != nil || begin != r.Begin { + t.Fatalf("Parsing port range '%s' did not succeed. Expected begin %d, got %d", r.Range, r.Begin, begin) + } + if err != nil || end != r.End { + t.Fatalf("Parsing port range '%s' did not succeed. Expected end %d, got %d", r.Range, r.End, end) + } + } + + invalidRanges := []string{ + "asdf", + "1asdf", + "9000-8000", + "9000-", + "-8000", + "-8000-", + } + + for _, r := range invalidRanges { + begin, end, err = ParsePortRange(r) + + if err == nil || begin != 0 || end != 0 { + t.Fatalf("Parsing port range '%s' succeeded", r) + } + } +} + func TestPort(t *testing.T) { p, err := NewPort("tcp", "1234") @@ -68,6 +118,20 @@ func TestPort(t *testing.T) { if err == nil { t.Fatal("tcp, asd1234 was supposed to fail") } + + p, err = NewPort("tcp", "1234-1230") + if err == nil { + t.Fatal("tcp, 1234-1230 was supposed to fail") + } + + p, err = NewPort("tcp", "1234-1242") + if err != nil { + t.Fatalf("tcp, 1234-1242 had a parsing issue: %v", err) + } + + if string(p) != "1234-1242/tcp" { + t.Fatal("tcp, 1234-1242 did not result in the string 1234-1242/tcp") + } } func TestSplitProtoPort(t *testing.T) { diff --git a/pkg/nat/sort.go b/pkg/nat/sort.go index 0a9dd078f0..1eb0fedda5 100644 --- a/pkg/nat/sort.go +++ b/pkg/nat/sort.go @@ -2,8 +2,9 @@ package nat import ( "sort" - "strconv" "strings" + + "github.com/docker/docker/pkg/parsers" ) type portSorter struct { @@ -88,8 +89,8 @@ func SortPortMap(ports []Port, bindings PortMap) { } } -func toInt(s string) int64 { - i, err := strconv.ParseInt(s, 10, 64) +func toInt(s string) uint64 { + i, _, err := parsers.ParsePortRange(s) if err != nil { i = 0 }