mirror of https://github.com/docker/docs.git
Merge pull request #261 from vieux/chanwit-constraints-expr
Proposal: constraint & affinity filter expression enhancement
This commit is contained in:
commit
10074faa75
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue