Skip to content

Commit

Permalink
scheduler: Add is_set/is_not_set constraints
Browse files Browse the repository at this point in the history
This adds constraints for asserting that a given attribute or value
exists, or does not exist. This acts as a companion to =, or !=
operators, e.g:

```hcl
constraint {
        attribute = "${attrs.type}"
        operator  = "!="
        value     = "database"
}

constraint {
        attribute = "${attrs.type}"
        operator  = "is_set"
}
```
  • Loading branch information
endocrimes committed Nov 14, 2018
1 parent 0925bfe commit 60f80fe
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 18 deletions.
20 changes: 13 additions & 7 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6324,13 +6324,15 @@ func (ta *TaskArtifact) validateChecksum() error {
}

const (
ConstraintDistinctProperty = "distinct_property"
ConstraintDistinctHosts = "distinct_hosts"
ConstraintRegex = "regexp"
ConstraintVersion = "version"
ConstraintSetContains = "set_contains"
ConstraintSetContainsAll = "set_contains_all"
ConstraintSetContainsAny = "set_contains_any"
ConstraintDistinctProperty = "distinct_property"
ConstraintDistinctHosts = "distinct_hosts"
ConstraintRegex = "regexp"
ConstraintVersion = "version"
ConstraintSetContains = "set_contains"
ConstraintSetContainsAll = "set_contains_all"
ConstraintSetContainsAny = "set_contains_any"
ConstraintAttributeIsSet = "is_set"
ConstraintAttributeIsNotSet = "is_not_set"
)

// Constraints are used to restrict placement options.
Expand Down Expand Up @@ -6401,6 +6403,10 @@ func (c *Constraint) Validate() error {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Distinct Property must have an allowed count of 1 or greater: %d < 1", count))
}
}
case ConstraintAttributeIsSet, ConstraintAttributeIsNotSet:
if c.RTarget != "" {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Operator %q does not support an RTarget", c.Operand))
}
case "=", "==", "is", "!=", "not", "<", "<=", ">", ">=":
if c.RTarget == "" {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Operator %q requires an RTarget", c.Operand))
Expand Down
23 changes: 13 additions & 10 deletions scheduler/feasible.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,18 +400,14 @@ func (c *ConstraintChecker) Feasible(option *structs.Node) bool {
func (c *ConstraintChecker) meetsConstraint(constraint *structs.Constraint, option *structs.Node) bool {
// Resolve the targets. Targets that are not present are treated as `nil`.
// This is to allow for matching constraints where a target is not present.
//
// TODO: There may be a case where there is a distinction between not
// present and nil, but I'm not aware of it. Perhaps we should plum this to
// checkConstraint and add an extra level of validation?
lVal, _ := resolveTarget(constraint.LTarget, option)
rVal, _ := resolveTarget(constraint.RTarget, option)
lVal, lOk := resolveTarget(constraint.LTarget, option)
rVal, rOk := resolveTarget(constraint.RTarget, option)

// Check if satisfied
return checkConstraint(c.ctx, constraint.Operand, lVal, rVal)
return checkConstraint(c.ctx, constraint.Operand, lVal, rVal, lOk, rOk)
}

// resolveTarget is used to resolve the LTarget and RTarget of a Constraint
// resolveTarget is used to resolve the LTarget and RTarget of a Constraint.
func resolveTarget(target string, node *structs.Node) (interface{}, bool) {
// If no prefix, this must be a literal value
if !strings.HasPrefix(target, "${") {
Expand All @@ -434,11 +430,13 @@ func resolveTarget(target string, node *structs.Node) (interface{}, bool) {

case strings.HasPrefix(target, "${attr."):
attr := strings.TrimSuffix(strings.TrimPrefix(target, "${attr."), "}")
// The value not being present is considered to be ok.
val, ok := node.Attributes[attr]
return val, ok

case strings.HasPrefix(target, "${meta."):
meta := strings.TrimSuffix(strings.TrimPrefix(target, "${meta."), "}")
// The value not being present is considered to be ok.
val, ok := node.Meta[meta]
return val, ok

Expand All @@ -449,7 +447,7 @@ func resolveTarget(target string, node *structs.Node) (interface{}, bool) {

// checkConstraint checks if a constraint is satisfied. The lVal and rVal
// interfaces may be nil.
func checkConstraint(ctx Context, operand string, lVal, rVal interface{}) bool {
func checkConstraint(ctx Context, operand string, lVal, rVal interface{}, lFound, rFound bool) bool {
// Check for constraints not handled by this checker.
switch operand {
case structs.ConstraintDistinctHosts, structs.ConstraintDistinctProperty:
Expand All @@ -465,6 +463,10 @@ func checkConstraint(ctx Context, operand string, lVal, rVal interface{}) bool {
return !reflect.DeepEqual(lVal, rVal)
case "<", "<=", ">", ">=":
return checkLexicalOrder(operand, lVal, rVal)
case structs.ConstraintAttributeIsSet:
return lFound
case structs.ConstraintAttributeIsNotSet:
return !lFound
case structs.ConstraintVersion:
return checkVersionMatch(ctx, lVal, rVal)
case structs.ConstraintRegex:
Expand All @@ -480,7 +482,8 @@ func checkConstraint(ctx Context, operand string, lVal, rVal interface{}) bool {

// checkAffinity checks if a specific affinity is satisfied
func checkAffinity(ctx Context, operand string, lVal, rVal interface{}) bool {
return checkConstraint(ctx, operand, lVal, rVal)
// We pass ok here for both values, because matchesAffinity prevalidates these
return checkConstraint(ctx, operand, lVal, rVal, true, true)
}

// checkAttributeAffinity checks if an affinity is satisfied
Expand Down
4 changes: 3 additions & 1 deletion scheduler/feasible_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ func TestResolveConstraintTarget(t *testing.T) {
{
target: "${attr.rand}",
node: node,
val: "",
result: false,
},
{
Expand All @@ -315,6 +316,7 @@ func TestResolveConstraintTarget(t *testing.T) {
{
target: "${meta.rand}",
node: node,
val: "",
result: false,
},
}
Expand Down Expand Up @@ -431,7 +433,7 @@ func TestCheckConstraint(t *testing.T) {

for _, tc := range cases {
_, ctx := testContext(t)
if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result {
if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal, tc.lVal != nil, tc.rVal != nil); res != tc.result {
t.Fatalf("TC: %#v, Result: %v", tc, res)
}
}
Expand Down

0 comments on commit 60f80fe

Please sign in to comment.