Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for regexp, version, and lexical ordering constraints #271

Merged
merged 9 commits into from
Oct 12, 2015
14 changes: 14 additions & 0 deletions jobspec/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,20 @@ func parseConstraints(result *[]*structs.Constraint, obj *hclobj.Object) error {
m["hard"] = true
}

// If "version" is provided, set the operand
// to "version" and the value to the "RTarget"
if constraint, ok := m["version"]; ok {
m["Operand"] = "version"
m["RTarget"] = constraint
}

// If "regexp" is provided, set the operand
// to "regexp" and the value to the "RTarget"
if constraint, ok := m["regexp"]; ok {
m["Operand"] = "regexp"
m["RTarget"] = constraint
}

// Build the constraint
var c structs.Constraint
if err := mapstructure.WeakDecode(m, &c); err != nil {
Expand Down
40 changes: 40 additions & 0 deletions jobspec/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,46 @@ func TestParse(t *testing.T) {
false,
},

{
"version-constraint.hcl",
&structs.Job{
ID: "foo",
Name: "foo",
Priority: 50,
Region: "global",
Type: "service",
Constraints: []*structs.Constraint{
&structs.Constraint{
Hard: true,
LTarget: "$attr.kernel.version",
RTarget: "~> 3.2",
Operand: "version",
},
},
},
false,
},

{
"regexp-constraint.hcl",
&structs.Job{
ID: "foo",
Name: "foo",
Priority: 50,
Region: "global",
Type: "service",
Constraints: []*structs.Constraint{
&structs.Constraint{
Hard: true,
LTarget: "$attr.kernel.version",
RTarget: "[0-9.]+",
Operand: "regexp",
},
},
},
false,
},

{
"specify-job.hcl",
&structs.Job{
Expand Down
6 changes: 6 additions & 0 deletions jobspec/test-fixtures/regexp-constraint.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
job "foo" {
constraint {
attribute = "$attr.kernel.version"
regexp = "[0-9.]+"
}
}
6 changes: 6 additions & 0 deletions jobspec/test-fixtures/version-constraint.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
job "foo" {
constraint {
attribute = "$attr.kernel.version"
version = "~> 3.2"
}
}
40 changes: 40 additions & 0 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"bytes"
"errors"
"fmt"
"regexp"
"strings"
"time"

"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
)

var (
Expand Down Expand Up @@ -809,6 +811,12 @@ func (j *Job) Validate() error {
if len(j.TaskGroups) == 0 {
mErr.Errors = append(mErr.Errors, errors.New("Missing job task groups"))
}
for idx, constr := range j.Constraints {
if err := constr.Validate(); err != nil {
outer := fmt.Errorf("Constraint %d validation failed: %s", idx+1, err)
mErr.Errors = append(mErr.Errors, outer)
}
}

// Check for duplicate task groups
taskGroups := make(map[string]int)
Expand Down Expand Up @@ -918,6 +926,12 @@ func (tg *TaskGroup) Validate() error {
if len(tg.Tasks) == 0 {
mErr.Errors = append(mErr.Errors, errors.New("Missing tasks for task group"))
}
for idx, constr := range tg.Constraints {
if err := constr.Validate(); err != nil {
outer := fmt.Errorf("Constraint %d validation failed: %s", idx+1, err)
mErr.Errors = append(mErr.Errors, outer)
}
}

// Check for duplicate tasks
tasks := make(map[string]int)
Expand Down Expand Up @@ -997,6 +1011,12 @@ func (t *Task) Validate() error {
if t.Resources == nil {
mErr.Errors = append(mErr.Errors, errors.New("Missing task resources"))
}
for idx, constr := range t.Constraints {
if err := constr.Validate(); err != nil {
outer := fmt.Errorf("Constraint %d validation failed: %s", idx+1, err)
mErr.Errors = append(mErr.Errors, outer)
}
}
return mErr.ErrorOrNil()
}

Expand All @@ -1015,6 +1035,26 @@ func (c *Constraint) String() string {
return fmt.Sprintf("%s %s %s", c.LTarget, c.Operand, c.RTarget)
}

func (c *Constraint) Validate() error {
var mErr multierror.Error
if c.Operand == "" {
mErr.Errors = append(mErr.Errors, errors.New("Missing constraint operand"))
}

// Perform additional validation based on operand
switch c.Operand {
case "regexp":
if _, err := regexp.Compile(c.RTarget); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Regular expression failed to compile: %v", err))
}
case "version":
if _, err := version.NewConstraint(c.RTarget); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Version constraint is invalid: %v", err))
}
}
return mErr.ErrorOrNil()
}

const (
AllocDesiredStatusRun = "run" // Allocation should run
AllocDesiredStatusStop = "stop" // Allocation should stop
Expand Down
37 changes: 37 additions & 0 deletions nomad/structs/structs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,43 @@ func TestTask_Validate(t *testing.T) {
}
}

func TestConstraint_Validate(t *testing.T) {
c := &Constraint{}
err := c.Validate()
mErr := err.(*multierror.Error)
if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") {
t.Fatalf("err: %s", err)
}

c = &Constraint{
LTarget: "$attr.kernel.name",
RTarget: "linux",
Operand: "=",
}
err = c.Validate()
if err != nil {
t.Fatalf("err: %v", err)
}

// Perform additional regexp validation
c.Operand = "regexp"
c.RTarget = "(foo"
err = c.Validate()
mErr = err.(*multierror.Error)
if !strings.Contains(mErr.Errors[0].Error(), "missing closing") {
t.Fatalf("err: %s", err)
}

// Perform version validation
c.Operand = "version"
c.RTarget = "~> foo"
err = c.Validate()
mErr = err.(*multierror.Error)
if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") {
t.Fatalf("err: %s", err)
}
}

func TestResource_NetIndex(t *testing.T) {
r := &Resources{
Networks: []*NetworkResource{
Expand Down
95 changes: 92 additions & 3 deletions scheduler/feasible.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package scheduler
import (
"fmt"
"reflect"
"regexp"
"strings"

"github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/nomad/structs"
)

Expand Down Expand Up @@ -248,12 +250,99 @@ func checkConstraint(operand string, lVal, rVal interface{}) bool {
case "!=", "not":
return !reflect.DeepEqual(lVal, rVal)
case "<", "<=", ">", ">=":
// TODO: Implement
return checkLexicalOrder(operand, lVal, rVal)
case "version":
return checkVersionConstraint(lVal, rVal)
case "regexp":
return checkRegexpConstraint(lVal, rVal)
default:
return false
}
}

// checkLexicalOrder is used to check for lexical ordering
func checkLexicalOrder(op string, lVal, rVal interface{}) bool {
// Ensure the values are strings
lStr, ok := lVal.(string)
if !ok {
return false
case "contains":
// TODO: Implement
}
rStr, ok := rVal.(string)
if !ok {
return false
}

switch op {
case "<":
return lStr < rStr
case "<=":
return lStr <= rStr
case ">":
return lStr > rStr
case ">=":
return lStr >= rStr
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)
}
Loading