docker/swarmkit

View on GitHub
manager/constraint/constraint.go

Summary

Maintainability
D
1 day
Test Coverage
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
}