Skip to content

Commit 47272f9

Browse files
committed
Adding support to publish on custom host port ranges
Signed-off-by: Don Kjer <don.kjer@gmail.com> Changing vendor/src/github.com/docker/libnetwork to match lindenlab/libnetwork custom-host-port-ranges-1.7 branch
1 parent aedd453 commit 47272f9

File tree

7 files changed

+192
-8
lines changed

7 files changed

+192
-8
lines changed

daemon/container_unix.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,10 +729,15 @@ func (container *Container) buildCreateEndpointOptions() ([]libnetwork.EndpointO
729729
for i := 0; i < len(binding); i++ {
730730
pbCopy := pb.GetCopy()
731731
newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort))
732+
var portStart, portEnd int
733+
if err == nil {
734+
portStart, portEnd, err = newP.Range()
735+
}
732736
if err != nil {
733737
return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err)
734738
}
735-
pbCopy.HostPort = uint16(newP.Int())
739+
pbCopy.HostPort = uint16(portStart)
740+
pbCopy.HostPortEnd = uint16(portEnd)
736741
pbCopy.HostIP = net.ParseIP(binding[i].HostIP)
737742
pbList = append(pbList, pbCopy)
738743
}

docs/reference/run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,7 @@ or override the Dockerfile's exposed defaults:
984984
format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
985985
Both hostPort and containerPort can be specified as a range of ports.
986986
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`)
987+
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`)
987988
(use 'docker port' to see the actual mapping)
988989
--link="" : Add link to another container (<name or id>:alias or <name or id>)
989990

docs/userguide/dockerlinks.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ container:
5050
And you saw why this isn't such a great idea because it constrains you to
5151
only one container on that specific port.
5252

53+
Instead, you may specify a range of host ports to bind a container port to
54+
that is different than the default *ephemeral port range*:
55+
56+
$ docker run -d -p 8000-9000:5000 training/webapp python app.py
57+
58+
This would bind port 5000 in the container to a randomly available port
59+
between 8000 and 9000 on the host.
60+
5361
There are also a few other ways you can configure the `-p` flag. By
5462
default the `-p` flag will bind the specified port to all interfaces on
5563
the host machine. But you can also specify a binding to a specific

integration-cli/docker_cli_port_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,81 @@ func (s *DockerSuite) TestPortList(c *check.C) {
7878
}
7979
dockerCmd(c, "rm", "-f", ID)
8080

81+
testRange := func() {
82+
// host port ranges used
83+
IDs := make([]string, 3)
84+
for i := 0; i < 3; i++ {
85+
out, _ = dockerCmd(c, "run", "-d",
86+
"-p", "9090-9092:80",
87+
"busybox", "top")
88+
IDs[i] = strings.TrimSpace(out)
89+
90+
out, _ = dockerCmd(c, "port", IDs[i])
91+
92+
if !assertPortList(c, out, []string{
93+
fmt.Sprintf("80/tcp -> 0.0.0.0:%d", 9090+i)}) {
94+
c.Error("Port list is not correct\n", out)
95+
}
96+
}
97+
98+
// test port range exhaustion
99+
out, _, err := dockerCmdWithError("run", "-d",
100+
"-p", "9090-9092:80",
101+
"busybox", "top")
102+
if err == nil {
103+
c.Errorf("Exhausted port range did not return an error. Out: %s", out)
104+
}
105+
106+
for i := 0; i < 3; i++ {
107+
dockerCmd(c, "rm", "-f", IDs[i])
108+
}
109+
}
110+
testRange()
111+
// Verify we ran re-use port ranges after they are no longer in use.
112+
testRange()
113+
114+
// test invalid port ranges
115+
for _, invalidRange := range []string{"9090-9089:80", "9090-:80", "-9090:80"} {
116+
out, _, err := dockerCmdWithError("run", "-d",
117+
"-p", invalidRange,
118+
"busybox", "top")
119+
if err == nil {
120+
c.Errorf("Port range should have returned an error. Out: %s", out)
121+
}
122+
}
123+
124+
// test host range:container range spec.
125+
out, _ = dockerCmd(c, "run", "-d",
126+
"-p", "9800-9803:80-83",
127+
"busybox", "top")
128+
ID = strings.TrimSpace(out)
129+
130+
out, _ = dockerCmd(c, "port", ID)
131+
132+
if !assertPortList(c, out, []string{
133+
"80/tcp -> 0.0.0.0:9800",
134+
"81/tcp -> 0.0.0.0:9801",
135+
"82/tcp -> 0.0.0.0:9802",
136+
"83/tcp -> 0.0.0.0:9803"}) {
137+
c.Error("Port list is not correct\n", out)
138+
}
139+
dockerCmd(c, "rm", "-f", ID)
140+
141+
// test mixing protocols in same port range
142+
out, _ = dockerCmd(c, "run", "-d",
143+
"-p", "8000-8080:80",
144+
"-p", "8000-8080:80/udp",
145+
"busybox", "top")
146+
ID = strings.TrimSpace(out)
147+
148+
out, _ = dockerCmd(c, "port", ID)
149+
150+
if !assertPortList(c, out, []string{
151+
"80/tcp -> 0.0.0.0:8000",
152+
"80/udp -> 0.0.0.0:8000"}) {
153+
c.Error("Port list is not correct\n", out)
154+
}
155+
dockerCmd(c, "rm", "-f", ID)
81156
}
82157

83158
func assertPortList(c *check.C, out string, expected []string) bool {

pkg/nat/nat.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,20 @@ type PortSet map[Port]struct{}
3434
// Port is a string containing port number and protocol in the format "80/tcp"
3535
type Port string
3636

37-
// NewPort creates a new instance of a Port given a protocol and port number
37+
// NewPort creates a new instance of a Port given a protocol and port number or port range
3838
func NewPort(proto, port string) (Port, error) {
3939
// Check for parsing issues on "port" now so we can avoid having
4040
// to check it later on.
4141

42-
portInt, err := ParsePort(port)
42+
portStartInt, portEndInt, err := ParsePortRange(port)
4343
if err != nil {
4444
return "", err
4545
}
4646

47-
return Port(fmt.Sprintf("%d/%s", portInt, proto)), nil
47+
if portStartInt == portEndInt {
48+
return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil
49+
}
50+
return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil
4851
}
4952

5053
// ParsePort parses the port number string and returns an int
@@ -59,6 +62,18 @@ func ParsePort(rawPort string) (int, error) {
5962
return int(port), nil
6063
}
6164

65+
// ParsePortRange parses the port range string and returns start/end ints
66+
func ParsePortRange(rawPort string) (int, int, error) {
67+
if len(rawPort) == 0 {
68+
return 0, 0, nil
69+
}
70+
start, end, err := parsers.ParsePortRange(rawPort)
71+
if err != nil {
72+
return 0, 0, err
73+
}
74+
return int(start), int(end), nil
75+
}
76+
6277
// Proto returns the protocol of a Port
6378
func (p Port) Proto() string {
6479
proto, _ := SplitProtoPort(string(p))
@@ -84,6 +99,11 @@ func (p Port) Int() int {
8499
return int(port)
85100
}
86101

102+
// Range returns the start/end port numbers of a Port range as ints
103+
func (p Port) Range() (int, int, error) {
104+
return ParsePortRange(p.Port())
105+
}
106+
87107
// SplitProtoPort splits a port in the format of proto/port
88108
func SplitProtoPort(rawPort string) (string, string) {
89109
parts := strings.Split(rawPort, "/")
@@ -162,7 +182,12 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding,
162182
}
163183

164184
if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
165-
return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
185+
// Allow host port range iff containerPort is not a range.
186+
// In this case, use the host port range as the dynamic
187+
// host port range to allocate into.
188+
if endPort != startPort {
189+
return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
190+
}
166191
}
167192

168193
if !validateProto(strings.ToLower(proto)) {
@@ -174,6 +199,11 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding,
174199
if len(hostPort) > 0 {
175200
hostPort = strconv.FormatUint(startHostPort+i, 10)
176201
}
202+
// Set hostPort to a range only if there is a single container port
203+
// and a dynamic host port.
204+
if startPort == endPort && startHostPort != endHostPort {
205+
hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10))
206+
}
177207
port, err := NewPort(strings.ToLower(proto), containerPort)
178208
if err != nil {
179209
return nil, nil, err

pkg/nat/nat_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,56 @@ func TestParsePort(t *testing.T) {
4141
}
4242
}
4343

44+
func TestParsePortRange(t *testing.T) {
45+
var (
46+
begin int
47+
end int
48+
err error
49+
)
50+
51+
type TestRange struct {
52+
Range string
53+
Begin int
54+
End int
55+
}
56+
validRanges := []TestRange{
57+
{"1234", 1234, 1234},
58+
{"1234-1234", 1234, 1234},
59+
{"1234-1235", 1234, 1235},
60+
{"8000-9000", 8000, 9000},
61+
{"0", 0, 0},
62+
{"0-0", 0, 0},
63+
}
64+
65+
for _, r := range validRanges {
66+
begin, end, err = ParsePortRange(r.Range)
67+
68+
if err != nil || begin != r.Begin {
69+
t.Fatalf("Parsing port range '%s' did not succeed. Expected begin %d, got %d", r.Range, r.Begin, begin)
70+
}
71+
if err != nil || end != r.End {
72+
t.Fatalf("Parsing port range '%s' did not succeed. Expected end %d, got %d", r.Range, r.End, end)
73+
}
74+
}
75+
76+
invalidRanges := []string{
77+
"asdf",
78+
"1asdf",
79+
"9000-8000",
80+
"9000-",
81+
"-8000",
82+
"-8000-",
83+
}
84+
85+
for _, r := range invalidRanges {
86+
begin, end, err = ParsePortRange(r)
87+
88+
if err == nil || begin != 0 || end != 0 {
89+
t.Fatalf("Parsing port range '%s' succeeded", r)
90+
}
91+
}
92+
}
93+
4494
func TestPort(t *testing.T) {
4595
p, err := NewPort("tcp", "1234")
4696

@@ -68,6 +118,20 @@ func TestPort(t *testing.T) {
68118
if err == nil {
69119
t.Fatal("tcp, asd1234 was supposed to fail")
70120
}
121+
122+
p, err = NewPort("tcp", "1234-1230")
123+
if err == nil {
124+
t.Fatal("tcp, 1234-1230 was supposed to fail")
125+
}
126+
127+
p, err = NewPort("tcp", "1234-1242")
128+
if err != nil {
129+
t.Fatalf("tcp, 1234-1242 had a parsing issue: %v", err)
130+
}
131+
132+
if string(p) != "1234-1242/tcp" {
133+
t.Fatal("tcp, 1234-1242 did not result in the string 1234-1242/tcp")
134+
}
71135
}
72136

73137
func TestSplitProtoPort(t *testing.T) {

pkg/nat/sort.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package nat
22

33
import (
44
"sort"
5-
"strconv"
65
"strings"
6+
7+
"github.com/docker/docker/pkg/parsers"
78
)
89

910
type portSorter struct {
@@ -88,8 +89,8 @@ func SortPortMap(ports []Port, bindings PortMap) {
8889
}
8990
}
9091

91-
func toInt(s string) int64 {
92-
i, err := strconv.ParseInt(s, 10, 64)
92+
func toInt(s string) uint64 {
93+
i, _, err := parsers.ParsePortRange(s)
9394
if err != nil {
9495
i = 0
9596
}

0 commit comments

Comments
 (0)