| package constraint |
| |
| import ( |
| "fmt" |
| "net" |
| "regexp" |
| "strings" |
| |
| "github.com/docker/swarmkit/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 |
| } |