Skip to content

Commit

Permalink
Merge pull request #1479 from marquiz/devel/api-internal
Browse files Browse the repository at this point in the history
source/custom: add internal rule api
  • Loading branch information
k8s-ci-robot authored Dec 18, 2023
2 parents 884edc6 + b28d5c1 commit 7ae2516
Show file tree
Hide file tree
Showing 11 changed files with 934 additions and 293 deletions.
182 changes: 1 addition & 181 deletions pkg/apis/nfd/v1alpha1/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ limitations under the License.
package v1alpha1

import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
strings "strings"

"golang.org/x/exp/maps"
"k8s.io/klog/v2"
Expand All @@ -42,63 +41,6 @@ var matchOps = map[MatchOp]struct{}{
MatchIsFalse: {},
}

// newMatchExpression returns a new MatchExpression instance.
func newMatchExpression(op MatchOp, values ...string) *MatchExpression {
return &MatchExpression{
Op: op,
Value: values,
}
}

// Validate validates the expression.
func (m *MatchExpression) Validate() error {
if _, ok := matchOps[m.Op]; !ok {
return fmt.Errorf("invalid Op %q", m.Op)
}
switch m.Op {
case MatchExists, MatchDoesNotExist, MatchIsTrue, MatchIsFalse, MatchAny:
if len(m.Value) != 0 {
return fmt.Errorf("value must be empty for Op %q (have %v)", m.Op, m.Value)
}
case MatchGt, MatchLt:
if len(m.Value) != 1 {
return fmt.Errorf("value must contain exactly one element for Op %q (have %v)", m.Op, m.Value)
}
if _, err := strconv.Atoi(m.Value[0]); err != nil {
return fmt.Errorf("value must be an integer for Op %q (have %v)", m.Op, m.Value[0])
}
case MatchGtLt:
if len(m.Value) != 2 {
return fmt.Errorf("value must contain exactly two elements for Op %q (have %v)", m.Op, m.Value)
}
var err error
v := make([]int, 2)
for i := 0; i < 2; i++ {
if v[i], err = strconv.Atoi(m.Value[i]); err != nil {
return fmt.Errorf("value must contain integers for Op %q (have %v)", m.Op, m.Value)
}
}
if v[0] >= v[1] {
return fmt.Errorf("value[0] must be less than Value[1] for Op %q (have %v)", m.Op, m.Value)
}
case MatchInRegexp:
if len(m.Value) == 0 {
return fmt.Errorf("value must be non-empty for Op %q", m.Op)
}
for _, v := range m.Value {
_, err := regexp.Compile(v)
if err != nil {
return fmt.Errorf("value must only contain valid regexps for Op %q (have %v)", m.Op, m.Value)
}
}
default:
if len(m.Value) == 0 {
return fmt.Errorf("value must be non-empty for Op %q", m.Op)
}
}
return nil
}

// Match evaluates the MatchExpression against a single input value.
func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
if _, ok := matchOps[m.Op]; !ok {
Expand Down Expand Up @@ -340,48 +282,6 @@ func (m *MatchExpression) MatchInstanceAttributeNames(instances []InstanceFeatur
return ret, nil
}

// matchExpression is a helper type for unmarshalling MatchExpression
type matchExpression MatchExpression

// UnmarshalJSON implements the Unmarshaler interface of "encoding/json"
func (m *MatchExpression) UnmarshalJSON(data []byte) error {
raw := new(interface{})

err := json.Unmarshal(data, raw)
if err != nil {
return err
}

switch v := (*raw).(type) {
case string:
*m = *newMatchExpression(MatchIn, v)
case bool:
*m = *newMatchExpression(MatchIn, strconv.FormatBool(v))
case float64:
*m = *newMatchExpression(MatchIn, strconv.FormatFloat(v, 'f', -1, 64))
case []interface{}:
values := make([]string, len(v))
for i, value := range v {
str, ok := value.(string)
if !ok {
return fmt.Errorf("invalid value %v in %v", value, v)
}
values[i] = str
}
*m = *newMatchExpression(MatchIn, values...)
case map[string]interface{}:
helper := &matchExpression{}
if err := json.Unmarshal(data, &helper); err != nil {
return err
}
*m = *newMatchExpression(helper.Op, helper.Value...)
default:
return fmt.Errorf("invalid rule '%v' (%T)", v, v)
}

return m.Validate()
}

// MatchKeys evaluates the MatchExpressionSet against a set of keys.
func (m *MatchExpressionSet) MatchKeys(keys map[string]Nil) (bool, error) {
matched, _, err := m.MatchGetKeys(keys)
Expand Down Expand Up @@ -464,83 +364,3 @@ func (m *MatchExpressionSet) MatchGetInstances(instances []InstanceFeature) ([]M
}
return ret, nil
}

// UnmarshalJSON implements the Unmarshaler interface of "encoding/json".
func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
*m = MatchExpressionSet{}

names := make([]string, 0)
if err := json.Unmarshal(data, &names); err == nil {
// Simplified slice form
for _, name := range names {
split := strings.SplitN(name, "=", 2)
if len(split) == 1 {
(*m)[split[0]] = newMatchExpression(MatchExists)
} else {
(*m)[split[0]] = newMatchExpression(MatchIn, split[1])
}
}
} else {
// Unmarshal the full map form
expressions := make(map[string]*MatchExpression)
if err := json.Unmarshal(data, &expressions); err != nil {
return err
}
for k, v := range expressions {
if v != nil {
(*m)[k] = v
} else {
(*m)[k] = newMatchExpression(MatchExists)
}
}
}

return nil
}

// UnmarshalJSON implements the Unmarshaler interface of "encoding/json".
func (m *MatchOp) UnmarshalJSON(data []byte) error {
var raw string

if err := json.Unmarshal(data, &raw); err != nil {
return err
}

if _, ok := matchOps[MatchOp(raw)]; !ok {
return fmt.Errorf("invalid Op %q", raw)
}
*m = MatchOp(raw)
return nil
}

// UnmarshalJSON implements the Unmarshaler interface of "encoding/json".
func (m *MatchValue) UnmarshalJSON(data []byte) error {
var raw interface{}

if err := json.Unmarshal(data, &raw); err != nil {
return err
}

switch v := raw.(type) {
case string:
*m = []string{v}
case bool:
*m = []string{strconv.FormatBool(v)}
case float64:
*m = []string{strconv.FormatFloat(v, 'f', -1, 64)}
case []interface{}:
values := make([]string, len(v))
for i, value := range v {
str, ok := value.(string)
if !ok {
return fmt.Errorf("invalid value %v in %v", value, v)
}
values[i] = str
}
*m = values
default:
return fmt.Errorf("invalid values '%v' (%T)", v, v)
}

return nil
}
67 changes: 0 additions & 67 deletions pkg/apis/nfd/v1alpha1/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,73 +29,6 @@ type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool

type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool

func TestMatchExpressionValidate(t *testing.T) {
type V = api.MatchValue
type TC struct {
name string
op api.MatchOp
values V
err ValueAssertionFunc
}

tcs := []TC{
{name: "1", op: api.MatchAny, err: assert.Nil}, // #0
{name: "2", op: api.MatchAny, values: V{"1"}, err: assert.NotNil},

{name: "3", op: api.MatchIn, err: assert.NotNil},
{name: "4", op: api.MatchIn, values: V{"1"}, err: assert.Nil},
{name: "5", op: api.MatchIn, values: V{"1", "2", "3", "4"}, err: assert.Nil},

{name: "6", op: api.MatchNotIn, err: assert.NotNil},
{name: "7", op: api.MatchNotIn, values: V{"1"}, err: assert.Nil},
{name: "8", op: api.MatchNotIn, values: V{"1", "2"}, err: assert.Nil},

{name: "9", op: api.MatchInRegexp, err: assert.NotNil},
{name: "10", op: api.MatchInRegexp, values: V{"1"}, err: assert.Nil},
{name: "11", op: api.MatchInRegexp, values: V{"()", "2", "3"}, err: assert.Nil},
{name: "12", op: api.MatchInRegexp, values: V{"("}, err: assert.NotNil},

{name: "13", op: api.MatchExists, err: assert.Nil},
{name: "14", op: api.MatchExists, values: V{"1"}, err: assert.NotNil},

{name: "15", op: api.MatchDoesNotExist, err: assert.Nil},
{name: "16", op: api.MatchDoesNotExist, values: V{"1"}, err: assert.NotNil},

{name: "17", op: api.MatchGt, err: assert.NotNil},
{name: "18", op: api.MatchGt, values: V{"1"}, err: assert.Nil},
{name: "19", op: api.MatchGt, values: V{"-10"}, err: assert.Nil},
{name: "20", op: api.MatchGt, values: V{"1", "2"}, err: assert.NotNil},
{name: "21", op: api.MatchGt, values: V{""}, err: assert.NotNil},

{name: "22", op: api.MatchLt, err: assert.NotNil},
{name: "23", op: api.MatchLt, values: V{"1"}, err: assert.Nil},
{name: "24", op: api.MatchLt, values: V{"-1"}, err: assert.Nil},
{name: "25", op: api.MatchLt, values: V{"1", "2", "3"}, err: assert.NotNil},
{name: "26", op: api.MatchLt, values: V{"a"}, err: assert.NotNil},

{name: "27", op: api.MatchGtLt, err: assert.NotNil},
{name: "28", op: api.MatchGtLt, values: V{"1"}, err: assert.NotNil},
{name: "29", op: api.MatchGtLt, values: V{"1", "2"}, err: assert.Nil},
{name: "30", op: api.MatchGtLt, values: V{"2", "1"}, err: assert.NotNil},
{name: "31", op: api.MatchGtLt, values: V{"1", "2", "3"}, err: assert.NotNil},
{name: "32", op: api.MatchGtLt, values: V{"a", "2"}, err: assert.NotNil},

{name: "33", op: api.MatchIsTrue, err: assert.Nil},
{name: "34", op: api.MatchIsTrue, values: V{"1"}, err: assert.NotNil},

{name: "35", op: api.MatchIsFalse, err: assert.Nil},
{name: "36", op: api.MatchIsFalse, values: V{"1", "2"}, err: assert.NotNil},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
me := api.MatchExpression{Op: tc.op, Value: tc.values}
err := me.Validate()
tc.err(t, err)
})
}
}

func TestMatch(t *testing.T) {
type V = api.MatchValue
type TC struct {
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/nfd/v1alpha1/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ import (
"github.com/stretchr/testify/assert"
)

// newMatchExpression returns a new MatchExpression instance.
func newMatchExpression(op MatchOp, values ...string) *MatchExpression {
return &MatchExpression{
Op: op,
Value: values,
}
}

func TestRule(t *testing.T) {
f := &Features{}
r1 := Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}}
Expand Down
Loading

0 comments on commit 7ae2516

Please sign in to comment.