Skip to content

Commit fd774a8

Browse files
author
Srini Brahmaroutu
committed
adding support for port ranges on --expose
Closes #1834 Signed-off-by: Srini Brahmaroutu <[email protected]>
1 parent 0f9f5f3 commit fd774a8

11 files changed

Lines changed: 171 additions & 55 deletions

File tree

daemon/networkdriver/bridge/driver.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"io/ioutil"
66
"net"
77
"os"
8-
"strings"
8+
"strconv"
99
"sync"
1010

1111
log "github.com/Sirupsen/logrus"
@@ -14,6 +14,7 @@ import (
1414
"github.com/docker/docker/daemon/networkdriver/portallocator"
1515
"github.com/docker/docker/daemon/networkdriver/portmapper"
1616
"github.com/docker/docker/engine"
17+
"github.com/docker/docker/nat"
1718
"github.com/docker/docker/pkg/iptables"
1819
"github.com/docker/docker/pkg/networkfs/resolvconf"
1920
"github.com/docker/docker/pkg/parsers/kernel"
@@ -515,18 +516,13 @@ func LinkContainers(job *engine.Job) engine.Status {
515516
ignoreErrors = job.GetenvBool("IgnoreErrors")
516517
ports = job.GetenvList("Ports")
517518
)
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)
519+
for _, value := range ports {
520+
port := nat.Port(value)
525521
if output, err := iptables.Raw(action, "FORWARD",
526522
"-i", bridgeIface, "-o", bridgeIface,
527-
"-p", proto,
523+
"-p", port.Proto(),
528524
"-s", parentIP,
529-
"--dport", port,
525+
"--dport", strconv.Itoa(port.Int()),
530526
"-d", childIP,
531527
"-j", "ACCEPT"); !ignoreErrors && err != nil {
532528
return job.Error(err)
@@ -536,9 +532,9 @@ func LinkContainers(job *engine.Job) engine.Status {
536532

537533
if output, err := iptables.Raw(action, "FORWARD",
538534
"-i", bridgeIface, "-o", bridgeIface,
539-
"-p", proto,
535+
"-p", port.Proto(),
540536
"-s", childIP,
541-
"--sport", port,
537+
"--sport", strconv.Itoa(port.Int()),
542538
"-d", parentIP,
543539
"-j", "ACCEPT"); !ignoreErrors && err != nil {
544540
return job.Error(err)

docs/man/docker-create.1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ docker-create - Create a new container
7979
Read in a line delimited file of environment variables
8080

8181
**--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
8383

8484
**-h**, **--hostname**=""
8585
Container host name

docs/man/docker-run.1.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,8 @@ ENTRYPOINT.
132132
**--env-file**=[]
133133
Read in a line delimited file of environment variables
134134

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
141137

142138
**-h**, **--hostname**=*hostname*
143139
Sets the container host name that is available inside the container.

docs/sources/reference/commandline/cli.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ Creates a new container.
509509
-e, --env=[] Set environment variables
510510
--entrypoint="" Overwrite the default ENTRYPOINT of the image
511511
--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
513513
-h, --hostname="" Container host name
514514
-i, --interactive=false Keep STDIN open even if not attached
515515
--link=[] Add link to another container in the form of name:alias
@@ -1211,7 +1211,7 @@ removed before the image is removed.
12111211
-e, --env=[] Set environment variables
12121212
--entrypoint="" Overwrite the default ENTRYPOINT of the image
12131213
--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
12151215
-h, --hostname="" Container host name
12161216
-i, --interactive=false Keep STDIN open even if not attached
12171217
--link=[] Add link to another container in the form of name:alias

docs/sources/reference/run.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ the `EXPOSE` instruction to give a hint to the operator about what
413413
incoming ports might provide services. The following options work with
414414
or override the Dockerfile's exposed defaults:
415415

416-
--expose=[]: Expose a port from the container
416+
--expose=[]: Expose a port or a range of ports from the container
417417
without publishing it to your host
418418
-P=false : Publish all exposed ports to the host interfaces
419419
-p=[] : Publish a container᾿s port to the host (format:
@@ -422,7 +422,7 @@ or override the Dockerfile's exposed defaults:
422422
(use 'docker port' to see the actual mapping)
423423
--link="" : Add link to another container (name:alias)
424424

425-
As mentioned previously, `EXPOSE` (and `--expose`) make a port available
425+
As mentioned previously, `EXPOSE` (and `--expose`) makes ports available
426426
**in** a container for incoming connections. The port number on the
427427
inside of the container (where the service listens) does not need to be
428428
the same number as the port exposed on the outside of the container

integration-cli/docker_cli_run_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import (
1313
"reflect"
1414
"regexp"
1515
"sort"
16+
"strconv"
1617
"strings"
1718
"sync"
1819
"testing"
1920
"time"
2021

22+
"github.com/docker/docker/nat"
2123
"github.com/docker/docker/pkg/mount"
2224
"github.com/docker/docker/pkg/networkfs/resolvconf"
2325
"github.com/kr/pty"
@@ -2473,3 +2475,31 @@ func TestRunSlowStdoutConsumer(t *testing.T) {
24732475

24742476
logDone("run - slow consumer")
24752477
}
2478+
2479+
func TestRunAllowPortRangeThroughExpose(t *testing.T) {
2480+
cmd := exec.Command(dockerBinary, "run", "-d", "--expose", "3000-3003", "-P", "busybox", "top")
2481+
out, _, err := runCommandWithOutput(cmd)
2482+
if err != nil {
2483+
t.Fatal(err)
2484+
}
2485+
id := strings.TrimSpace(out)
2486+
portstr, err := inspectFieldJSON(id, "NetworkSettings.Ports")
2487+
if err != nil {
2488+
t.Fatal(err)
2489+
}
2490+
var ports nat.PortMap
2491+
err = unmarshalJSON([]byte(portstr), &ports)
2492+
for port, binding := range ports {
2493+
portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0])
2494+
if portnum < 3000 || portnum > 3003 {
2495+
t.Fatalf("Port is out of range ", portnum, binding, out)
2496+
}
2497+
if binding == nil || len(binding) != 1 || len(binding[0].HostPort) == 0 {
2498+
t.Fatal("Port is not mapped for the port "+port, out)
2499+
}
2500+
}
2501+
if err := deleteContainer(id); err != nil {
2502+
t.Fatal(err)
2503+
}
2504+
logDone("run - allow port range through --expose flag")
2505+
}

links/links.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ func (l *Link) Alias() string {
4747
return alias
4848
}
4949

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+
5064
func (l *Link) ToEnv() []string {
5165
env := []string{}
5266
alias := strings.Replace(strings.ToUpper(l.Alias()), "-", "_", -1)
@@ -55,12 +69,35 @@ func (l *Link) ToEnv() []string {
5569
env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port()))
5670
}
5771

58-
// Load exposed ports into the environment
59-
for _, p := range l.Ports {
72+
//sort the ports so that we can bulk the continuous ports together
73+
nat.Sort(l.Ports, func(ip, jp nat.Port) bool {
74+
// If the two ports have the same number, tcp takes priority
75+
// Sort in desc order
76+
return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp")
77+
})
78+
79+
for i := 0; i < len(l.Ports); {
80+
p := l.Ports[i]
81+
j := nextContiguous(l.Ports, p.Int(), i)
82+
if j > i+1 {
83+
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()))
84+
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP))
85+
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto()))
86+
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_START=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port()))
87+
88+
q := l.Ports[j]
89+
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()))
90+
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_END=%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Port()))
91+
92+
i = j + 1
93+
continue
94+
}
95+
6096
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()))
6197
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP))
6298
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port()))
6399
env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto()))
100+
i++
64101
}
65102

66103
// Load the linked container's name into the environment
@@ -125,7 +162,7 @@ func (l *Link) toggle(action string, ignoreErrors bool) error {
125162

126163
out := make([]string, len(l.Ports))
127164
for i, p := range l.Ports {
128-
out[i] = fmt.Sprintf("%s/%s", p.Port(), p.Proto())
165+
out[i] = string(p)
129166
}
130167
job.SetenvList("Ports", out)
131168

links/links_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,52 @@ func TestLinkEnv(t *testing.T) {
107107
t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"])
108108
}
109109
}
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+
}

nat/nat.go

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,44 +42,37 @@ func ParsePort(rawPort string) (int, error) {
4242
}
4343

4444
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
5047
}
5148

5249
func (p Port) Port() string {
53-
return strings.Split(string(p), "/")[0]
50+
_, port := SplitProtoPort(string(p))
51+
return port
5452
}
5553

5654
func (p Port) Int() int {
57-
i, err := ParsePort(p.Port())
55+
port, err := ParsePort(p.Port())
5856
if err != nil {
5957
panic(err)
6058
}
61-
return i
59+
return port
6260
}
6361

6462
// Splits a port in the format of proto/port
6563
func SplitProtoPort(rawPort string) (string, string) {
66-
var port string
67-
var proto string
68-
6964
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]
8174
}
82-
return proto, port
75+
return parts[1], parts[0]
8376
}
8477

8578
func validateProto(proto string) bool {

nat/nat_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ func TestSplitProtoPort(t *testing.T) {
7676
proto, port = SplitProtoPort("")
7777

7878
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)
8080
}
8181

8282
proto, port = SplitProtoPort("1234")
8383

8484
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)
8686
}
8787

8888
proto, port = SplitProtoPort("1234/")

0 commit comments

Comments
 (0)