Dependency Filter: co-schedule dependent containers on the same node.

Supported dependencies:
- Shared volumes: `--volumes-from=dependency`
- Links: `--link=dependency:alias`
- Shared network stack: `--net=container:dependency`

Fixes #251

Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
This commit is contained in:
Andrea Luzzardi 2015-02-10 11:09:45 -08:00
parent 67e347fa1a
commit 97334b0193
5 changed files with 295 additions and 2 deletions

View File

@ -86,12 +86,12 @@ var (
}
// hack for go vet
flFilterValue = cli.StringSlice([]string{"constraint", "affinity", "health", "port"})
flFilterValue = cli.StringSlice([]string{"constraint", "affinity", "health", "port", "dependency"})
DEFAULT_FILTER_NUMBER = len(flFilterValue)
flFilter = cli.StringSliceFlag{
Name: "filter, f",
Usage: "filter to use [constraint, affinity, health, port]",
Usage: "filter to use [constraint, affinity, health, port, dependency]",
Value: &flFilterValue,
}
)

View File

@ -236,6 +236,25 @@ $ docker run -d -p 80:80 nginx
2014/10/29 00:33:20 Error response from daemon: no resources available to schedule container
```
## Dependency Filter
This filter co-schedules dependent containers on the same node.
Currently, dependencies are declared as follows:
- Shared volumes: `--volumes-from=dependency`
- Links: `--link=dependency:alias`
- Shared network stack: `--net=container:dependency`
Swarm will attempt to co-locate the dependent container on the same node. If it
cannot be done (because the dependent container doesn't exist, or because the
node doesn't have enough resources), it will prevent the container creation.
The combination of multiple dependencies will be honored if possible. For
instance, `--volumes-from=A --net=container:B` will attempt to co-locate the
container on the same node as `A` and `B`. If those containers are running on
different nodes, Swarm will prevent you from scheduling the container.
## Health Filter
This filter will prevent scheduling containers on unhealthy nodes.

View File

@ -0,0 +1,71 @@
package filter
import (
"fmt"
"strings"
"github.com/docker/swarm/cluster"
"github.com/samalba/dockerclient"
)
// DependencyFilter co-schedules dependent containers on the same node.
type DependencyFilter struct {
}
func (f *DependencyFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) {
if len(nodes) == 0 {
return nodes, nil
}
// Extract containers from links.
links := []string{}
for _, link := range config.HostConfig.Links {
links = append(links, strings.SplitN(link, ":", 2)[0])
}
// Check if --net points to a container.
net := []string{}
if strings.HasPrefix(config.HostConfig.NetworkMode, "container:") {
net = append(net, strings.TrimPrefix(config.HostConfig.NetworkMode, "container:"))
}
candidates := []*cluster.Node{}
for _, node := range nodes {
if f.check(config.HostConfig.VolumesFrom, node) &&
f.check(links, node) &&
f.check(net, node) {
candidates = append(candidates, node)
}
}
if len(candidates) == 0 {
return nil, fmt.Errorf("Unable to find a node fulfilling all dependencies: %s", f.String(config))
}
return candidates, nil
}
// Get a string representation of the dependencies found in the container config.
func (f *DependencyFilter) String(config *dockerclient.ContainerConfig) string {
dependencies := []string{}
for _, volume := range config.HostConfig.VolumesFrom {
dependencies = append(dependencies, fmt.Sprintf("--volumes-from=%s", volume))
}
for _, link := range config.HostConfig.Links {
dependencies = append(dependencies, fmt.Sprintf("--link=%s", link))
}
if strings.HasPrefix(config.HostConfig.NetworkMode, "container:") {
dependencies = append(dependencies, fmt.Sprintf("--net=%s", config.HostConfig.NetworkMode))
}
return strings.Join(dependencies, " ")
}
// Ensure that the node contains all dependent containers.
func (f *DependencyFilter) check(dependencies []string, node *cluster.Node) bool {
for _, dependency := range dependencies {
if node.Container(dependency) == nil {
return false
}
}
return true
}

View File

@ -0,0 +1,202 @@
package filter
import (
"testing"
"github.com/docker/swarm/cluster"
"github.com/samalba/dockerclient"
"github.com/stretchr/testify/assert"
)
func TestDependencyFilterSimple(t *testing.T) {
var (
f = DependencyFilter{}
nodes = []*cluster.Node{
cluster.NewNode("node-1", 0),
cluster.NewNode("node-2", 0),
cluster.NewNode("node-3", 0),
}
result []*cluster.Node
err error
container *cluster.Container
config *dockerclient.ContainerConfig
)
container = &cluster.Container{Container: dockerclient.Container{Id: "c0"}}
assert.NoError(t, nodes[0].AddContainer(container))
container = &cluster.Container{Container: dockerclient.Container{Id: "c1"}}
assert.NoError(t, nodes[1].AddContainer(container))
container = &cluster.Container{Container: dockerclient.Container{Id: "c2"}}
assert.NoError(t, nodes[2].AddContainer(container))
// No dependencies - make sure we don't filter anything out.
config = &dockerclient.ContainerConfig{}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Equal(t, result, nodes)
// volumes-from.
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
VolumesFrom: []string{"c0"},
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0])
// link.
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
Links: []string{"c1:foobar"},
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[1])
// net.
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
NetworkMode: "container:c2",
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[2])
// net not prefixed by "container:" should be ignored.
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
NetworkMode: "bridge",
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Equal(t, result, nodes)
}
func TestDependencyFilterMulti(t *testing.T) {
var (
f = DependencyFilter{}
nodes = []*cluster.Node{
cluster.NewNode("node-1", 0),
cluster.NewNode("node-2", 0),
cluster.NewNode("node-3", 0),
}
result []*cluster.Node
err error
container *cluster.Container
config *dockerclient.ContainerConfig
)
// nodes[0] has c0 and c1
container = &cluster.Container{Container: dockerclient.Container{Id: "c0"}}
assert.NoError(t, nodes[0].AddContainer(container))
container = &cluster.Container{Container: dockerclient.Container{Id: "c1"}}
assert.NoError(t, nodes[0].AddContainer(container))
// nodes[1] has c2
container = &cluster.Container{Container: dockerclient.Container{Id: "c2"}}
assert.NoError(t, nodes[1].AddContainer(container))
// nodes[2] has nothing
// Depend on c0 which is on nodes[0]
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
VolumesFrom: []string{"c0"},
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0])
// Depend on c1 which is on nodes[0]
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
VolumesFrom: []string{"c1"},
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0])
// Depend on c0 AND c1 which are both on nodes[0]
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
VolumesFrom: []string{"c0", "c1"},
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0])
// Depend on c0 AND c2 which are on different nodes.
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
VolumesFrom: []string{"c0", "c2"},
},
}
result, err = f.Filter(config, nodes)
assert.Error(t, err)
}
func TestDependencyFilterChaining(t *testing.T) {
var (
f = DependencyFilter{}
nodes = []*cluster.Node{
cluster.NewNode("node-1", 0),
cluster.NewNode("node-2", 0),
cluster.NewNode("node-3", 0),
}
result []*cluster.Node
err error
container *cluster.Container
config *dockerclient.ContainerConfig
)
// nodes[0] has c0 and c1
container = &cluster.Container{Container: dockerclient.Container{Id: "c0"}}
assert.NoError(t, nodes[0].AddContainer(container))
container = &cluster.Container{Container: dockerclient.Container{Id: "c1"}}
assert.NoError(t, nodes[0].AddContainer(container))
// nodes[1] has c2
container = &cluster.Container{Container: dockerclient.Container{Id: "c2"}}
assert.NoError(t, nodes[1].AddContainer(container))
// nodes[2] has nothing
// Different dependencies on c0 and c1
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
VolumesFrom: []string{"c0"},
Links: []string{"c1"},
NetworkMode: "container:c1",
},
}
result, err = f.Filter(config, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0])
// Different dependencies on c0 and c2
config = &dockerclient.ContainerConfig{
HostConfig: dockerclient.HostConfig{
VolumesFrom: []string{"c0"},
Links: []string{"c2"},
NetworkMode: "container:c1",
},
}
result, err = f.Filter(config, nodes)
assert.Error(t, err)
}

View File

@ -24,6 +24,7 @@ func init() {
"health": &HealthFilter{},
"constraint": &ConstraintFilter{},
"port": &PortFilter{},
"dependency": &DependencyFilter{},
}
}