diff --git a/docs/scheduler/filter.md b/docs/scheduler/filter.md index 70478eb3df..8b1b021bea 100644 --- a/docs/scheduler/filter.md +++ b/docs/scheduler/filter.md @@ -25,6 +25,7 @@ Each filter has a name that identifies it. The node filters are: * `constraint` * `health` +* `containerslots` The container configuration filters are: @@ -48,6 +49,8 @@ $ swarm manage --filter=health --filter=dependency When creating a container or building an image, you use a `constraint` or `health` filter to select a subset of nodes to consider for scheduling. +If there are nodes in the swarm that have a label with key as `containerslots` +and a number-value, Swarm will not launch more containers than the given number. ### Use a constraint filter @@ -175,6 +178,16 @@ The node `health` filter prevents the scheduler form running containers on unhealthy nodes. A node is considered unhealthy if the node is down or it can't communicate with the cluster store. +### Use the containerslots filter + +You may give your Docker nodes the containerslots label +```bash +$ docker daemon --label containerslots=3 +``` +Swarm will prevent running more than three containers at this node, if +all nodes are "full", an error is thrown. If the value is not castable +to an integer number or is not present, there will be no limit. + ## Container filters When creating a container, you can use three types of container filters: diff --git a/scheduler/filter/filter.go b/scheduler/filter/filter.go index e33a2f5832..3aa722fbe0 100644 --- a/scheduler/filter/filter.go +++ b/scheduler/filter/filter.go @@ -30,6 +30,7 @@ func init() { filters = []Filter{ &HealthFilter{}, &PortFilter{}, + &SlotsFilter{}, &DependencyFilter{}, &AffinityFilter{}, &ConstraintFilter{}, diff --git a/scheduler/filter/slots.go b/scheduler/filter/slots.go new file mode 100644 index 0000000000..32e513380c --- /dev/null +++ b/scheduler/filter/slots.go @@ -0,0 +1,51 @@ +package filter + +import ( + "errors" + "github.com/docker/swarm/cluster" + "github.com/docker/swarm/scheduler/node" + "strconv" +) + +var ( + // ErrNoNodeWithFreeSlotsAvailable is exported + ErrNoNodeWithFreeSlotsAvailable = errors.New("No node with enough open slots available in the cluster") +) + +//SlotsFilter only schedules containers with open slots. +type SlotsFilter struct { +} + +// Name returns the name of the filter +func (f *SlotsFilter) Name() string { + return "containerslots" +} + +// Filter is exported +func (f *SlotsFilter) Filter(_ *cluster.ContainerConfig, nodes []*node.Node, _ bool) ([]*node.Node, error) { + result := []*node.Node{} + + for _, node := range nodes { + + if slotsString, ok := node.Labels["containerslots"]; ok { + slots, err := strconv.Atoi(slotsString) //if err => cannot cast to int, so ignore the label + if err != nil || len(node.Containers) < slots { + result = append(result, node) + } + } else { + //no limit if label is missing + result = append(result, node) + } + } + + if len(result) == 0 { + return nil, ErrNoNodeWithFreeSlotsAvailable + } + + return result, nil +} + +// GetFilters returns just the info that this node failed, because there where no free slots +func (f *SlotsFilter) GetFilters(config *cluster.ContainerConfig) ([]string, error) { + return []string{"free slots"}, nil +} diff --git a/scheduler/filter/slots_test.go b/scheduler/filter/slots_test.go new file mode 100644 index 0000000000..288a3faaa1 --- /dev/null +++ b/scheduler/filter/slots_test.go @@ -0,0 +1,167 @@ +package filter + +import ( + "testing" + + "github.com/docker/engine-api/types" + "github.com/docker/swarm/cluster" + "github.com/docker/swarm/scheduler/node" + "github.com/stretchr/testify/assert" +) + +var labelsWithSlots = make(map[string]string) +var labelsWithoutSlots = make(map[string]string) +var labelsWithStringSlot = make(map[string]string) + +func testFixturesAllFreeNode() []*node.Node { + return []*node.Node{ + { + ID: "node-0-id", + Name: "node-0-name", + Labels: labelsWithSlots, + Containers: []*cluster.Container{ + {Container: types.Container{}}, + }, + }, + { + ID: "node-1-id", + Name: "node-1-name", + Labels: labelsWithSlots, + Containers: []*cluster.Container{}, + }, + } +} + +func testFixturesPartlyFreeNode() []*node.Node { + return []*node.Node{ + { + ID: "node-0-id", + Name: "node-0-name", + Labels: labelsWithSlots, + Containers: []*cluster.Container{ + {Container: types.Container{}}, + {Container: types.Container{}}, + {Container: types.Container{}}, + }, + }, + { + ID: "node-1-id", + Name: "node-1-name", + Labels: labelsWithSlots, + Containers: []*cluster.Container{}, + }, + } +} + +func testFixturesAllNoLabelNode() []*node.Node { + return []*node.Node{ + { + ID: "node-0-id", + Name: "node-0-name", + Labels: labelsWithoutSlots, + Containers: []*cluster.Container{ + {Container: types.Container{}}, + {Container: types.Container{}}, + {Container: types.Container{}}, + }, + }, + + { + ID: "node-1-id", + Name: "node-1-name", + Labels: labelsWithoutSlots, + Containers: []*cluster.Container{}, + }, + } +} + +func testFixturesNoFreeNode() []*node.Node { + return []*node.Node{ + { + ID: "node-0-id", + Name: "node-0-name", + Labels: labelsWithSlots, + Containers: []*cluster.Container{ + {Container: types.Container{}}, + {Container: types.Container{}}, + {Container: types.Container{}}, + }, + }, + + { + ID: "node-1-id", + Name: "node-1-name", + Labels: labelsWithSlots, + Containers: []*cluster.Container{ + {Container: types.Container{}}, + {Container: types.Container{}}, + {Container: types.Container{}}, + }, + }, + } +} + +func testFixturesNoFreeNodeButStringLabel() []*node.Node { + return []*node.Node{ + { + ID: "node-0-id", + Name: "node-0-name", + Labels: labelsWithSlots, + Containers: []*cluster.Container{ + {Container: types.Container{}}, + {Container: types.Container{}}, + {Container: types.Container{}}, + }, + }, + + { + ID: "node-1-id", + Name: "node-1-name", + Labels: labelsWithStringSlot, + Containers: []*cluster.Container{ + {Container: types.Container{}}, + {Container: types.Container{}}, + {Container: types.Container{}}, + }, + }, + } +} + +func TestSlotsFilter(t *testing.T) { + + labelsWithSlots["containerslots"] = "3" + labelsWithStringSlot["containerslots"] = "foo" + + var ( + f = SlotsFilter{} + nodesAllFree = testFixturesAllFreeNode() + nodesPartlyFree = testFixturesPartlyFreeNode() + nodesAllNoLabel = testFixturesAllNoLabelNode() + nodesNoFree = testFixturesNoFreeNode() + nodesNoFreeButStringLabel = testFixturesNoFreeNodeButStringLabel() + result []*node.Node + err error + ) + + result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllFree, true) + assert.NoError(t, err) + assert.Equal(t, result, nodesAllFree) + + result, err = f.Filter(&cluster.ContainerConfig{}, nodesPartlyFree, true) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodesPartlyFree[1]) + + result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllNoLabel, true) + assert.NoError(t, err) + assert.Equal(t, result, nodesAllNoLabel) + + result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoFree, true) + assert.Equal(t, err, ErrNoNodeWithFreeSlotsAvailable) + assert.Nil(t, result) + + result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoFreeButStringLabel, true) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodesNoFreeButStringLabel[1]) +} diff --git a/test/integration/build_with_filters.bats b/test/integration/build_with_filters.bats index 90ade594ef..a06ff18066 100644 --- a/test/integration/build_with_filters.bats +++ b/test/integration/build_with_filters.bats @@ -18,7 +18,7 @@ function teardown() { run docker_swarm build --build-arg="constraint:node==node-9" $TESTDATA/build [ "$status" -eq 1 ] [[ "${lines[1]}" == *"Unable to find a node that satisfies the following conditions"* ]] - [[ "${lines[2]}" == *"[node==node-9]"* ]] + [[ "${lines[3]}" == *"[node==node-9]"* ]] run docker_swarm images -q [ "$status" -eq 0 ] diff --git a/test/integration/containerslots-filter.bats b/test/integration/containerslots-filter.bats new file mode 100644 index 0000000000..20519e91c4 --- /dev/null +++ b/test/integration/containerslots-filter.bats @@ -0,0 +1,78 @@ +#!/usr/bin/env bats + +load helpers + +function teardown() { + swarm_manage_cleanup + stop_docker +} + +@test "containerslots filter" { + start_docker_with_busybox 2 --label containerslots=2 + swarm_manage + + # Use busybox to save image pulling time for integration test. + # Running the first 4 containers, it should be fine. + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + + # When trying to start the 5th one, it should be error finding a node with free slots. + run docker_swarm run -d -t busybox sh + [ "$status" -ne 0 ] + [[ "${lines[0]}" == *"Unable to find a node that satisfies the following conditions"* ]] + [[ "${lines[1]}" == *"free slots"* ]] + + # And the number of running containers should be still 4. + run docker_swarm ps + [ "${#lines[@]}" -eq 5 ] +} + +@test "containerslots without existing label" { + start_docker_with_busybox 2 + swarm_manage + + # Use busybox to save image pulling time for integration test. + # Running more than 5 containers, it should be fine. + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + + # And the number of running containers should be 5. + run docker_swarm ps + [ "${#lines[@]}" -eq 6 ] +} + +@test "containerslots with invalid label" { + start_docker_with_busybox 2 --label containerslots="foo" + swarm_manage + + # Use busybox to save image pulling time for integration test. + # Running more than 5 containers, it should be fine. + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + run docker_swarm run -d -t busybox sh + [ "$status" -eq 0 ] + + # And the number of running containers should be 5. + run docker_swarm ps + [ "${#lines[@]}" -eq 6 ] +} \ No newline at end of file