Browse code

adding support for port ranges on --expose Closes #1834

Signed-off-by: Srini Brahmaroutu <srbrahma@us.ibm.com>

Srini Brahmaroutu authored on 2014/09/17 10:08:30
Showing 11 changed files
... ...
@@ -5,7 +5,7 @@ import (
5 5
 	"io/ioutil"
6 6
 	"net"
7 7
 	"os"
8
-	"strings"
8
+	"strconv"
9 9
 	"sync"
10 10
 
11 11
 	log "github.com/Sirupsen/logrus"
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/docker/docker/daemon/networkdriver/portallocator"
15 15
 	"github.com/docker/docker/daemon/networkdriver/portmapper"
16 16
 	"github.com/docker/docker/engine"
17
+	"github.com/docker/docker/nat"
17 18
 	"github.com/docker/docker/pkg/iptables"
18 19
 	"github.com/docker/docker/pkg/networkfs/resolvconf"
19 20
 	"github.com/docker/docker/pkg/parsers/kernel"
... ...
@@ -515,18 +516,13 @@ func LinkContainers(job *engine.Job) engine.Status {
515 515
 		ignoreErrors = job.GetenvBool("IgnoreErrors")
516 516
 		ports        = job.GetenvList("Ports")
517 517
 	)
518
-	split := func(p string) (string, string) {
519
-		parts := strings.Split(p, "/")
520
-		return parts[0], parts[1]
521
-	}
522
-
523
-	for _, p := range ports {
524
-		port, proto := split(p)
518
+	for _, value := range ports {
519
+		port := nat.Port(value)
525 520
 		if output, err := iptables.Raw(action, "FORWARD",
526 521
 			"-i", bridgeIface, "-o", bridgeIface,
527
-			"-p", proto,
522
+			"-p", port.Proto(),
528 523
 			"-s", parentIP,
529
-			"--dport", port,
524
+			"--dport", strconv.Itoa(port.Int()),
530 525
 			"-d", childIP,
531 526
 			"-j", "ACCEPT"); !ignoreErrors && err != nil {
532 527
 			return job.Error(err)
... ...
@@ -536,9 +532,9 @@ func LinkContainers(job *engine.Job) engine.Status {
536 536
 
537 537
 		if output, err := iptables.Raw(action, "FORWARD",
538 538
 			"-i", bridgeIface, "-o", bridgeIface,
539
-			"-p", proto,
539
+			"-p", port.Proto(),
540 540
 			"-s", childIP,
541
-			"--sport", port,
541
+			"--sport", strconv.Itoa(port.Int()),
542 542
 			"-d", parentIP,
543 543
 			"-j", "ACCEPT"); !ignoreErrors && err != nil {
544 544
 			return job.Error(err)
... ...
@@ -79,7 +79,7 @@ docker-create - Create a new container
79 79
    Read in a line delimited file of environment variables
80 80
 
81 81
 **--expose**=[]
82
-   Expose a port from the container without publishing it to your host
82
+   Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host
83 83
 
84 84
 **-h**, **--hostname**=""
85 85
    Container host name
... ...
@@ -132,12 +132,8 @@ ENTRYPOINT.
132 132
 **--env-file**=[]
133 133
    Read in a line delimited file of environment variables
134 134
 
135
-**--expose**=*port*
136
-   Expose a port from the container without publishing it to your host. A
137
-containers port can be exposed to other containers in three ways: 1) The
138
-developer can expose the port using the EXPOSE parameter of the Dockerfile, 2)
139
-the operator can use the **--expose** option with **docker run**, or 3) the
140
-container can be started with the **--link**.
135
+**--expose**=[]
136
+   Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host
141 137
 
142 138
 **-h**, **--hostname**=*hostname*
143 139
    Sets the container host name that is available inside the container.
... ...
@@ -509,7 +509,7 @@ Creates a new container.
509 509
       -e, --env=[]               Set environment variables
510 510
       --entrypoint=""            Overwrite the default ENTRYPOINT of the image
511 511
       --env-file=[]              Read in a line delimited file of environment variables
512
-      --expose=[]                Expose a port from the container without publishing it to your host
512
+      --expose=[]                Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host
513 513
       -h, --hostname=""          Container host name
514 514
       -i, --interactive=false    Keep STDIN open even if not attached
515 515
       --link=[]                  Add link to another container in the form of name:alias
... ...
@@ -1211,7 +1211,7 @@ removed before the image is removed.
1211 1211
       -e, --env=[]               Set environment variables
1212 1212
       --entrypoint=""            Overwrite the default ENTRYPOINT of the image
1213 1213
       --env-file=[]              Read in a line delimited file of environment variables
1214
-      --expose=[]                Expose a port from the container without publishing it to your host
1214
+      --expose=[]                Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host
1215 1215
       -h, --hostname=""          Container host name
1216 1216
       -i, --interactive=false    Keep STDIN open even if not attached
1217 1217
       --link=[]                  Add link to another container in the form of name:alias
... ...
@@ -413,7 +413,7 @@ the `EXPOSE` instruction to give a hint to the operator about what
413 413
 incoming ports might provide services. The following options work with
414 414
 or override the Dockerfile's exposed defaults:
415 415
 
416
-    --expose=[]: Expose a port from the container
416
+    --expose=[]: Expose a port or a range of ports from the container
417 417
                 without publishing it to your host
418 418
     -P=false   : Publish all exposed ports to the host interfaces
419 419
     -p=[]      : Publish a container᾿s port to the host (format:
... ...
@@ -422,7 +422,7 @@ or override the Dockerfile's exposed defaults:
422 422
                  (use 'docker port' to see the actual mapping)
423 423
     --link=""  : Add link to another container (name:alias)
424 424
 
425
-As mentioned previously, `EXPOSE` (and `--expose`) make a port available
425
+As mentioned previously, `EXPOSE` (and `--expose`) makes ports available
426 426
 **in** a container for incoming connections. The port number on the
427 427
 inside of the container (where the service listens) does not need to be
428 428
 the same number as the port exposed on the outside of the container
... ...
@@ -13,11 +13,13 @@ import (
13 13
 	"reflect"
14 14
 	"regexp"
15 15
 	"sort"
16
+	"strconv"
16 17
 	"strings"
17 18
 	"sync"
18 19
 	"testing"
19 20
 	"time"
20 21
 
22
+	"github.com/docker/docker/nat"
21 23
 	"github.com/docker/docker/pkg/mount"
22 24
 	"github.com/docker/docker/pkg/networkfs/resolvconf"
23 25
 	"github.com/kr/pty"
... ...
@@ -2473,3 +2475,31 @@ func TestRunSlowStdoutConsumer(t *testing.T) {
2473 2473
 
2474 2474
 	logDone("run - slow consumer")
2475 2475
 }
2476
+
2477
+func TestRunAllowPortRangeThroughExpose(t *testing.T) {
2478
+	cmd := exec.Command(dockerBinary, "run", "-d", "--expose", "3000-3003", "-P", "busybox", "top")
2479
+	out, _, err := runCommandWithOutput(cmd)
2480
+	if err != nil {
2481
+		t.Fatal(err)
2482
+	}
2483
+	id := strings.TrimSpace(out)
2484
+	portstr, err := inspectFieldJSON(id, "NetworkSettings.Ports")
2485
+	if err != nil {
2486
+		t.Fatal(err)
2487
+	}
2488
+	var ports nat.PortMap
2489
+	err = unmarshalJSON([]byte(portstr), &ports)
2490
+	for port, binding := range ports {
2491
+		portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0])
2492
+		if portnum < 3000 || portnum > 3003 {
2493
+			t.Fatalf("Port is out of range ", portnum, binding, out)
2494
+		}
2495
+		if binding == nil || len(binding) != 1 || len(binding[0].HostPort) == 0 {
2496
+			t.Fatal("Port is not mapped for the port "+port, out)
2497
+		}
2498
+	}
2499
+	if err := deleteContainer(id); err != nil {
2500
+		t.Fatal(err)
2501
+	}
2502
+	logDone("run - allow port range through --expose flag")
2503
+}
... ...
@@ -47,6 +47,20 @@ func (l *Link) Alias() string {
47 47
 	return alias
48 48
 }
49 49
 
50
+func nextContiguous(ports []nat.Port, value int, index int) int {
51
+	if index+1 == len(ports) {
52
+		return index
53
+	}
54
+	for i := index + 1; i < len(ports); i++ {
55
+		if ports[i].Int() > value+1 {
56
+			return i - 1
57
+		}
58
+
59
+		value++
60
+	}
61
+	return len(ports) - 1
62
+}
63
+
50 64
 func (l *Link) ToEnv() []string {
51 65
 	env := []string{}
52 66
 	alias := strings.Replace(strings.ToUpper(l.Alias()), "-", "_", -1)
... ...
@@ -55,12 +69,35 @@ func (l *Link) ToEnv() []string {
55 55
 		env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port()))
56 56
 	}
57 57
 
58
-	// Load exposed ports into the environment
59
-	for _, p := range l.Ports {
58
+	//sort the ports so that we can bulk the continuous ports together
59
+	nat.Sort(l.Ports, func(ip, jp nat.Port) bool {
60
+		// If the two ports have the same number, tcp takes priority
61
+		// Sort in desc order
62
+		return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp")
63
+	})
64
+
65
+	for i := 0; i < len(l.Ports); {
66
+		p := l.Ports[i]
67
+		j := nextContiguous(l.Ports, p.Int(), i)
68
+		if j > i+1 {
69
+			env = append(env, fmt.Sprintf("%s_PORT_%s_%s_START=%s://%s:%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.ChildIP, p.Port()))
70
+			env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP))
71
+			env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto()))
72
+			env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_START=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port()))
73
+
74
+			q := l.Ports[j]
75
+			env = append(env, fmt.Sprintf("%s_PORT_%s_%s_END=%s://%s:%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Proto(), l.ChildIP, q.Port()))
76
+			env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_END=%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Port()))
77
+
78
+			i = j + 1
79
+			continue
80
+		}
81
+
60 82
 		env = append(env, fmt.Sprintf("%s_PORT_%s_%s=%s://%s:%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.ChildIP, p.Port()))
61 83
 		env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP))
62 84
 		env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port()))
63 85
 		env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto()))
86
+		i++
64 87
 	}
65 88
 
66 89
 	// Load the linked container's name into the environment
... ...
@@ -125,7 +162,7 @@ func (l *Link) toggle(action string, ignoreErrors bool) error {
125 125
 
126 126
 	out := make([]string, len(l.Ports))
127 127
 	for i, p := range l.Ports {
128
-		out[i] = fmt.Sprintf("%s/%s", p.Port(), p.Proto())
128
+		out[i] = string(p)
129 129
 	}
130 130
 	job.SetenvList("Ports", out)
131 131
 
... ...
@@ -107,3 +107,52 @@ func TestLinkEnv(t *testing.T) {
107 107
 		t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"])
108 108
 	}
109 109
 }
110
+
111
+func TestLinkMultipleEnv(t *testing.T) {
112
+	ports := make(nat.PortSet)
113
+	ports[nat.Port("6379/tcp")] = struct{}{}
114
+	ports[nat.Port("6380/tcp")] = struct{}{}
115
+	ports[nat.Port("6381/tcp")] = struct{}{}
116
+
117
+	link, err := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, ports, nil)
118
+	if err != nil {
119
+		t.Fatal(err)
120
+	}
121
+
122
+	rawEnv := link.ToEnv()
123
+	env := make(map[string]string, len(rawEnv))
124
+	for _, e := range rawEnv {
125
+		parts := strings.Split(e, "=")
126
+		if len(parts) != 2 {
127
+			t.FailNow()
128
+		}
129
+		env[parts[0]] = parts[1]
130
+	}
131
+	if env["DOCKER_PORT"] != "tcp://172.0.17.2:6379" {
132
+		t.Fatalf("Expected 172.0.17.2:6379, got %s", env["DOCKER_PORT"])
133
+	}
134
+	if env["DOCKER_PORT_6379_TCP_START"] != "tcp://172.0.17.2:6379" {
135
+		t.Fatalf("Expected tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT_6379_TCP_START"])
136
+	}
137
+	if env["DOCKER_PORT_6379_TCP_END"] != "tcp://172.0.17.2:6381" {
138
+		t.Fatalf("Expected tcp://172.0.17.2:6381, got %s", env["DOCKER_PORT_6379_TCP_END"])
139
+	}
140
+	if env["DOCKER_PORT_6379_TCP_PROTO"] != "tcp" {
141
+		t.Fatalf("Expected tcp, got %s", env["DOCKER_PORT_6379_TCP_PROTO"])
142
+	}
143
+	if env["DOCKER_PORT_6379_TCP_ADDR"] != "172.0.17.2" {
144
+		t.Fatalf("Expected 172.0.17.2, got %s", env["DOCKER_PORT_6379_TCP_ADDR"])
145
+	}
146
+	if env["DOCKER_PORT_6379_TCP_PORT_START"] != "6379" {
147
+		t.Fatalf("Expected 6379, got %s", env["DOCKER_PORT_6379_TCP_PORT_START"])
148
+	}
149
+	if env["DOCKER_PORT_6379_TCP_PORT_END"] != "6381" {
150
+		t.Fatalf("Expected 6381, got %s", env["DOCKER_PORT_6379_TCP_PORT_END"])
151
+	}
152
+	if env["DOCKER_NAME"] != "/db/docker" {
153
+		t.Fatalf("Expected /db/docker, got %s", env["DOCKER_NAME"])
154
+	}
155
+	if env["DOCKER_ENV_PASSWORD"] != "gordon" {
156
+		t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"])
157
+	}
158
+}
... ...
@@ -42,44 +42,37 @@ func ParsePort(rawPort string) (int, error) {
42 42
 }
43 43
 
44 44
 func (p Port) Proto() string {
45
-	parts := strings.Split(string(p), "/")
46
-	if len(parts) == 1 {
47
-		return "tcp"
48
-	}
49
-	return parts[1]
45
+	proto, _ := SplitProtoPort(string(p))
46
+	return proto
50 47
 }
51 48
 
52 49
 func (p Port) Port() string {
53
-	return strings.Split(string(p), "/")[0]
50
+	_, port := SplitProtoPort(string(p))
51
+	return port
54 52
 }
55 53
 
56 54
 func (p Port) Int() int {
57
-	i, err := ParsePort(p.Port())
55
+	port, err := ParsePort(p.Port())
58 56
 	if err != nil {
59 57
 		panic(err)
60 58
 	}
61
-	return i
59
+	return port
62 60
 }
63 61
 
64 62
 // Splits a port in the format of proto/port
65 63
 func SplitProtoPort(rawPort string) (string, string) {
66
-	var port string
67
-	var proto string
68
-
69 64
 	parts := strings.Split(rawPort, "/")
70
-
71
-	if len(parts) == 0 || parts[0] == "" { // we have "" or ""/
72
-		port = ""
73
-		proto = ""
74
-	} else { // we have # or #/  or #/...
75
-		port = parts[0]
76
-		if len(parts) > 1 && parts[1] != "" {
77
-			proto = parts[1] // we have #/...
78
-		} else {
79
-			proto = "tcp" // we have # or #/
80
-		}
65
+	l := len(parts)
66
+	if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 {
67
+		return "", ""
68
+	}
69
+	if l == 1 {
70
+		return "tcp", rawPort
71
+	}
72
+	if len(parts[1]) == 0 {
73
+		return "tcp", parts[0]
81 74
 	}
82
-	return proto, port
75
+	return parts[1], parts[0]
83 76
 }
84 77
 
85 78
 func validateProto(proto string) bool {
... ...
@@ -76,13 +76,13 @@ func TestSplitProtoPort(t *testing.T) {
76 76
 	proto, port = SplitProtoPort("")
77 77
 
78 78
 	if proto != "" || port != "" {
79
-		t.Fatal("parsing an empty string yielded surprising results")
79
+		t.Fatal("parsing an empty string yielded surprising results", proto, port)
80 80
 	}
81 81
 
82 82
 	proto, port = SplitProtoPort("1234")
83 83
 
84 84
 	if proto != "tcp" || port != "1234" {
85
-		t.Fatal("tcp is not the default protocol for portspec '1234'")
85
+		t.Fatal("tcp is not the default protocol for portspec '1234'", proto, port)
86 86
 	}
87 87
 
88 88
 	proto, port = SplitProtoPort("1234/")
... ...
@@ -71,7 +71,7 @@ func Parse(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Config,
71 71
 	cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a line delimited file of environment variables")
72 72
 
73 73
 	cmd.Var(&flPublish, []string{"p", "-publish"}, fmt.Sprintf("Publish a container's port to the host\nformat: %s\n(use 'docker port' to see the actual mapping)", nat.PortSpecTemplateFormat))
74
-	cmd.Var(&flExpose, []string{"#expose", "-expose"}, "Expose a port from the container without publishing it to your host")
74
+	cmd.Var(&flExpose, []string{"#expose", "-expose"}, "Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host")
75 75
 	cmd.Var(&flDns, []string{"#dns", "-dns"}, "Set custom DNS servers")
76 76
 	cmd.Var(&flDnsSearch, []string{"-dns-search"}, "Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain)")
77 77
 	cmd.Var(&flExtraHosts, []string{"-add-host"}, "Add a custom host-to-IP mapping (host:ip)")
... ...
@@ -197,9 +197,24 @@ func Parse(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Config,
197 197
 		if strings.Contains(e, ":") {
198 198
 			return nil, nil, cmd, fmt.Errorf("Invalid port format for --expose: %s", e)
199 199
 		}
200
-		p := nat.NewPort(nat.SplitProtoPort(e))
201
-		if _, exists := ports[p]; !exists {
202
-			ports[p] = struct{}{}
200
+		//support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>]
201
+		if strings.Contains(e, "-") {
202
+			proto, port := nat.SplitProtoPort(e)
203
+			//parse the start and end port and create a sequence of ports to expose
204
+			parts := strings.Split(port, "-")
205
+			start, _ := strconv.Atoi(parts[0])
206
+			end, _ := strconv.Atoi(parts[1])
207
+			for i := start; i <= end; i++ {
208
+				p := nat.NewPort(proto, strconv.Itoa(i))
209
+				if _, exists := ports[p]; !exists {
210
+					ports[p] = struct{}{}
211
+				}
212
+			}
213
+		} else {
214
+			p := nat.NewPort(nat.SplitProtoPort(e))
215
+			if _, exists := ports[p]; !exists {
216
+				ports[p] = struct{}{}
217
+			}
203 218
 		}
204 219
 	}
205 220