github.com/bigcommerce/nomad@v0.9.3-bc/scheduler/feasible.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  
    10  	version "github.com/hashicorp/go-version"
    11  	"github.com/hashicorp/nomad/nomad/structs"
    12  	psstructs "github.com/hashicorp/nomad/plugins/shared/structs"
    13  )
    14  
    15  // FeasibleIterator is used to iteratively yield nodes that
    16  // match feasibility constraints. The iterators may manage
    17  // some state for performance optimizations.
    18  type FeasibleIterator interface {
    19  	// Next yields a feasible node or nil if exhausted
    20  	Next() *structs.Node
    21  
    22  	// Reset is invoked when an allocation has been placed
    23  	// to reset any stale state.
    24  	Reset()
    25  }
    26  
    27  // JobContextualIterator is an iterator that can have the job and task group set
    28  // on it.
    29  type ContextualIterator interface {
    30  	SetJob(*structs.Job)
    31  	SetTaskGroup(*structs.TaskGroup)
    32  }
    33  
    34  // FeasibilityChecker is used to check if a single node meets feasibility
    35  // constraints.
    36  type FeasibilityChecker interface {
    37  	Feasible(*structs.Node) bool
    38  }
    39  
    40  // StaticIterator is a FeasibleIterator which returns nodes
    41  // in a static order. This is used at the base of the iterator
    42  // chain only for testing due to deterministic behavior.
    43  type StaticIterator struct {
    44  	ctx    Context
    45  	nodes  []*structs.Node
    46  	offset int
    47  	seen   int
    48  }
    49  
    50  // NewStaticIterator constructs a random iterator from a list of nodes
    51  func NewStaticIterator(ctx Context, nodes []*structs.Node) *StaticIterator {
    52  	iter := &StaticIterator{
    53  		ctx:   ctx,
    54  		nodes: nodes,
    55  	}
    56  	return iter
    57  }
    58  
    59  func (iter *StaticIterator) Next() *structs.Node {
    60  	// Check if exhausted
    61  	n := len(iter.nodes)
    62  	if iter.offset == n || iter.seen == n {
    63  		if iter.seen != n {
    64  			iter.offset = 0
    65  		} else {
    66  			return nil
    67  		}
    68  	}
    69  
    70  	// Return the next offset
    71  	offset := iter.offset
    72  	iter.offset += 1
    73  	iter.seen += 1
    74  	iter.ctx.Metrics().EvaluateNode()
    75  	return iter.nodes[offset]
    76  }
    77  
    78  func (iter *StaticIterator) Reset() {
    79  	iter.seen = 0
    80  }
    81  
    82  func (iter *StaticIterator) SetNodes(nodes []*structs.Node) {
    83  	iter.nodes = nodes
    84  	iter.offset = 0
    85  	iter.seen = 0
    86  }
    87  
    88  // NewRandomIterator constructs a static iterator from a list of nodes
    89  // after applying the Fisher-Yates algorithm for a random shuffle. This
    90  // is applied in-place
    91  func NewRandomIterator(ctx Context, nodes []*structs.Node) *StaticIterator {
    92  	// shuffle with the Fisher-Yates algorithm
    93  	shuffleNodes(nodes)
    94  
    95  	// Create a static iterator
    96  	return NewStaticIterator(ctx, nodes)
    97  }
    98  
    99  // DriverChecker is a FeasibilityChecker which returns whether a node has the
   100  // drivers necessary to scheduler a task group.
   101  type DriverChecker struct {
   102  	ctx     Context
   103  	drivers map[string]struct{}
   104  }
   105  
   106  // NewDriverChecker creates a DriverChecker from a set of drivers
   107  func NewDriverChecker(ctx Context, drivers map[string]struct{}) *DriverChecker {
   108  	return &DriverChecker{
   109  		ctx:     ctx,
   110  		drivers: drivers,
   111  	}
   112  }
   113  
   114  func (c *DriverChecker) SetDrivers(d map[string]struct{}) {
   115  	c.drivers = d
   116  }
   117  
   118  func (c *DriverChecker) Feasible(option *structs.Node) bool {
   119  	// Use this node if possible
   120  	if c.hasDrivers(option) {
   121  		return true
   122  	}
   123  	c.ctx.Metrics().FilterNode(option, "missing drivers")
   124  	return false
   125  }
   126  
   127  // hasDrivers is used to check if the node has all the appropriate
   128  // drivers for this task group. Drivers are registered as node attribute
   129  // like "driver.docker=1" with their corresponding version.
   130  func (c *DriverChecker) hasDrivers(option *structs.Node) bool {
   131  	for driver := range c.drivers {
   132  		driverStr := fmt.Sprintf("driver.%s", driver)
   133  
   134  		// COMPAT: Remove in 0.10: As of Nomad 0.8, nodes have a DriverInfo that
   135  		// corresponds with every driver. As a Nomad server might be on a later
   136  		// version than a Nomad client, we need to check for compatibility here
   137  		// to verify the client supports this.
   138  		if driverInfo, ok := option.Drivers[driver]; ok {
   139  			if driverInfo == nil {
   140  				c.ctx.Logger().Named("driver_checker").Warn("node has no driver info set", "node_id", option.ID, "driver", driver)
   141  				return false
   142  			}
   143  
   144  			return driverInfo.Detected && driverInfo.Healthy
   145  		}
   146  
   147  		value, ok := option.Attributes[driverStr]
   148  		if !ok {
   149  			return false
   150  		}
   151  
   152  		enabled, err := strconv.ParseBool(value)
   153  		if err != nil {
   154  			c.ctx.Logger().Named("driver_checker").Warn("node has invalid driver setting", "node_id", option.ID, "driver", driver, "val", value)
   155  			return false
   156  		}
   157  
   158  		if !enabled {
   159  			return false
   160  		}
   161  	}
   162  	return true
   163  }
   164  
   165  // DistinctHostsIterator is a FeasibleIterator which returns nodes that pass the
   166  // distinct_hosts constraint. The constraint ensures that multiple allocations
   167  // do not exist on the same node.
   168  type DistinctHostsIterator struct {
   169  	ctx    Context
   170  	source FeasibleIterator
   171  	tg     *structs.TaskGroup
   172  	job    *structs.Job
   173  
   174  	// Store whether the Job or TaskGroup has a distinct_hosts constraints so
   175  	// they don't have to be calculated every time Next() is called.
   176  	tgDistinctHosts  bool
   177  	jobDistinctHosts bool
   178  }
   179  
   180  // NewDistinctHostsIterator creates a DistinctHostsIterator from a source.
   181  func NewDistinctHostsIterator(ctx Context, source FeasibleIterator) *DistinctHostsIterator {
   182  	return &DistinctHostsIterator{
   183  		ctx:    ctx,
   184  		source: source,
   185  	}
   186  }
   187  
   188  func (iter *DistinctHostsIterator) SetTaskGroup(tg *structs.TaskGroup) {
   189  	iter.tg = tg
   190  	iter.tgDistinctHosts = iter.hasDistinctHostsConstraint(tg.Constraints)
   191  }
   192  
   193  func (iter *DistinctHostsIterator) SetJob(job *structs.Job) {
   194  	iter.job = job
   195  	iter.jobDistinctHosts = iter.hasDistinctHostsConstraint(job.Constraints)
   196  }
   197  
   198  func (iter *DistinctHostsIterator) hasDistinctHostsConstraint(constraints []*structs.Constraint) bool {
   199  	for _, con := range constraints {
   200  		if con.Operand == structs.ConstraintDistinctHosts {
   201  			return true
   202  		}
   203  	}
   204  
   205  	return false
   206  }
   207  
   208  func (iter *DistinctHostsIterator) Next() *structs.Node {
   209  	for {
   210  		// Get the next option from the source
   211  		option := iter.source.Next()
   212  
   213  		// Hot-path if the option is nil or there are no distinct_hosts or
   214  		// distinct_property constraints.
   215  		hosts := iter.jobDistinctHosts || iter.tgDistinctHosts
   216  		if option == nil || !hosts {
   217  			return option
   218  		}
   219  
   220  		// Check if the host constraints are satisfied
   221  		if !iter.satisfiesDistinctHosts(option) {
   222  			iter.ctx.Metrics().FilterNode(option, structs.ConstraintDistinctHosts)
   223  			continue
   224  		}
   225  
   226  		return option
   227  	}
   228  }
   229  
   230  // satisfiesDistinctHosts checks if the node satisfies a distinct_hosts
   231  // constraint either specified at the job level or the TaskGroup level.
   232  func (iter *DistinctHostsIterator) satisfiesDistinctHosts(option *structs.Node) bool {
   233  	// Check if there is no constraint set.
   234  	if !(iter.jobDistinctHosts || iter.tgDistinctHosts) {
   235  		return true
   236  	}
   237  
   238  	// Get the proposed allocations
   239  	proposed, err := iter.ctx.ProposedAllocs(option.ID)
   240  	if err != nil {
   241  		iter.ctx.Logger().Named("distinct_hosts").Error("failed to get proposed allocations", "error", err)
   242  		return false
   243  	}
   244  
   245  	// Skip the node if the task group has already been allocated on it.
   246  	for _, alloc := range proposed {
   247  		// If the job has a distinct_hosts constraint we only need an alloc
   248  		// collision on the JobID but if the constraint is on the TaskGroup then
   249  		// we need both a job and TaskGroup collision.
   250  		jobCollision := alloc.JobID == iter.job.ID
   251  		taskCollision := alloc.TaskGroup == iter.tg.Name
   252  		if iter.jobDistinctHosts && jobCollision || jobCollision && taskCollision {
   253  			return false
   254  		}
   255  	}
   256  
   257  	return true
   258  }
   259  
   260  func (iter *DistinctHostsIterator) Reset() {
   261  	iter.source.Reset()
   262  }
   263  
   264  // DistinctPropertyIterator is a FeasibleIterator which returns nodes that pass the
   265  // distinct_property constraint. The constraint ensures that multiple allocations
   266  // do not use the same value of the given property.
   267  type DistinctPropertyIterator struct {
   268  	ctx    Context
   269  	source FeasibleIterator
   270  	tg     *structs.TaskGroup
   271  	job    *structs.Job
   272  
   273  	hasDistinctPropertyConstraints bool
   274  	jobPropertySets                []*propertySet
   275  	groupPropertySets              map[string][]*propertySet
   276  }
   277  
   278  // NewDistinctPropertyIterator creates a DistinctPropertyIterator from a source.
   279  func NewDistinctPropertyIterator(ctx Context, source FeasibleIterator) *DistinctPropertyIterator {
   280  	return &DistinctPropertyIterator{
   281  		ctx:               ctx,
   282  		source:            source,
   283  		groupPropertySets: make(map[string][]*propertySet),
   284  	}
   285  }
   286  
   287  func (iter *DistinctPropertyIterator) SetTaskGroup(tg *structs.TaskGroup) {
   288  	iter.tg = tg
   289  
   290  	// Build the property set at the taskgroup level
   291  	if _, ok := iter.groupPropertySets[tg.Name]; !ok {
   292  		for _, c := range tg.Constraints {
   293  			if c.Operand != structs.ConstraintDistinctProperty {
   294  				continue
   295  			}
   296  
   297  			pset := NewPropertySet(iter.ctx, iter.job)
   298  			pset.SetTGConstraint(c, tg.Name)
   299  			iter.groupPropertySets[tg.Name] = append(iter.groupPropertySets[tg.Name], pset)
   300  		}
   301  	}
   302  
   303  	// Check if there is a distinct property
   304  	iter.hasDistinctPropertyConstraints = len(iter.jobPropertySets) != 0 || len(iter.groupPropertySets[tg.Name]) != 0
   305  }
   306  
   307  func (iter *DistinctPropertyIterator) SetJob(job *structs.Job) {
   308  	iter.job = job
   309  
   310  	// Build the property set at the job level
   311  	for _, c := range job.Constraints {
   312  		if c.Operand != structs.ConstraintDistinctProperty {
   313  			continue
   314  		}
   315  
   316  		pset := NewPropertySet(iter.ctx, job)
   317  		pset.SetJobConstraint(c)
   318  		iter.jobPropertySets = append(iter.jobPropertySets, pset)
   319  	}
   320  }
   321  
   322  func (iter *DistinctPropertyIterator) Next() *structs.Node {
   323  	for {
   324  		// Get the next option from the source
   325  		option := iter.source.Next()
   326  
   327  		// Hot path if there is nothing to check
   328  		if option == nil || !iter.hasDistinctPropertyConstraints {
   329  			return option
   330  		}
   331  
   332  		// Check if the constraints are met
   333  		if !iter.satisfiesProperties(option, iter.jobPropertySets) ||
   334  			!iter.satisfiesProperties(option, iter.groupPropertySets[iter.tg.Name]) {
   335  			continue
   336  		}
   337  
   338  		return option
   339  	}
   340  }
   341  
   342  // satisfiesProperties returns whether the option satisfies the set of
   343  // properties. If not it will be filtered.
   344  func (iter *DistinctPropertyIterator) satisfiesProperties(option *structs.Node, set []*propertySet) bool {
   345  	for _, ps := range set {
   346  		if satisfies, reason := ps.SatisfiesDistinctProperties(option, iter.tg.Name); !satisfies {
   347  			iter.ctx.Metrics().FilterNode(option, reason)
   348  			return false
   349  		}
   350  	}
   351  
   352  	return true
   353  }
   354  
   355  func (iter *DistinctPropertyIterator) Reset() {
   356  	iter.source.Reset()
   357  
   358  	for _, ps := range iter.jobPropertySets {
   359  		ps.PopulateProposed()
   360  	}
   361  
   362  	for _, sets := range iter.groupPropertySets {
   363  		for _, ps := range sets {
   364  			ps.PopulateProposed()
   365  		}
   366  	}
   367  }
   368  
   369  // ConstraintChecker is a FeasibilityChecker which returns nodes that match a
   370  // given set of constraints. This is used to filter on job, task group, and task
   371  // constraints.
   372  type ConstraintChecker struct {
   373  	ctx         Context
   374  	constraints []*structs.Constraint
   375  }
   376  
   377  // NewConstraintChecker creates a ConstraintChecker for a set of constraints
   378  func NewConstraintChecker(ctx Context, constraints []*structs.Constraint) *ConstraintChecker {
   379  	return &ConstraintChecker{
   380  		ctx:         ctx,
   381  		constraints: constraints,
   382  	}
   383  }
   384  
   385  func (c *ConstraintChecker) SetConstraints(constraints []*structs.Constraint) {
   386  	c.constraints = constraints
   387  }
   388  
   389  func (c *ConstraintChecker) Feasible(option *structs.Node) bool {
   390  	// Use this node if possible
   391  	for _, constraint := range c.constraints {
   392  		if !c.meetsConstraint(constraint, option) {
   393  			c.ctx.Metrics().FilterNode(option, constraint.String())
   394  			return false
   395  		}
   396  	}
   397  	return true
   398  }
   399  
   400  func (c *ConstraintChecker) meetsConstraint(constraint *structs.Constraint, option *structs.Node) bool {
   401  	// Resolve the targets. Targets that are not present are treated as `nil`.
   402  	// This is to allow for matching constraints where a target is not present.
   403  	lVal, lOk := resolveTarget(constraint.LTarget, option)
   404  	rVal, rOk := resolveTarget(constraint.RTarget, option)
   405  
   406  	// Check if satisfied
   407  	return checkConstraint(c.ctx, constraint.Operand, lVal, rVal, lOk, rOk)
   408  }
   409  
   410  // resolveTarget is used to resolve the LTarget and RTarget of a Constraint.
   411  func resolveTarget(target string, node *structs.Node) (interface{}, bool) {
   412  	// If no prefix, this must be a literal value
   413  	if !strings.HasPrefix(target, "${") {
   414  		return target, true
   415  	}
   416  
   417  	// Handle the interpolations
   418  	switch {
   419  	case "${node.unique.id}" == target:
   420  		return node.ID, true
   421  
   422  	case "${node.datacenter}" == target:
   423  		return node.Datacenter, true
   424  
   425  	case "${node.unique.name}" == target:
   426  		return node.Name, true
   427  
   428  	case "${node.class}" == target:
   429  		return node.NodeClass, true
   430  
   431  	case strings.HasPrefix(target, "${attr."):
   432  		attr := strings.TrimSuffix(strings.TrimPrefix(target, "${attr."), "}")
   433  		val, ok := node.Attributes[attr]
   434  		return val, ok
   435  
   436  	case strings.HasPrefix(target, "${meta."):
   437  		meta := strings.TrimSuffix(strings.TrimPrefix(target, "${meta."), "}")
   438  		val, ok := node.Meta[meta]
   439  		return val, ok
   440  
   441  	default:
   442  		return nil, false
   443  	}
   444  }
   445  
   446  // checkConstraint checks if a constraint is satisfied. The lVal and rVal
   447  // interfaces may be nil.
   448  func checkConstraint(ctx Context, operand string, lVal, rVal interface{}, lFound, rFound bool) bool {
   449  	// Check for constraints not handled by this checker.
   450  	switch operand {
   451  	case structs.ConstraintDistinctHosts, structs.ConstraintDistinctProperty:
   452  		return true
   453  	default:
   454  		break
   455  	}
   456  
   457  	switch operand {
   458  	case "=", "==", "is":
   459  		return lFound && rFound && reflect.DeepEqual(lVal, rVal)
   460  	case "!=", "not":
   461  		return !reflect.DeepEqual(lVal, rVal)
   462  	case "<", "<=", ">", ">=":
   463  		return lFound && rFound && checkLexicalOrder(operand, lVal, rVal)
   464  	case structs.ConstraintAttributeIsSet:
   465  		return lFound
   466  	case structs.ConstraintAttributeIsNotSet:
   467  		return !lFound
   468  	case structs.ConstraintVersion:
   469  		return lFound && rFound && checkVersionMatch(ctx, lVal, rVal)
   470  	case structs.ConstraintRegex:
   471  		return lFound && rFound && checkRegexpMatch(ctx, lVal, rVal)
   472  	case structs.ConstraintSetContains, structs.ConstraintSetContainsAll:
   473  		return lFound && rFound && checkSetContainsAll(ctx, lVal, rVal)
   474  	case structs.ConstraintSetContainsAny:
   475  		return lFound && rFound && checkSetContainsAny(lVal, rVal)
   476  	default:
   477  		return false
   478  	}
   479  }
   480  
   481  // checkAffinity checks if a specific affinity is satisfied
   482  func checkAffinity(ctx Context, operand string, lVal, rVal interface{}, lFound, rFound bool) bool {
   483  	return checkConstraint(ctx, operand, lVal, rVal, lFound, rFound)
   484  }
   485  
   486  // checkAttributeAffinity checks if an affinity is satisfied
   487  func checkAttributeAffinity(ctx Context, operand string, lVal, rVal *psstructs.Attribute, lFound, rFound bool) bool {
   488  	return checkAttributeConstraint(ctx, operand, lVal, rVal, lFound, rFound)
   489  }
   490  
   491  // checkLexicalOrder is used to check for lexical ordering
   492  func checkLexicalOrder(op string, lVal, rVal interface{}) bool {
   493  	// Ensure the values are strings
   494  	lStr, ok := lVal.(string)
   495  	if !ok {
   496  		return false
   497  	}
   498  	rStr, ok := rVal.(string)
   499  	if !ok {
   500  		return false
   501  	}
   502  
   503  	switch op {
   504  	case "<":
   505  		return lStr < rStr
   506  	case "<=":
   507  		return lStr <= rStr
   508  	case ">":
   509  		return lStr > rStr
   510  	case ">=":
   511  		return lStr >= rStr
   512  	default:
   513  		return false
   514  	}
   515  }
   516  
   517  // checkVersionMatch is used to compare a version on the
   518  // left hand side with a set of constraints on the right hand side
   519  func checkVersionMatch(ctx Context, lVal, rVal interface{}) bool {
   520  	// Parse the version
   521  	var versionStr string
   522  	switch v := lVal.(type) {
   523  	case string:
   524  		versionStr = v
   525  	case int:
   526  		versionStr = fmt.Sprintf("%d", v)
   527  	default:
   528  		return false
   529  	}
   530  
   531  	// Parse the version
   532  	vers, err := version.NewVersion(versionStr)
   533  	if err != nil {
   534  		return false
   535  	}
   536  
   537  	// Constraint must be a string
   538  	constraintStr, ok := rVal.(string)
   539  	if !ok {
   540  		return false
   541  	}
   542  
   543  	// Check the cache for a match
   544  	cache := ctx.VersionConstraintCache()
   545  	constraints := cache[constraintStr]
   546  
   547  	// Parse the constraints
   548  	if constraints == nil {
   549  		constraints, err = version.NewConstraint(constraintStr)
   550  		if err != nil {
   551  			return false
   552  		}
   553  		cache[constraintStr] = constraints
   554  	}
   555  
   556  	// Check the constraints against the version
   557  	return constraints.Check(vers)
   558  }
   559  
   560  // checkAttributeVersionMatch is used to compare a version on the
   561  // left hand side with a set of constraints on the right hand side
   562  func checkAttributeVersionMatch(ctx Context, lVal, rVal *psstructs.Attribute) bool {
   563  	// Parse the version
   564  	var versionStr string
   565  	if s, ok := lVal.GetString(); ok {
   566  		versionStr = s
   567  	} else if i, ok := lVal.GetInt(); ok {
   568  		versionStr = fmt.Sprintf("%d", i)
   569  	} else {
   570  		return false
   571  	}
   572  
   573  	// Parse the version
   574  	vers, err := version.NewVersion(versionStr)
   575  	if err != nil {
   576  		return false
   577  	}
   578  
   579  	// Constraint must be a string
   580  	constraintStr, ok := rVal.GetString()
   581  	if !ok {
   582  		return false
   583  	}
   584  
   585  	// Check the cache for a match
   586  	cache := ctx.VersionConstraintCache()
   587  	constraints := cache[constraintStr]
   588  
   589  	// Parse the constraints
   590  	if constraints == nil {
   591  		constraints, err = version.NewConstraint(constraintStr)
   592  		if err != nil {
   593  			return false
   594  		}
   595  		cache[constraintStr] = constraints
   596  	}
   597  
   598  	// Check the constraints against the version
   599  	return constraints.Check(vers)
   600  }
   601  
   602  // checkRegexpMatch is used to compare a value on the
   603  // left hand side with a regexp on the right hand side
   604  func checkRegexpMatch(ctx Context, lVal, rVal interface{}) bool {
   605  	// Ensure left-hand is string
   606  	lStr, ok := lVal.(string)
   607  	if !ok {
   608  		return false
   609  	}
   610  
   611  	// Regexp must be a string
   612  	regexpStr, ok := rVal.(string)
   613  	if !ok {
   614  		return false
   615  	}
   616  
   617  	// Check the cache
   618  	cache := ctx.RegexpCache()
   619  	re := cache[regexpStr]
   620  
   621  	// Parse the regexp
   622  	if re == nil {
   623  		var err error
   624  		re, err = regexp.Compile(regexpStr)
   625  		if err != nil {
   626  			return false
   627  		}
   628  		cache[regexpStr] = re
   629  	}
   630  
   631  	// Look for a match
   632  	return re.MatchString(lStr)
   633  }
   634  
   635  // checkSetContainsAll is used to see if the left hand side contains the
   636  // string on the right hand side
   637  func checkSetContainsAll(ctx Context, lVal, rVal interface{}) bool {
   638  	// Ensure left-hand is string
   639  	lStr, ok := lVal.(string)
   640  	if !ok {
   641  		return false
   642  	}
   643  
   644  	// Regexp must be a string
   645  	rStr, ok := rVal.(string)
   646  	if !ok {
   647  		return false
   648  	}
   649  
   650  	input := strings.Split(lStr, ",")
   651  	lookup := make(map[string]struct{}, len(input))
   652  	for _, in := range input {
   653  		cleaned := strings.TrimSpace(in)
   654  		lookup[cleaned] = struct{}{}
   655  	}
   656  
   657  	for _, r := range strings.Split(rStr, ",") {
   658  		cleaned := strings.TrimSpace(r)
   659  		if _, ok := lookup[cleaned]; !ok {
   660  			return false
   661  		}
   662  	}
   663  
   664  	return true
   665  }
   666  
   667  // checkSetContainsAny is used to see if the left hand side contains any
   668  // values on the right hand side
   669  func checkSetContainsAny(lVal, rVal interface{}) bool {
   670  	// Ensure left-hand is string
   671  	lStr, ok := lVal.(string)
   672  	if !ok {
   673  		return false
   674  	}
   675  
   676  	// RHS must be a string
   677  	rStr, ok := rVal.(string)
   678  	if !ok {
   679  		return false
   680  	}
   681  
   682  	input := strings.Split(lStr, ",")
   683  	lookup := make(map[string]struct{}, len(input))
   684  	for _, in := range input {
   685  		cleaned := strings.TrimSpace(in)
   686  		lookup[cleaned] = struct{}{}
   687  	}
   688  
   689  	for _, r := range strings.Split(rStr, ",") {
   690  		cleaned := strings.TrimSpace(r)
   691  		if _, ok := lookup[cleaned]; ok {
   692  			return true
   693  		}
   694  	}
   695  
   696  	return false
   697  }
   698  
   699  // FeasibilityWrapper is a FeasibleIterator which wraps both job and task group
   700  // FeasibilityCheckers in which feasibility checking can be skipped if the
   701  // computed node class has previously been marked as eligible or ineligible.
   702  type FeasibilityWrapper struct {
   703  	ctx         Context
   704  	source      FeasibleIterator
   705  	jobCheckers []FeasibilityChecker
   706  	tgCheckers  []FeasibilityChecker
   707  	tg          string
   708  }
   709  
   710  // NewFeasibilityWrapper returns a FeasibleIterator based on the passed source
   711  // and FeasibilityCheckers.
   712  func NewFeasibilityWrapper(ctx Context, source FeasibleIterator,
   713  	jobCheckers, tgCheckers []FeasibilityChecker) *FeasibilityWrapper {
   714  	return &FeasibilityWrapper{
   715  		ctx:         ctx,
   716  		source:      source,
   717  		jobCheckers: jobCheckers,
   718  		tgCheckers:  tgCheckers,
   719  	}
   720  }
   721  
   722  func (w *FeasibilityWrapper) SetTaskGroup(tg string) {
   723  	w.tg = tg
   724  }
   725  
   726  func (w *FeasibilityWrapper) Reset() {
   727  	w.source.Reset()
   728  }
   729  
   730  // Next returns an eligible node, only running the FeasibilityCheckers as needed
   731  // based on the sources computed node class.
   732  func (w *FeasibilityWrapper) Next() *structs.Node {
   733  	evalElig := w.ctx.Eligibility()
   734  	metrics := w.ctx.Metrics()
   735  
   736  OUTER:
   737  	for {
   738  		// Get the next option from the source
   739  		option := w.source.Next()
   740  		if option == nil {
   741  			return nil
   742  		}
   743  
   744  		// Check if the job has been marked as eligible or ineligible.
   745  		jobEscaped, jobUnknown := false, false
   746  		switch evalElig.JobStatus(option.ComputedClass) {
   747  		case EvalComputedClassIneligible:
   748  			// Fast path the ineligible case
   749  			metrics.FilterNode(option, "computed class ineligible")
   750  			continue
   751  		case EvalComputedClassEscaped:
   752  			jobEscaped = true
   753  		case EvalComputedClassUnknown:
   754  			jobUnknown = true
   755  		}
   756  
   757  		// Run the job feasibility checks.
   758  		for _, check := range w.jobCheckers {
   759  			feasible := check.Feasible(option)
   760  			if !feasible {
   761  				// If the job hasn't escaped, set it to be ineligible since it
   762  				// failed a job check.
   763  				if !jobEscaped {
   764  					evalElig.SetJobEligibility(false, option.ComputedClass)
   765  				}
   766  				continue OUTER
   767  			}
   768  		}
   769  
   770  		// Set the job eligibility if the constraints weren't escaped and it
   771  		// hasn't been set before.
   772  		if !jobEscaped && jobUnknown {
   773  			evalElig.SetJobEligibility(true, option.ComputedClass)
   774  		}
   775  
   776  		// Check if the task group has been marked as eligible or ineligible.
   777  		tgEscaped, tgUnknown := false, false
   778  		switch evalElig.TaskGroupStatus(w.tg, option.ComputedClass) {
   779  		case EvalComputedClassIneligible:
   780  			// Fast path the ineligible case
   781  			metrics.FilterNode(option, "computed class ineligible")
   782  			continue
   783  		case EvalComputedClassEligible:
   784  			// Fast path the eligible case
   785  			return option
   786  		case EvalComputedClassEscaped:
   787  			tgEscaped = true
   788  		case EvalComputedClassUnknown:
   789  			tgUnknown = true
   790  		}
   791  
   792  		// Run the task group feasibility checks.
   793  		for _, check := range w.tgCheckers {
   794  			feasible := check.Feasible(option)
   795  			if !feasible {
   796  				// If the task group hasn't escaped, set it to be ineligible
   797  				// since it failed a check.
   798  				if !tgEscaped {
   799  					evalElig.SetTaskGroupEligibility(false, w.tg, option.ComputedClass)
   800  				}
   801  				continue OUTER
   802  			}
   803  		}
   804  
   805  		// Set the task group eligibility if the constraints weren't escaped and
   806  		// it hasn't been set before.
   807  		if !tgEscaped && tgUnknown {
   808  			evalElig.SetTaskGroupEligibility(true, w.tg, option.ComputedClass)
   809  		}
   810  
   811  		return option
   812  	}
   813  }
   814  
   815  // DeviceChecker is a FeasibilityChecker which returns whether a node has the
   816  // devices necessary to scheduler a task group.
   817  type DeviceChecker struct {
   818  	ctx Context
   819  
   820  	// required is the set of requested devices that must exist on the node
   821  	required []*structs.RequestedDevice
   822  
   823  	// requiresDevices indicates if the task group requires devices
   824  	requiresDevices bool
   825  }
   826  
   827  // NewDeviceChecker creates a DeviceChecker
   828  func NewDeviceChecker(ctx Context) *DeviceChecker {
   829  	return &DeviceChecker{
   830  		ctx: ctx,
   831  	}
   832  }
   833  
   834  func (c *DeviceChecker) SetTaskGroup(tg *structs.TaskGroup) {
   835  	c.required = nil
   836  	for _, task := range tg.Tasks {
   837  		c.required = append(c.required, task.Resources.Devices...)
   838  	}
   839  	c.requiresDevices = len(c.required) != 0
   840  }
   841  
   842  func (c *DeviceChecker) Feasible(option *structs.Node) bool {
   843  	if c.hasDevices(option) {
   844  		return true
   845  	}
   846  
   847  	c.ctx.Metrics().FilterNode(option, "missing devices")
   848  	return false
   849  }
   850  
   851  func (c *DeviceChecker) hasDevices(option *structs.Node) bool {
   852  	if !c.requiresDevices {
   853  		return true
   854  	}
   855  
   856  	// COMPAT(0.11): Remove in 0.11
   857  	// The node does not have the new resources object so it can not have any
   858  	// devices
   859  	if option.NodeResources == nil {
   860  		return false
   861  	}
   862  
   863  	// Check if the node has any devices
   864  	nodeDevs := option.NodeResources.Devices
   865  	if len(nodeDevs) == 0 {
   866  		return false
   867  	}
   868  
   869  	// Create a mapping of node devices to the remaining count
   870  	available := make(map[*structs.NodeDeviceResource]uint64, len(nodeDevs))
   871  	for _, d := range nodeDevs {
   872  		var healthy uint64 = 0
   873  		for _, instance := range d.Instances {
   874  			if instance.Healthy {
   875  				healthy++
   876  			}
   877  		}
   878  		if healthy != 0 {
   879  			available[d] = healthy
   880  		}
   881  	}
   882  
   883  	// Go through the required devices trying to find matches
   884  OUTER:
   885  	for _, req := range c.required {
   886  		// Determine how many there are to place
   887  		desiredCount := req.Count
   888  
   889  		// Go through the device resources and see if we have a match
   890  		for d, unused := range available {
   891  			if unused == 0 {
   892  				// Depleted
   893  				continue
   894  			}
   895  
   896  			// First check we have enough instances of the device since this is
   897  			// cheaper than checking the constraints
   898  			if unused < desiredCount {
   899  				continue
   900  			}
   901  
   902  			// Check the constraints
   903  			if nodeDeviceMatches(c.ctx, d, req) {
   904  				// Consume the instances
   905  				available[d] -= desiredCount
   906  
   907  				// Move on to the next request
   908  				continue OUTER
   909  			}
   910  		}
   911  
   912  		// We couldn't match the request for the device
   913  		return false
   914  	}
   915  
   916  	// Only satisfied if there are no more devices to place
   917  	return true
   918  }
   919  
   920  // nodeDeviceMatches checks if the device matches the request and its
   921  // constraints. It doesn't check the count.
   922  func nodeDeviceMatches(ctx Context, d *structs.NodeDeviceResource, req *structs.RequestedDevice) bool {
   923  	if !d.ID().Matches(req.ID()) {
   924  		return false
   925  	}
   926  
   927  	// There are no constraints to consider
   928  	if len(req.Constraints) == 0 {
   929  		return true
   930  	}
   931  
   932  	for _, c := range req.Constraints {
   933  		// Resolve the targets
   934  		lVal, lOk := resolveDeviceTarget(c.LTarget, d)
   935  		rVal, rOk := resolveDeviceTarget(c.RTarget, d)
   936  
   937  		// Check if satisfied
   938  		if !checkAttributeConstraint(ctx, c.Operand, lVal, rVal, lOk, rOk) {
   939  			return false
   940  		}
   941  	}
   942  
   943  	return true
   944  }
   945  
   946  // resolveDeviceTarget is used to resolve the LTarget and RTarget of a Constraint
   947  // when used on a device
   948  func resolveDeviceTarget(target string, d *structs.NodeDeviceResource) (*psstructs.Attribute, bool) {
   949  	// If no prefix, this must be a literal value
   950  	if !strings.HasPrefix(target, "${") {
   951  		return psstructs.ParseAttribute(target), true
   952  	}
   953  
   954  	// Handle the interpolations
   955  	switch {
   956  	case "${device.model}" == target:
   957  		return psstructs.NewStringAttribute(d.Name), true
   958  
   959  	case "${device.vendor}" == target:
   960  		return psstructs.NewStringAttribute(d.Vendor), true
   961  
   962  	case "${device.type}" == target:
   963  		return psstructs.NewStringAttribute(d.Type), true
   964  
   965  	case strings.HasPrefix(target, "${device.attr."):
   966  		attr := strings.TrimPrefix(target, "${device.attr.")
   967  		attr = strings.TrimSuffix(attr, "}")
   968  		val, ok := d.Attributes[attr]
   969  		return val, ok
   970  
   971  	default:
   972  		return nil, false
   973  	}
   974  }
   975  
   976  // checkAttributeConstraint checks if a constraint is satisfied. nil equality
   977  // comparisons are considered to be false.
   978  func checkAttributeConstraint(ctx Context, operand string, lVal, rVal *psstructs.Attribute, lFound, rFound bool) bool {
   979  	// Check for constraints not handled by this checker.
   980  	switch operand {
   981  	case structs.ConstraintDistinctHosts, structs.ConstraintDistinctProperty:
   982  		return true
   983  	default:
   984  		break
   985  	}
   986  
   987  	switch operand {
   988  	case "!=", "not":
   989  		// Neither value was provided, nil != nil == false
   990  		if !(lFound || rFound) {
   991  			return false
   992  		}
   993  
   994  		// Only 1 value was provided, therefore nil != some == true
   995  		if lFound != rFound {
   996  			return true
   997  		}
   998  
   999  		// Both values were provided, so actually compare them
  1000  		v, ok := lVal.Compare(rVal)
  1001  		if !ok {
  1002  			return false
  1003  		}
  1004  
  1005  		return v != 0
  1006  
  1007  	case "<", "<=", ">", ">=", "=", "==", "is":
  1008  		if !(lFound && rFound) {
  1009  			return false
  1010  		}
  1011  
  1012  		v, ok := lVal.Compare(rVal)
  1013  		if !ok {
  1014  			return false
  1015  		}
  1016  
  1017  		switch operand {
  1018  		case "is", "==", "=":
  1019  			return v == 0
  1020  		case "<":
  1021  			return v == -1
  1022  		case "<=":
  1023  			return v != 1
  1024  		case ">":
  1025  			return v == 1
  1026  		case ">=":
  1027  			return v != -1
  1028  		default:
  1029  			return false
  1030  		}
  1031  
  1032  	case structs.ConstraintVersion:
  1033  		if !(lFound && rFound) {
  1034  			return false
  1035  		}
  1036  
  1037  		return checkAttributeVersionMatch(ctx, lVal, rVal)
  1038  	case structs.ConstraintRegex:
  1039  		if !(lFound && rFound) {
  1040  			return false
  1041  		}
  1042  
  1043  		ls, ok := lVal.GetString()
  1044  		rs, ok2 := rVal.GetString()
  1045  		if !ok || !ok2 {
  1046  			return false
  1047  		}
  1048  		return checkRegexpMatch(ctx, ls, rs)
  1049  	case structs.ConstraintSetContains, structs.ConstraintSetContainsAll:
  1050  		if !(lFound && rFound) {
  1051  			return false
  1052  		}
  1053  
  1054  		ls, ok := lVal.GetString()
  1055  		rs, ok2 := rVal.GetString()
  1056  		if !ok || !ok2 {
  1057  			return false
  1058  		}
  1059  
  1060  		return checkSetContainsAll(ctx, ls, rs)
  1061  	case structs.ConstraintSetContainsAny:
  1062  		if !(lFound && rFound) {
  1063  			return false
  1064  		}
  1065  
  1066  		ls, ok := lVal.GetString()
  1067  		rs, ok2 := rVal.GetString()
  1068  		if !ok || !ok2 {
  1069  			return false
  1070  		}
  1071  
  1072  		return checkSetContainsAny(ls, rs)
  1073  	case structs.ConstraintAttributeIsSet:
  1074  		return lFound
  1075  	case structs.ConstraintAttributeIsNotSet:
  1076  		return !lFound
  1077  	default:
  1078  		return false
  1079  	}
  1080  
  1081  }