Skip to content

Commit

Permalink
Merge pull request #271 from hashicorp/f-version-constraint
Browse files Browse the repository at this point in the history
Adding support for regexp, version, and lexical ordering constraints
  • Loading branch information
armon committed Oct 12, 2015
2 parents ba17a44 + 6fdbdd8 commit 3dc54e0
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 4 deletions.
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

0 comments on commit 3dc54e0

Please sign in to comment.