mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #12927 from lindenlab/custom-host-port-ranges
Proposal: Change --publish=SPEC to allow binding to custom host port ranges
This commit is contained in:
commit
59e49e1db0
7 changed files with 192 additions and 8 deletions
|
@ -727,10 +727,15 @@ func (container *Container) buildCreateEndpointOptions() ([]libnetwork.EndpointO
|
|||
for i := 0; i < len(binding); i++ {
|
||||
pbCopy := pb.GetCopy()
|
||||
newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort))
|
||||
var portStart, portEnd int
|
||||
if err == nil {
|
||||
portStart, portEnd, err = newP.Range()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err)
|
||||
}
|
||||
pbCopy.HostPort = uint16(newP.Int())
|
||||
pbCopy.HostPort = uint16(portStart)
|
||||
pbCopy.HostPortEnd = uint16(portEnd)
|
||||
pbCopy.HostIP = net.ParseIP(binding[i].HostIP)
|
||||
pbList = append(pbList, pbCopy)
|
||||
}
|
||||
|
|
|
@ -984,6 +984,7 @@ or override the Dockerfile's exposed defaults:
|
|||
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`)
|
||||
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`)
|
||||
(use 'docker port' to see the actual mapping)
|
||||
--link="" : Add link to another container (<name or id>:alias or <name or id>)
|
||||
|
||||
|
|
|
@ -50,6 +50,14 @@ container:
|
|||
And you saw why this isn't such a great idea because it constrains you to
|
||||
only one container on that specific port.
|
||||
|
||||
Instead, you may specify a range of host ports to bind a container port to
|
||||
that is different than the default *ephemeral port range*:
|
||||
|
||||
$ docker run -d -p 8000-9000:5000 training/webapp python app.py
|
||||
|
||||
This would bind port 5000 in the container to a randomly available port
|
||||
between 8000 and 9000 on the host.
|
||||
|
||||
There are also a few other ways you can configure the `-p` flag. By
|
||||
default the `-p` flag will bind the specified port to all interfaces on
|
||||
the host machine. But you can also specify a binding to a specific
|
||||
|
|
|
@ -78,6 +78,81 @@ func (s *DockerSuite) TestPortList(c *check.C) {
|
|||
}
|
||||
dockerCmd(c, "rm", "-f", ID)
|
||||
|
||||
testRange := func() {
|
||||
// host port ranges used
|
||||
IDs := make([]string, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
out, _ = dockerCmd(c, "run", "-d",
|
||||
"-p", "9090-9092:80",
|
||||
"busybox", "top")
|
||||
IDs[i] = strings.TrimSpace(out)
|
||||
|
||||
out, _ = dockerCmd(c, "port", IDs[i])
|
||||
|
||||
if !assertPortList(c, out, []string{
|
||||
fmt.Sprintf("80/tcp -> 0.0.0.0:%d", 9090+i)}) {
|
||||
c.Error("Port list is not correct\n", out)
|
||||
}
|
||||
}
|
||||
|
||||
// test port range exhaustion
|
||||
out, _, err := dockerCmdWithError("run", "-d",
|
||||
"-p", "9090-9092:80",
|
||||
"busybox", "top")
|
||||
if err == nil {
|
||||
c.Errorf("Exhausted port range did not return an error. Out: %s", out)
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
dockerCmd(c, "rm", "-f", IDs[i])
|
||||
}
|
||||
}
|
||||
testRange()
|
||||
// Verify we ran re-use port ranges after they are no longer in use.
|
||||
testRange()
|
||||
|
||||
// test invalid port ranges
|
||||
for _, invalidRange := range []string{"9090-9089:80", "9090-:80", "-9090:80"} {
|
||||
out, _, err := dockerCmdWithError("run", "-d",
|
||||
"-p", invalidRange,
|
||||
"busybox", "top")
|
||||
if err == nil {
|
||||
c.Errorf("Port range should have returned an error. Out: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// test host range:container range spec.
|
||||
out, _ = dockerCmd(c, "run", "-d",
|
||||
"-p", "9800-9803:80-83",
|
||||
"busybox", "top")
|
||||
ID = strings.TrimSpace(out)
|
||||
|
||||
out, _ = dockerCmd(c, "port", ID)
|
||||
|
||||
if !assertPortList(c, out, []string{
|
||||
"80/tcp -> 0.0.0.0:9800",
|
||||
"81/tcp -> 0.0.0.0:9801",
|
||||
"82/tcp -> 0.0.0.0:9802",
|
||||
"83/tcp -> 0.0.0.0:9803"}) {
|
||||
c.Error("Port list is not correct\n", out)
|
||||
}
|
||||
dockerCmd(c, "rm", "-f", ID)
|
||||
|
||||
// test mixing protocols in same port range
|
||||
out, _ = dockerCmd(c, "run", "-d",
|
||||
"-p", "8000-8080:80",
|
||||
"-p", "8000-8080:80/udp",
|
||||
"busybox", "top")
|
||||
ID = strings.TrimSpace(out)
|
||||
|
||||
out, _ = dockerCmd(c, "port", ID)
|
||||
|
||||
if !assertPortList(c, out, []string{
|
||||
"80/tcp -> 0.0.0.0:8000",
|
||||
"80/udp -> 0.0.0.0:8000"}) {
|
||||
c.Error("Port list is not correct\n", out)
|
||||
}
|
||||
dockerCmd(c, "rm", "-f", ID)
|
||||
}
|
||||
|
||||
func assertPortList(c *check.C, out string, expected []string) bool {
|
||||
|
|
|
@ -34,17 +34,20 @@ type PortSet map[Port]struct{}
|
|||
// Port is a string containing port number and protocol in the format "80/tcp"
|
||||
type Port string
|
||||
|
||||
// NewPort creates a new instance of a Port given a protocol and port number
|
||||
// NewPort creates a new instance of a Port given a protocol and port number or port range
|
||||
func NewPort(proto, port string) (Port, error) {
|
||||
// Check for parsing issues on "port" now so we can avoid having
|
||||
// to check it later on.
|
||||
|
||||
portInt, err := ParsePort(port)
|
||||
portStartInt, portEndInt, err := ParsePortRange(port)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Port(fmt.Sprintf("%d/%s", portInt, proto)), nil
|
||||
if portStartInt == portEndInt {
|
||||
return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil
|
||||
}
|
||||
return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil
|
||||
}
|
||||
|
||||
// ParsePort parses the port number string and returns an int
|
||||
|
@ -59,6 +62,18 @@ func ParsePort(rawPort string) (int, error) {
|
|||
return int(port), nil
|
||||
}
|
||||
|
||||
// ParsePortRange parses the port range string and returns start/end ints
|
||||
func ParsePortRange(rawPort string) (int, int, error) {
|
||||
if len(rawPort) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
start, end, err := parsers.ParsePortRange(rawPort)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return int(start), int(end), nil
|
||||
}
|
||||
|
||||
// Proto returns the protocol of a Port
|
||||
func (p Port) Proto() string {
|
||||
proto, _ := SplitProtoPort(string(p))
|
||||
|
@ -84,6 +99,11 @@ func (p Port) Int() int {
|
|||
return int(port)
|
||||
}
|
||||
|
||||
// Range returns the start/end port numbers of a Port range as ints
|
||||
func (p Port) Range() (int, int, error) {
|
||||
return ParsePortRange(p.Port())
|
||||
}
|
||||
|
||||
// SplitProtoPort splits a port in the format of proto/port
|
||||
func SplitProtoPort(rawPort string) (string, string) {
|
||||
parts := strings.Split(rawPort, "/")
|
||||
|
@ -162,7 +182,12 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding,
|
|||
}
|
||||
|
||||
if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
|
||||
return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
|
||||
// Allow host port range iff containerPort is not a range.
|
||||
// In this case, use the host port range as the dynamic
|
||||
// host port range to allocate into.
|
||||
if endPort != startPort {
|
||||
return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
|
||||
}
|
||||
}
|
||||
|
||||
if !validateProto(strings.ToLower(proto)) {
|
||||
|
@ -174,6 +199,11 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding,
|
|||
if len(hostPort) > 0 {
|
||||
hostPort = strconv.FormatUint(startHostPort+i, 10)
|
||||
}
|
||||
// Set hostPort to a range only if there is a single container port
|
||||
// and a dynamic host port.
|
||||
if startPort == endPort && startHostPort != endHostPort {
|
||||
hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10))
|
||||
}
|
||||
port, err := NewPort(strings.ToLower(proto), containerPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
@ -41,6 +41,56 @@ func TestParsePort(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParsePortRange(t *testing.T) {
|
||||
var (
|
||||
begin int
|
||||
end int
|
||||
err error
|
||||
)
|
||||
|
||||
type TestRange struct {
|
||||
Range string
|
||||
Begin int
|
||||
End int
|
||||
}
|
||||
validRanges := []TestRange{
|
||||
{"1234", 1234, 1234},
|
||||
{"1234-1234", 1234, 1234},
|
||||
{"1234-1235", 1234, 1235},
|
||||
{"8000-9000", 8000, 9000},
|
||||
{"0", 0, 0},
|
||||
{"0-0", 0, 0},
|
||||
}
|
||||
|
||||
for _, r := range validRanges {
|
||||
begin, end, err = ParsePortRange(r.Range)
|
||||
|
||||
if err != nil || begin != r.Begin {
|
||||
t.Fatalf("Parsing port range '%s' did not succeed. Expected begin %d, got %d", r.Range, r.Begin, begin)
|
||||
}
|
||||
if err != nil || end != r.End {
|
||||
t.Fatalf("Parsing port range '%s' did not succeed. Expected end %d, got %d", r.Range, r.End, end)
|
||||
}
|
||||
}
|
||||
|
||||
invalidRanges := []string{
|
||||
"asdf",
|
||||
"1asdf",
|
||||
"9000-8000",
|
||||
"9000-",
|
||||
"-8000",
|
||||
"-8000-",
|
||||
}
|
||||
|
||||
for _, r := range invalidRanges {
|
||||
begin, end, err = ParsePortRange(r)
|
||||
|
||||
if err == nil || begin != 0 || end != 0 {
|
||||
t.Fatalf("Parsing port range '%s' succeeded", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPort(t *testing.T) {
|
||||
p, err := NewPort("tcp", "1234")
|
||||
|
||||
|
@ -68,6 +118,20 @@ func TestPort(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatal("tcp, asd1234 was supposed to fail")
|
||||
}
|
||||
|
||||
p, err = NewPort("tcp", "1234-1230")
|
||||
if err == nil {
|
||||
t.Fatal("tcp, 1234-1230 was supposed to fail")
|
||||
}
|
||||
|
||||
p, err = NewPort("tcp", "1234-1242")
|
||||
if err != nil {
|
||||
t.Fatalf("tcp, 1234-1242 had a parsing issue: %v", err)
|
||||
}
|
||||
|
||||
if string(p) != "1234-1242/tcp" {
|
||||
t.Fatal("tcp, 1234-1242 did not result in the string 1234-1242/tcp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitProtoPort(t *testing.T) {
|
||||
|
|
|
@ -2,8 +2,9 @@ package nat
|
|||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
)
|
||||
|
||||
type portSorter struct {
|
||||
|
@ -88,8 +89,8 @@ func SortPortMap(ports []Port, bindings PortMap) {
|
|||
}
|
||||
}
|
||||
|
||||
func toInt(s string) int64 {
|
||||
i, err := strconv.ParseInt(s, 10, 64)
|
||||
func toInt(s string) uint64 {
|
||||
i, _, err := parsers.ParsePortRange(s)
|
||||
if err != nil {
|
||||
i = 0
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue