Browse code

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

Don Kjer authored on 2015/05/02 04:35:26
Showing 7 changed files
... ...
@@ -729,10 +729,15 @@ func (container *Container) buildCreateEndpointOptions() ([]libnetwork.EndpointO
729 729
 		for i := 0; i < len(binding); i++ {
730 730
 			pbCopy := pb.GetCopy()
731 731
 			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
+			}
732 736
 			if err != nil {
733 737
 				return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err)
734 738
 			}
735
-			pbCopy.HostPort = uint16(newP.Int())
739
+			pbCopy.HostPort = uint16(portStart)
740
+			pbCopy.HostPortEnd = uint16(portEnd)
736 741
 			pbCopy.HostIP = net.ParseIP(binding[i].HostIP)
737 742
 			pbList = append(pbList, pbCopy)
738 743
 		}
... ...
@@ -984,6 +984,7 @@ or override the Dockerfile's exposed defaults:
984 984
                    format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
985 985
                    Both hostPort and containerPort can be specified as a range of ports.
986 986
                    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`)
987 988
                    (use 'docker port' to see the actual mapping)
988 989
     --link=""  : Add link to another container (<name or id>:alias or <name or id>)
989 990
 
... ...
@@ -50,6 +50,14 @@ container:
50 50
 And you saw why this isn't such a great idea because it constrains you to
51 51
 only one container on that specific port.
52 52
 
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
+
53 61
 There are also a few other ways you can configure the `-p` flag. By
54 62
 default the `-p` flag will bind the specified port to all interfaces on
55 63
 the host machine. But you can also specify a binding to a specific
... ...
@@ -78,6 +78,81 @@ func (s *DockerSuite) TestPortList(c *check.C) {
78 78
 	}
79 79
 	dockerCmd(c, "rm", "-f", ID)
80 80
 
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)
81 156
 }
82 157
 
83 158
 func assertPortList(c *check.C, out string, expected []string) bool {
... ...
@@ -34,17 +34,20 @@ type PortSet map[Port]struct{}
34 34
 // Port is a string containing port number and protocol in the format "80/tcp"
35 35
 type Port string
36 36
 
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
38 38
 func NewPort(proto, port string) (Port, error) {
39 39
 	// Check for parsing issues on "port" now so we can avoid having
40 40
 	// to check it later on.
41 41
 
42
-	portInt, err := ParsePort(port)
42
+	portStartInt, portEndInt, err := ParsePortRange(port)
43 43
 	if err != nil {
44 44
 		return "", err
45 45
 	}
46 46
 
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
48 51
 }
49 52
 
50 53
 // ParsePort parses the port number string and returns an int
... ...
@@ -59,6 +62,18 @@ func ParsePort(rawPort string) (int, error) {
59 59
 	return int(port), nil
60 60
 }
61 61
 
62
+// ParsePortRange parses the port range string and returns start/end ints
63
+func ParsePortRange(rawPort string) (int, int, error) {
64
+	if len(rawPort) == 0 {
65
+		return 0, 0, nil
66
+	}
67
+	start, end, err := parsers.ParsePortRange(rawPort)
68
+	if err != nil {
69
+		return 0, 0, err
70
+	}
71
+	return int(start), int(end), nil
72
+}
73
+
62 74
 // Proto returns the protocol of a Port
63 75
 func (p Port) Proto() string {
64 76
 	proto, _ := SplitProtoPort(string(p))
... ...
@@ -84,6 +99,11 @@ func (p Port) Int() int {
84 84
 	return int(port)
85 85
 }
86 86
 
87
+// Range returns the start/end port numbers of a Port range as ints
88
+func (p Port) Range() (int, int, error) {
89
+	return ParsePortRange(p.Port())
90
+}
91
+
87 92
 // SplitProtoPort splits a port in the format of proto/port
88 93
 func SplitProtoPort(rawPort string) (string, string) {
89 94
 	parts := strings.Split(rawPort, "/")
... ...
@@ -162,7 +182,12 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding,
162 162
 		}
163 163
 
164 164
 		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)
165
+			// Allow host port range iff containerPort is not a range.
166
+			// In this case, use the host port range as the dynamic
167
+			// host port range to allocate into.
168
+			if endPort != startPort {
169
+				return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
170
+			}
166 171
 		}
167 172
 
168 173
 		if !validateProto(strings.ToLower(proto)) {
... ...
@@ -174,6 +199,11 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding,
174 174
 			if len(hostPort) > 0 {
175 175
 				hostPort = strconv.FormatUint(startHostPort+i, 10)
176 176
 			}
177
+			// Set hostPort to a range only if there is a single container port
178
+			// and a dynamic host port.
179
+			if startPort == endPort && startHostPort != endHostPort {
180
+				hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10))
181
+			}
177 182
 			port, err := NewPort(strings.ToLower(proto), containerPort)
178 183
 			if err != nil {
179 184
 				return nil, nil, err
... ...
@@ -41,6 +41,56 @@ func TestParsePort(t *testing.T) {
41 41
 	}
42 42
 }
43 43
 
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
+
44 94
 func TestPort(t *testing.T) {
45 95
 	p, err := NewPort("tcp", "1234")
46 96
 
... ...
@@ -68,6 +118,20 @@ func TestPort(t *testing.T) {
68 68
 	if err == nil {
69 69
 		t.Fatal("tcp, asd1234 was supposed to fail")
70 70
 	}
71
+
72
+	p, err = NewPort("tcp", "1234-1230")
73
+	if err == nil {
74
+		t.Fatal("tcp, 1234-1230 was supposed to fail")
75
+	}
76
+
77
+	p, err = NewPort("tcp", "1234-1242")
78
+	if err != nil {
79
+		t.Fatalf("tcp, 1234-1242 had a parsing issue: %v", err)
80
+	}
81
+
82
+	if string(p) != "1234-1242/tcp" {
83
+		t.Fatal("tcp, 1234-1242 did not result in the string 1234-1242/tcp")
84
+	}
71 85
 }
72 86
 
73 87
 func TestSplitProtoPort(t *testing.T) {
... ...
@@ -2,8 +2,9 @@ package nat
2 2
 
3 3
 import (
4 4
 	"sort"
5
-	"strconv"
6 5
 	"strings"
6
+
7
+	"github.com/docker/docker/pkg/parsers"
7 8
 )
8 9
 
9 10
 type portSorter struct {
... ...
@@ -88,8 +89,8 @@ func SortPortMap(ports []Port, bindings PortMap) {
88 88
 	}
89 89
 }
90 90
 
91
-func toInt(s string) int64 {
92
-	i, err := strconv.ParseInt(s, 10, 64)
91
+func toInt(s string) uint64 {
92
+	i, _, err := parsers.ParsePortRange(s)
93 93
 	if err != nil {
94 94
 		i = 0
95 95
 	}