1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2024-12-14 11:57:51 +00:00

source/custom: expression based label rules

Implement a framework for more flexible rule configuration and matching,
mimicking the MatchExpressions pattern from K8s nodeselector.

The basic building block is MatchExpression which contains an operator
and a list of values. The operator specifies that "function" that is
applied when evaluating a given input agains the list of values.
Available operators are:

- MatchIn
- MatchNotIn
- MatchInRegexp
- MatchExists
- MatchDoesNotExist
- MatchGt
- MatchLt
- MatchIsTrue
- MatchIsFalse

Another building block of the framework is MatchExpressionSet which is a
map of string-MatchExpression pairs. It is a helper for specifying
multiple expressions that can be matched against a set of set of
features.

This patch converts all existing custom rules to utilize the new
expression-based framework.
This commit is contained in:
Markus Lehtonen 2021-03-04 07:35:17 +02:00
parent 5299ca2ab4
commit 8b4314bbbb
11 changed files with 902 additions and 201 deletions

View file

@ -56,6 +56,10 @@ type customSource struct {
config *config config *config
} }
type legacyRule interface {
Match() (bool, error)
}
// Singleton source instance // Singleton source instance
var ( var (
src = customSource{config: newDefaultConfig()} src = customSource{config: newDefaultConfig()}
@ -114,7 +118,7 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
func (s *customSource) discoverFeature(feature FeatureSpec) (bool, error) { func (s *customSource) discoverFeature(feature FeatureSpec) (bool, error) {
for _, matchRules := range feature.MatchOn { for _, matchRules := range feature.MatchOn {
allRules := []rules.Rule{ allRules := []legacyRule{
matchRules.PciID, matchRules.PciID,
matchRules.UsbID, matchRules.UsbID,
matchRules.LoadedKMod, matchRules.LoadedKMod,
@ -124,7 +128,7 @@ func (s *customSource) discoverFeature(feature FeatureSpec) (bool, error) {
} }
// return true, nil if all rules match // return true, nil if all rules match
matchRules := func(rules []rules.Rule) (bool, error) { matchRules := func(rules []legacyRule) (bool, error) {
for _, rule := range rules { for _, rule := range rules {
if reflect.ValueOf(rule).IsNil() { if reflect.ValueOf(rule).IsNil() {
continue continue

View file

@ -0,0 +1,424 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package expression
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"k8s.io/klog/v2"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
)
// MatchExpressionSet contains a set of MatchExpressions, each of which is
// evaluated against a set of input values.
type MatchExpressionSet map[string]*MatchExpression
// MatchExpression specifies an expression to evaluate against a set of input
// values. It contains an operator that is applied when matching the input and
// an array of values that the operator evaluates the input against.
// NB: CreateMatchExpression or MustCreateMatchExpression() should be used for
// creating new instances.
// NB: Validate() must be called if Op or Value fields are modified or if a new
// instance is created from scratch without using the helper functions.
type MatchExpression struct {
// Op is the operator to be applied.
Op MatchOp
// Value is the list of values that the operand evaluates the input
// against. Value should be empty if the operator is Exists, DoesNotExist,
// IsTrue or IsFalse. Value should contain exactly one element if the
// operator is Gt or Lt. In other cases Value should contain at least one
// element.
Value MatchValue `json:",omitempty"`
// valueRe caches compiled regexps for "InRegexp" operator
valueRe []*regexp.Regexp
}
// MatchOp is the match operator that is applied on values when evaluating a
// MatchExpression.
type MatchOp string
// MatchValue is the list of values associated with a MatchExpression.
type MatchValue []string
const (
// MatchAny returns always true.
MatchAny MatchOp = ""
// MatchIn returns true if any of the values stored in the expression is
// equal to the input.
MatchIn MatchOp = "In"
// MatchIn returns true if none of the values in the expression are equal
// to the input.
MatchNotIn MatchOp = "NotIn"
// MatchInRegexp treats values of the expression as regular expressions and
// returns true if any of them matches the input.
MatchInRegexp MatchOp = "InRegexp"
// MatchExists returns true if the input is valid. The expression must not
// have any values.
MatchExists MatchOp = "Exists"
// MatchDoesNotExist returns true if the input is not valid. The expression
// must not have any values.
MatchDoesNotExist MatchOp = "DoesNotExist"
// MatchGt returns true if the input is greater than the value of the
// expression (number of values in the expression must be exactly one).
// Both the input and value must be integer numbers, otherwise an error is
// returned.
MatchGt MatchOp = "Gt"
// MatchLt returns true if the input is less than the value of the
// expression (number of values in the expression must be exactly one).
// Both the input and value must be integer numbers, otherwise an error is
// returned.
MatchLt MatchOp = "Lt"
// MatchIsTrue returns true if the input holds the value "true". The
// expression must not have any values.
MatchIsTrue MatchOp = "IsTrue"
// MatchIsTrue returns true if the input holds the value "false". The
// expression must not have any values.
MatchIsFalse MatchOp = "IsFalse"
)
var matchOps = map[MatchOp]struct{}{
MatchAny: struct{}{},
MatchIn: struct{}{},
MatchNotIn: struct{}{},
MatchInRegexp: struct{}{},
MatchExists: struct{}{},
MatchDoesNotExist: struct{}{},
MatchGt: struct{}{},
MatchLt: struct{}{},
MatchIsTrue: struct{}{},
MatchIsFalse: struct{}{},
}
// CreateMatchExpression creates a new MatchExpression instance. Returns an
// error if validation fails.
func CreateMatchExpression(op MatchOp, values ...string) (*MatchExpression, error) {
m := newMatchExpression(op, values...)
return m, m.Validate()
}
// MustCreateMatchExpression creates a new MatchExpression instance. Panics if
// validation fails.
func MustCreateMatchExpression(op MatchOp, values ...string) *MatchExpression {
m, err := CreateMatchExpression(op, values...)
if err != nil {
panic(err)
}
return m
}
// 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 {
m.valueRe = nil
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 MatchInRegexp:
if len(m.Value) == 0 {
return fmt.Errorf("Value must be non-empty for Op %q", m.Op)
}
m.valueRe = make([]*regexp.Regexp, len(m.Value))
for i, v := range m.Value {
re, 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)
}
m.valueRe[i] = re
}
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) {
switch m.Op {
case MatchAny:
return true, nil
case MatchExists:
return valid, nil
case MatchDoesNotExist:
return !valid, nil
}
if valid {
value := fmt.Sprintf("%v", value)
switch m.Op {
case MatchIn:
for _, v := range m.Value {
if value == v {
return true, nil
}
}
case MatchNotIn:
for _, v := range m.Value {
if value == v {
return false, nil
}
}
return true, nil
case MatchInRegexp:
if m.valueRe == nil {
return false, fmt.Errorf("BUG: MatchExpression has not been initialized properly, regexps missing")
}
for _, re := range m.valueRe {
if re.MatchString(value) {
return true, nil
}
}
case MatchGt, MatchLt:
l, err := strconv.Atoi(value)
if err != nil {
return false, fmt.Errorf("not a number %q", value)
}
r, err := strconv.Atoi(m.Value[0])
if err != nil {
return false, fmt.Errorf("not a number %q in %v", m.Value[0], m)
}
if (l < r && m.Op == MatchLt) || (l > r && m.Op == MatchGt) {
return true, nil
}
case MatchIsTrue:
return value == "true", nil
case MatchIsFalse:
return value == "false", nil
default:
return false, fmt.Errorf("unsupported Op %q", m.Op)
}
}
return false, nil
}
// MatchKeys evaluates the MatchExpression against a set of keys.
func (m *MatchExpression) MatchKeys(name string, keys map[string]feature.Nil) (bool, error) {
klog.V(3).Infof("matching %q %q against %v", name, m.Op, keys)
_, ok := keys[name]
switch m.Op {
case MatchAny:
return true, nil
case MatchExists:
return ok, nil
case MatchDoesNotExist:
return !ok, nil
default:
return false, fmt.Errorf("invalid Op %q when matching keys", m.Op)
}
}
// MatchValues evaluates the MatchExpression against a set of key-value pairs.
func (m *MatchExpression) MatchValues(name string, values map[string]string) (bool, error) {
klog.V(3).Infof("matching %q %q %v against %v", name, m.Op, m.Value, values)
v, ok := values[name]
return m.Match(ok, v)
}
// 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]feature.Nil) (bool, error) {
for n, e := range *m {
match, err := e.MatchKeys(n, keys)
if err != nil {
return false, err
}
if !match {
return false, nil
}
}
return true, nil
}
// MatchValues evaluates the MatchExpressionSet against a set of key-value pairs.
func (m *MatchExpressionSet) MatchValues(values map[string]string) (bool, error) {
for n, e := range *m {
match, err := e.MatchValues(n, values)
if err != nil {
return false, err
}
if !match {
return false, nil
}
}
return true, nil
}
// MatchInstances evaluates the MatchExpressionSet against a set of instance
// features, each of which is an individual set of key-value pairs
// (attributes).
func (m *MatchExpressionSet) MatchInstances(instances []feature.InstanceFeature) (bool, error) {
for _, i := range instances {
if match, err := m.MatchValues(i.Attributes); err != nil {
return false, err
} else if match {
return true, nil
}
}
return false, nil
}
// UnmarshalJSON implements the Unmarshaler interface of "encoding/json".
func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
*m = make(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
} else {
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
}

View file

@ -0,0 +1,418 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package expression_test
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/yaml"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
e "sigs.k8s.io/node-feature-discovery/source/custom/expression"
)
type BoolAssertionFuncf func(assert.TestingT, bool, string, ...interface{}) bool
type ValueAssertionFuncf func(assert.TestingT, interface{}, string, ...interface{}) bool
func TestCreateMatchExpression(t *testing.T) {
type V = e.MatchValue
type TC struct {
op e.MatchOp
values V
err ValueAssertionFuncf
}
tcs := []TC{
{op: e.MatchAny, err: assert.Nilf}, // #0
{op: e.MatchAny, values: V{"1"}, err: assert.NotNilf},
{op: e.MatchIn, err: assert.NotNilf},
{op: e.MatchIn, values: V{"1"}, err: assert.Nilf},
{op: e.MatchIn, values: V{"1", "2", "3", "4"}, err: assert.Nilf},
{op: e.MatchNotIn, err: assert.NotNilf},
{op: e.MatchNotIn, values: V{"1"}, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"1", "2"}, err: assert.Nilf},
{op: e.MatchInRegexp, err: assert.NotNilf},
{op: e.MatchInRegexp, values: V{"1"}, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"()", "2", "3"}, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"("}, err: assert.NotNilf},
{op: e.MatchExists, err: assert.Nilf},
{op: e.MatchExists, values: V{"1"}, err: assert.NotNilf},
{op: e.MatchDoesNotExist, err: assert.Nilf},
{op: e.MatchDoesNotExist, values: V{"1"}, err: assert.NotNilf},
{op: e.MatchGt, err: assert.NotNilf},
{op: e.MatchGt, values: V{"1"}, err: assert.Nilf},
{op: e.MatchGt, values: V{"-10"}, err: assert.Nilf},
{op: e.MatchGt, values: V{"1", "2"}, err: assert.NotNilf},
{op: e.MatchGt, values: V{""}, err: assert.NotNilf},
{op: e.MatchLt, err: assert.NotNilf},
{op: e.MatchLt, values: V{"1"}, err: assert.Nilf},
{op: e.MatchLt, values: V{"-1"}, err: assert.Nilf},
{op: e.MatchLt, values: V{"1", "2", "3"}, err: assert.NotNilf},
{op: e.MatchLt, values: V{"a"}, err: assert.NotNilf},
{op: e.MatchIsTrue, err: assert.Nilf},
{op: e.MatchIsTrue, values: V{"1"}, err: assert.NotNilf},
{op: e.MatchIsFalse, err: assert.Nilf},
{op: e.MatchIsFalse, values: V{"1", "2"}, err: assert.NotNilf},
}
for i, tc := range tcs {
_, err := e.CreateMatchExpression(tc.op, tc.values...)
tc.err(t, err, "test case #%d (%v) failed", i, tc)
}
}
func TestMatch(t *testing.T) {
type V = e.MatchValue
type TC struct {
op e.MatchOp
values V
input interface{}
valid bool
result BoolAssertionFuncf
err ValueAssertionFuncf
}
tcs := []TC{
{op: e.MatchAny, result: assert.Truef, err: assert.Nilf},
{op: e.MatchAny, input: "2", valid: false, result: assert.Truef, err: assert.Nilf},
{op: e.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchExists, input: nil, valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchExists, input: nil, valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, input: false, valid: false, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, input: false, valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, input: "3a", valid: true, result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchLt, values: V{"2"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchIsTrue, input: true, valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIsTrue, input: true, valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchIsTrue, input: false, valid: true, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIsFalse, input: "false", valid: false, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIsFalse, input: "false", valid: true, result: assert.Truef, err: assert.Nilf},
{op: e.MatchIsFalse, input: "true", valid: true, result: assert.Falsef, err: assert.Nilf},
}
for i, tc := range tcs {
me := e.MustCreateMatchExpression(tc.op, tc.values...)
res, err := me.Match(tc.valid, tc.input)
tc.result(t, res, "test case #%d (%v) failed", i, tc)
tc.err(t, err, "test case #%d (%v) failed", i, tc)
}
// Check some special error cases separately because MustCreateMatch panics
tcs = []TC{
{op: e.MatchGt, values: V{"3.0"}, input: 1, valid: true},
{op: e.MatchLt, values: V{"0x2"}, input: 1, valid: true},
{op: "non-existent-op", values: V{"1"}, input: 1, valid: true},
}
for i, tc := range tcs {
me := e.MatchExpression{Op: tc.op, Value: tc.values}
res, err := me.Match(tc.valid, tc.input)
assert.Falsef(t, res, "err test case #%d (%v) failed", i, tc)
assert.NotNilf(t, err, "err test case #%d (%v) failed", i, tc)
}
}
func TestMatchKeys(t *testing.T) {
type V = e.MatchValue
type I = map[string]feature.Nil
type TC struct {
op e.MatchOp
values V
name string
input I
result BoolAssertionFuncf
err ValueAssertionFuncf
}
tcs := []TC{
{op: e.MatchAny, result: assert.Truef, err: assert.Nilf},
{op: e.MatchExists, name: "foo", input: nil, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchExists, name: "foo", input: I{"bar": {}}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchExists, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, name: "foo", input: I{}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, name: "foo", input: I{"bar": {}}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Falsef, err: assert.Nilf},
// All other ops should return an error
{op: e.MatchIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchNotIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchInRegexp, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchGt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchLt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchIsFalse, name: "foo", result: assert.Falsef, err: assert.NotNilf},
}
for i, tc := range tcs {
me := e.MustCreateMatchExpression(tc.op, tc.values...)
res, err := me.MatchKeys(tc.name, tc.input)
tc.result(t, res, "test case #%d (%v) failed", i, tc)
tc.err(t, err, "test case #%d (%v) failed", i, tc)
}
}
func TestMatchValues(t *testing.T) {
type V = []string
type I = map[string]string
type TC struct {
op e.MatchOp
values V
name string
input I
result BoolAssertionFuncf
err ValueAssertionFuncf
}
tcs := []TC{
{op: e.MatchAny, result: assert.Truef, err: assert.Nilf},
{op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "9"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchExists, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchExists, name: "foo", input: I{"foo": "1"}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf},
{op: e.MatchDoesNotExist, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
{op: e.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIsTrue, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIsTrue, name: "foo", input: I{"foo": "true"}, result: assert.Truef, err: assert.Nilf},
{op: e.MatchIsFalse, name: "foo", input: I{"foo": "true"}, result: assert.Falsef, err: assert.Nilf},
{op: e.MatchIsFalse, name: "foo", input: I{"foo": "false"}, result: assert.Truef, err: assert.Nilf},
}
for i, tc := range tcs {
me := e.MustCreateMatchExpression(tc.op, tc.values...)
res, err := me.MatchValues(tc.name, tc.input)
tc.result(t, res, "test case #%d (%v) failed", i, tc)
tc.err(t, err, "test case #%d (%v) failed", i, tc)
}
}
func TestMESMatchKeys(t *testing.T) {
type I = map[string]feature.Nil
type TC struct {
mes string
input I
result BoolAssertionFuncf
err ValueAssertionFuncf
}
tcs := []TC{
{result: assert.Truef, err: assert.Nilf},
{input: I{"foo": {}}, result: assert.Truef, err: assert.Nilf},
{mes: `
foo: { op: DoesNotExist }
bar: { op: Exists }
`,
input: I{"bar": {}, "baz": {}},
result: assert.Truef, err: assert.Nilf},
{mes: `
foo: { op: DoesNotExist }
bar: { op: Exists }
`,
input: I{"foo": {}, "bar": {}, "baz": {}},
result: assert.Falsef, err: assert.Nilf},
{mes: `
foo: { op: In, value: ["bar"] }
bar: { op: Exists }
`,
input: I{"bar": {}, "baz": {}},
result: assert.Falsef, err: assert.NotNilf},
}
for i, tc := range tcs {
mes := &e.MatchExpressionSet{}
if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
}
res, err := mes.MatchKeys(tc.input)
tc.result(t, res, "test case #%d (%v) failed", i, tc)
tc.err(t, err, "test case #%d (%v) failed", i, tc)
}
}
func TestMESMatchValues(t *testing.T) {
type I = map[string]string
type TC struct {
mes string
input I
result BoolAssertionFuncf
err ValueAssertionFuncf
}
tcs := []TC{
{result: assert.Truef, err: assert.Nilf},
{input: I{"foo": "bar"}, result: assert.Truef, err: assert.Nilf},
{mes: `
foo: { op: Exists }
bar: { op: In, value: ["val", "wal"] }
baz: { op: Gt, value: ["10"] }
`,
input: I{"bar": "val"},
result: assert.Falsef, err: assert.Nilf},
{mes: `
foo: { op: Exists }
bar: { op: In, value: ["val", "wal"] }
baz: { op: Gt, value: ["10"] }
`,
input: I{"foo": "1", "bar": "val", "baz": "123"},
result: assert.Truef, err: assert.Nilf},
{mes: `
foo: { op: Exists }
bar: { op: In, value: ["val"] }
baz: { op: Gt, value: ["10"] }
`,
input: I{"foo": "1", "bar": "val", "baz": "123.0"},
result: assert.Falsef, err: assert.NotNilf},
}
for i, tc := range tcs {
mes := &e.MatchExpressionSet{}
if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
}
res, err := mes.MatchValues(tc.input)
tc.result(t, res, "test case #%d (%v) failed", i, tc)
tc.err(t, err, "test case #%d (%v) failed", i, tc)
}
}
func TestMESMatchInstances(t *testing.T) {
type I = feature.InstanceFeature
type A = map[string]string
type TC struct {
mes string
input []I
result BoolAssertionFuncf
err ValueAssertionFuncf
}
tcs := []TC{
{result: assert.Falsef, err: assert.Nilf}, // nil instances -> false
{input: []I{}, result: assert.Falsef, err: assert.Nilf}, // zero instances -> false
{input: []I{I{Attributes: A{}}}, result: assert.Truef, err: assert.Nilf}, // one "empty" instance
{mes: `
foo: { op: Exists }
bar: { op: Lt, value: ["10"] }
`,
input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}},
result: assert.Falsef, err: assert.Nilf},
{mes: `
foo: { op: Exists }
bar: { op: Lt, value: ["10"] }
`,
input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}},
result: assert.Truef, err: assert.Nilf},
{mes: `
bar: { op: Lt, value: ["10"] }
`,
input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}},
result: assert.Falsef, err: assert.NotNilf},
}
for i, tc := range tcs {
mes := &e.MatchExpressionSet{}
if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
}
res, err := mes.MatchInstances(tc.input)
tc.result(t, res, "test case #%d (%v) failed", i, tc)
tc.err(t, err, "test case #%d (%v) failed", i, tc)
}
}

View file

@ -21,21 +21,18 @@ import (
"sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/cpu" "sigs.k8s.io/node-feature-discovery/source/cpu"
"sigs.k8s.io/node-feature-discovery/source/custom/expression"
) )
// CpuIDRule implements Rule for the custom source // CpuIDRule implements Rule for the custom source
type CpuIDRule []string type CpuIDRule struct {
expression.MatchExpressionSet
}
func (cpuids *CpuIDRule) Match() (bool, error) { func (r *CpuIDRule) Match() (bool, error) {
flags, ok := source.GetFeatureSource("cpu").GetFeatures().Keys[cpu.CpuidFeature] flags, ok := source.GetFeatureSource("cpu").GetFeatures().Keys[cpu.CpuidFeature]
if !ok { if !ok {
return false, fmt.Errorf("cpuid information not available") return false, fmt.Errorf("cpuid information not available")
} }
return r.MatchKeys(flags.Elements)
for _, f := range *cpuids {
if _, ok := flags.Elements[f]; !ok {
return false, nil
}
}
return true, nil
} }

View file

@ -17,48 +17,22 @@ limitations under the License.
package rules package rules
import ( import (
"encoding/json"
"fmt" "fmt"
"strings"
"sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/custom/expression"
"sigs.k8s.io/node-feature-discovery/source/kernel" "sigs.k8s.io/node-feature-discovery/source/kernel"
) )
// KconfigRule implements Rule // KconfigRule implements Rule for the custom source
type KconfigRule []kconfig type KconfigRule struct {
expression.MatchExpressionSet
type kconfig struct {
Name string
Value string
} }
func (kconfigs *KconfigRule) Match() (bool, error) { func (r *KconfigRule) Match() (bool, error) {
options, ok := source.GetFeatureSource("kernel").GetFeatures().Values[kernel.ConfigFeature] options, ok := source.GetFeatureSource("kernel").GetFeatures().Values[kernel.ConfigFeature]
if !ok { if !ok {
return false, fmt.Errorf("kernel config options not available") return false, fmt.Errorf("kernel config options not available")
} }
return r.MatchValues(options.Elements)
for _, f := range *kconfigs {
if v, ok := options.Elements[f.Name]; !ok || f.Value != v {
return false, nil
}
}
return true, nil
}
func (c *kconfig) UnmarshalJSON(data []byte) error {
var raw string
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
split := strings.SplitN(raw, "=", 2)
c.Name = split[0]
if len(split) == 1 {
c.Value = "true"
} else {
c.Value = split[1]
}
return nil
} }

View file

@ -20,24 +20,21 @@ import (
"fmt" "fmt"
"sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/custom/expression"
"sigs.k8s.io/node-feature-discovery/source/kernel" "sigs.k8s.io/node-feature-discovery/source/kernel"
) )
// LoadedKModRule matches loaded kernel modules in the system // LoadedKModRule matches loaded kernel modules in the system
type LoadedKModRule []string type LoadedKModRule struct {
expression.MatchExpressionSet
}
// Match loaded kernel modules on provided list of kernel modules // Match loaded kernel modules on provided list of kernel modules
func (kmods *LoadedKModRule) Match() (bool, error) { func (r *LoadedKModRule) Match() (bool, error) {
modules, ok := source.GetFeatureSource("kernel").GetFeatures().Keys[kernel.LoadedModuleFeature] modules, ok := source.GetFeatureSource("kernel").GetFeatures().Keys[kernel.LoadedModuleFeature]
if !ok { if !ok {
return false, fmt.Errorf("info about loaded modules not available") return false, fmt.Errorf("info about loaded modules not available")
} }
for _, kmod := range *kmods { return r.MatchKeys(modules.Elements)
if _, ok := modules.Elements[kmod]; !ok {
// kernel module not loaded
return false, nil
}
}
return true, nil
} }

View file

@ -17,40 +17,35 @@ limitations under the License.
package rules package rules
import ( import (
"encoding/json"
"fmt" "fmt"
"regexp"
"k8s.io/klog/v2"
"sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/custom/expression"
"sigs.k8s.io/node-feature-discovery/source/system" "sigs.k8s.io/node-feature-discovery/source/system"
) )
// NodenameRule matches on nodenames configured in a ConfigMap // NodenameRule matches on nodenames configured in a ConfigMap
type NodenameRule []string type NodenameRule struct {
expression.MatchExpression
}
// Force implementation of Rule func (r *NodenameRule) Match() (bool, error) {
var _ Rule = NodenameRule{}
func (n NodenameRule) Match() (bool, error) {
nodeName, ok := source.GetFeatureSource("system").GetFeatures().Values[system.NameFeature].Elements["nodename"] nodeName, ok := source.GetFeatureSource("system").GetFeatures().Values[system.NameFeature].Elements["nodename"]
if !ok { if !ok || nodeName == "" {
return false, fmt.Errorf("node name not available") return false, fmt.Errorf("node name not available")
} }
return r.MatchExpression.Match(true, nodeName)
for _, nodenamePattern := range n { }
klog.V(1).Infof("matchNodename %s", nodenamePattern)
match, err := regexp.MatchString(nodenamePattern, nodeName) func (r *NodenameRule) UnmarshalJSON(data []byte) error {
if err != nil { if err := json.Unmarshal(data, &r.MatchExpression); err != nil {
klog.Errorf("nodename rule: invalid nodename regexp %q: %v", nodenamePattern, err) return err
continue }
} // Force regexp matching
if !match { if r.Op == expression.MatchIn {
klog.V(2).Infof("nodename rule: No match for pattern %q with node %q", nodenamePattern, nodeName) r.Op = expression.MatchInRegexp
continue }
} // We need to run Validate() because operator forcing above
klog.V(2).Infof("nodename rule: Match for pattern %q with node %q", nodenamePattern, nodeName) return r.Validate()
return true, nil
}
return false, nil
} }

View file

@ -19,23 +19,13 @@ package rules
import ( import (
"fmt" "fmt"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
"sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/custom/expression"
"sigs.k8s.io/node-feature-discovery/source/pci" "sigs.k8s.io/node-feature-discovery/source/pci"
) )
// Rule that matches on the following PCI device attributes: <class, vendor, device>
// each device attribute will be a list elements(strings).
// Match operation: OR will be performed per element and AND will be performed per attribute.
// An empty attribute will not be included in the matching process.
type PciIDRuleInput struct {
Class []string `json:"class,omitempty"`
Vendor []string `json:"vendor,omitempty"`
Device []string `json:"device,omitempty"`
}
type PciIDRule struct { type PciIDRule struct {
PciIDRuleInput expression.MatchExpressionSet
} }
// Match PCI devices on provided PCI device attributes // Match PCI devices on provided PCI device attributes
@ -45,46 +35,5 @@ func (r *PciIDRule) Match() (bool, error) {
return false, fmt.Errorf("cpuid information not available") return false, fmt.Errorf("cpuid information not available")
} }
devAttr := map[string]bool{} return r.MatchInstances(devs.Elements)
for _, attr := range []string{"class", "vendor", "device"} {
devAttr[attr] = true
}
for _, dev := range devs.Elements {
// match rule on a single device
if r.matchDevOnRule(dev) {
return true, nil
}
}
return false, nil
}
func (r *PciIDRule) matchDevOnRule(dev feature.InstanceFeature) bool {
if len(r.Class) == 0 && len(r.Vendor) == 0 && len(r.Device) == 0 {
return false
}
attrs := dev.Attributes
if len(r.Class) > 0 && !in(attrs["class"], r.Class) {
return false
}
if len(r.Vendor) > 0 && !in(attrs["vendor"], r.Vendor) {
return false
}
if len(r.Device) > 0 && !in(attrs["device"], r.Device) {
return false
}
return true
}
func in(item string, arr []string) bool {
for _, val := range arr {
if val == item {
return true
}
}
return false
} }

View file

@ -1,22 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rules
type Rule interface {
// Match on rule
Match() (bool, error)
}

View file

@ -19,24 +19,13 @@ package rules
import ( import (
"fmt" "fmt"
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
"sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source"
"sigs.k8s.io/node-feature-discovery/source/custom/expression"
"sigs.k8s.io/node-feature-discovery/source/usb" "sigs.k8s.io/node-feature-discovery/source/usb"
) )
// Rule that matches on the following USB device attributes: <class, vendor, device>
// each device attribute will be a list elements(strings).
// Match operation: OR will be performed per element and AND will be performed per attribute.
// An empty attribute will not be included in the matching process.
type UsbIDRuleInput struct {
Class []string `json:"class,omitempty"`
Vendor []string `json:"vendor,omitempty"`
Device []string `json:"device,omitempty"`
Serial []string `json:"serial,omitempty"`
}
type UsbIDRule struct { type UsbIDRule struct {
UsbIDRuleInput expression.MatchExpressionSet
} }
// Match USB devices on provided USB device attributes // Match USB devices on provided USB device attributes
@ -45,37 +34,5 @@ func (r *UsbIDRule) Match() (bool, error) {
if !ok { if !ok {
return false, fmt.Errorf("usb device information not available") return false, fmt.Errorf("usb device information not available")
} }
return r.MatchInstances(devs.Elements)
for _, dev := range devs.Elements {
// match rule on a single device
if r.matchDevOnRule(dev) {
return true, nil
}
}
return false, nil
}
func (r *UsbIDRule) matchDevOnRule(dev feature.InstanceFeature) bool {
if len(r.Class) == 0 && len(r.Vendor) == 0 && len(r.Device) == 0 {
return false
}
attrs := dev.Attributes
if len(r.Class) > 0 && !in(attrs["class"], r.Class) {
return false
}
if len(r.Vendor) > 0 && !in(attrs["vendor"], r.Vendor) {
return false
}
if len(r.Device) > 0 && !in(attrs["device"], r.Device) {
return false
}
if len(r.Serial) > 0 && !in(attrs["serial"], r.Serial) {
return false
}
return true
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
package custom package custom
import ( import (
"sigs.k8s.io/node-feature-discovery/source/custom/expression"
"sigs.k8s.io/node-feature-discovery/source/custom/rules" "sigs.k8s.io/node-feature-discovery/source/custom/rules"
) )
@ -29,7 +30,9 @@ func getStaticFeatureConfig() []FeatureSpec {
MatchOn: []MatchRule{ MatchOn: []MatchRule{
{ {
PciID: &rules.PciIDRule{ PciID: &rules.PciIDRule{
PciIDRuleInput: rules.PciIDRuleInput{Vendor: []string{"15b3"}}, MatchExpressionSet: expression.MatchExpressionSet{
"vendor": expression.MustCreateMatchExpression(expression.MatchIn, "15b3"),
},
}, },
}, },
}, },
@ -38,7 +41,12 @@ func getStaticFeatureConfig() []FeatureSpec {
Name: "rdma.available", Name: "rdma.available",
MatchOn: []MatchRule{ MatchOn: []MatchRule{
{ {
LoadedKMod: &rules.LoadedKModRule{"ib_uverbs", "rdma_ucm"}, LoadedKMod: &rules.LoadedKModRule{
MatchExpressionSet: expression.MatchExpressionSet{
"ib_uverbs": expression.MustCreateMatchExpression(expression.MatchExists),
"rdma_ucm": expression.MustCreateMatchExpression(expression.MatchExists),
},
},
}, },
}, },
}, },