From faa519905ed30a445054d3b8274d24aafeb59cf6 Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Mon, 26 Jan 2015 17:29:20 -0800 Subject: [PATCH] Port filter: Random port assignment conflict detection. 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 #274 Fixes #272 Signed-off-by: Andrea Luzzardi --- scheduler/filter/port.go | 45 ++++++++++++++++----- scheduler/filter/port_test.go | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/scheduler/filter/port.go b/scheduler/filter/port.go index e6a0914e8d..411f503732 100644 --- a/scheduler/filter/port.go +++ b/scheduler/filter/port.go @@ -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 } } } diff --git a/scheduler/filter/port_test.go b/scheduler/filter/port_test.go index 48a5ec5e2b..d9b930fd41 100644 --- a/scheduler/filter/port_test.go +++ b/scheduler/filter/port_test.go @@ -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]) +}