manager/constraint/constraint.go
package constraint
import (
"fmt"
"net"
"regexp"
"strings"
"github.com/moby/swarmkit/v2/api"
)
const (
eq = iota
noteq
// NodeLabelPrefix is the constraint key prefix for node labels.
NodeLabelPrefix = "node.labels."
// EngineLabelPrefix is the constraint key prefix for engine 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 {
return nil, fmt.Errorf("key '%s' is invalid", part0)
}
part1 := strings.TrimSpace(parts[1])
// validate Value
matched = valuePattern.MatchString(part1)
if !matched {
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.ip"):
nodeIP := net.ParseIP(n.Status.Addr)
// single IP address, node.ip == 2001:db8::2
if ip := net.ParseIP(constraint.exp); ip != nil {
ipEq := ip.Equal(nodeIP)
if (ipEq && constraint.operator != eq) || (!ipEq && constraint.operator == eq) {
return false
}
continue
}
// CIDR subnet, node.ip != 210.8.4.0/24
if _, subnet, err := net.ParseCIDR(constraint.exp); err == nil {
within := subnet.Contains(nodeIP)
if (within && constraint.operator != eq) || (!within && constraint.operator == eq) {
return false
}
continue
}
// reject constraint with malformed address/network
return false
case strings.EqualFold(constraint.key, "node.role"):
if !constraint.Match(n.Role.String()) {
return false
}
case strings.EqualFold(constraint.key, "node.platform.os"):
if n.Description == nil || n.Description.Platform == nil {
if !constraint.Match("") {
return false
}
continue
}
if !constraint.Match(n.Description.Platform.OS) {
return false
}
case strings.EqualFold(constraint.key, "node.platform.arch"):
if n.Description == nil || n.Description.Platform == nil {
if !constraint.Match("") {
return false
}
continue
}
if !constraint.Match(n.Description.Platform.Architecture) {
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
}