mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-03-13 20:30:03 +00:00
Merge pull request #1479 from marquiz/devel/api-internal
source/custom: add internal rule api
This commit is contained in:
commit
7ae25167fe
11 changed files with 934 additions and 293 deletions
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"}}
|
||||
|
|
111
source/custom/api/conversion.go
Normal file
111
source/custom/api/conversion.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2023 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 api
|
||||
|
||||
import (
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||
)
|
||||
|
||||
// convertFeaturematchertermToV1alpha1 converts the internal api type to nfdv1alpha1.
|
||||
func convertFeaturematchertermToV1alpha1(in *FeatureMatcherTerm, out *nfdv1alpha1.FeatureMatcherTerm) error {
|
||||
out.Feature = in.Feature
|
||||
if in.MatchExpressions != nil {
|
||||
inME := in.MatchExpressions
|
||||
outME := make(nfdv1alpha1.MatchExpressionSet, len(*inME))
|
||||
for key := range *inME {
|
||||
me := &nfdv1alpha1.MatchExpression{}
|
||||
if err := convertMatchexpressionToV1alpha1((*inME)[key], me); err != nil {
|
||||
return err
|
||||
}
|
||||
outME[key] = me
|
||||
}
|
||||
out.MatchExpressions = &outME
|
||||
} else {
|
||||
out.MatchExpressions = nil
|
||||
}
|
||||
|
||||
if in.MatchName != nil {
|
||||
out.MatchName = &nfdv1alpha1.MatchExpression{}
|
||||
if err := convertMatchexpressionToV1alpha1(in.MatchName, out.MatchName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
out.MatchName = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertMatchanyelemToV1alpha1 converts the internal api type to nfdv1alpha1.
|
||||
func convertMatchanyelemToV1alpha1(in *MatchAnyElem, out *nfdv1alpha1.MatchAnyElem) error {
|
||||
if in.MatchFeatures != nil {
|
||||
inMF, outMF := &in.MatchFeatures, &out.MatchFeatures
|
||||
*outMF = make(nfdv1alpha1.FeatureMatcher, len(*inMF))
|
||||
for i := range *inMF {
|
||||
if err := convertFeaturematchertermToV1alpha1(&(*inMF)[i], &(*outMF)[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.MatchFeatures = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertMatchexpressionToV1alpha1 converts the internal api type to nfdv1alpha1.
|
||||
func convertMatchexpressionToV1alpha1(in *MatchExpression, out *nfdv1alpha1.MatchExpression) error {
|
||||
out.Op = nfdv1alpha1.MatchOp(in.Op)
|
||||
if in.Value != nil {
|
||||
in, out := &in.Value, &out.Value
|
||||
*out = make(nfdv1alpha1.MatchValue, len(*in))
|
||||
copy(*out, *in)
|
||||
} else {
|
||||
out.Value = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertRuleToV1alpha1 converts the internal api type to nfdv1alpha1.
|
||||
func ConvertRuleToV1alpha1(in *Rule, out *nfdv1alpha1.Rule) error {
|
||||
out.Name = in.Name
|
||||
out.Labels = in.Labels
|
||||
out.LabelsTemplate = in.LabelsTemplate
|
||||
out.Vars = in.Vars
|
||||
out.VarsTemplate = in.VarsTemplate
|
||||
if in.MatchFeatures != nil {
|
||||
in, out := &in.MatchFeatures, &out.MatchFeatures
|
||||
*out = make(nfdv1alpha1.FeatureMatcher, len(*in))
|
||||
for i := range *in {
|
||||
if err := convertFeaturematchertermToV1alpha1(&(*in)[i], &(*out)[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.MatchFeatures = nil
|
||||
}
|
||||
if in.MatchAny != nil {
|
||||
in, out := &in.MatchAny, &out.MatchAny
|
||||
*out = make([]nfdv1alpha1.MatchAnyElem, len(*in))
|
||||
for i := range *in {
|
||||
if err := convertMatchanyelemToV1alpha1(&(*in)[i], &(*out)[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.MatchAny = nil
|
||||
}
|
||||
return nil
|
||||
}
|
196
source/custom/api/conversion_test.go
Normal file
196
source/custom/api/conversion_test.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
Copyright 2023 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 api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||
)
|
||||
|
||||
func TestRuleConversion(t *testing.T) {
|
||||
type TC struct {
|
||||
name string
|
||||
internal Rule
|
||||
external nfdv1alpha1.Rule
|
||||
}
|
||||
tcs := []TC{
|
||||
{
|
||||
name: "empty rule",
|
||||
internal: Rule{},
|
||||
external: nfdv1alpha1.Rule{},
|
||||
},
|
||||
{
|
||||
name: "all fields populated",
|
||||
internal: Rule{
|
||||
Name: "test rule 1",
|
||||
Labels: map[string]string{
|
||||
"label-1": "val-1",
|
||||
"label-2": "val-2",
|
||||
},
|
||||
LabelsTemplate: "{{ range .fake.attribute }}example.com/fake-{{ .Name }}={{ .Value }}\n{{ end }}",
|
||||
Vars: map[string]string{
|
||||
"var-a": "val-a",
|
||||
"var-b": "val-b",
|
||||
},
|
||||
VarsTemplate: "{{ range .fake.attribute }}fake-{{ .Name }}={{ .Value }}\n{{ end }}",
|
||||
MatchFeatures: FeatureMatcher{
|
||||
FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &MatchExpressionSet{
|
||||
"attr_1": &MatchExpression{Op: MatchIn, Value: MatchValue{"true"}},
|
||||
"attr_2": &MatchExpression{Op: MatchInRegexp, Value: MatchValue{"^f"}},
|
||||
},
|
||||
MatchName: &MatchExpression{Op: MatchIn, Value: MatchValue{"elem-1"}},
|
||||
},
|
||||
},
|
||||
MatchAny: []MatchAnyElem{
|
||||
{
|
||||
MatchFeatures: FeatureMatcher{
|
||||
FeatureMatcherTerm{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &MatchExpressionSet{
|
||||
"name": &MatchExpression{Op: MatchNotIn, Value: MatchValue{"instance_1"}},
|
||||
},
|
||||
MatchName: &MatchExpression{Op: MatchIn, Value: MatchValue{"elem-2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
external: nfdv1alpha1.Rule{
|
||||
Name: "test rule 1",
|
||||
Labels: map[string]string{
|
||||
"label-1": "val-1",
|
||||
"label-2": "val-2",
|
||||
},
|
||||
LabelsTemplate: "{{ range .fake.attribute }}example.com/fake-{{ .Name }}={{ .Value }}\n{{ end }}",
|
||||
Vars: map[string]string{
|
||||
"var-a": "val-a",
|
||||
"var-b": "val-b",
|
||||
},
|
||||
VarsTemplate: "{{ range .fake.attribute }}fake-{{ .Name }}={{ .Value }}\n{{ end }}",
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
|
||||
"attr_1": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"true"}},
|
||||
"attr_2": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchInRegexp, Value: nfdv1alpha1.MatchValue{"^f"}},
|
||||
},
|
||||
MatchName: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"elem-1"}},
|
||||
},
|
||||
},
|
||||
MatchAny: []nfdv1alpha1.MatchAnyElem{
|
||||
{
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
|
||||
"name": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchNotIn, Value: nfdv1alpha1.MatchValue{"instance_1"}},
|
||||
},
|
||||
MatchName: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"elem-2"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "matchName is nil",
|
||||
internal: Rule{
|
||||
Name: "test rule 1",
|
||||
MatchFeatures: FeatureMatcher{
|
||||
FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &MatchExpressionSet{
|
||||
"attr_1": &MatchExpression{Op: MatchIsTrue},
|
||||
},
|
||||
MatchName: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
external: nfdv1alpha1.Rule{
|
||||
Name: "test rule 1",
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
|
||||
"attr_1": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIsTrue},
|
||||
},
|
||||
MatchName: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "matchExpressions is empty",
|
||||
internal: Rule{
|
||||
Name: "test rule 1",
|
||||
MatchFeatures: FeatureMatcher{
|
||||
FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &MatchExpressionSet{},
|
||||
},
|
||||
},
|
||||
},
|
||||
external: nfdv1alpha1.Rule{
|
||||
Name: "test rule 1",
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "matchExpressions is nil",
|
||||
internal: Rule{
|
||||
Name: "test rule 1",
|
||||
MatchFeatures: FeatureMatcher{
|
||||
FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: nil,
|
||||
MatchName: &MatchExpression{Op: MatchInRegexp, Value: MatchValue{"^elem-"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
external: nfdv1alpha1.Rule{
|
||||
Name: "test rule 1",
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: nil,
|
||||
MatchName: &v1alpha1.MatchExpression{Op: v1alpha1.MatchInRegexp, Value: v1alpha1.MatchValue{"^elem-"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := nfdv1alpha1.Rule{}
|
||||
err := ConvertRuleToV1alpha1(&tc.internal, &out)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tc.external, out)
|
||||
})
|
||||
}
|
||||
}
|
218
source/custom/api/expression.go
Normal file
218
source/custom/api/expression.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
Copyright 2023 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 api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var matchOps = map[MatchOp]struct{}{
|
||||
MatchAny: {},
|
||||
MatchIn: {},
|
||||
MatchNotIn: {},
|
||||
MatchInRegexp: {},
|
||||
MatchExists: {},
|
||||
MatchDoesNotExist: {},
|
||||
MatchGt: {},
|
||||
MatchLt: {},
|
||||
MatchGtLt: {},
|
||||
MatchIsTrue: {},
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
188
source/custom/api/expression_test.go
Normal file
188
source/custom/api/expression_test.go
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
Copyright 2023 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 api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool
|
||||
|
||||
type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool
|
||||
|
||||
func TestMatchExpressionValidate(t *testing.T) {
|
||||
type V = MatchValue
|
||||
type TC struct {
|
||||
name string
|
||||
op MatchOp
|
||||
values V
|
||||
err ValueAssertionFunc
|
||||
}
|
||||
|
||||
tcs := []TC{
|
||||
{name: "1", op: MatchAny, err: assert.Nil}, // #0
|
||||
{name: "2", op: MatchAny, values: V{"1"}, err: assert.NotNil},
|
||||
|
||||
{name: "3", op: MatchIn, err: assert.NotNil},
|
||||
{name: "4", op: MatchIn, values: V{"1"}, err: assert.Nil},
|
||||
{name: "5", op: MatchIn, values: V{"1", "2", "3", "4"}, err: assert.Nil},
|
||||
|
||||
{name: "6", op: MatchNotIn, err: assert.NotNil},
|
||||
{name: "7", op: MatchNotIn, values: V{"1"}, err: assert.Nil},
|
||||
{name: "8", op: MatchNotIn, values: V{"1", "2"}, err: assert.Nil},
|
||||
|
||||
{name: "9", op: MatchInRegexp, err: assert.NotNil},
|
||||
{name: "10", op: MatchInRegexp, values: V{"1"}, err: assert.Nil},
|
||||
{name: "11", op: MatchInRegexp, values: V{"()", "2", "3"}, err: assert.Nil},
|
||||
{name: "12", op: MatchInRegexp, values: V{"("}, err: assert.NotNil},
|
||||
|
||||
{name: "13", op: MatchExists, err: assert.Nil},
|
||||
{name: "14", op: MatchExists, values: V{"1"}, err: assert.NotNil},
|
||||
|
||||
{name: "15", op: MatchDoesNotExist, err: assert.Nil},
|
||||
{name: "16", op: MatchDoesNotExist, values: V{"1"}, err: assert.NotNil},
|
||||
|
||||
{name: "17", op: MatchGt, err: assert.NotNil},
|
||||
{name: "18", op: MatchGt, values: V{"1"}, err: assert.Nil},
|
||||
{name: "19", op: MatchGt, values: V{"-10"}, err: assert.Nil},
|
||||
{name: "20", op: MatchGt, values: V{"1", "2"}, err: assert.NotNil},
|
||||
{name: "21", op: MatchGt, values: V{""}, err: assert.NotNil},
|
||||
|
||||
{name: "22", op: MatchLt, err: assert.NotNil},
|
||||
{name: "23", op: MatchLt, values: V{"1"}, err: assert.Nil},
|
||||
{name: "24", op: MatchLt, values: V{"-1"}, err: assert.Nil},
|
||||
{name: "25", op: MatchLt, values: V{"1", "2", "3"}, err: assert.NotNil},
|
||||
{name: "26", op: MatchLt, values: V{"a"}, err: assert.NotNil},
|
||||
|
||||
{name: "27", op: MatchGtLt, err: assert.NotNil},
|
||||
{name: "28", op: MatchGtLt, values: V{"1"}, err: assert.NotNil},
|
||||
{name: "29", op: MatchGtLt, values: V{"1", "2"}, err: assert.Nil},
|
||||
{name: "30", op: MatchGtLt, values: V{"2", "1"}, err: assert.NotNil},
|
||||
{name: "31", op: MatchGtLt, values: V{"1", "2", "3"}, err: assert.NotNil},
|
||||
{name: "32", op: MatchGtLt, values: V{"a", "2"}, err: assert.NotNil},
|
||||
|
||||
{name: "33", op: MatchIsTrue, err: assert.Nil},
|
||||
{name: "34", op: MatchIsTrue, values: V{"1"}, err: assert.NotNil},
|
||||
|
||||
{name: "35", op: MatchIsFalse, err: assert.Nil},
|
||||
{name: "36", op: MatchIsFalse, values: V{"1", "2"}, err: assert.NotNil},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
me := MatchExpression{Op: tc.op, Value: tc.values}
|
||||
err := me.Validate()
|
||||
tc.err(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalMatchExpressionSet(t *testing.T) {
|
||||
type TC struct {
|
||||
name string
|
||||
data string
|
||||
out MatchExpressionSet
|
||||
err ValueAssertionFunc
|
||||
}
|
||||
|
||||
tcs := []TC{
|
||||
{
|
||||
name: "empty",
|
||||
data: "{}",
|
||||
out: MatchExpressionSet{},
|
||||
err: assert.Nil,
|
||||
},
|
||||
{
|
||||
name: "multiple expressions",
|
||||
data: "{}",
|
||||
out: MatchExpressionSet{},
|
||||
err: assert.Nil,
|
||||
},
|
||||
{
|
||||
name: "multiple expressions",
|
||||
data: `{
|
||||
"key-1":{"op":"Exists"},
|
||||
"key-2":{"op":"DoesNotExist"},
|
||||
"key-3":{"op":"IsTrue"},
|
||||
"key-4":{"op":"IsFalse"},
|
||||
"key-5":{"op":"In","value":["str","true"]},
|
||||
"key-6":{"op":"InRegexp","value":["^foo$"]},
|
||||
"key-7":{"op":"Lt","value":1},
|
||||
"key-8":{"op":"Gt","value":2},
|
||||
"key-9":{"op":"GtLt","value":["0","3"]}}`,
|
||||
out: MatchExpressionSet{
|
||||
"key-1": &MatchExpression{Op: MatchExists},
|
||||
"key-2": &MatchExpression{Op: MatchDoesNotExist},
|
||||
"key-3": &MatchExpression{Op: MatchIsTrue},
|
||||
"key-4": &MatchExpression{Op: MatchIsFalse},
|
||||
"key-5": &MatchExpression{Op: MatchIn, Value: MatchValue{"str", "true"}},
|
||||
"key-6": &MatchExpression{Op: MatchInRegexp, Value: MatchValue{"^foo$"}},
|
||||
"key-7": &MatchExpression{Op: MatchLt, Value: MatchValue{"1"}},
|
||||
"key-8": &MatchExpression{Op: MatchGt, Value: MatchValue{"2"}},
|
||||
"key-9": &MatchExpression{Op: MatchGtLt, Value: MatchValue{"0", "3"}},
|
||||
},
|
||||
err: assert.Nil,
|
||||
},
|
||||
{
|
||||
name: "special values",
|
||||
data: `{
|
||||
"key-1":{"op":"In","value":"str"},
|
||||
"key-2":{"op":"In","value":true},
|
||||
"key-3":{"op":"In","value":1.23}}`,
|
||||
out: MatchExpressionSet{
|
||||
"key-1": &MatchExpression{Op: MatchIn, Value: MatchValue{"str"}},
|
||||
"key-2": &MatchExpression{Op: MatchIn, Value: MatchValue{"true"}},
|
||||
"key-3": &MatchExpression{Op: MatchIn, Value: MatchValue{"1.23"}},
|
||||
},
|
||||
err: assert.Nil,
|
||||
},
|
||||
{
|
||||
name: "shortform array",
|
||||
data: `["key-1","key-2=val-2"]`,
|
||||
out: MatchExpressionSet{
|
||||
"key-1": &MatchExpression{Op: MatchExists},
|
||||
"key-2": &MatchExpression{Op: MatchIn, Value: MatchValue{"val-2"}},
|
||||
},
|
||||
err: assert.Nil,
|
||||
},
|
||||
{
|
||||
name: "shortform string",
|
||||
data: `{"key":"value"}`,
|
||||
out: MatchExpressionSet{
|
||||
"key": &MatchExpression{Op: MatchIn, Value: MatchValue{"value"}},
|
||||
},
|
||||
err: assert.Nil,
|
||||
},
|
||||
{
|
||||
name: "Lt nan error",
|
||||
data: `{"key-7":{"op":"Lt","value":"str"}}`,
|
||||
out: MatchExpressionSet{},
|
||||
err: assert.NotNil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mes := &MatchExpressionSet{}
|
||||
err := mes.UnmarshalJSON([]byte(tc.data))
|
||||
tc.err(t, err)
|
||||
assert.Equal(t, tc.out, *mes)
|
||||
})
|
||||
}
|
||||
}
|
151
source/custom/api/types.go
Normal file
151
source/custom/api/types.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
Copyright 2023 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 api
|
||||
|
||||
// Rule defines a rule for node customization such as labeling.
|
||||
type Rule struct {
|
||||
// Name of the rule.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Labels to create if the rule matches.
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels"`
|
||||
|
||||
// LabelsTemplate specifies a template to expand for dynamically generating
|
||||
// multiple labels. Data (after template expansion) must be keys with an
|
||||
// optional value (<key>[=<value>]) separated by newlines.
|
||||
// +optional
|
||||
LabelsTemplate string `json:"labelsTemplate"`
|
||||
|
||||
// Vars is the variables to store if the rule matches. Variables do not
|
||||
// directly inflict any changes in the node object. However, they can be
|
||||
// referenced from other rules enabling more complex rule hierarchies,
|
||||
// without exposing intermediary output values as labels.
|
||||
// +optional
|
||||
Vars map[string]string `json:"vars"`
|
||||
|
||||
// VarsTemplate specifies a template to expand for dynamically generating
|
||||
// multiple variables. Data (after template expansion) must be keys with an
|
||||
// optional value (<key>[=<value>]) separated by newlines.
|
||||
// +optional
|
||||
VarsTemplate string `json:"varsTemplate"`
|
||||
|
||||
// MatchFeatures specifies a set of matcher terms all of which must match.
|
||||
// +optional
|
||||
MatchFeatures FeatureMatcher `json:"matchFeatures"`
|
||||
|
||||
// MatchAny specifies a list of matchers one of which must match.
|
||||
// +optional
|
||||
MatchAny []MatchAnyElem `json:"matchAny"`
|
||||
}
|
||||
|
||||
// MatchAnyElem specifies one sub-matcher of MatchAny.
|
||||
type MatchAnyElem struct {
|
||||
// MatchFeatures specifies a set of matcher terms all of which must match.
|
||||
MatchFeatures FeatureMatcher `json:"matchFeatures"`
|
||||
}
|
||||
|
||||
// FeatureMatcher specifies a set of feature matcher terms (i.e. per-feature
|
||||
// matchers), all of which must match.
|
||||
type FeatureMatcher []FeatureMatcherTerm
|
||||
|
||||
// FeatureMatcherTerm defines requirements against one feature set. All
|
||||
// requirements (specified as MatchExpressions) are evaluated against each
|
||||
// element in the feature set.
|
||||
type FeatureMatcherTerm struct {
|
||||
// Feature is the name of the feature set to match against.
|
||||
Feature string `json:"feature"`
|
||||
// MatchExpressions is the set of per-element expressions evaluated. These
|
||||
// match against the value of the specified elements.
|
||||
// +optional
|
||||
MatchExpressions *MatchExpressionSet `json:"matchExpressions"`
|
||||
// MatchName in an expression that is matched against the name of each
|
||||
// element in the feature set.
|
||||
// +optional
|
||||
MatchName *MatchExpression `json:"matchName"`
|
||||
}
|
||||
|
||||
// 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: 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 `json:"op"`
|
||||
|
||||
// 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 and exactly two elements if the operator is GtLt.
|
||||
// In other cases Value should contain at least one element.
|
||||
// +optional
|
||||
Value MatchValue `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// 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"
|
||||
// MatchNotIn 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"
|
||||
// MatchGtLt returns true if the input is between two values, i.e. greater
|
||||
// than the first value and less than the second value of the expression
|
||||
// (number of values in the expression must be exactly two). Both the input
|
||||
// and values must be integer numbers, otherwise an error is returned.
|
||||
MatchGtLt MatchOp = "GtLt"
|
||||
// MatchIsTrue returns true if the input holds the value "true". The
|
||||
// expression must not have any values.
|
||||
MatchIsTrue MatchOp = "IsTrue"
|
||||
// MatchIsFalse returns true if the input holds the value "false". The
|
||||
// expression must not have any values.
|
||||
MatchIsFalse MatchOp = "IsFalse"
|
||||
)
|
|
@ -18,22 +18,21 @@ package custom
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||
"sigs.k8s.io/node-feature-discovery/pkg/utils"
|
||||
"sigs.k8s.io/node-feature-discovery/source"
|
||||
api "sigs.k8s.io/node-feature-discovery/source/custom/api"
|
||||
)
|
||||
|
||||
// Name of this feature source
|
||||
const Name = "custom"
|
||||
|
||||
type CustomRule struct {
|
||||
nfdv1alpha1.Rule
|
||||
}
|
||||
|
||||
type config []CustomRule
|
||||
// The config files use the internal API type.
|
||||
type config []api.Rule
|
||||
|
||||
// newDefaultConfig returns a new config with pre-populated defaults
|
||||
func newDefaultConfig() *config {
|
||||
|
@ -43,13 +42,19 @@ func newDefaultConfig() *config {
|
|||
// customSource implements the LabelSource and ConfigurableSource interfaces.
|
||||
type customSource struct {
|
||||
config *config
|
||||
// The rules are stored in the NFD API format that is a superset of our
|
||||
// internal API and provides the functions for rule matching.
|
||||
rules []nfdv1alpha1.Rule
|
||||
}
|
||||
|
||||
// Singleton source instance
|
||||
var (
|
||||
src = customSource{config: newDefaultConfig()}
|
||||
_ source.LabelSource = &src
|
||||
_ source.ConfigurableSource = &src
|
||||
src = customSource{
|
||||
config: &config{},
|
||||
rules: []nfdv1alpha1.Rule{},
|
||||
}
|
||||
_ source.LabelSource = &src
|
||||
_ source.ConfigurableSource = &src
|
||||
)
|
||||
|
||||
// Name returns the name of the feature source
|
||||
|
@ -65,6 +70,8 @@ func (s *customSource) GetConfig() source.Config { return s.config }
|
|||
func (s *customSource) SetConfig(conf source.Config) {
|
||||
switch v := conf.(type) {
|
||||
case *config:
|
||||
r := []api.Rule(*v)
|
||||
s.rules = convertInternalRulesToNfdApi(&r)
|
||||
s.config = v
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid config type: %T", conf))
|
||||
|
@ -80,8 +87,8 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
|
|||
features := source.GetAllFeatures()
|
||||
|
||||
labels := source.FeatureLabels{}
|
||||
allFeatureConfig := append(getStaticFeatureConfig(), *s.config...)
|
||||
allFeatureConfig = append(allFeatureConfig, getDirectoryFeatureConfig()...)
|
||||
allFeatureConfig := append(getStaticRules(), s.rules...)
|
||||
allFeatureConfig = append(allFeatureConfig, getDropinDirRules()...)
|
||||
klog.V(2).InfoS("resolving custom features", "configuration", utils.DelayedDumper(allFeatureConfig))
|
||||
// Iterate over features
|
||||
for _, rule := range allFeatureConfig {
|
||||
|
@ -102,6 +109,17 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
|
|||
return labels, nil
|
||||
}
|
||||
|
||||
func convertInternalRulesToNfdApi(in *[]api.Rule) []nfdv1alpha1.Rule {
|
||||
out := make([]nfdv1alpha1.Rule, len(*in))
|
||||
for i := range *in {
|
||||
if err := api.ConvertRuleToV1alpha1(&(*in)[i], &out[i]); err != nil {
|
||||
klog.ErrorS(err, "FATAL: API conversion failed")
|
||||
os.Exit(255)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func init() {
|
||||
source.Register(&src)
|
||||
}
|
||||
|
|
|
@ -22,22 +22,24 @@ import (
|
|||
"strings"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||
api "sigs.k8s.io/node-feature-discovery/source/custom/api"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Directory stores the full path for the custom sources folder
|
||||
const Directory = "/etc/kubernetes/node-feature-discovery/custom.d"
|
||||
|
||||
// getDirectoryFeatureConfig returns features configured in the "/etc/kubernetes/node-feature-discovery/custom.d"
|
||||
// getDropinDirRules returns features configured in the "/etc/kubernetes/node-feature-discovery/custom.d"
|
||||
// host directory and its 1st level subdirectories, which can be populated e.g. by ConfigMaps
|
||||
func getDirectoryFeatureConfig() []CustomRule {
|
||||
func getDropinDirRules() []nfdv1alpha1.Rule {
|
||||
features := readDir(Directory, true)
|
||||
klog.V(3).InfoS("all custom feature specs from config dir", "featureSpecs", features)
|
||||
return features
|
||||
}
|
||||
|
||||
func readDir(dirName string, recursive bool) []CustomRule {
|
||||
features := make([]CustomRule, 0)
|
||||
func readDir(dirName string, recursive bool) []nfdv1alpha1.Rule {
|
||||
features := make([]nfdv1alpha1.Rule, 0)
|
||||
|
||||
klog.V(4).InfoS("reading directory", "path", dirName)
|
||||
files, err := os.ReadDir(dirName)
|
||||
|
@ -74,14 +76,14 @@ func readDir(dirName string, recursive bool) []CustomRule {
|
|||
continue
|
||||
}
|
||||
|
||||
config := &[]CustomRule{}
|
||||
config := &[]api.Rule{}
|
||||
err = yaml.UnmarshalStrict(bytes, config)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "could not parse file", "path", fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
features = append(features, *config...)
|
||||
features = append(features, convertInternalRulesToNfdApi(config)...)
|
||||
}
|
||||
return features
|
||||
}
|
||||
|
|
|
@ -20,40 +20,36 @@ import (
|
|||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||
)
|
||||
|
||||
// getStaticFeatures returns statically configured custom features to discover
|
||||
// getStaticRules returns statically configured custom features to discover
|
||||
// e.g RMDA related features. NFD configuration file may extend these custom features by adding rules.
|
||||
func getStaticFeatureConfig() []CustomRule {
|
||||
return []CustomRule{
|
||||
{
|
||||
nfdv1alpha1.Rule{
|
||||
Name: "RDMA capable static rule",
|
||||
Labels: map[string]string{"rdma.capable": "true"},
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "pci.device",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
|
||||
"vendor": &nfdv1alpha1.MatchExpression{
|
||||
Op: nfdv1alpha1.MatchIn,
|
||||
Value: nfdv1alpha1.MatchValue{"15b3"}},
|
||||
},
|
||||
func getStaticRules() []nfdv1alpha1.Rule {
|
||||
return []nfdv1alpha1.Rule{
|
||||
nfdv1alpha1.Rule{
|
||||
Name: "RDMA capable static rule",
|
||||
Labels: map[string]string{"rdma.capable": "true"},
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "pci.device",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
|
||||
"vendor": &nfdv1alpha1.MatchExpression{
|
||||
Op: nfdv1alpha1.MatchIn,
|
||||
Value: nfdv1alpha1.MatchValue{"15b3"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
nfdv1alpha1.Rule{
|
||||
Name: "RDMA available static rule",
|
||||
Labels: map[string]string{"rdma.available": "true"},
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "kernel.loadedmodule",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
|
||||
"ib_uverbs": &nfdv1alpha1.MatchExpression{
|
||||
Op: nfdv1alpha1.MatchExists,
|
||||
},
|
||||
"rdma_ucm": &nfdv1alpha1.MatchExpression{
|
||||
Op: nfdv1alpha1.MatchExists,
|
||||
},
|
||||
nfdv1alpha1.Rule{
|
||||
Name: "RDMA available static rule",
|
||||
Labels: map[string]string{"rdma.available": "true"},
|
||||
MatchFeatures: nfdv1alpha1.FeatureMatcher{
|
||||
nfdv1alpha1.FeatureMatcherTerm{
|
||||
Feature: "kernel.loadedmodule",
|
||||
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
|
||||
"ib_uverbs": &nfdv1alpha1.MatchExpression{
|
||||
Op: nfdv1alpha1.MatchExists,
|
||||
},
|
||||
"rdma_ucm": &nfdv1alpha1.MatchExpression{
|
||||
Op: nfdv1alpha1.MatchExists,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue