New constraints management.
Co-authored-by: Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
This commit is contained in:
parent
e9792b446f
commit
fe68e9e243
40 changed files with 658 additions and 630 deletions
100
pkg/provider/constraints/constraints.go
Normal file
100
pkg/provider/constraints/constraints.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package constraints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/vulcand/predicate"
|
||||
)
|
||||
|
||||
// MarathonConstraintPrefix is the prefix for each label's key created from a Marathon application constraint.
|
||||
// It is used in order to create a specific and unique pattern for these labels.
|
||||
const MarathonConstraintPrefix = "Traefik-Marathon-505F9E15-BDC7-45E7-828D-C06C7BAB8091"
|
||||
|
||||
type constraintFunc func(map[string]string) bool
|
||||
|
||||
// Match reports whether the expression matches with the given labels.
|
||||
// The expression must match any logical boolean combination of:
|
||||
// - `Label(labelName, labelValue)`
|
||||
// - `LabelRegex(labelName, regexValue)`
|
||||
// - `MarathonConstraint(field:operator:value)`
|
||||
func Match(labels map[string]string, expr string) (bool, error) {
|
||||
if expr == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
p, err := predicate.NewParser(predicate.Def{
|
||||
Operators: predicate.Operators{
|
||||
AND: andFunc,
|
||||
NOT: notFunc,
|
||||
OR: orFunc,
|
||||
},
|
||||
Functions: map[string]interface{}{
|
||||
"Label": labelFn,
|
||||
"LabelRegex": labelRegexFn,
|
||||
"MarathonConstraint": marathonFn,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
parse, err := p.Parse(expr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fn, ok := parse.(constraintFunc)
|
||||
if !ok {
|
||||
return false, errors.New("not a constraintFunc")
|
||||
}
|
||||
return fn(labels), nil
|
||||
}
|
||||
|
||||
func labelFn(name, value string) constraintFunc {
|
||||
return func(labels map[string]string) bool {
|
||||
return labels[name] == value
|
||||
}
|
||||
}
|
||||
|
||||
func labelRegexFn(name, expr string) constraintFunc {
|
||||
return func(labels map[string]string) bool {
|
||||
matched, err := regexp.MatchString(expr, labels[name])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
}
|
||||
}
|
||||
|
||||
func marathonFn(value string) constraintFunc {
|
||||
return func(labels map[string]string) bool {
|
||||
for k, v := range labels {
|
||||
if strings.HasPrefix(k, MarathonConstraintPrefix) {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func andFunc(a, b constraintFunc) constraintFunc {
|
||||
return func(labels map[string]string) bool {
|
||||
return a(labels) && b(labels)
|
||||
}
|
||||
}
|
||||
|
||||
func orFunc(a, b constraintFunc) constraintFunc {
|
||||
return func(labels map[string]string) bool {
|
||||
return a(labels) || b(labels)
|
||||
}
|
||||
}
|
||||
|
||||
func notFunc(a constraintFunc) constraintFunc {
|
||||
return func(labels map[string]string) bool {
|
||||
return !a(labels)
|
||||
}
|
||||
}
|
204
pkg/provider/constraints/constraints_test.go
Normal file
204
pkg/provider/constraints/constraints_test.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
package constraints
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
expr string
|
||||
labels map[string]string
|
||||
expected bool
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
expr: `Label("hello", "world")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "worlds")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `Label("hi", "world")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `!Label("hello", "world")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "world") && Label("foo", "bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "worlds") && Label("foo", "bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "world") && !Label("foo", "bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "world") || Label("foo", "bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "worlds") || Label("foo", "bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "world") || !Label("foo", "bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
expr: `Foo("hello")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "bar")`,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: ``,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `MarathonConstraint("bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
MarathonConstraintPrefix + "-1": "bar",
|
||||
MarathonConstraintPrefix + "-2": "foo",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `MarathonConstraint("bur")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
MarathonConstraintPrefix + "-1": "bar",
|
||||
MarathonConstraintPrefix + "-2": "foo",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `Label("hello", "world") && MarathonConstraint("bar")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
MarathonConstraintPrefix + "-1": "bar",
|
||||
MarathonConstraintPrefix + "-2": "foo",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `LabelRegex("hello", "w\\w+")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
expr: `LabelRegex("hello", "w\\w+s")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `LabelRegex("hi", "w\\w+")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `!LabelRegex("hello", "w\\w+")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
expr: `LabelRegex("hello", "w(\\w+")`,
|
||||
labels: map[string]string{
|
||||
"hello": "world",
|
||||
"foo": "bar",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.expr, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
matches, err := Match(test.labels, test.expr)
|
||||
if test.expectedErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, test.expected, matches)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue