diff --git a/jobspec/parse.go b/jobspec/parse.go index 7d01f09e6..f9910e4b5 100644 --- a/jobspec/parse.go +++ b/jobspec/parse.go @@ -407,6 +407,7 @@ func parseConstraints(result *[]*structs.Constraint, list *ast.ObjectList) error "version", "regexp", "distinct_hosts", + "set_contains", } if err := checkHCLKeys(o.Val, valid); err != nil { return err @@ -435,6 +436,13 @@ func parseConstraints(result *[]*structs.Constraint, list *ast.ObjectList) error m["RTarget"] = constraint } + // If "set_contains" is provided, set the operand + // to "set_contains" and the value to the "RTarget" + if constraint, ok := m[structs.ConstraintSetContains]; ok { + m["Operand"] = structs.ConstraintSetContains + m["RTarget"] = constraint + } + if value, ok := m[structs.ConstraintDistinctHosts]; ok { enabled, err := parseBool(value) if err != nil { diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 66996adc7..1e2a282a0 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -282,6 +282,25 @@ func TestParse(t *testing.T) { false, }, + { + "set-contains-constraint.hcl", + &structs.Job{ + ID: "foo", + Name: "foo", + Priority: 50, + Region: "global", + Type: "service", + Constraints: []*structs.Constraint{ + &structs.Constraint{ + LTarget: "$meta.data", + RTarget: "foo,bar,baz", + Operand: structs.ConstraintSetContains, + }, + }, + }, + false, + }, + { "distinctHosts-constraint.hcl", &structs.Job{ diff --git a/jobspec/test-fixtures/set-contains-constraint.hcl b/jobspec/test-fixtures/set-contains-constraint.hcl new file mode 100644 index 000000000..170f72118 --- /dev/null +++ b/jobspec/test-fixtures/set-contains-constraint.hcl @@ -0,0 +1,6 @@ +job "foo" { + constraint { + attribute = "$meta.data" + set_contains = "foo,bar,baz" + } +} diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index b6d3e2581..7459b88fd 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -2729,6 +2729,7 @@ const ( ConstraintDistinctHosts = "distinct_hosts" ConstraintRegex = "regexp" ConstraintVersion = "version" + ConstraintSetContains = "set_contains" ) // Constraints are used to restrict placement options. diff --git a/scheduler/feasible.go b/scheduler/feasible.go index 9ff2d2b36..2ca75ee17 100644 --- a/scheduler/feasible.go +++ b/scheduler/feasible.go @@ -344,6 +344,8 @@ func checkConstraint(ctx Context, operand string, lVal, rVal interface{}) bool { return checkVersionConstraint(ctx, lVal, rVal) case structs.ConstraintRegex: return checkRegexpConstraint(ctx, lVal, rVal) + case structs.ConstraintSetContains: + return checkSetContainsConstraint(ctx, lVal, rVal) default: return false } @@ -451,6 +453,38 @@ func checkRegexpConstraint(ctx Context, lVal, rVal interface{}) bool { return re.MatchString(lStr) } +// checkSetContainsConstraint is used to see if the left hand side contains the +// string on the right hand side +func checkSetContainsConstraint(ctx Context, lVal, rVal interface{}) bool { + // Ensure left-hand is string + lStr, ok := lVal.(string) + if !ok { + return false + } + + // Regexp must be a string + rStr, ok := rVal.(string) + if !ok { + return false + } + + input := strings.Split(lStr, ",") + lookup := make(map[string]struct{}, len(input)) + for _, in := range input { + cleaned := strings.TrimSpace(in) + lookup[cleaned] = struct{}{} + } + + for _, r := range strings.Split(rStr, ",") { + cleaned := strings.TrimSpace(r) + if _, ok := lookup[cleaned]; !ok { + return false + } + } + + return true +} + // FeasibilityWrapper is a FeasibleIterator which wraps both job and task group // FeasibilityCheckers in which feasibility checking can be skipped if the // computed node class has previously been marked as eligible or ineligible. diff --git a/scheduler/feasible_test.go b/scheduler/feasible_test.go index a6e4126c4..e6fe430e4 100644 --- a/scheduler/feasible_test.go +++ b/scheduler/feasible_test.go @@ -304,6 +304,16 @@ func TestCheckConstraint(t *testing.T) { lVal: "foo", rVal: "bar", result: false, }, + { + op: structs.ConstraintSetContains, + lVal: "foo,bar,baz", rVal: "foo, bar ", + result: true, + }, + { + op: structs.ConstraintSetContains, + lVal: "foo,bar,baz", rVal: "foo,bam", + result: false, + }, } for _, tc := range cases { diff --git a/website/source/docs/jobspec/index.html.md b/website/source/docs/jobspec/index.html.md index 3c0dc33ac..20b9c5b4f 100644 --- a/website/source/docs/jobspec/index.html.md +++ b/website/source/docs/jobspec/index.html.md @@ -419,6 +419,12 @@ The `constraint` object supports the following keys: the attribute. This sets the operator to "regexp" and the `value` to the regular expression. +* `set_contains` - Specifies a set contains constraint against + the attribute. This sets the operator to "set_contains" and the `value` + to the what is specified. This will check that the given attribute contains + each of the specified elements. The attribute and the list being checked are + split using commas. + * `distinct_hosts` - `distinct_hosts` accepts a boolean value and defaults to `false`. If set, the scheduler will not co-locate any task groups on the same machine. This can be specified as a job constraint which applies the diff --git a/website/source/docs/jobspec/json.html.md b/website/source/docs/jobspec/json.html.md index 458e43663..d15770cb7 100644 --- a/website/source/docs/jobspec/json.html.md +++ b/website/source/docs/jobspec/json.html.md @@ -443,6 +443,9 @@ The `Constraint` object supports the following keys: * `regexp` - Allows the `RTarget` to be a regular expression to be matched. + * `set_contains` - Allows the `RTarget` to be a comma separated list of values + that should be contained in the LTarget's value. + * `distinct_host` - If set, the scheduler will not co-locate any task groups on the same machine. This can be specified as a job constraint which applies the constraint to all task groups in the job, or as a task group constraint which