Skip to content

Commit

Permalink
refactor and remove <= and >=
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Vieux <[email protected]>
  • Loading branch information
vieux committed Jan 21, 2015
1 parent bf852ec commit e546965
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 174 deletions.
8 changes: 4 additions & 4 deletions contrib/demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ docker run -d -p 80:80 nginx
# clean up cluster
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 run -d -e constraint:storagedriver=devicemapper redis
docker run -d -e constraint:storagedriver==devicemapper redis
docker ps

docker run -d -e constraint:storagedriver=aufs redis
docker run -d -e constraint:storagedriver==aufs redis
docker ps

docker run -d -e constraint:node=fedora-1 redis
docker run -d -e constraint:node==fedora-1 redis
docker ps

# clean up cluster
Expand Down
5 changes: 1 addition & 4 deletions scheduler/filter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ A `value` must be one of the following:
* 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 `<=`.
Relative comparisons, `>=` and `<=` are supported, but limited to `string` comparison only.
Current `swarm` supports affinity/constraint operators as the following: `==` and `!=`.

For example,
* `constraint:name==node1` will match nodes named with `node1`.
Expand All @@ -169,8 +168,6 @@ For example,
* `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.
* `constraint:kernel>=3.0` will match all nodes with label `kernel` greater than or equal to "3.0". This is the string, not numeric, comparison.
* `constraint:group<=3` will match all nodes with `group` less than or equal to "3". This is also the string, not numeric, comparison.

## Port Filter

Expand Down
22 changes: 8 additions & 14 deletions scheduler/filter/affinity.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,33 @@ type AffinityFilter struct {
}

func (f *AffinityFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) {
affinities, err := extractEnv("affinity", config.Env)
affinities, err := parseExprs("affinity", config.Env)
if err != nil {
return nil, err
}

for k, v := range affinities {
log.Debugf("matching affinity: %s%s%s", k, v[0], v[1])
for _, affinity := range affinities {
log.Debugf("matching affinity: %s%s%s", affinity.key, OPERATORS[affinity.operator], affinity.value)

candidates := []*cluster.Node{}
for _, node := range nodes {
switch k {
switch affinity.key {
case "container":
for _, container := range node.Containers() {
matchResult := false
if v[0] != "!=" {
matchResult = match(v, container.Id) || match(v, container.Names[0])
} else if v[0] == "!=" {
matchResult = match(v, container.Id) && match(v, container.Names[0])
}
if matchResult {
if affinity.Match(container.Id, container.Names[0]) {
candidates = append(candidates, node)
break
}
}
case "image":
done:
for _, image := range node.Images() {
if match(v, image.Id) {
if affinity.Match(image.Id) {
candidates = append(candidates, node)
break
}
for _, tag := range image.RepoTags {
if match(v, tag) {
if affinity.Match(tag) {
candidates = append(candidates, node)
break done
}
Expand All @@ -54,7 +48,7 @@ func (f *AffinityFilter) Filter(config *dockerclient.ContainerConfig, nodes []*c
}
}
if len(candidates) == 0 {
return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", k, v[0], v[1])
return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", affinity.key, OPERATORS[affinity.operator], affinity.value)
}
nodes = candidates
}
Expand Down
22 changes: 8 additions & 14 deletions scheduler/filter/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,32 @@ type ConstraintFilter struct {
}

func (f *ConstraintFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) {
constraints, err := extractEnv("constraint", config.Env)
constraints, err := parseExprs("constraint", config.Env)
if err != nil {
return nil, err
}

for k, v := range constraints {
log.Debugf("matching constraint: %s %s %s", k, v[0], v[1])
for _, constraint := range constraints {
log.Debugf("matching constraint: %s %s %s", constraint.key, OPERATORS[constraint.operator], constraint.value)

candidates := []*cluster.Node{}
for _, node := range nodes {
switch k {
switch constraint.key {
case "node":
// "node" label is a special case pinning a container to a specific node.
matchResult := false
if v[0] != "!=" {
matchResult = match(v, node.ID) || match(v, node.Name)
} else if v[0] == "!=" {
matchResult = match(v, node.ID) && match(v, node.Name)
}
if matchResult {
if constraint.Match(node.ID, node.Name) {
candidates = append(candidates, node)
}
default:
if label, ok := node.Labels[k]; ok {
if match(v, label) {
if label, ok := node.Labels[constraint.key]; ok {
if constraint.Match(label) {
candidates = append(candidates, node)
}
}
}
}
if len(candidates) == 0 {
return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", k, v[0], v[1])
return nil, fmt.Errorf("unable to find a node that satisfies %s%s%s", constraint.key, OPERATORS[constraint.operator], constraint.value)
}
nodes = candidates
}
Expand Down
1 change: 1 addition & 0 deletions scheduler/filter/constraint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ func TestFilterRegExpCaseInsensitive(t *testing.T) {
}

func TestFilterWithRelativeComparisons(t *testing.T) {
t.Skip()
var (
f = ConstraintFilter{}
nodes = testFixtures()
Expand Down
106 changes: 106 additions & 0 deletions scheduler/filter/expr.go
Original file line number Diff line number Diff line change
@@ -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
}
59 changes: 59 additions & 0 deletions scheduler/filter/expr_test.go
Original file line number Diff line number Diff line change
@@ -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"))
}
Loading

0 comments on commit e546965

Please sign in to comment.