324 lines
7.6 KiB
Go
324 lines
7.6 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
)
|
|
|
|
// FeasibleIterator is used to iteratively yield nodes that
|
|
// match feasibility constraints. The iterators may manage
|
|
// some state for performance optimizations.
|
|
type FeasibleIterator interface {
|
|
// Next yields a feasible node or nil if exhausted
|
|
Next() *structs.Node
|
|
|
|
// Reset is invoked when an allocation has been placed
|
|
// to reset any stale state.
|
|
Reset()
|
|
}
|
|
|
|
// StaticIterator is a FeasibleIterator which returns nodes
|
|
// in a static order. This is used at the base of the iterator
|
|
// chain only for testing due to deterministic behavior.
|
|
type StaticIterator struct {
|
|
ctx Context
|
|
nodes []*structs.Node
|
|
offset int
|
|
seen int
|
|
}
|
|
|
|
// NewStaticIterator constructs a random iterator from a list of nodes
|
|
func NewStaticIterator(ctx Context, nodes []*structs.Node) *StaticIterator {
|
|
iter := &StaticIterator{
|
|
ctx: ctx,
|
|
nodes: nodes,
|
|
}
|
|
return iter
|
|
}
|
|
|
|
func (iter *StaticIterator) Next() *structs.Node {
|
|
// Check if exhausted
|
|
n := len(iter.nodes)
|
|
if iter.offset == n || iter.seen == n {
|
|
if iter.seen != n {
|
|
iter.offset = 0
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Return the next offset
|
|
offset := iter.offset
|
|
iter.offset += 1
|
|
iter.seen += 1
|
|
iter.ctx.Metrics().EvaluateNode()
|
|
return iter.nodes[offset]
|
|
}
|
|
|
|
func (iter *StaticIterator) Reset() {
|
|
iter.seen = 0
|
|
}
|
|
|
|
func (iter *StaticIterator) SetNodes(nodes []*structs.Node) {
|
|
iter.nodes = nodes
|
|
iter.offset = 0
|
|
iter.seen = 0
|
|
}
|
|
|
|
// NewRandomIterator constructs a static iterator from a list of nodes
|
|
// after applying the Fisher-Yates algorithm for a random shuffle. This
|
|
// is applied in-place
|
|
func NewRandomIterator(ctx Context, nodes []*structs.Node) *StaticIterator {
|
|
// shuffle with the Fisher-Yates algorithm
|
|
shuffleNodes(nodes)
|
|
|
|
// Create a static iterator
|
|
return NewStaticIterator(ctx, nodes)
|
|
}
|
|
|
|
// DriverIterator is a FeasibleIterator which returns nodes that
|
|
// have the drivers necessary to scheduler a task group.
|
|
type DriverIterator struct {
|
|
ctx Context
|
|
source FeasibleIterator
|
|
drivers map[string]struct{}
|
|
}
|
|
|
|
// NewDriverIterator creates a DriverIterator from a source and set of drivers
|
|
func NewDriverIterator(ctx Context, source FeasibleIterator, drivers map[string]struct{}) *DriverIterator {
|
|
iter := &DriverIterator{
|
|
ctx: ctx,
|
|
source: source,
|
|
drivers: drivers,
|
|
}
|
|
return iter
|
|
}
|
|
|
|
func (iter *DriverIterator) SetDrivers(d map[string]struct{}) {
|
|
iter.drivers = d
|
|
}
|
|
|
|
func (iter *DriverIterator) Next() *structs.Node {
|
|
for {
|
|
// Get the next option from the source
|
|
option := iter.source.Next()
|
|
if option == nil {
|
|
return nil
|
|
}
|
|
|
|
// Use this node if possible
|
|
if iter.hasDrivers(option) {
|
|
return option
|
|
}
|
|
iter.ctx.Metrics().FilterNode(option, "missing drivers")
|
|
}
|
|
}
|
|
|
|
func (iter *DriverIterator) Reset() {
|
|
iter.source.Reset()
|
|
}
|
|
|
|
// hasDrivers is used to check if the node has all the appropriate
|
|
// drivers for this task group. Drivers are registered as node attribute
|
|
// like "driver.docker=1" with their corresponding version.
|
|
func (iter *DriverIterator) hasDrivers(option *structs.Node) bool {
|
|
for driver := range iter.drivers {
|
|
driverStr := fmt.Sprintf("driver.%s", driver)
|
|
_, ok := option.Attributes[driverStr]
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ConstraintIterator is a FeasibleIterator which returns nodes
|
|
// that match a given set of constraints. This is used to filter
|
|
// on job, task group, and task constraints.
|
|
type ConstraintIterator struct {
|
|
ctx Context
|
|
source FeasibleIterator
|
|
constraints []*structs.Constraint
|
|
}
|
|
|
|
// NewConstraintIterator creates a ConstraintIterator from a source and set of constraints
|
|
func NewConstraintIterator(ctx Context, source FeasibleIterator, constraints []*structs.Constraint) *ConstraintIterator {
|
|
iter := &ConstraintIterator{
|
|
ctx: ctx,
|
|
source: source,
|
|
constraints: constraints,
|
|
}
|
|
return iter
|
|
}
|
|
|
|
func (iter *ConstraintIterator) SetConstraints(c []*structs.Constraint) {
|
|
iter.constraints = c
|
|
}
|
|
|
|
func (iter *ConstraintIterator) Next() *structs.Node {
|
|
for {
|
|
// Get the next option from the source
|
|
option := iter.source.Next()
|
|
if option == nil {
|
|
return nil
|
|
}
|
|
|
|
// Use this node if possible
|
|
if iter.meetsConstraints(option) {
|
|
return option
|
|
}
|
|
}
|
|
}
|
|
|
|
func (iter *ConstraintIterator) Reset() {
|
|
iter.source.Reset()
|
|
}
|
|
|
|
func (iter *ConstraintIterator) meetsConstraints(option *structs.Node) bool {
|
|
for _, constraint := range iter.constraints {
|
|
if !iter.meetsConstraint(constraint, option) {
|
|
iter.ctx.Metrics().FilterNode(option, constraint.String())
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (iter *ConstraintIterator) meetsConstraint(constraint *structs.Constraint, option *structs.Node) bool {
|
|
// Only enforce hard constraints, soft constraints are used for ranking
|
|
if !constraint.Hard {
|
|
return true
|
|
}
|
|
|
|
// Resolve the targets
|
|
lVal, ok := resolveConstraintTarget(constraint.LTarget, option)
|
|
if !ok {
|
|
return false
|
|
}
|
|
rVal, ok := resolveConstraintTarget(constraint.RTarget, option)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Check if satisfied
|
|
return checkConstraint(constraint.Operand, lVal, rVal)
|
|
}
|
|
|
|
// resolveConstraintTarget is used to resolve the LTarget and RTarget of a Constraint
|
|
func resolveConstraintTarget(target string, node *structs.Node) (interface{}, bool) {
|
|
// If no prefix, this must be a literal value
|
|
if !strings.HasPrefix(target, "$") {
|
|
return target, true
|
|
}
|
|
|
|
// Handle the interpolations
|
|
switch {
|
|
case "$node.id" == target:
|
|
return node.ID, true
|
|
|
|
case "$node.datacenter" == target:
|
|
return node.Datacenter, true
|
|
|
|
case "$node.name" == target:
|
|
return node.Name, true
|
|
|
|
case strings.HasPrefix(target, "$attr."):
|
|
attr := strings.TrimPrefix(target, "$attr.")
|
|
val, ok := node.Attributes[attr]
|
|
return val, ok
|
|
|
|
case strings.HasPrefix(target, "$meta."):
|
|
meta := strings.TrimPrefix(target, "$meta.")
|
|
val, ok := node.Meta[meta]
|
|
return val, ok
|
|
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
// checkConstraint checks if a constraint is satisfied
|
|
func checkConstraint(operand string, lVal, rVal interface{}) bool {
|
|
switch operand {
|
|
case "=", "==", "is":
|
|
return reflect.DeepEqual(lVal, rVal)
|
|
case "!=", "not":
|
|
return !reflect.DeepEqual(lVal, rVal)
|
|
case "<", "<=", ">", ">=":
|
|
// TODO: Implement
|
|
return false
|
|
case "version":
|
|
return checkVersionConstraint(lVal, rVal)
|
|
case "regexp":
|
|
return checkRegexpConstraint(lVal, rVal)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// checkVersionConstraint is used to compare a version on the
|
|
// left hand side with a set of constraints on the right hand side
|
|
func checkVersionConstraint(lVal, rVal interface{}) bool {
|
|
// Parse the version
|
|
var versionStr string
|
|
switch v := lVal.(type) {
|
|
case string:
|
|
versionStr = v
|
|
case int:
|
|
versionStr = fmt.Sprintf("%d", v)
|
|
default:
|
|
return false
|
|
}
|
|
|
|
// Parse the verison
|
|
vers, err := version.NewVersion(versionStr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Constraint must be a string
|
|
constraintStr, ok := rVal.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Parse the constraints
|
|
constraints, err := version.NewConstraint(constraintStr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Check the constraints against the version
|
|
return constraints.Check(vers)
|
|
}
|
|
|
|
// checkRegexpConstraint is used to compare a value on the
|
|
// left hand side with a regexp on the right hand side
|
|
func checkRegexpConstraint(lVal, rVal interface{}) bool {
|
|
// Ensure left-hand is string
|
|
lStr, ok := lVal.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Regexp must be a string
|
|
regexpStr, ok := rVal.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Parse the regexp
|
|
re, err := regexp.Compile(regexpStr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Look for a match
|
|
return re.MatchString(lStr)
|
|
}
|