add ability to publish range of ports

Closes #8899
Signed-off-by: Srini Brahmaroutu <srbrahma@us.ibm.com>
This commit is contained in:
Srini Brahmaroutu 2014-11-03 18:15:55 +00:00 committed by Srini Brahmaroutu
parent b2ab733c99
commit 2338a9cf5a
12 changed files with 332 additions and 32 deletions

View File

@ -121,8 +121,10 @@ IMAGE [COMMAND] [ARG...]
Publish all exposed ports to the host interfaces. The default is *false*. Publish all exposed ports to the host interfaces. The default is *false*.
**-p**, **--publish**=[] **-p**, **--publish**=[]
Publish a container's port to the host Publish a container's port, or a range of ports, to the host
format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
Both hostPort and containerPort can be specified as a range of ports.
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`)
(use 'docker port' to see the actual mapping) (use 'docker port' to see the actual mapping)
**--privileged**=*true*|*false* **--privileged**=*true*|*false*

View File

@ -146,7 +146,7 @@ ENTRYPOINT.
Read in a line delimited file of environment variables Read in a line delimited file of environment variables
**--expose**=[] **--expose**=[]
Expose a port or a range of ports (e.g. --expose=3300-3310) 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**="" **-h**, **--hostname**=""
Container host name Container host name
@ -224,8 +224,10 @@ ports to a random port on the host between 49153 and 65535. To find the
mapping between the host ports and the exposed ports, use **docker port**. mapping between the host ports and the exposed ports, use **docker port**.
**-p**, **--publish**=[] **-p**, **--publish**=[]
Publish a container's port to the host Publish a container's port, or range of ports, to the host.
format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
Both hostPort and containerPort can be specified as a range of ports.
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`)
(use 'docker port' to see the actual mapping) (use 'docker port' to see the actual mapping)
**--privileged**=*true*|*false* **--privileged**=*true*|*false*

View File

@ -686,8 +686,10 @@ Creates a new container.
'container:<name|id>': reuses another container network stack 'container:<name|id>': reuses another container network stack
'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. 'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure.
-P, --publish-all=false Publish all exposed ports to the host interfaces -P, --publish-all=false Publish all exposed ports to the host interfaces
-p, --publish=[] Publish a container's port to the host -p, --publish=[] Publish a container's port, or a range of ports (e.g., `-p 3300-3310`), to the host
format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
Both hostPort and containerPort can be specified as a range of ports.
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`)
(use 'docker port' to see the actual mapping) (use 'docker port' to see the actual mapping)
--privileged=false Give extended privileges to this container --privileged=false Give extended privileges to this container
--restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always)
@ -1514,6 +1516,8 @@ removed before the image is removed.
-P, --publish-all=false Publish all exposed ports to the host interfaces -P, --publish-all=false Publish all exposed ports to the host interfaces
-p, --publish=[] Publish a container's port to the host -p, --publish=[] Publish a container's port to the host
format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
Both hostPort and containerPort can be specified as a range of ports.
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`)
(use 'docker port' to see the actual mapping) (use 'docker port' to see the actual mapping)
--privileged=false Give extended privileges to this container --privileged=false Give extended privileges to this container
--restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always)

View File

@ -487,10 +487,11 @@ or override the Dockerfile's exposed defaults:
--expose=[]: Expose a port or a range of ports from the container --expose=[]: Expose a port or a range of ports from the container
without publishing it to your host without publishing it to your host
-P=false : Publish all exposed ports to the host interfaces -P=false : Publish all exposed ports to the host interfaces
-p=[] : Publish a container᾿s port to the host (format: -p=[] : Publish a container᾿s port or a range of ports to the host
ip:hostPort:containerPort | ip::containerPort | format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
hostPort:containerPort | containerPort) Both hostPort and containerPort can be specified as a range of ports.
(use 'docker port' to see the actual mapping) 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`)
(use 'docker port' to see the actual mapping)
--link="" : Add link to another container (name:alias) --link="" : Add link to another container (name:alias)
As mentioned previously, `EXPOSE` (and `--expose`) makes ports available As mentioned previously, `EXPOSE` (and `--expose`) makes ports available

View File

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"github.com/docker/docker/nat"
"os" "os"
"os/exec" "os/exec"
"testing" "testing"
@ -102,6 +103,104 @@ func TestCreateHostConfig(t *testing.T) {
logDone("create - hostconfig") logDone("create - hostconfig")
} }
func TestCreateWithPortRange(t *testing.T) {
runCmd := exec.Command(dockerBinary, "create", "-p", "3300-3303:3300-3303/tcp", "busybox", "echo")
out, _, _, err := runCommandWithStdoutStderr(runCmd)
if err != nil {
t.Fatal(out, err)
}
cleanedContainerID := stripTrailingCharacters(out)
inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID)
out, _, err = runCommandWithOutput(inspectCmd)
if err != nil {
t.Fatalf("out should've been a container id: %s, %v", out, err)
}
containers := []struct {
HostConfig *struct {
PortBindings map[nat.Port][]nat.PortBinding
}
}{}
if err := json.Unmarshal([]byte(out), &containers); err != nil {
t.Fatalf("Error inspecting the container: %s", err)
}
if len(containers) != 1 {
t.Fatalf("Unexpected container count. Expected 0, received: %d", len(containers))
}
c := containers[0]
if c.HostConfig == nil {
t.Fatalf("Expected HostConfig, got none")
}
if len(c.HostConfig.PortBindings) != 4 {
t.Fatalf("Expected 4 ports bindings, got %d", len(c.HostConfig.PortBindings))
}
for k, v := range c.HostConfig.PortBindings {
if len(v) != 1 {
t.Fatalf("Expected 1 ports binding, for the port %s but found %s", k, v)
}
if k.Port() != v[0].HostPort {
t.Fatalf("Expected host port %d to match published port %d", k.Port(), v[0].HostPort)
}
}
deleteAllContainers()
logDone("create - port range")
}
func TestCreateWithiLargePortRange(t *testing.T) {
runCmd := exec.Command(dockerBinary, "create", "-p", "1-65535:1-65535/tcp", "busybox", "echo")
out, _, _, err := runCommandWithStdoutStderr(runCmd)
if err != nil {
t.Fatal(out, err)
}
cleanedContainerID := stripTrailingCharacters(out)
inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID)
out, _, err = runCommandWithOutput(inspectCmd)
if err != nil {
t.Fatalf("out should've been a container id: %s, %v", out, err)
}
containers := []struct {
HostConfig *struct {
PortBindings map[nat.Port][]nat.PortBinding
}
}{}
if err := json.Unmarshal([]byte(out), &containers); err != nil {
t.Fatalf("Error inspecting the container: %s", err)
}
if len(containers) != 1 {
t.Fatalf("Unexpected container count. Expected 0, received: %d", len(containers))
}
c := containers[0]
if c.HostConfig == nil {
t.Fatalf("Expected HostConfig, got none")
}
if len(c.HostConfig.PortBindings) != 65535 {
t.Fatalf("Expected 65535 ports bindings, got %d", len(c.HostConfig.PortBindings))
}
for k, v := range c.HostConfig.PortBindings {
if len(v) != 1 {
t.Fatalf("Expected 1 ports binding, for the port %s but found %s", k, v)
}
if k.Port() != v[0].HostPort {
t.Fatalf("Expected host port %d to match published port %d", k.Port(), v[0].HostPort)
}
}
deleteAllContainers()
logDone("create - large port range")
}
// "test123" should be printed by docker create + start // "test123" should be printed by docker create + start
func TestCreateEchoStdout(t *testing.T) { func TestCreateEchoStdout(t *testing.T) {
runCmd := exec.Command(dockerBinary, "create", "busybox", "echo", "test123") runCmd := exec.Command(dockerBinary, "create", "busybox", "echo", "test123")

View File

@ -2737,3 +2737,27 @@ func TestRunNetHost(t *testing.T) {
logDone("run - net host mode") logDone("run - net host mode")
} }
func TestRunAllowPortRangeThroughPublish(t *testing.T) {
cmd := exec.Command(dockerBinary, "run", "-d", "--expose", "3000-3003", "-p", "3000-3003", "busybox", "top")
out, _, err := runCommandWithOutput(cmd)
defer deleteAllContainers()
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)
}
}
logDone("run - allow port range through --expose flag")
}

View File

@ -122,31 +122,48 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding,
if containerPort == "" { if containerPort == "" {
return nil, nil, fmt.Errorf("No port specified: %s<empty>", rawPort) return nil, nil, fmt.Errorf("No port specified: %s<empty>", rawPort)
} }
if _, err := strconv.ParseUint(containerPort, 10, 16); err != nil {
startPort, endPort, err := parsers.ParsePortRange(containerPort)
if err != nil {
return nil, nil, fmt.Errorf("Invalid containerPort: %s", containerPort) return nil, nil, fmt.Errorf("Invalid containerPort: %s", containerPort)
} }
if _, err := strconv.ParseUint(hostPort, 10, 16); hostPort != "" && err != nil {
return nil, nil, fmt.Errorf("Invalid hostPort: %s", hostPort) var startHostPort, endHostPort uint64 = 0, 0
if len(hostPort) > 0 {
startHostPort, endHostPort, err = parsers.ParsePortRange(hostPort)
if err != nil {
return nil, nil, fmt.Errorf("Invalid hostPort: %s", hostPort)
}
}
if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
} }
if !validateProto(proto) { if !validateProto(proto) {
return nil, nil, fmt.Errorf("Invalid proto: %s", proto) return nil, nil, fmt.Errorf("Invalid proto: %s", proto)
} }
port := NewPort(proto, containerPort) for i := uint64(0); i <= (endPort - startPort); i++ {
if _, exists := exposedPorts[port]; !exists { containerPort = strconv.FormatUint(startPort+i, 10)
exposedPorts[port] = struct{}{} if len(hostPort) > 0 {
} hostPort = strconv.FormatUint(startHostPort+i, 10)
}
port := NewPort(proto, containerPort)
if _, exists := exposedPorts[port]; !exists {
exposedPorts[port] = struct{}{}
}
binding := PortBinding{ binding := PortBinding{
HostIp: rawIp, HostIp: rawIp,
HostPort: hostPort, HostPort: hostPort,
}
bslice, exists := bindings[port]
if !exists {
bslice = []PortBinding{}
}
bindings[port] = append(bslice, binding)
} }
bslice, exists := bindings[port]
if !exists {
bslice = []PortBinding{}
}
bindings[port] = append(bslice, binding)
} }
return exposedPorts, bindings, nil return exposedPorts, bindings, nil
} }

View File

@ -108,7 +108,7 @@ func TestParsePortSpecs(t *testing.T) {
portMap, bindingMap, err = ParsePortSpecs([]string{"1234/tcp", "2345/udp"}) portMap, bindingMap, err = ParsePortSpecs([]string{"1234/tcp", "2345/udp"})
if err != nil { if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) t.Fatalf("Error while processing ParsePortSpecs: %s", err)
} }
if _, ok := portMap[Port("1234/tcp")]; !ok { if _, ok := portMap[Port("1234/tcp")]; !ok {
@ -136,7 +136,7 @@ func TestParsePortSpecs(t *testing.T) {
portMap, bindingMap, err = ParsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp"}) portMap, bindingMap, err = ParsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp"})
if err != nil { if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) t.Fatalf("Error while processing ParsePortSpecs: %s", err)
} }
if _, ok := portMap[Port("1234/tcp")]; !ok { if _, ok := portMap[Port("1234/tcp")]; !ok {
@ -166,7 +166,7 @@ func TestParsePortSpecs(t *testing.T) {
portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp"}) portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp"})
if err != nil { if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) t.Fatalf("Error while processing ParsePortSpecs: %s", err)
} }
if _, ok := portMap[Port("1234/tcp")]; !ok { if _, ok := portMap[Port("1234/tcp")]; !ok {
@ -199,3 +199,95 @@ func TestParsePortSpecs(t *testing.T) {
t.Fatal("Received no error while trying to parse a hostname instead of ip") t.Fatal("Received no error while trying to parse a hostname instead of ip")
} }
} }
func TestParsePortSpecsWithRange(t *testing.T) {
var (
portMap map[Port]struct{}
bindingMap map[Port][]PortBinding
err error
)
portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[Port("1235/tcp")]; !ok {
t.Fatal("1234/tcp was not parsed properly")
}
if _, ok := portMap[Port("2346/udp")]; !ok {
t.Fatal("2345/udp was not parsed properly")
}
for portspec, bindings := range bindingMap {
if len(bindings) != 1 {
t.Fatalf("%s should have exactly one binding", portspec)
}
if bindings[0].HostIp != "" {
t.Fatalf("HostIp should not be set for %s", portspec)
}
if bindings[0].HostPort != "" {
t.Fatalf("HostPort should not be set for %s", portspec)
}
}
portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[Port("1235/tcp")]; !ok {
t.Fatal("1234/tcp was not parsed properly")
}
if _, ok := portMap[Port("2346/udp")]; !ok {
t.Fatal("2345/udp was not parsed properly")
}
for portspec, bindings := range bindingMap {
_, port := SplitProtoPort(string(portspec))
if len(bindings) != 1 {
t.Fatalf("%s should have exactly one binding", portspec)
}
if bindings[0].HostIp != "" {
t.Fatalf("HostIp should not be set for %s", portspec)
}
if bindings[0].HostPort != port {
t.Fatalf("HostPort should be %s for %s", port, portspec)
}
}
portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234-1236:1234-1236/tcp", "0.0.0.0:2345-2347:2345-2347/udp"})
if err != nil {
t.Fatalf("Error while processing ParsePortSpecs: %s", err)
}
if _, ok := portMap[Port("1235/tcp")]; !ok {
t.Fatal("1234/tcp was not parsed properly")
}
if _, ok := portMap[Port("2346/udp")]; !ok {
t.Fatal("2345/udp was not parsed properly")
}
for portspec, bindings := range bindingMap {
_, port := SplitProtoPort(string(portspec))
if len(bindings) != 1 || bindings[0].HostIp != "0.0.0.0" || bindings[0].HostPort != port {
t.Fatalf("Expect single binding to port %d but found %s", port, bindings)
}
}
_, _, err = ParsePortSpecs([]string{"localhost:1234-1236:1234-1236/tcp"})
if err == nil {
t.Fatal("Received no error while trying to parse a hostname instead of ip")
}
}

View File

@ -104,3 +104,28 @@ func ParseKeyValueOpt(opt string) (string, string, error) {
} }
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil
} }
func ParsePortRange(ports string) (uint64, uint64, error) {
if ports == "" {
return 0, 0, fmt.Errorf("Empty string specified for ports.")
}
if !strings.Contains(ports, "-") {
start, err := strconv.ParseUint(ports, 10, 16)
end := start
return start, end, err
}
parts := strings.Split(ports, "-")
start, err := strconv.ParseUint(parts[0], 10, 16)
if err != nil {
return 0, 0, err
}
end, err := strconv.ParseUint(parts[1], 10, 16)
if err != nil {
return 0, 0, err
}
if end < start {
return 0, 0, fmt.Errorf("Invalid range specified for the Port: %s", ports)
}
return start, end, nil
}

View File

@ -1,6 +1,7 @@
package parsers package parsers
import ( import (
"strings"
"testing" "testing"
) )
@ -81,3 +82,35 @@ func TestParsePortMapping(t *testing.T) {
t.Fail() t.Fail()
} }
} }
func TestParsePortRange(t *testing.T) {
if start, end, err := ParsePortRange("8000-8080"); err != nil || start != 8000 || end != 8080 {
t.Fatalf("Error: %s or Expecting {start,end} values {8000,8080} but found {%d,%d}.", err, start, end)
}
}
func TestParsePortRangeIncorrectRange(t *testing.T) {
if _, _, err := ParsePortRange("9000-8080"); err == nil || !strings.Contains(err.Error(), "Invalid range specified for the Port") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
}
func TestParsePortRangeIncorrectEndRange(t *testing.T) {
if _, _, err := ParsePortRange("8000-a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
if _, _, err := ParsePortRange("8000-30a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
}
func TestParsePortRangeIncorrectStartRange(t *testing.T) {
if _, _, err := ParsePortRange("a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
if _, _, err := ParsePortRange("30a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") {
t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err)
}
}

View File

@ -256,8 +256,8 @@ func TestMerge(t *testing.T) {
t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts)) t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts))
} }
for portSpecs := range configUser.ExposedPorts { for portSpecs := range configUser.ExposedPorts {
if portSpecs.Port() != "0000" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { if portSpecs.Port() != "0" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" {
t.Fatalf("Expected 0000 or 1111 or 2222 or 3333, found %s", portSpecs) t.Fatalf("Expected %q or %q or %q or %q, found %s", 0, 1111, 2222, 3333, portSpecs)
} }
} }

View File

@ -197,11 +197,12 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
if strings.Contains(e, "-") { if strings.Contains(e, "-") {
proto, port := nat.SplitProtoPort(e) proto, port := nat.SplitProtoPort(e)
//parse the start and end port and create a sequence of ports to expose //parse the start and end port and create a sequence of ports to expose
parts := strings.Split(port, "-") start, end, err := parsers.ParsePortRange(port)
start, _ := strconv.Atoi(parts[0]) if err != nil {
end, _ := strconv.Atoi(parts[1]) return nil, nil, cmd, fmt.Errorf("Invalid range format for --expose: %s, error: %s", e, err)
}
for i := start; i <= end; i++ { for i := start; i <= end; i++ {
p := nat.NewPort(proto, strconv.Itoa(i)) p := nat.NewPort(proto, strconv.FormatUint(i, 10))
if _, exists := ports[p]; !exists { if _, exists := ports[p]; !exists {
ports[p] = struct{}{} ports[p] = struct{}{}
} }