Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only create global service tasks on nodes that satisfy the constraints #1570

Merged
merged 3 commits into from
Oct 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions manager/constraint/constraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package constraint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Shouldn't this be a subpackage of scheduler?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that make sense, since now orchestrator uses it as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a moot point. To me it felt like it was part of the scheduler (which at the end of the day is the component responsible to place tasks onto nodes), and it turns out another component (global orchestrator) wants to use a piece of the scheduler. I don't think the scheduler and orchestrator are sharing a common piece, more like the orchestrator wants to use the scheduler for some stuff.

But it's a minor personal preference, no objective arguments


import (
"fmt"
"regexp"
"strings"

"github.com/docker/swarmkit/api"
)

const (
eq = iota
noteq

nodeLabelPrefix = "node.labels."
engineLabelPrefix = "engine.labels."
)

var (
alphaNumeric = regexp.MustCompile(`^(?i)[a-z_][a-z0-9\-_.]+$`)
// value can be alphanumeric and some special characters. it shouldn't container
// current or future operators like '>, <, ~', etc.
valuePattern = regexp.MustCompile(`^(?i)[a-z0-9:\-_\s\.\*\(\)\?\+\[\]\\\^\$\|\/]+$`)

// operators defines list of accepted operators
operators = []string{"==", "!="}
)

// Constraint defines a constraint.
type Constraint struct {
key string
operator int
exp string
}

// Parse parses list of constraints.
func Parse(env []string) ([]Constraint, error) {
exprs := []Constraint{}
for _, e := range env {
found := false
// each expr is in the form of "key op value"
for i, op := range operators {
if !strings.Contains(e, op) {
continue
}
// split with the op
parts := strings.SplitN(e, op, 2)

if len(parts) < 2 {
return nil, fmt.Errorf("invalid expr: %s", e)
}

part0 := strings.TrimSpace(parts[0])
// validate key
matched := alphaNumeric.MatchString(part0)
if matched == false {
return nil, fmt.Errorf("key '%s' is invalid", part0)
}

part1 := strings.TrimSpace(parts[1])

// validate Value
matched = valuePattern.MatchString(part1)
if matched == false {
return nil, fmt.Errorf("value '%s' is invalid", part1)
}
// TODO(dongluochen): revisit requirements to see if globing or regex are useful
exprs = append(exprs, Constraint{key: part0, operator: i, exp: part1})

found = true
break // found an op, move to next entry
}
if !found {
return nil, fmt.Errorf("constraint expected one operator from %s", strings.Join(operators, ", "))
}
}
return exprs, nil
}

// Match checks if the Constraint matches the target strings.
func (c *Constraint) Match(whats ...string) bool {
var match bool

// full string match
for _, what := range whats {
// case insensitive compare
if strings.EqualFold(c.exp, what) {
match = true
break
}
}

switch c.operator {
case eq:
return match
case noteq:
return !match
}

return false
}

// NodeMatches returns true if the node satisfies the given constraints.
func NodeMatches(constraints []Constraint, n *api.Node) bool {
for _, constraint := range constraints {
switch {
case strings.EqualFold(constraint.key, "node.id"):
if !constraint.Match(n.ID) {
return false
}
case strings.EqualFold(constraint.key, "node.hostname"):
// if this node doesn't have hostname
// it's equivalent to match an empty hostname
// where '==' would fail, '!=' matches
if n.Description == nil {
if !constraint.Match("") {
return false
}
continue
}
if !constraint.Match(n.Description.Hostname) {
return false
}
case strings.EqualFold(constraint.key, "node.role"):
if !constraint.Match(n.Spec.Role.String()) {
return false
}

// node labels constraint in form like 'node.labels.key==value'
case len(constraint.key) > len(nodeLabelPrefix) && strings.EqualFold(constraint.key[:len(nodeLabelPrefix)], nodeLabelPrefix):
if n.Spec.Annotations.Labels == nil {
if !constraint.Match("") {
return false
}
continue
}
label := constraint.key[len(nodeLabelPrefix):]
// label itself is case sensitive
val := n.Spec.Annotations.Labels[label]
if !constraint.Match(val) {
return false
}

// engine labels constraint in form like 'engine.labels.key!=value'
case len(constraint.key) > len(engineLabelPrefix) && strings.EqualFold(constraint.key[:len(engineLabelPrefix)], engineLabelPrefix):
if n.Description == nil || n.Description.Engine == nil || n.Description.Engine.Labels == nil {
if !constraint.Match("") {
return false
}
continue
}
label := constraint.key[len(engineLabelPrefix):]
val := n.Description.Engine.Labels[label]
if !constraint.Match(val) {
return false
}
default:
// key doesn't match predefined syntax
return false
}
}

return true
}
Original file line number Diff line number Diff line change
@@ -1,104 +1,104 @@
package scheduler
package constraint

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseExprs(t *testing.T) {
func TestParse(t *testing.T) {
// empty string
_, err := ParseExprs([]string{""})
_, err := Parse([]string{""})
assert.Error(t, err)

_, err = ParseExprs([]string{" "})
_, err = Parse([]string{" "})
assert.Error(t, err)

// no operator
_, err = ParseExprs([]string{"nodeabc"})
_, err = Parse([]string{"nodeabc"})
assert.Error(t, err)

// incorrect operator
_, err = ParseExprs([]string{"node ~ abc"})
_, err = Parse([]string{"node ~ abc"})
assert.Error(t, err)

// Cannot use the leading digit for Key
_, err = ParseExprs([]string{"1node==a2"})
// Cannot use the leading digit for key
_, err = Parse([]string{"1node==a2"})
assert.Error(t, err)

// leading and trailing white space are ignored
_, err = ParseExprs([]string{" node == node1"})
_, err = Parse([]string{" node == node1"})
assert.NoError(t, err)

// Key cannot container white space in the middle
_, err = ParseExprs([]string{"no de== node1"})
// key cannot container white space in the middle
_, err = Parse([]string{"no de== node1"})
assert.Error(t, err)

// Cannot use * in Key
_, err = ParseExprs([]string{"no*de==node1"})
// Cannot use * in key
_, err = Parse([]string{"no*de==node1"})
assert.Error(t, err)

// Key cannot be empty
_, err = ParseExprs([]string{"==node1"})
// key cannot be empty
_, err = Parse([]string{"==node1"})
assert.Error(t, err)

// value cannot be empty
_, err = ParseExprs([]string{"node=="})
_, err = Parse([]string{"node=="})
assert.Error(t, err)

// value cannot be an empty space
_, err = ParseExprs([]string{"node== "})
_, err = Parse([]string{"node== "})
assert.Error(t, err)

// Cannot use $ in Key
_, err = ParseExprs([]string{"no$de==node1"})
// Cannot use $ in key
_, err = Parse([]string{"no$de==node1"})
assert.Error(t, err)

// Allow CAPS in Key
exprs, err := ParseExprs([]string{"NoDe==node1"})
// Allow CAPS in key
exprs, err := Parse([]string{"NoDe==node1"})
assert.NoError(t, err)
assert.Equal(t, exprs[0].Key, "NoDe")
assert.Equal(t, exprs[0].key, "NoDe")

// Allow dot in Key
exprs, err = ParseExprs([]string{"no.de==node1"})
// Allow dot in key
exprs, err = Parse([]string{"no.de==node1"})
assert.NoError(t, err)
assert.Equal(t, exprs[0].Key, "no.de")
assert.Equal(t, exprs[0].key, "no.de")

// Allow leading underscore
exprs, err = ParseExprs([]string{"_node==_node1"})
exprs, err = Parse([]string{"_node==_node1"})
assert.NoError(t, err)
assert.Equal(t, exprs[0].Key, "_node")
assert.Equal(t, exprs[0].key, "_node")

// Allow special characters in exp
exprs, err = ParseExprs([]string{"node==[a-b]+c*(n|b)/"})
exprs, err = Parse([]string{"node==[a-b]+c*(n|b)/"})
assert.NoError(t, err)
assert.Equal(t, exprs[0].Key, "node")
assert.Equal(t, exprs[0].key, "node")
assert.Equal(t, exprs[0].exp, "[a-b]+c*(n|b)/")

// Allow space in Exp
exprs, err = ParseExprs([]string{"node==node 1"})
exprs, err = Parse([]string{"node==node 1"})
assert.NoError(t, err)
assert.Equal(t, exprs[0].Key, "node")
assert.Equal(t, exprs[0].key, "node")
assert.Equal(t, exprs[0].exp, "node 1")
}

func TestMatch(t *testing.T) {
exprs, err := ParseExprs([]string{"node.name==foo"})
exprs, err := Parse([]string{"node.name==foo"})
assert.NoError(t, err)
e := exprs[0]
assert.True(t, e.Match("foo"))
assert.False(t, e.Match("fo"))
assert.False(t, e.Match("fooE"))

exprs, err = ParseExprs([]string{"node.name!=foo"})
exprs, err = Parse([]string{"node.name!=foo"})
assert.NoError(t, err)
e = exprs[0]
assert.False(t, e.Match("foo"))
assert.True(t, e.Match("bar"))
assert.True(t, e.Match("fo"))
assert.True(t, e.Match("fooExtra"))

exprs, err = ParseExprs([]string{"node.name==f*o"})
exprs, err = Parse([]string{"node.name==f*o"})
assert.NoError(t, err)
e = exprs[0]
assert.False(t, e.Match("fo"))
Expand All @@ -109,7 +109,7 @@ func TestMatch(t *testing.T) {
assert.False(t, e.Match("foo"))

// test special characters
exprs, err = ParseExprs([]string{"node.name==f.-$o"})
exprs, err = Parse([]string{"node.name==f.-$o"})
assert.NoError(t, err)
e = exprs[0]
assert.False(t, e.Match("fa-$o"))
Expand Down
4 changes: 2 additions & 2 deletions manager/controlapi/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/docker/distribution/reference"
"github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/identity"
"github.com/docker/swarmkit/manager/scheduler"
"github.com/docker/swarmkit/manager/constraint"
"github.com/docker/swarmkit/manager/state/store"
"github.com/docker/swarmkit/protobuf/ptypes"
"golang.org/x/net/context"
Expand Down Expand Up @@ -81,7 +81,7 @@ func validatePlacement(placement *api.Placement) error {
if placement == nil {
return nil
}
_, err := scheduler.ParseExprs(placement.Constraints)
_, err := constraint.Parse(placement.Constraints)
return err
}

Expand Down
Loading