Skip to content

Commit

Permalink
Port filter: Random port assignment conflict detection.
Browse files Browse the repository at this point in the history
This change takes into account ports that have been randomly allocated
on the node.

When starting containers with no explicit external port (e.g. `-p 80`),
Docker allocates a random port on the node.

The problem is that Swarm didn't see that random port as taken, meaning
that another container could explicitely ask for it, resulting in a
broken container.

Replaces docker-archive#274
Fixes docker-archive#272

Signed-off-by: Andrea Luzzardi <[email protected]>
  • Loading branch information
aluzzardi committed Jan 27, 2015
1 parent 77a9d77 commit faa5199
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 10 deletions.
45 changes: 35 additions & 10 deletions scheduler/filter/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,41 @@ func (p *PortFilter) Filter(config *dockerclient.ContainerConfig, nodes []*clust

func (p *PortFilter) portAlreadyInUse(node *cluster.Node, requested dockerclient.PortBinding) bool {
for _, c := range node.Containers() {
for _, port := range c.Info.HostConfig.PortBindings {
for _, binding := range port {
if binding.HostPort == requested.HostPort {
// Another container on the same host is binding on the same
// port/protocol. Verify if they are requesting the same
// binding IP, or if the other container is already binding on
// every interface.
if requested.HostIp == binding.HostIp || bindsAllInterfaces(requested) || bindsAllInterfaces(binding) {
return true
}
// HostConfig.PortBindings contains the requested ports.
// NetworkSettings.Ports contains the actual ports.
//
// We have to check both because:
// 1/ If the port was not specifically bound (e.g. -p 80), then
// HostConfig.PortBindings.HostPort will be empty and we have to check
// NetworkSettings.Port.HostPort to find out which port got dynamically
// allocated.
// 2/ If the port was bound (e.g. -p 80:80) but the container is stopped,
// NetworkSettings.Port will be null and we have to check
// HostConfig.PortBindings to find out the mapping.

if p.compare(requested, c.Info.HostConfig.PortBindings) || p.compare(requested, c.Info.NetworkSettings.Ports) {
return true
}
}
return false
}

func (p *PortFilter) compare(requested dockerclient.PortBinding, bindings map[string][]dockerclient.PortBinding) bool {
for _, binding := range bindings {
for _, b := range binding {
if b.HostPort == "" {
// Skip undefined HostPorts. This happens in bindings that
// didn't explicitely specify an external port.
continue
}

if b.HostPort == requested.HostPort {
// Another container on the same host is binding on the same
// port/protocol. Verify if they are requesting the same
// binding IP, or if the other container is already binding on
// every interface.
if requested.HostIp == b.HostIp || bindsAllInterfaces(requested) || bindsAllInterfaces(b) {
return true
}
}
}
Expand Down
73 changes: 73 additions & 0 deletions scheduler/filter/port_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,76 @@ func TestPortFilterDifferentInterfaces(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, result, nodes[1])
}

func TestPortFilterRandomAssignment(t *testing.T) {
var (
p = PortFilter{}
nodes = []*cluster.Node{
cluster.NewNode("node-1", 0),
cluster.NewNode("node-2", 0),
cluster.NewNode("node-3", 0),
}
result []*cluster.Node
err error
)

// Simulate a container that requested to map 80 to a random port.
// In this case, HostConfig.PortBindings should contain a binding with no
// HostPort defined and NetworkSettings.Ports should contain the actual
// mapped port.
container := &cluster.Container{
Container: dockerclient.Container{Id: "c1"},
Info: dockerclient.ContainerInfo{
HostConfig: &dockerclient.HostConfig{
PortBindings: map[string][]dockerclient.PortBinding{
"80/tcp": {
{
HostIp: "",
HostPort: "",
},
},
},
},
NetworkSettings: struct {
IpAddress string
IpPrefixLen int
Gateway string
Bridge string
Ports map[string][]dockerclient.PortBinding
}{
Ports: map[string][]dockerclient.PortBinding{
"80/tcp": {
{
HostIp: "127.0.0.1",
HostPort: "1234",
},
},
},
},
},
}

assert.NoError(t, nodes[0].AddContainer(container))

// Request port 80.
config := &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
PortBindings: makeBinding("", "80"),
},
}

// Since port "80" has been mapped to "1234", we should be able to request "80".
result, err = p.Filter(config, nodes)
assert.NoError(t, err)
assert.Equal(t, result, nodes)

// However, we should not be able to request "1234" since it has been used for a random assignment.
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
PortBindings: makeBinding("", "1234"),
},
}
result, err = p.Filter(config, nodes)
assert.NoError(t, err)
assert.NotContains(t, result, nodes[0])
}

0 comments on commit faa5199

Please sign in to comment.