From fd774a818c7d8942922b4f74eabd2a4e14094e1a Mon Sep 17 00:00:00 2001 From: Srini Brahmaroutu Date: Wed, 17 Sep 2014 01:08:30 +0000 Subject: [PATCH] adding support for port ranges on --expose Closes #1834 Signed-off-by: Srini Brahmaroutu --- daemon/networkdriver/bridge/driver.go | 20 ++++----- docs/man/docker-create.1.md | 2 +- docs/man/docker-run.1.md | 8 +--- docs/sources/reference/commandline/cli.md | 4 +- docs/sources/reference/run.md | 4 +- integration-cli/docker_cli_run_test.go | 30 ++++++++++++++ links/links.go | 43 ++++++++++++++++++-- links/links_test.go | 49 +++++++++++++++++++++++ nat/nat.go | 39 ++++++++---------- nat/nat_test.go | 4 +- runconfig/parse.go | 23 +++++++++-- 11 files changed, 171 insertions(+), 55 deletions(-) diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index c967aebb79..5d0040a8e7 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -5,7 +5,7 @@ import ( "io/ioutil" "net" "os" - "strings" + "strconv" "sync" log "github.com/Sirupsen/logrus" @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/daemon/networkdriver/portallocator" "github.com/docker/docker/daemon/networkdriver/portmapper" "github.com/docker/docker/engine" + "github.com/docker/docker/nat" "github.com/docker/docker/pkg/iptables" "github.com/docker/docker/pkg/networkfs/resolvconf" "github.com/docker/docker/pkg/parsers/kernel" @@ -515,18 +516,13 @@ func LinkContainers(job *engine.Job) engine.Status { ignoreErrors = job.GetenvBool("IgnoreErrors") ports = job.GetenvList("Ports") ) - split := func(p string) (string, string) { - parts := strings.Split(p, "/") - return parts[0], parts[1] - } - - for _, p := range ports { - port, proto := split(p) + for _, value := range ports { + port := nat.Port(value) if output, err := iptables.Raw(action, "FORWARD", "-i", bridgeIface, "-o", bridgeIface, - "-p", proto, + "-p", port.Proto(), "-s", parentIP, - "--dport", port, + "--dport", strconv.Itoa(port.Int()), "-d", childIP, "-j", "ACCEPT"); !ignoreErrors && err != nil { return job.Error(err) @@ -536,9 +532,9 @@ func LinkContainers(job *engine.Job) engine.Status { if output, err := iptables.Raw(action, "FORWARD", "-i", bridgeIface, "-o", bridgeIface, - "-p", proto, + "-p", port.Proto(), "-s", childIP, - "--sport", port, + "--sport", strconv.Itoa(port.Int()), "-d", parentIP, "-j", "ACCEPT"); !ignoreErrors && err != nil { return job.Error(err) diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index 92e34125a4..bc431aa975 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -79,7 +79,7 @@ docker-create - Create a new container Read in a line delimited file of environment variables **--expose**=[] - Expose a port from the container without publishing it to your host + Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host **-h**, **--hostname**="" Container host name diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 8da95af6f8..485965381c 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -132,12 +132,8 @@ ENTRYPOINT. **--env-file**=[] Read in a line delimited file of environment variables -**--expose**=*port* - Expose a port from the container without publishing it to your host. A -containers port can be exposed to other containers in three ways: 1) The -developer can expose the port using the EXPOSE parameter of the Dockerfile, 2) -the operator can use the **--expose** option with **docker run**, or 3) the -container can be started with the **--link**. +**--expose**=[] + Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host **-h**, **--hostname**=*hostname* Sets the container host name that is available inside the container. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index dd82ae40df..ea73b16714 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -509,7 +509,7 @@ Creates a new container. -e, --env=[] Set environment variables --entrypoint="" Overwrite the default ENTRYPOINT of the image --env-file=[] Read in a line delimited file of environment variables - --expose=[] Expose a port from the container without publishing it to your host + --expose=[] Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host -h, --hostname="" Container host name -i, --interactive=false Keep STDIN open even if not attached --link=[] Add link to another container in the form of name:alias @@ -1211,7 +1211,7 @@ removed before the image is removed. -e, --env=[] Set environment variables --entrypoint="" Overwrite the default ENTRYPOINT of the image --env-file=[] Read in a line delimited file of environment variables - --expose=[] Expose a port from the container without publishing it to your host + --expose=[] Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host -h, --hostname="" Container host name -i, --interactive=false Keep STDIN open even if not attached --link=[] Add link to another container in the form of name:alias diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index 88e3f5d491..b17afde23f 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -413,7 +413,7 @@ the `EXPOSE` instruction to give a hint to the operator about what incoming ports might provide services. The following options work with or override the Dockerfile's exposed defaults: - --expose=[]: Expose a port from the container + --expose=[]: Expose a port or a range of ports from the container without publishing it to your host -P=false : Publish all exposed ports to the host interfaces -p=[] : Publish a container᾿s port to the host (format: @@ -422,7 +422,7 @@ or override the Dockerfile's exposed defaults: (use 'docker port' to see the actual mapping) --link="" : Add link to another container (name:alias) -As mentioned previously, `EXPOSE` (and `--expose`) make a port available +As mentioned previously, `EXPOSE` (and `--expose`) makes ports available **in** a container for incoming connections. The port number on the inside of the container (where the service listens) does not need to be the same number as the port exposed on the outside of the container diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 95cb0c86d1..c529be7df2 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -13,11 +13,13 @@ import ( "reflect" "regexp" "sort" + "strconv" "strings" "sync" "testing" "time" + "github.com/docker/docker/nat" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/networkfs/resolvconf" "github.com/kr/pty" @@ -2473,3 +2475,31 @@ func TestRunSlowStdoutConsumer(t *testing.T) { logDone("run - slow consumer") } + +func TestRunAllowPortRangeThroughExpose(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "-d", "--expose", "3000-3003", "-P", "busybox", "top") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err) + } + id := strings.TrimSpace(out) + portstr, err := inspectFieldJSON(id, "NetworkSettings.Ports") + if err != nil { + t.Fatal(err) + } + var ports nat.PortMap + err = unmarshalJSON([]byte(portstr), &ports) + for port, binding := range ports { + portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0]) + if portnum < 3000 || portnum > 3003 { + t.Fatalf("Port is out of range ", portnum, binding, out) + } + if binding == nil || len(binding) != 1 || len(binding[0].HostPort) == 0 { + t.Fatal("Port is not mapped for the port "+port, out) + } + } + if err := deleteContainer(id); err != nil { + t.Fatal(err) + } + logDone("run - allow port range through --expose flag") +} diff --git a/links/links.go b/links/links.go index d2d699398e..fc4d95ab08 100644 --- a/links/links.go +++ b/links/links.go @@ -47,6 +47,20 @@ func (l *Link) Alias() string { return alias } +func nextContiguous(ports []nat.Port, value int, index int) int { + if index+1 == len(ports) { + return index + } + for i := index + 1; i < len(ports); i++ { + if ports[i].Int() > value+1 { + return i - 1 + } + + value++ + } + return len(ports) - 1 +} + func (l *Link) ToEnv() []string { env := []string{} alias := strings.Replace(strings.ToUpper(l.Alias()), "-", "_", -1) @@ -55,12 +69,35 @@ func (l *Link) ToEnv() []string { env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port())) } - // Load exposed ports into the environment - for _, p := range l.Ports { + //sort the ports so that we can bulk the continuous ports together + nat.Sort(l.Ports, func(ip, jp nat.Port) bool { + // If the two ports have the same number, tcp takes priority + // Sort in desc order + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp") + }) + + for i := 0; i < len(l.Ports); { + p := l.Ports[i] + j := nextContiguous(l.Ports, p.Int(), i) + if j > i+1 { + 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())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP)) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_START=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port())) + + q := l.Ports[j] + 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())) + env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT_END=%s", alias, p.Port(), strings.ToUpper(q.Proto()), q.Port())) + + i = j + 1 + continue + } + 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())) env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP)) env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port())) env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto())) + i++ } // Load the linked container's name into the environment @@ -125,7 +162,7 @@ func (l *Link) toggle(action string, ignoreErrors bool) error { out := make([]string, len(l.Ports)) for i, p := range l.Ports { - out[i] = fmt.Sprintf("%s/%s", p.Port(), p.Proto()) + out[i] = string(p) } job.SetenvList("Ports", out) diff --git a/links/links_test.go b/links/links_test.go index c26559e599..7ba9513ea0 100644 --- a/links/links_test.go +++ b/links/links_test.go @@ -107,3 +107,52 @@ func TestLinkEnv(t *testing.T) { t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"]) } } + +func TestLinkMultipleEnv(t *testing.T) { + ports := make(nat.PortSet) + ports[nat.Port("6379/tcp")] = struct{}{} + ports[nat.Port("6380/tcp")] = struct{}{} + ports[nat.Port("6381/tcp")] = struct{}{} + + link, err := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, ports, nil) + if err != nil { + t.Fatal(err) + } + + rawEnv := link.ToEnv() + env := make(map[string]string, len(rawEnv)) + for _, e := range rawEnv { + parts := strings.Split(e, "=") + if len(parts) != 2 { + t.FailNow() + } + env[parts[0]] = parts[1] + } + if env["DOCKER_PORT"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected 172.0.17.2:6379, got %s", env["DOCKER_PORT"]) + } + if env["DOCKER_PORT_6379_TCP_START"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT_6379_TCP_START"]) + } + if env["DOCKER_PORT_6379_TCP_END"] != "tcp://172.0.17.2:6381" { + t.Fatalf("Expected tcp://172.0.17.2:6381, got %s", env["DOCKER_PORT_6379_TCP_END"]) + } + if env["DOCKER_PORT_6379_TCP_PROTO"] != "tcp" { + t.Fatalf("Expected tcp, got %s", env["DOCKER_PORT_6379_TCP_PROTO"]) + } + if env["DOCKER_PORT_6379_TCP_ADDR"] != "172.0.17.2" { + t.Fatalf("Expected 172.0.17.2, got %s", env["DOCKER_PORT_6379_TCP_ADDR"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_START"] != "6379" { + t.Fatalf("Expected 6379, got %s", env["DOCKER_PORT_6379_TCP_PORT_START"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_END"] != "6381" { + t.Fatalf("Expected 6381, got %s", env["DOCKER_PORT_6379_TCP_PORT_END"]) + } + if env["DOCKER_NAME"] != "/db/docker" { + t.Fatalf("Expected /db/docker, got %s", env["DOCKER_NAME"]) + } + if env["DOCKER_ENV_PASSWORD"] != "gordon" { + t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"]) + } +} diff --git a/nat/nat.go b/nat/nat.go index b0177289ce..1246626b0d 100644 --- a/nat/nat.go +++ b/nat/nat.go @@ -42,44 +42,37 @@ func ParsePort(rawPort string) (int, error) { } func (p Port) Proto() string { - parts := strings.Split(string(p), "/") - if len(parts) == 1 { - return "tcp" - } - return parts[1] + proto, _ := SplitProtoPort(string(p)) + return proto } func (p Port) Port() string { - return strings.Split(string(p), "/")[0] + _, port := SplitProtoPort(string(p)) + return port } func (p Port) Int() int { - i, err := ParsePort(p.Port()) + port, err := ParsePort(p.Port()) if err != nil { panic(err) } - return i + return port } // Splits a port in the format of proto/port func SplitProtoPort(rawPort string) (string, string) { - var port string - var proto string - parts := strings.Split(rawPort, "/") - - if len(parts) == 0 || parts[0] == "" { // we have "" or ""/ - port = "" - proto = "" - } else { // we have # or #/ or #/... - port = parts[0] - if len(parts) > 1 && parts[1] != "" { - proto = parts[1] // we have #/... - } else { - proto = "tcp" // we have # or #/ - } + l := len(parts) + if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 { + return "", "" } - return proto, port + if l == 1 { + return "tcp", rawPort + } + if len(parts[1]) == 0 { + return "tcp", parts[0] + } + return parts[1], parts[0] } func validateProto(proto string) bool { diff --git a/nat/nat_test.go b/nat/nat_test.go index a8c2cb584e..4ae9f4ece5 100644 --- a/nat/nat_test.go +++ b/nat/nat_test.go @@ -76,13 +76,13 @@ func TestSplitProtoPort(t *testing.T) { proto, port = SplitProtoPort("") if proto != "" || port != "" { - t.Fatal("parsing an empty string yielded surprising results") + t.Fatal("parsing an empty string yielded surprising results", proto, port) } proto, port = SplitProtoPort("1234") if proto != "tcp" || port != "1234" { - t.Fatal("tcp is not the default protocol for portspec '1234'") + t.Fatal("tcp is not the default protocol for portspec '1234'", proto, port) } proto, port = SplitProtoPort("1234/") diff --git a/runconfig/parse.go b/runconfig/parse.go index f1258e5b7f..9635e9402d 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -71,7 +71,7 @@ func Parse(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Config, cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a line delimited file of environment variables") 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)) - cmd.Var(&flExpose, []string{"#expose", "-expose"}, "Expose a port from the container without publishing it to your host") + 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") cmd.Var(&flDns, []string{"#dns", "-dns"}, "Set custom DNS servers") cmd.Var(&flDnsSearch, []string{"-dns-search"}, "Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain)") 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, if strings.Contains(e, ":") { return nil, nil, cmd, fmt.Errorf("Invalid port format for --expose: %s", e) } - p := nat.NewPort(nat.SplitProtoPort(e)) - if _, exists := ports[p]; !exists { - ports[p] = struct{}{} + //support two formats for expose, original format /[] or /[] + if strings.Contains(e, "-") { + proto, port := nat.SplitProtoPort(e) + //parse the start and end port and create a sequence of ports to expose + parts := strings.Split(port, "-") + start, _ := strconv.Atoi(parts[0]) + end, _ := strconv.Atoi(parts[1]) + for i := start; i <= end; i++ { + p := nat.NewPort(proto, strconv.Itoa(i)) + if _, exists := ports[p]; !exists { + ports[p] = struct{}{} + } + } + } else { + p := nat.NewPort(nat.SplitProtoPort(e)) + if _, exists := ports[p]; !exists { + ports[p] = struct{}{} + } } }