diff --git a/scheduler/filter/affinity.go b/scheduler/filter/affinity.go index 6028cfd6c7..342984bfb5 100644 --- a/scheduler/filter/affinity.go +++ b/scheduler/filter/affinity.go @@ -19,14 +19,17 @@ func (f *AffinityFilter) Name() string { } // Filter is exported -func (f *AffinityFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (f *AffinityFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node, soft bool) ([]*node.Node, error) { affinities, err := parseExprs(config.Affinities()) if err != nil { return nil, err } for _, affinity := range affinities { - log.Debugf("matching affinity: %s%s%s", affinity.key, OPERATORS[affinity.operator], affinity.value) + if !soft && affinity.isSoft { + continue + } + log.Debugf("matching affinity: %s%s%s (soft=%t)", affinity.key, OPERATORS[affinity.operator], affinity.value, affinity.isSoft) candidates := []*node.Node{} for _, node := range nodes { @@ -65,12 +68,10 @@ func (f *AffinityFilter) Filter(config *cluster.ContainerConfig, nodes []*node.N } } if len(candidates) == 0 { - if affinity.isSoft { - continue - } return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", affinity.key, OPERATORS[affinity.operator], affinity.value) } nodes = candidates } + return nodes, nil } diff --git a/scheduler/filter/affinity_test.go b/scheduler/filter/affinity_test.go index 0103415715..aa38aed6fe 100644 --- a/scheduler/filter/affinity_test.go +++ b/scheduler/filter/affinity_test.go @@ -62,137 +62,140 @@ func TestAffinityFilter(t *testing.T) { ) // Without constraints we should get the unfiltered list of nodes back. - result, err = f.Filter(&cluster.ContainerConfig{}, nodes) + result, err = f.Filter(&cluster.ContainerConfig{}, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) // Set a constraint that cannot be fulfilled and expect an error back. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==does_not_exsits"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==does_not_exsits"}}), nodes, true) assert.Error(t, err) // Set a constraint that can only be filled by a single node. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // This constraint can only be fulfilled by a subset of nodes. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[2]) // Validate by id. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0-0-id"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0-0-id"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Validate by id. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-0-id"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-0-id"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[0]) // Validate by id. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-1-id"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-1-id"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[0]) // Validate by name. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n1-0-name"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n1-0-name"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Validate by name. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-0-name"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-0-name"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[1]) // Validate by name. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-1-name"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-1-name"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[1]) // Conflicting Constraint - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-1-name", "affinity:container==container-n1-1-name"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-1-name", "affinity:container==container-n1-1-name"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) // Validate images by id - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-0-id"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-0-id"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Validate images by name - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-0:tag3"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-0:tag3"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Validate images by name - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-0:tag3"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-0:tag3"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) // Validate images by name - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-1"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-1"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Validate images by name - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-1"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-1"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) // Ensure that constraints can be chained. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-1-id", "affinity:container!=container-n1-1-id"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-1-id", "affinity:container!=container-n1-1-id"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) // Ensure that constraints can be chained. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0-1-id", "affinity:container==container-n1-1-id"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0-1-id", "affinity:container==container-n1-1-id"}}), nodes, true) assert.Error(t, err) //Tests for Soft affinity - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-0:tag3"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-0:tag3"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~ima~ge-0:tag3"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~ima~ge-0:tag3"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-1:tag3"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-1:tag3"}}), nodes, true) + assert.Error(t, err) + assert.Len(t, result, 0) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-1:tag3"}}), nodes, false) assert.NoError(t, err) assert.Len(t, result, 3) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=~image-*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=~image-*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~/image-\\d*/"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==~/image-\\d*/"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) // Not support = any more - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image=image-0:tag3"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image=image-0:tag3"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) // Not support =! any more - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image=!image-0:tag3"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image=!image-0:tag3"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) @@ -237,22 +240,22 @@ func TestAffinityFilterLabels(t *testing.T) { err error ) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-1"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image==image-1"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-1"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-1"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Labels: map[string]string{"com.docker.swarm.affinities": "[\"image==image-1\"]"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Labels: map[string]string{"com.docker.swarm.affinities": "[\"image==image-1\"]"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Labels: map[string]string{"com.docker.swarm.affinities": "[\"image!=image-1\"]"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Labels: map[string]string{"com.docker.swarm.affinities": "[\"image!=image-1\"]"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) diff --git a/scheduler/filter/constraint.go b/scheduler/filter/constraint.go index b95a587149..841d5feb3b 100644 --- a/scheduler/filter/constraint.go +++ b/scheduler/filter/constraint.go @@ -18,14 +18,17 @@ func (f *ConstraintFilter) Name() string { } // Filter is exported -func (f *ConstraintFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (f *ConstraintFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node, soft bool) ([]*node.Node, error) { constraints, err := parseExprs(config.Constraints()) if err != nil { return nil, err } for _, constraint := range constraints { - log.Debugf("matching constraint: %s %s %s", constraint.key, OPERATORS[constraint.operator], constraint.value) + if !soft && constraint.isSoft { + continue + } + log.Debugf("matching constraint: %s%s%s (soft=%t)", constraint.key, OPERATORS[constraint.operator], constraint.value, constraint.isSoft) candidates := []*node.Node{} for _, node := range nodes { @@ -42,9 +45,6 @@ func (f *ConstraintFilter) Filter(config *cluster.ContainerConfig, nodes []*node } } if len(candidates) == 0 { - if constraint.isSoft { - continue - } return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", constraint.key, OPERATORS[constraint.operator], constraint.value) } nodes = candidates diff --git a/scheduler/filter/constraint_test.go b/scheduler/filter/constraint_test.go index efd7c4647b..d26af9323a 100644 --- a/scheduler/filter/constraint_test.go +++ b/scheduler/filter/constraint_test.go @@ -61,54 +61,54 @@ func TestConstrainteFilter(t *testing.T) { ) // Without constraints we should get the unfiltered list of nodes back. - result, err = f.Filter(&cluster.ContainerConfig{}, nodes) + result, err = f.Filter(&cluster.ContainerConfig{}, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) // Set a constraint that cannot be fulfilled and expect an error back. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:does_not_exist==true"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:does_not_exist==true"}}), nodes, true) assert.Error(t, err) // Set a contraint that can only be filled by a single node. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name==node1"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name==node1"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // This constraint can only be fulfilled by a subset of nodes. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:group==1"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:group==1"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[2]) // Validate node pinning by id. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==node-2-id"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==node-2-id"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) // Validate node pinning by name. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Make sure constraints are evaluated as logical ANDs. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name==node0", "constraint:group==1"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name==node0", "constraint:group==1"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Check matching - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==us"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==us"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==*us*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==*us*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) } @@ -122,22 +122,22 @@ func TestConstraintNotExpr(t *testing.T) { ) // Check not (!) expression - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name!=node0"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name!=node0"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 3) // Check not does_not_exist. All should be found - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name!=does_not_exist"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name!=does_not_exist"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 4) // Check name must not start with n - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name!=n*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name!=n*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) // Check not with globber pattern - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region!=us*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region!=us*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) } @@ -151,22 +151,22 @@ func TestConstraintRegExp(t *testing.T) { ) // Check with regular expression /node\d/ matches node{0..2} - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/node\d/`}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/node\d/`}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 3) // Check with regular expression /node\d/ matches node{0..2} - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/node[12]/`}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/node[12]/`}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) // Check with regular expression ! and regexp /node[12]/ matches node[0] and node[3] - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name!=/node[12]/`}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name!=/node[12]/`}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) // Validate node pinning by ! and regexp. - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node!=/node-[01]-id/"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node!=/node-[01]-id/"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) } @@ -187,19 +187,19 @@ func TestFilterRegExpCaseInsensitive(t *testing.T) { } // Case-sensitive, so not match - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/abcdef/`}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/abcdef/`}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) // Match with case-insensitive - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/(?i)abcdef/`}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name==/(?i)abcdef/`}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[3]) assert.Equal(t, result[0].Labels["name"], "aBcDeF") // Test ! filter combined with case insensitive - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name!=/(?i)abc*/`}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name!=/(?i)abc*/`}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 3) } @@ -213,17 +213,17 @@ func TestFilterEquals(t *testing.T) { ) // Check == comparison - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name==node0"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name==node0"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) // Test == with glob - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) // Validate node name with == - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) @@ -237,11 +237,11 @@ func TestUnsupportedOperators(t *testing.T) { err error ) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name=node0"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name=node0"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name=!node0"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:name=!node0"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) } @@ -254,26 +254,26 @@ func TestFilterSoftConstraint(t *testing.T) { err error ) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==~node-1-name"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:node==~node-1-name"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name!=~/(?i)abc*/`}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{`constraint:name!=~/(?i)abc*/`}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 4) // Check not with globber pattern - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region!=~us*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region!=~us*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 2) - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region!=~can*"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region!=~can*"}}), nodes, true) assert.NoError(t, err) assert.Len(t, result, 4) // Check matching - result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==~us~"}}), nodes) + result, err = f.Filter(cluster.BuildContainerConfig(dockerclient.ContainerConfig{Env: []string{"constraint:region==~us~"}}), nodes, true) assert.Error(t, err) assert.Len(t, result, 0) } diff --git a/scheduler/filter/dependency.go b/scheduler/filter/dependency.go index d0cf90468c..4a0ea9fdf0 100644 --- a/scheduler/filter/dependency.go +++ b/scheduler/filter/dependency.go @@ -18,7 +18,7 @@ func (f *DependencyFilter) Name() string { } // Filter is exported -func (f *DependencyFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (f *DependencyFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node, _ bool) ([]*node.Node, error) { if len(nodes) == 0 { return nodes, nil } diff --git a/scheduler/filter/dependency_test.go b/scheduler/filter/dependency_test.go index db3244b9f9..aebb2bdc44 100644 --- a/scheduler/filter/dependency_test.go +++ b/scheduler/filter/dependency_test.go @@ -50,7 +50,7 @@ func TestDependencyFilterSimple(t *testing.T) { // No dependencies - make sure we don't filter anything out. config = &cluster.ContainerConfig{} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) @@ -58,7 +58,7 @@ func TestDependencyFilterSimple(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) @@ -67,7 +67,7 @@ func TestDependencyFilterSimple(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0:rw"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) @@ -76,7 +76,7 @@ func TestDependencyFilterSimple(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0:ro"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) @@ -85,7 +85,7 @@ func TestDependencyFilterSimple(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ Links: []string{"c1:foobar"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) @@ -94,7 +94,7 @@ func TestDependencyFilterSimple(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ NetworkMode: "container:c2", }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) @@ -103,7 +103,7 @@ func TestDependencyFilterSimple(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ NetworkMode: "bridge", }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) } @@ -158,7 +158,7 @@ func TestDependencyFilterMulti(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) @@ -167,7 +167,7 @@ func TestDependencyFilterMulti(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c1"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) @@ -176,7 +176,7 @@ func TestDependencyFilterMulti(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0", "c1"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) @@ -185,7 +185,7 @@ func TestDependencyFilterMulti(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0", "c2"}, }}} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.Error(t, err) } @@ -243,7 +243,7 @@ func TestDependencyFilterChaining(t *testing.T) { NetworkMode: "container:c1", }, }} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) @@ -256,6 +256,6 @@ func TestDependencyFilterChaining(t *testing.T) { NetworkMode: "container:c1", }, }} - result, err = f.Filter(config, nodes) + result, err = f.Filter(config, nodes, true) assert.Error(t, err) } diff --git a/scheduler/filter/filter.go b/scheduler/filter/filter.go index 038b0fd560..3e56d5dfcc 100644 --- a/scheduler/filter/filter.go +++ b/scheduler/filter/filter.go @@ -13,7 +13,7 @@ type Filter interface { Name() string // Return a subset of nodes that were accepted by the filtering policy. - Filter(*cluster.ContainerConfig, []*node.Node) ([]*node.Node, error) + Filter(*cluster.ContainerConfig, []*node.Node, bool) ([]*node.Node, error) } var ( @@ -55,15 +55,28 @@ func New(names []string) ([]Filter, error) { // ApplyFilters applies a set of filters in batch. func ApplyFilters(filters []Filter, config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { - var err error + candidates, err := applyFilters(filters, config, nodes, true) + + if err != nil { + candidates, err = applyFilters(filters, config, nodes, false) + } + + return candidates, err +} + +func applyFilters(filters []Filter, config *cluster.ContainerConfig, nodes []*node.Node, soft bool) ([]*node.Node, error) { + var ( + err error + candidates = nodes + ) for _, filter := range filters { - nodes, err = filter.Filter(config, nodes) + candidates, err = filter.Filter(config, candidates, soft) if err != nil { return nil, err } } - return nodes, nil + return candidates, nil } // List returns the names of all the available filters diff --git a/scheduler/filter/health.go b/scheduler/filter/health.go index 97966c7e38..75bbddc2ef 100644 --- a/scheduler/filter/health.go +++ b/scheduler/filter/health.go @@ -22,7 +22,7 @@ func (f *HealthFilter) Name() string { } // Filter is exported -func (f *HealthFilter) Filter(_ *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (f *HealthFilter) Filter(_ *cluster.ContainerConfig, nodes []*node.Node, _ bool) ([]*node.Node, error) { result := []*node.Node{} for _, node := range nodes { if node.IsHealthy { diff --git a/scheduler/filter/health_test.go b/scheduler/filter/health_test.go index 9ec8ae9ca5..f4699e22c1 100644 --- a/scheduler/filter/health_test.go +++ b/scheduler/filter/health_test.go @@ -66,16 +66,16 @@ func TestHealthyFilter(t *testing.T) { err error ) - result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllHealth) + result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllHealth, true) assert.NoError(t, err) assert.Equal(t, result, nodesAllHealth) - result, err = f.Filter(&cluster.ContainerConfig{}, nodesPartHealth) + result, err = f.Filter(&cluster.ContainerConfig{}, nodesPartHealth, true) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodesPartHealth[1]) - result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoHealth) + result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoHealth, true) assert.Equal(t, err, ErrNoHealthyNodeAvailable) assert.Nil(t, result) } diff --git a/scheduler/filter/port.go b/scheduler/filter/port.go index 35bef22bff..757a6c6b6a 100644 --- a/scheduler/filter/port.go +++ b/scheduler/filter/port.go @@ -20,7 +20,7 @@ func (p *PortFilter) Name() string { } // Filter is exported -func (p *PortFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (p *PortFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node, _ bool) ([]*node.Node, error) { if config.HostConfig.NetworkMode == "host" { return p.filterHost(config, nodes) } diff --git a/scheduler/filter/port_test.go b/scheduler/filter/port_test.go index bb95c5b227..842efa8d4c 100644 --- a/scheduler/filter/port_test.go +++ b/scheduler/filter/port_test.go @@ -50,7 +50,7 @@ func TestPortFilterNoConflicts(t *testing.T) { PortBindings: map[string][]dockerclient.PortBinding{}, }}} // Make sure we don't filter anything out. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) @@ -61,7 +61,7 @@ func TestPortFilterNoConflicts(t *testing.T) { // Since there are no other containers in the cluster, this shouldn't // filter anything either. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) @@ -70,7 +70,7 @@ func TestPortFilterNoConflicts(t *testing.T) { assert.NoError(t, nodes[0].AddContainer(container)) // Since no node is using port 80, there should be no filter - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) } @@ -109,7 +109,7 @@ func TestPortFilterSimple(t *testing.T) { }}} // nodes[0] should be excluded since port 80 is taken away. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.NotContains(t, result, nodes[0]) } @@ -149,7 +149,7 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { // nodes[0] should be excluded since port 80 is taken away for every // interface. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.NotContains(t, result, nodes[0]) @@ -163,7 +163,7 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { }}} // nodes[1] should be excluded since port 4242 is already taken on that // interface. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.NotContains(t, result, nodes[1]) @@ -172,7 +172,7 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { PortBindings: makeBinding("0.0.0.0", "4242"), }}} // nodes[1] should still be excluded since the port is not available on the same interface. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.NotContains(t, result, nodes[1]) @@ -181,7 +181,7 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { PortBindings: makeBinding("", "4242"), }}} // nodes[1] should still be excluded since the port is not available on the same interface. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.NotContains(t, result, nodes[1]) @@ -191,7 +191,7 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { }}} // nodes[1] should be included this time since the port is available on the // other interface. - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.Contains(t, result, nodes[1]) } @@ -262,7 +262,7 @@ func TestPortFilterRandomAssignment(t *testing.T) { }}} // Since port "80" has been mapped to "1234", we should be able to request "80". - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.Equal(t, result, nodes) @@ -270,7 +270,7 @@ func TestPortFilterRandomAssignment(t *testing.T) { config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("", "1234"), }}} - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.NotContains(t, result, nodes[0]) } @@ -323,7 +323,7 @@ func TestPortFilterForHostMode(t *testing.T) { }} // nodes[0] should be excluded since port 80 is taken away - result, err = p.Filter(config, nodes) + result, err = p.Filter(config, nodes, true) assert.NoError(t, err) assert.Equal(t, 2, len(result)) assert.NotContains(t, result, nodes[0]) diff --git a/test/integration/api/run.bats b/test/integration/api/run.bats index 0a71db097f..383da53d3d 100644 --- a/test/integration/api/run.bats +++ b/test/integration/api/run.bats @@ -145,6 +145,26 @@ function teardown() { [[ "${output}" == *"not found"* ]] } +@test "docker run - constraint and soft affinities" { + start_docker_with_busybox 1 --label group=A + start_docker_with_busybox 1 --label group=B + swarm_manage + + # start c0 on a node in group=A + docker_swarm run -d --name c0 -e constraint:group==A -e affinity:container==~c0 busybox sleep 100 + + # check container running on node-0 + run docker_swarm ps + [[ "${output}" == *"node-0/c0"* ]] + + # start c2 on a node in group==B (soft affinity shouldn't matter here) + docker_swarm run -d --name c2 -e constraint:group==B -e affinity:container==~c0 busybox sleep 100 + + # check container running on a node in group=B + run docker_swarm ps + [[ "${output}" == *"node-1/c2"* ]] +} + @test "docker run - with not exist volume driver" { start_docker_with_busybox 2 swarm_manage