Merge pull request #261 from vieux/chanwit-constraints-expr

Proposal: constraint & affinity filter expression enhancement
This commit is contained in:
Andrea Luzzardi 2015-01-20 16:38:24 -08:00
commit 10074faa75
9 changed files with 529 additions and 96 deletions

View File

@ -43,16 +43,16 @@ docker run -d -p 80:80 nginx
# clean up cluster # clean up cluster
docker rm -f `docker ps -aq` docker rm -f `docker ps -aq`
docker run -d -e "constraint:operatingsystem=fedora*" redis docker run -d -e "constraint:operatingsystem==fedora*" redis
docker ps docker ps
docker run -d -e constraint:storagedriver=devicemapper redis docker run -d -e constraint:storagedriver==devicemapper redis
docker ps docker ps
docker run -d -e constraint:storagedriver=aufs redis docker run -d -e constraint:storagedriver==aufs redis
docker ps docker ps
docker run -d -e constraint:node=fedora-1 redis docker run -d -e constraint:node==fedora-1 redis
docker ps docker ps
# clean up cluster # clean up cluster

View File

@ -45,7 +45,7 @@ Once the nodes are registered with the cluster, the master pulls their respectiv
Let's start a MySQL server and make sure it gets good I/O performance by selecting nodes with flash drives: Let's start a MySQL server and make sure it gets good I/O performance by selecting nodes with flash drives:
``` ```
$ docker run -d -P -e constraint:storage=ssd --name db mysql $ docker run -d -P -e constraint:storage==ssd --name db mysql
f8b693db9cd6 f8b693db9cd6
$ docker ps $ docker ps
@ -59,7 +59,7 @@ In this case, the master selected all nodes that met the `storage=ssd` constrain
Now we want to run an `nginx` frontend in our cluster. However, we don't want *flash* drives since we'll mostly write logs to disk. Now we want to run an `nginx` frontend in our cluster. However, we don't want *flash* drives since we'll mostly write logs to disk.
``` ```
$ docker run -d -P -e constraint:storage=disk --name frontend nginx $ docker run -d -P -e constraint:storage==disk --name frontend nginx
f8b693db9cd6 f8b693db9cd6
$ docker ps $ docker ps
@ -95,11 +95,11 @@ CONTAINER ID IMAGE COMMAND CREATED
87c4376856a8 nginx:latest "nginx" Less than a second ago running 192.168.0.42:80->80/tcp node-1 front 87c4376856a8 nginx:latest "nginx" Less than a second ago running 192.168.0.42:80->80/tcp node-1 front
``` ```
Using `-e affinity:container=front` will schedule a container next to the container `front`. Using `-e affinity:container==front` will schedule a container next to the container `front`.
You can also use IDs instead of name: `-e affinity:container=87c4376856a8` You can also use IDs instead of name: `-e affinity:container==87c4376856a8`
``` ```
$ docker run -d --name logger -e affinity:container=front logger $ docker run -d --name logger -e affinity:container==front logger
87c4376856a8 87c4376856a8
$ docker ps $ docker ps
@ -124,14 +124,14 @@ Here only `node-1` and `node-3` have the `redis` image. Using `-e affinity:image
schedule container only on these 2 nodes. You can also use the image ID instead of it's name. schedule container only on these 2 nodes. You can also use the image ID instead of it's name.
``` ```
$ docker run -d --name redis1 -e affinity:image=redis redis $ docker run -d --name redis1 -e affinity:image==redis redis
$ docker run -d --name redis2 -e affinity:image=redis redis $ docker run -d --name redis2 -e affinity:image==redis redis
$ docker run -d --name redis3 -e affinity:image=redis redis $ docker run -d --name redis3 -e affinity:image==redis redis
$ docker run -d --name redis4 -e affinity:image=redis redis $ docker run -d --name redis4 -e affinity:image==redis redis
$ docker run -d --name redis5 -e affinity:image=redis redis $ docker run -d --name redis5 -e affinity:image==redis redis
$ docker run -d --name redis6 -e affinity:image=redis redis $ docker run -d --name redis6 -e affinity:image==redis redis
$ docker run -d --name redis7 -e affinity:image=redis redis $ docker run -d --name redis7 -e affinity:image==redis redis
$ docker run -d --name redis8 -e affinity:image=redis redis $ docker run -d --name redis8 -e affinity:image==redis redis
$ docker ps $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NODE NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NODE NAMES
@ -147,6 +147,28 @@ CONTAINER ID IMAGE COMMAND CREATED
As you can see here, the containers were only scheduled on nodes with the redis imagealreayd pulled. As you can see here, the containers were only scheduled on nodes with the redis imagealreayd pulled.
#### Expression Syntax
An affinity or a constraint expression consists of a `key` and a `value`.
A `key` must conform the alpha-numeric pattern, with the leading alphabet or underscore.
A `value` must be one of the following:
* An alpha-numeric string, dots, hyphens, and underscores.
* A globbing pattern, i.e., `abc*`.
* A regular expression in the form of `/regexp/`. We support the Go's regular expression syntax.
Current `swarm` supports affinity/constraint operators as the following: `==` and `!=`.
For example,
* `constraint:name==node1` will match nodes named with `node1`.
* `constraint:name!=node1` will match all nodes, except `node1`.
* `constraint:region!=us*` will match all nodes outside the regions prefixed with `us`.
* `constraint:name==/node[12]/` will match nodes named `node1` and `node2`.
* `constraint:name==/node\d/` will match all nodes named with `node` + 1 digit.
* `constraint:node!=/node-[01]-id/` will match all nodes, except those with ids `node-0-id` and `node-1-id`.
* `constraint:name!=/foo\[bar\]/` will match all nodes, except those with name `foo[bar]`. You can see the use of escape characters here.
* `constraint:name==/(?i)node1/` will match all nodes named with `node1` case-insensitive. So 'NoDe1' or 'NODE1' will also matched.
## Port Filter ## Port Filter
With this filter, `ports` are considered as a unique resource. With this filter, `ports` are considered as a unique resource.

View File

@ -13,15 +13,20 @@ type AffinityFilter struct {
} }
func (f *AffinityFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) { func (f *AffinityFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) {
affinities := extractEnv("affinity", config.Env) affinities, err := parseExprs("affinity", config.Env)
for k, v := range affinities { if err != nil {
log.Debugf("matching affinity: %s=%s", k, v) return nil, err
}
for _, affinity := range affinities {
log.Debugf("matching affinity: %s%s%s", affinity.key, OPERATORS[affinity.operator], affinity.value)
candidates := []*cluster.Node{} candidates := []*cluster.Node{}
for _, node := range nodes { for _, node := range nodes {
switch k { switch affinity.key {
case "container": case "container":
for _, container := range node.Containers() { for _, container := range node.Containers() {
if match(v, container.Id) || match(v, container.Names[0]) { if affinity.Match(container.Id, container.Names[0]) {
candidates = append(candidates, node) candidates = append(candidates, node)
break break
} }
@ -29,12 +34,12 @@ func (f *AffinityFilter) Filter(config *dockerclient.ContainerConfig, nodes []*c
case "image": case "image":
done: done:
for _, image := range node.Images() { for _, image := range node.Images() {
if match(v, image.Id) { if affinity.Match(image.Id) {
candidates = append(candidates, node) candidates = append(candidates, node)
break break
} }
for _, t := range image.RepoTags { for _, tag := range image.RepoTags {
if match(v, t) { if affinity.Match(tag) {
candidates = append(candidates, node) candidates = append(candidates, node)
break done break done
} }
@ -43,7 +48,7 @@ func (f *AffinityFilter) Filter(config *dockerclient.ContainerConfig, nodes []*c
} }
} }
if len(candidates) == 0 { if len(candidates) == 0 {
return nil, fmt.Errorf("unable to find a node that satisfies %s == %s", k, v) return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", affinity.key, OPERATORS[affinity.operator], affinity.value)
} }
nodes = candidates nodes = candidates
} }

View File

@ -56,13 +56,13 @@ func TestAffinityFilter(t *testing.T) {
// Set a constraint that cannot be fullfilled and expect an error back. // Set a constraint that cannot be fullfilled and expect an error back.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:container=does_not_exsits"}, Env: []string{"affinity:container==does_not_exsits"},
}, nodes) }, nodes)
assert.Error(t, err) assert.Error(t, err)
// Set a contraint that can only be filled by a single node. // Set a contraint that can only be filled by a single node.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:container=container-0*"}, Env: []string{"affinity:container==container-0*"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
@ -70,7 +70,7 @@ func TestAffinityFilter(t *testing.T) {
// This constraint can only be fullfilled by a subset of nodes. // This constraint can only be fullfilled by a subset of nodes.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:container=container-*"}, Env: []string{"affinity:container==container-*"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 2) assert.Len(t, result, 2)
@ -78,23 +78,39 @@ func TestAffinityFilter(t *testing.T) {
// Validate by id. // Validate by id.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:container=container-0-id"}, Env: []string{"affinity:container==container-0-id"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0]) assert.Equal(t, result[0], nodes[0])
// Validate by id.
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:container!=container-0-id"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.NotContains(t, result, nodes[0])
// Validate by name. // Validate by name.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:container=container-1-name"}, Env: []string{"affinity:container==container-1-name"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[1]) assert.Equal(t, result[0], nodes[1])
// Validate by name.
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:container!=container-1-name"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.NotContains(t, result, nodes[1])
// Validate images by id // Validate images by id
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:image=image-0-id"}, Env: []string{"affinity:image==image-0-id"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
@ -102,9 +118,30 @@ func TestAffinityFilter(t *testing.T) {
// Validate images by name // Validate images by name
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:image=image-0:tag3"}, Env: []string{"affinity:image==image-0:tag3"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[1]) assert.Equal(t, result[0], nodes[1])
// Validate images by name
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:image!=image-0:tag3"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 2)
// Not support = any more
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:image=image-0:tag3"},
}, nodes)
assert.Error(t, err)
assert.Len(t, result, 0)
// Not support =! any more
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"affinity:image=!image-0:tag3"},
}, nodes)
assert.Error(t, err)
assert.Len(t, result, 0)
} }

View File

@ -13,28 +13,32 @@ type ConstraintFilter struct {
} }
func (f *ConstraintFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) { func (f *ConstraintFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) {
constraints := extractEnv("constraint", config.Env) constraints, err := parseExprs("constraint", config.Env)
for k, v := range constraints { if err != nil {
log.Debugf("matching constraint: %s=%s", k, v) return nil, err
}
for _, constraint := range constraints {
log.Debugf("matching constraint: %s %s %s", constraint.key, OPERATORS[constraint.operator], constraint.value)
candidates := []*cluster.Node{} candidates := []*cluster.Node{}
for _, node := range nodes { for _, node := range nodes {
switch k { switch constraint.key {
case "node": case "node":
// "node" label is a special case pinning a container to a specific node. // "node" label is a special case pinning a container to a specific node.
if match(v, node.ID) || match(v, node.Name) { if constraint.Match(node.ID, node.Name) {
candidates = append(candidates, node) candidates = append(candidates, node)
} }
default: default:
// By default match the node labels. if label, ok := node.Labels[constraint.key]; ok {
if label, ok := node.Labels[k]; ok { if constraint.Match(label) {
if match(v, label) {
candidates = append(candidates, node) candidates = append(candidates, node)
} }
} }
} }
} }
if len(candidates) == 0 { if len(candidates) == 0 {
return nil, fmt.Errorf("unable to find a node that satisfies %s == %s", k, v) return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", constraint.key, OPERATORS[constraint.operator], constraint.value)
} }
nodes = candidates nodes = candidates
} }

View File

@ -8,18 +8,12 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestConstraintFilter(t *testing.T) { func testFixtures() (nodes []*cluster.Node) {
var (
f = ConstraintFilter{}
nodes = []*cluster.Node{ nodes = []*cluster.Node{
cluster.NewNode("node-0", 0), cluster.NewNode("node-0", 0),
cluster.NewNode("node-1", 0), cluster.NewNode("node-1", 0),
cluster.NewNode("node-2", 0), cluster.NewNode("node-2", 0),
} }
result []*cluster.Node
err error
)
nodes[0].ID = "node-0-id" nodes[0].ID = "node-0-id"
nodes[0].Name = "node-0-name" nodes[0].Name = "node-0-name"
nodes[0].Labels = map[string]string{ nodes[0].Labels = map[string]string{
@ -43,6 +37,16 @@ func TestConstraintFilter(t *testing.T) {
"group": "2", "group": "2",
"region": "eu", "region": "eu",
} }
return
}
func TestConstrainteFilter(t *testing.T) {
var (
f = ConstraintFilter{}
nodes = testFixtures()
result []*cluster.Node
err error
)
// Without constraints we should get the unfiltered list of nodes back. // Without constraints we should get the unfiltered list of nodes back.
result, err = f.Filter(&dockerclient.ContainerConfig{}, nodes) result, err = f.Filter(&dockerclient.ContainerConfig{}, nodes)
@ -51,13 +55,13 @@ func TestConstraintFilter(t *testing.T) {
// Set a constraint that cannot be fullfilled and expect an error back. // Set a constraint that cannot be fullfilled and expect an error back.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:does_not_exist=true"}, Env: []string{"constraint:does_not_exist==true"},
}, nodes) }, nodes)
assert.Error(t, err) assert.Error(t, err)
// Set a contraint that can only be filled by a single node. // Set a contraint that can only be filled by a single node.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name=node1"}, Env: []string{"constraint:name==node1"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
@ -65,7 +69,7 @@ func TestConstraintFilter(t *testing.T) {
// This constraint can only be fullfilled by a subset of nodes. // This constraint can only be fullfilled by a subset of nodes.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:group=1"}, Env: []string{"constraint:group==1"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 2) assert.Len(t, result, 2)
@ -73,7 +77,7 @@ func TestConstraintFilter(t *testing.T) {
// Validate node pinning by id. // Validate node pinning by id.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:node=node-2-id"}, Env: []string{"constraint:node==node-2-id"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
@ -81,7 +85,7 @@ func TestConstraintFilter(t *testing.T) {
// Validate node pinning by name. // Validate node pinning by name.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:node=node-1-name"}, Env: []string{"constraint:node==node-1-name"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
@ -89,7 +93,7 @@ func TestConstraintFilter(t *testing.T) {
// Make sure constraints are evaluated as logical ANDs. // Make sure constraints are evaluated as logical ANDs.
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name=node0", "constraint:group=1"}, Env: []string{"constraint:name==node0", "constraint:group==1"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
@ -97,14 +101,244 @@ func TestConstraintFilter(t *testing.T) {
// Check matching // Check matching
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:region=us"}, Env: []string{"constraint:region==us"},
}, nodes) }, nodes)
assert.Error(t, err) assert.Error(t, err)
assert.Len(t, result, 0) assert.Len(t, result, 0)
result, err = f.Filter(&dockerclient.ContainerConfig{ result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:region=us*"}, Env: []string{"constraint:region==us*"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 2)
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:region==*us*"},
}, nodes) }, nodes)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 2) assert.Len(t, result, 2)
} }
func TestConstraintNotExpr(t *testing.T) {
var (
f = ConstraintFilter{}
nodes = testFixtures()
result []*cluster.Node
err error
)
// Check not (!) expression
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name!=node0"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 2)
// Check not does_not_exist. All should be found
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name!=does_not_exist"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 3)
// Check name must not start with n
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name!=n*"},
}, nodes)
assert.Error(t, err)
assert.Len(t, result, 0)
// Check not with globber pattern
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:region!=us*"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0].Labels["region"], "eu")
}
func TestConstraintRegExp(t *testing.T) {
var (
f = ConstraintFilter{}
nodes = testFixtures()
result []*cluster.Node
err error
)
// Check with regular expression /node\d/ matches node{0..2}
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:name==/node\d/`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 3)
// Check with regular expression /node\d/ matches node{0..2}
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:name==/node[12]/`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 2)
// Check with regular expression ! and regexp /node[12]/ matches node[0]
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:name!=/node[12]/`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0])
// Validate node pinning by ! and regexp.
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:node!=/node-[01]-id/"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[2])
}
func TestFilterRegExpCaseInsensitive(t *testing.T) {
var (
f = ConstraintFilter{}
nodes = testFixtures()
result []*cluster.Node
err error
)
// Prepare node with a strange name
node3 := cluster.NewNode("node-3", 0)
node3.ID = "node-3-id"
node3.Name = "node-3-name"
node3.Labels = map[string]string{
"name": "aBcDeF",
"group": "2",
"region": "eu",
}
nodes = append(nodes, node3)
// Case-sensitive, so not match
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:name==/abcdef/`},
}, nodes)
assert.Error(t, err)
assert.Len(t, result, 0)
// Match with case-insensitive
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:name==/(?i)abcdef/`},
}, nodes)
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(&dockerclient.ContainerConfig{
Env: []string{`constraint:name!=/(?i)abc*/`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 3)
}
func TestFilterWithRelativeComparisons(t *testing.T) {
t.Skip()
var (
f = ConstraintFilter{}
nodes = testFixtures()
result []*cluster.Node
err error
)
// Prepare node with a strange name
node3 := cluster.NewNode("node-3", 0)
node3.ID = "node-3-id"
node3.Name = "node-3-name"
node3.Labels = map[string]string{
"name": "aBcDeF",
"group": "4",
"kernel": "3.1",
"region": "eu",
}
nodes = append(nodes, node3)
// Check with less than or equal
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:group<=3`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 3)
// Check with greater than or equal
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:group>=4`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
// Another gte check with a complex string
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:kernel>=3.0`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[3])
assert.Equal(t, result[0].Labels["kernel"], "3.1")
// Check with greater than or equal. This should match node-3-id.
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{`constraint:node>=node-3`},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
}
func TestFilterEquals(t *testing.T) {
var (
f = ConstraintFilter{}
nodes = testFixtures()
result []*cluster.Node
err error
)
// Check == comparison
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name==node0"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
// Test == with glob
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:region==us*"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 2)
// Validate node name with ==
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:node==node-1-name"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[1])
}
func TestUnsupportedOperators(t *testing.T) {
var (
f = ConstraintFilter{}
nodes = testFixtures()
result []*cluster.Node
err error
)
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name=node0"},
}, nodes)
assert.Error(t, err)
assert.Len(t, result, 0)
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:name=!node0"},
}, nodes)
assert.Error(t, err)
assert.Len(t, result, 0)
}

106
scheduler/filter/expr.go Normal file
View File

@ -0,0 +1,106 @@
package filter
import (
"fmt"
"regexp"
"strings"
log "github.com/Sirupsen/logrus"
)
const (
EQ = iota
NOTEQ
)
var OPERATORS = []string{"==", "!="}
type expr struct {
key string
operator int
value string
}
func parseExprs(key string, env []string) ([]expr, error) {
exprs := []expr{}
for _, e := range env {
if strings.HasPrefix(e, key+":") {
entry := strings.TrimPrefix(e, key+":")
found := false
for i, op := range OPERATORS {
if strings.Contains(entry, op) {
// split with the op
parts := strings.SplitN(entry, op, 2)
// validate key
// allow alpha-numeric
matched, err := regexp.MatchString(`^(?i)[a-z_][a-z0-9\-_]+$`, parts[0])
if err != nil {
return nil, err
}
if matched == false {
return nil, fmt.Errorf("Key '%s' is invalid", parts[0])
}
if len(parts) == 2 {
// validate value
// allow leading = in case of using ==
// allow * for globbing
// allow regexp
matched, err := regexp.MatchString(`^(?i)[=!\/]?[a-z0-9:\-_\.\*/\(\)\?\+\[\]\\\^\$]+$`, parts[1])
if err != nil {
return nil, err
}
if matched == false {
return nil, fmt.Errorf("Value '%s' is invalid", parts[1])
}
exprs = append(exprs, expr{key: strings.ToLower(parts[0]), operator: i, value: parts[1]})
} else {
exprs = append(exprs, expr{key: strings.ToLower(parts[0]), operator: i})
}
found = true
break // found an op, move to next entry
}
}
if !found {
return nil, fmt.Errorf("One of operator ==, != is expected")
}
}
}
return exprs, nil
}
func (e *expr) Match(whats ...string) bool {
var (
pattern string
match bool
err error
)
if e.value[0] == '/' && e.value[len(e.value)-1] == '/' {
// regexp
pattern = e.value[1 : len(e.value)-1]
} else {
// simple match, create the regex for globbing (ex: ub*t* -> ^ub.*t.*$) and match.
pattern = "^" + strings.Replace(e.value, "*", ".*", -1) + "$"
}
for _, what := range whats {
if match, err = regexp.MatchString(pattern, what); match {
break
} else if err != nil {
log.Error(err)
}
}
switch e.operator {
case EQ:
return match
case NOTEQ:
return !match
}
return false
}

View File

@ -0,0 +1,59 @@
package filter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseExprs(t *testing.T) {
// Cannot use the leading digit for key
_, err := parseExprs("constraint", []string{"constraint:1node"})
assert.Error(t, err)
// Cannot use space in key
_, err = parseExprs("constraint", []string{"constraint:node ==node1"})
assert.Error(t, err)
// Cannot use dot in key
_, err = parseExprs("constraint", []string{"constraint:no.de==node1"})
assert.Error(t, err)
// Cannot use * in key
_, err = parseExprs("constraint", []string{"constraint:no*de==node1"})
assert.Error(t, err)
// Allow leading underscore
_, err = parseExprs("constraint", []string{"constraint:_node==_node1"})
assert.NoError(t, err)
// Allow globbing
_, err = parseExprs("constraint", []string{"constraint:node==*node*"})
assert.NoError(t, err)
// Allow regexp in value
_, err = parseExprs("constraint", []string{"constraint:node==/(?i)^[a-b]+c*$/"})
assert.NoError(t, err)
}
func TestMatch(t *testing.T) {
e := expr{operator: EQ, value: "foo"}
assert.True(t, e.Match("foo"))
assert.False(t, e.Match("bar"))
assert.True(t, e.Match("foo", "bar"))
e = expr{operator: NOTEQ, value: "foo"}
assert.False(t, e.Match("foo"))
assert.True(t, e.Match("bar"))
assert.False(t, e.Match("foo", "bar"))
e = expr{operator: EQ, value: "f*o"}
assert.True(t, e.Match("foo"))
assert.True(t, e.Match("fuo"))
assert.True(t, e.Match("foo", "fuo", "bar"))
e = expr{operator: NOTEQ, value: "f*o"}
assert.False(t, e.Match("foo"))
assert.False(t, e.Match("fuo"))
assert.False(t, e.Match("foo", "fuo", "bar"))
}

View File

@ -1,34 +0,0 @@
package filter
import (
"regexp"
"strings"
log "github.com/Sirupsen/logrus"
)
func extractEnv(key string, env []string) map[string]string {
values := make(map[string]string)
for _, e := range env {
if strings.HasPrefix(e, key+":") {
value := strings.TrimPrefix(e, key+":")
parts := strings.SplitN(value, "=", 2)
if len(parts) == 2 {
values[strings.ToLower(parts[0])] = strings.ToLower(parts[1])
} else {
values[strings.ToLower(parts[0])] = ""
}
}
}
return values
}
// Create the regex for globbing (ex: ub*t* -> ^ub.*t.*$) and match.
func match(pattern, s string) bool {
regex := "^" + strings.Replace(pattern, "*", ".*", -1) + "$"
matched, err := regexp.MatchString(regex, strings.ToLower(s))
if err != nil {
log.Error(err)
}
return matched
}