1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-05 16:27:05 +00:00

Extend NFR code with MatchStatus and introduce failFast strategy.

MatchStatus provides details about successful expressions and their results,
which are the matched host features. Additionally, a new flag controls
rule processing behavior: it can either stop at the first error or
continue processing all expressions and rules.

Signed-off-by: Marcin Franczyk <marcin0franczyk@gmail.com>
This commit is contained in:
Marcin Franczyk 2024-12-16 16:00:28 +01:00
parent 0188aade60
commit 51bbbe202d
9 changed files with 291 additions and 126 deletions

View file

@ -0,0 +1,48 @@
/*
Copyright 2024 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 v1alpha1
import (
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
)
// ArtifactType is a type of OCI artifact that contains image compatibility metadata.
const (
ArtifactType = "application/vnd.nfd.image-compatibility.v1alpha1"
Version = "v1alpha1"
)
// Spec represents image compatibility metadata.
type Spec struct {
// Version of the spec.
Version string `json:"version"`
// Compatibilities contains list of compatibility sets.
Compatibilties []Compatibility `json:"compatibilities"`
}
// Compatibility represents image compatibility metadata
// that describe the image requirements for the host and OS.
type Compatibility struct {
// Rules represents a list of Node Feature Rules.
Rules []nfdv1alpha1.Rule `json:"rules"`
// Weight indicates the priority of the compatibility set.
Weight int `json:"weight,omitempty"`
// Tag enables grouping or distinguishing between compatibility sets.
Tag string `json:"tag,omitempty"`
// Description of the compatibility set.
Description string `json:"description,omitempty"`
}

View file

@ -17,6 +17,8 @@ limitations under the License.
package v1alpha1
import (
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -298,6 +300,13 @@ type MatchExpression struct {
Value MatchValue `json:"value,omitempty"`
}
func (m MatchExpression) String() string {
if len(m.Value) < 1 {
return fmt.Sprintf("{op: %q}", m.Op)
}
return fmt.Sprintf("{op: %q, value: %q}", m.Op, m.Value)
}
// MatchOp is the match operator that is applied on values when evaluating a
// MatchExpression.
// +kubebuilder:validation:Enum="In";"NotIn";"InRegexp";"Exists";"DoesNotExist";"Gt";"Lt";"GtLt";"IsTrue";"IsFalse"

View file

@ -116,7 +116,7 @@ bar: { op: Exists }
t.Fatal("failed to parse data of test case")
}
res, out, err := api.MatchGetKeys(mes, tc.input)
res, out, _, err := api.MatchGetKeys(mes, tc.input)
tc.result(t, res)
assert.Equal(t, tc.output, out)
tc.err(t, err)
@ -183,12 +183,12 @@ baz: { op: Gt, value: ["10"] }
t.Fatal("failed to parse data of test case")
}
res, out, err := api.MatchGetValues(mes, tc.input)
res, out, _, err := api.MatchGetValues(mes, tc.input, true)
tc.result(t, res)
assert.Equal(t, tc.output, out)
tc.err(t, err)
res, err = api.MatchValues(mes, tc.input)
res, _, err = api.MatchValues(mes, tc.input, true)
tc.result(t, res)
tc.err(t, err)
})
@ -248,12 +248,12 @@ bar: { op: Lt, value: ["10"] }
t.Fatal("failed to parse data of test case")
}
res, out, err := api.MatchGetInstances(mes, tc.input)
res, out, _, err := api.MatchGetInstances(mes, tc.input, true)
assert.Equal(t, tc.output, out)
tc.result(t, res)
tc.err(t, err)
res, err = api.MatchInstances(mes, tc.input)
res, err = api.MatchInstances(mes, tc.input, true)
tc.result(t, res)
tc.err(t, err)
})
@ -745,7 +745,7 @@ func TestMatchMulti(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
res, out, err := api.MatchMulti(tc.mes, tc.inputKeys, tc.inputValues, tc.inputInstances)
res, out, _, err := api.MatchMulti(tc.mes, tc.inputKeys, tc.inputValues, tc.inputInstances, true)
if tc.expectErr {
assert.NotNil(t, err)
} else {

View file

@ -29,6 +29,13 @@ import (
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
)
const (
// MatchedKeyName is the name of the matched flag/attribute element.
MatchedKeyName = "Name"
// MatchedKeyValue is the value of the matched attribute element.
MatchedKeyValue = "Value"
)
var matchOps = map[nfdv1alpha1.MatchOp]struct{}{
nfdv1alpha1.MatchAny: {},
nfdv1alpha1.MatchIn: {},
@ -202,16 +209,16 @@ func MatchKeyNames(m *nfdv1alpha1.MatchExpression, keys map[string]nfdv1alpha1.N
if match, err := evaluateMatchExpression(m, true, k); err != nil {
return false, nil, err
} else if match {
ret = append(ret, MatchedElement{"Name": k})
ret = append(ret, MatchedElement{MatchedKeyName: k})
}
}
// Sort for reproducible output
sort.Slice(ret, func(i, j int) bool { return ret[i]["Name"] < ret[j]["Name"] })
sort.Slice(ret, func(i, j int) bool { return ret[i][MatchedKeyName] < ret[j][MatchedKeyName] })
if klogV3 := klog.V(3); klogV3.Enabled() {
mk := make([]string, len(ret))
for i, v := range ret {
mk[i] = v["Name"]
mk[i] = v[MatchedKeyName]
}
mkMsg := strings.Join(mk, ", ")
@ -238,16 +245,16 @@ func MatchValueNames(m *nfdv1alpha1.MatchExpression, values map[string]string) (
if match, err := evaluateMatchExpression(m, true, k); err != nil {
return false, nil, err
} else if match {
ret = append(ret, MatchedElement{"Name": k, "Value": v})
ret = append(ret, MatchedElement{MatchedKeyName: k, MatchedKeyValue: v})
}
}
// Sort for reproducible output
sort.Slice(ret, func(i, j int) bool { return ret[i]["Name"] < ret[j]["Name"] })
sort.Slice(ret, func(i, j int) bool { return ret[i][MatchedKeyName] < ret[j][MatchedKeyName] })
if klogV3 := klog.V(3); klogV3.Enabled() {
mk := make([]string, len(ret))
for i, v := range ret {
mk[i] = v["Name"]
mk[i] = v[MatchedKeyName]
}
mkMsg := strings.Join(mk, ", ")
@ -278,7 +285,7 @@ func MatchInstanceAttributeNames(m *nfdv1alpha1.MatchExpression, instances []nfd
// MatchKeys evaluates the MatchExpressionSet against a set of keys.
func MatchKeys(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil) (bool, error) {
matched, _, err := MatchGetKeys(m, keys)
matched, _, _, err := MatchGetKeys(m, keys)
return matched, err
}
@ -288,56 +295,65 @@ type MatchedElement map[string]string
// MatchGetKeys evaluates the MatchExpressionSet against a set of keys and
// returns all matched keys or nil if no match was found. Note that an empty
// MatchExpressionSet returns a match with an empty slice of matched features.
func MatchGetKeys(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil) (bool, []MatchedElement, error) {
ret := make([]MatchedElement, 0, len(*m))
func MatchGetKeys(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil) (bool, []MatchedElement, *nfdv1alpha1.MatchExpressionSet, error) {
matchedElements := make([]MatchedElement, 0, len(*m))
matchedExpressions := make(nfdv1alpha1.MatchExpressionSet)
for n, e := range *m {
match, err := evaluateMatchExpressionKeys(e, n, keys)
if err != nil {
return false, nil, err
return false, nil, nil, err
}
if !match {
return false, nil, nil
return false, nil, nil, nil
}
ret = append(ret, MatchedElement{"Name": n})
matchedElements = append(matchedElements, MatchedElement{MatchedKeyName: n})
matchedExpressions[n] = e
}
// Sort for reproducible output
sort.Slice(ret, func(i, j int) bool { return ret[i]["Name"] < ret[j]["Name"] })
return true, ret, nil
sort.Slice(matchedElements, func(i, j int) bool { return matchedElements[i][MatchedKeyName] < matchedElements[j][MatchedKeyName] })
return true, matchedElements, &matchedExpressions, nil
}
// MatchValues evaluates the MatchExpressionSet against a set of key-value pairs.
func MatchValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string) (bool, error) {
matched, _, err := MatchGetValues(m, values)
return matched, err
func MatchValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string, failFast bool) (bool, *nfdv1alpha1.MatchExpressionSet, error) {
matched, _, matchedExpressions, err := MatchGetValues(m, values, failFast)
return matched, matchedExpressions, err
}
// MatchGetValues evaluates the MatchExpressionSet against a set of key-value
// pairs and returns all matched key-value pairs. Note that an empty
// MatchExpressionSet returns a match with an empty slice of matched features.
func MatchGetValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string) (bool, []MatchedElement, error) {
ret := make([]MatchedElement, 0, len(*m))
func MatchGetValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string, failFast bool) (bool, []MatchedElement, *nfdv1alpha1.MatchExpressionSet, error) {
matchedElements := make([]MatchedElement, 0, len(*m))
matchedExpressions := make(nfdv1alpha1.MatchExpressionSet)
isMatch := true
for n, e := range *m {
match, err := evaluateMatchExpressionValues(e, n, values)
if err != nil {
return false, nil, err
return false, nil, nil, err
}
if !match {
return false, nil, nil
if match {
matchedElements = append(matchedElements, MatchedElement{MatchedKeyName: n, MatchedKeyValue: values[n]})
matchedExpressions[n] = e
} else {
if failFast {
return false, nil, nil, nil
}
isMatch = false
}
ret = append(ret, MatchedElement{"Name": n, "Value": values[n]})
}
// Sort for reproducible output
sort.Slice(ret, func(i, j int) bool { return ret[i]["Name"] < ret[j]["Name"] })
return true, ret, nil
sort.Slice(matchedElements, func(i, j int) bool { return matchedElements[i][MatchedKeyName] < matchedElements[j][MatchedKeyName] })
return isMatch, matchedElements, &matchedExpressions, nil
}
// MatchInstances evaluates the MatchExpressionSet against a set of instance
// features, each of which is an individual set of key-value pairs
// (attributes).
func MatchInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) (bool, error) {
isMatch, _, err := MatchGetInstances(m, instances)
func MatchInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature, failFast bool) (bool, error) {
isMatch, _, _, err := MatchGetInstances(m, instances, failFast)
return isMatch, err
}
@ -346,17 +362,28 @@ func MatchInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.I
// (attributes). Returns a boolean that reports whether the expression matched.
// Also, returns a slice containing all matching instances. An empty (non-nil)
// slice is returned if no matching instances were found.
func MatchGetInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) (bool, []MatchedElement, error) {
ret := []MatchedElement{}
func MatchGetInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature, failFast bool) (bool, []MatchedElement, *nfdv1alpha1.MatchExpressionSet, error) {
var (
match bool
err error
expressionSet *nfdv1alpha1.MatchExpressionSet
)
matchedElements := []MatchedElement{}
matchedExpressions := &nfdv1alpha1.MatchExpressionSet{}
for _, i := range instances {
if match, err := MatchValues(m, i.Attributes); err != nil {
return false, nil, err
if match, expressionSet, err = MatchValues(m, i.Attributes, failFast); err != nil {
return false, nil, nil, err
} else if match {
ret = append(ret, i.Attributes)
matchedElements = append(matchedElements, i.Attributes)
}
if expressionSet != nil {
for name, exp := range *expressionSet {
(*matchedExpressions)[name] = exp
}
}
}
return len(ret) > 0, ret, nil
return len(matchedElements) > 0, matchedElements, matchedExpressions, nil
}
// MatchMulti evaluates a MatchExpressionSet against key, value and instance
@ -365,8 +392,9 @@ func MatchGetInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha
// handled separately as the way of evaluating match expressions is different.
// This function is written to handle "multi-type" features where one feature
// (say "cpu.cpuid") contains multiple types (flag, attribute and/or instance).
func MatchMulti(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil, values map[string]string, instances []nfdv1alpha1.InstanceFeature) (bool, []MatchedElement, error) {
func MatchMulti(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil, values map[string]string, instances []nfdv1alpha1.InstanceFeature, failFast bool) (bool, []MatchedElement, *nfdv1alpha1.MatchExpressionSet, error) {
matchedElems := []MatchedElement{}
matchedExpressions := nfdv1alpha1.MatchExpressionSet{}
isMatch := false
// Keys and values are handled as a union, it is enough to find a match in
@ -384,14 +412,19 @@ func MatchMulti(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.N
if keys != nil {
matchK, err = evaluateMatchExpressionKeys(e, n, keys)
if err != nil {
return false, nil, err
return false, nil, nil, err
}
if matchK {
matchedElems = append(matchedElems, MatchedElement{"Name": n})
matchedElems = append(matchedElems, MatchedElement{MatchedKeyName: n})
matchedExpressions[n] = e
} else if e.Op == nfdv1alpha1.MatchDoesNotExist {
// DoesNotExist is special in that both "keys" and "values" should match (i.e. the name is not found in either of them).
isMatch = false
if !failFast {
continue
}
matchedElems = []MatchedElement{}
matchedExpressions = nfdv1alpha1.MatchExpressionSet{}
break
}
}
@ -399,39 +432,53 @@ func MatchMulti(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.N
if values != nil {
matchV, err = evaluateMatchExpressionValues(e, n, values)
if err != nil {
return false, nil, err
return false, nil, nil, err
}
if matchV {
matchedElems = append(matchedElems, MatchedElement{"Name": n, "Value": values[n]})
matchedElems = append(matchedElems, MatchedElement{MatchedKeyName: n, MatchedKeyValue: values[n]})
matchedExpressions[n] = e
} else if e.Op == nfdv1alpha1.MatchDoesNotExist {
// DoesNotExist is special in that both "keys" and "values" should match (i.e. the name is not found in either of them).
isMatch = false
if !failFast {
continue
}
matchedElems = []MatchedElement{}
matchedExpressions = nfdv1alpha1.MatchExpressionSet{}
break
}
}
if !matchK && !matchV {
isMatch = false
if !failFast {
continue
}
matchedElems = []MatchedElement{}
matchedExpressions = nfdv1alpha1.MatchExpressionSet{}
break
}
}
// Sort for reproducible output
sort.Slice(matchedElems, func(i, j int) bool { return matchedElems[i]["Name"] < matchedElems[j]["Name"] })
sort.Slice(matchedElems, func(i, j int) bool { return matchedElems[i][MatchedKeyName] < matchedElems[j][MatchedKeyName] })
// Instances are handled separately as the logic is fundamentally different
// from keys and values and cannot be combined with them. We want to find
// instance(s) that match all match expressions. I.e. the set of all match
// expressions are evaluated against every instance separately.
ma, me, err := MatchGetInstances(m, instances)
ma, melems, mexps, err := MatchGetInstances(m, instances, failFast)
if err != nil {
return false, nil, err
return false, nil, nil, err
}
isMatch = isMatch || ma
matchedElems = append(matchedElems, me...)
matchedElems = append(matchedElems, melems...)
if mexps != nil {
for k, v := range *mexps {
matchedExpressions[k] = v
}
}
return isMatch, matchedElems, nil
return isMatch, matchedElems, &matchedExpressions, nil
}
// MatchNamesMulti evaluates the MatchExpression against the names of key,
@ -440,12 +487,11 @@ func MatchMulti(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.N
// types (flag, attribute and/or instance).
func MatchNamesMulti(m *nfdv1alpha1.MatchExpression, keys map[string]nfdv1alpha1.Nil, values map[string]string, instances []nfdv1alpha1.InstanceFeature) (bool, []MatchedElement, error) {
ret := []MatchedElement{}
for k := range keys {
if match, err := evaluateMatchExpression(m, true, k); err != nil {
return false, nil, err
} else if match {
ret = append(ret, MatchedElement{"Name": k})
ret = append(ret, MatchedElement{MatchedKeyName: k})
}
}
@ -453,12 +499,12 @@ func MatchNamesMulti(m *nfdv1alpha1.MatchExpression, keys map[string]nfdv1alpha1
if match, err := evaluateMatchExpression(m, true, k); err != nil {
return false, nil, err
} else if match {
ret = append(ret, MatchedElement{"Name": k, "Value": v})
ret = append(ret, MatchedElement{MatchedKeyName: k, MatchedKeyValue: v})
}
}
// Sort for reproducible output
sort.Slice(ret, func(i, j int) bool { return ret[i]["Name"] < ret[j]["Name"] })
sort.Slice(ret, func(i, j int) bool { return ret[i][MatchedKeyName] < ret[j][MatchedKeyName] })
_, me, err := MatchInstanceAttributeNames(m, instances)
if err != nil {
@ -469,7 +515,7 @@ func MatchNamesMulti(m *nfdv1alpha1.MatchExpression, keys map[string]nfdv1alpha1
if klogV3 := klog.V(3); klogV3.Enabled() {
mk := make([]string, len(ret))
for i, v := range ret {
mk[i] = v["Name"]
mk[i] = v[MatchedKeyName]
}
mkMsg := strings.Join(mk, ", ")

View file

@ -31,6 +31,31 @@ import (
"sigs.k8s.io/node-feature-discovery/pkg/utils"
)
// MatchStatus represents the status of a processed rule.
// It includes information about successful expressions and their results, which are the matched host features.
// For example, for the expression: cpu.cpuid: {op: "InRegexp", value: ["^AVX"]},
// the result could include matched host features such as AVX, AVX2, AVX512 etc.
// +k8s:deepcopy-gen=false
type MatchStatus struct {
*MatchFeatureStatus
// IsMatch informes whether a rule succeeded or failed.
IsMatch bool
// MatchAny represents an array of logical OR conditions between MatchFeatureStatus entries.
MatchAny []*MatchFeatureStatus
}
// MatchFeatureStatus represents a matched expression
// with its result, which is matched host features.
// +k8s:deepcopy-gen=false
type MatchFeatureStatus struct {
// MatchedFeatures represents the features matched on the host,
// which is a result of the FeatureMatcher.
MatchedFeatures matchedFeatures
// MatchedFeaturesTerms represents the expressions that successfully matched on the host.
MatchedFeaturesTerms nfdv1alpha1.FeatureMatcher
}
// RuleOutput contains the output out rule execution.
// +k8s:deepcopy-gen=false
type RuleOutput struct {
@ -39,56 +64,69 @@ type RuleOutput struct {
Annotations map[string]string
Vars map[string]string
Taints []corev1.Taint
MatchStatus *MatchStatus
}
// Execute the rule against a set of input features.
func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features) (RuleOutput, error) {
func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features, failFast bool) (RuleOutput, error) {
var (
matchStatus MatchStatus
isMatch bool
err error
)
labels := make(map[string]string)
vars := make(map[string]string)
if len(r.MatchAny) > 0 {
if n := len(r.MatchAny); n > 0 {
matchStatus.MatchAny = make([]*MatchFeatureStatus, 0, n)
// Logical OR over the matchAny matchers
matched := false
var (
featureStatus *MatchFeatureStatus
matched bool
)
for _, matcher := range r.MatchAny {
if isMatch, matches, err := evaluateMatchAnyElem(&matcher, features); err != nil {
if matched, featureStatus, err = evaluateMatchAnyElem(&matcher, features, failFast); err != nil {
return RuleOutput{}, err
} else if isMatch {
matched = true
klog.V(4).InfoS("matchAny matched", "ruleName", r.Name, "matchedFeatures", utils.DelayedDumper(matches))
} else if matched {
isMatch = true
klog.V(4).InfoS("matchAny matched", "ruleName", r.Name, "matchedFeatures", utils.DelayedDumper(featureStatus.MatchedFeatures))
if r.LabelsTemplate == "" && r.VarsTemplate == "" {
if r.LabelsTemplate == "" && r.VarsTemplate == "" && failFast {
// there's no need to evaluate other matchers in MatchAny
// if there are no templates to be executed on them - so
// short-circuit and stop on first match here
break
}
if err := executeLabelsTemplate(r, matches, labels); err != nil {
if err := executeLabelsTemplate(r, featureStatus.MatchedFeatures, labels); err != nil {
return RuleOutput{}, err
}
if err := executeVarsTemplate(r, matches, vars); err != nil {
if err := executeVarsTemplate(r, featureStatus.MatchedFeatures, vars); err != nil {
return RuleOutput{}, err
}
}
matchStatus.MatchAny = append(matchStatus.MatchAny, featureStatus)
}
if !matched {
if !isMatch {
klog.V(2).InfoS("rule did not match", "ruleName", r.Name)
return RuleOutput{}, nil
return RuleOutput{MatchStatus: &matchStatus}, nil
}
}
if len(r.MatchFeatures) > 0 {
if isMatch, matches, err := evaluateFeatureMatcher(&r.MatchFeatures, features); err != nil {
if isMatch, matchStatus.MatchFeatureStatus, err = evaluateFeatureMatcher(&r.MatchFeatures, features, failFast); err != nil {
return RuleOutput{}, err
} else if !isMatch {
klog.V(2).InfoS("rule did not match", "ruleName", r.Name)
return RuleOutput{}, nil
return RuleOutput{MatchStatus: &matchStatus}, nil
} else {
klog.V(4).InfoS("matchFeatures matched", "ruleName", r.Name, "matchedFeatures", utils.DelayedDumper(matches))
if err := executeLabelsTemplate(r, matches, labels); err != nil {
klog.V(4).InfoS("matchFeatures matched", "ruleName", r.Name, "matchedFeatures", utils.DelayedDumper(matchStatus.MatchedFeatures))
if err := executeLabelsTemplate(r, matchStatus.MatchedFeatures, labels); err != nil {
return RuleOutput{}, err
}
if err := executeVarsTemplate(r, matches, vars); err != nil {
if err := executeVarsTemplate(r, matchStatus.MatchedFeatures, vars); err != nil {
return RuleOutput{}, err
}
}
@ -96,6 +134,7 @@ func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features) (RuleOutput, e
maps.Copy(labels, r.Labels)
maps.Copy(vars, r.Vars)
matchStatus.IsMatch = true
ret := RuleOutput{
Labels: labels,
@ -103,6 +142,7 @@ func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features) (RuleOutput, e
Annotations: maps.Clone(r.Annotations),
ExtendedResources: maps.Clone(r.ExtendedResources),
Taints: slices.Clone(r.Taints),
MatchStatus: &matchStatus,
}
klog.V(2).InfoS("rule matched", "ruleName", r.Name, "ruleOutput", utils.DelayedDumper(ret))
return ret, nil
@ -110,12 +150,12 @@ func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features) (RuleOutput, e
// ExecuteGroupRule executes the GroupRule against a set of input features, and return true if the
// rule matches.
func ExecuteGroupRule(r *nfdv1alpha1.GroupRule, features *nfdv1alpha1.Features) (bool, error) {
func ExecuteGroupRule(r *nfdv1alpha1.GroupRule, features *nfdv1alpha1.Features, failFast bool) (bool, error) {
matched := false
if len(r.MatchAny) > 0 {
// Logical OR over the matchAny matchers
for _, matcher := range r.MatchAny {
if isMatch, matches, err := evaluateMatchAnyElem(&matcher, features); err != nil {
if isMatch, matches, err := evaluateMatchAnyElem(&matcher, features, failFast); err != nil {
return false, err
} else if isMatch {
matched = true
@ -131,7 +171,7 @@ func ExecuteGroupRule(r *nfdv1alpha1.GroupRule, features *nfdv1alpha1.Features)
}
if len(r.MatchFeatures) > 0 {
if isMatch, _, err := evaluateFeatureMatcher(&r.MatchFeatures, features); err != nil {
if isMatch, _, err := evaluateFeatureMatcher(&r.MatchFeatures, features, failFast); err != nil {
return false, err
} else if !isMatch {
klog.V(2).InfoS("rule did not match", "ruleName", r.Name)
@ -187,12 +227,18 @@ type matchedFeatures map[string]domainMatchedFeatures
type domainMatchedFeatures map[string][]MatchedElement
func evaluateMatchAnyElem(e *nfdv1alpha1.MatchAnyElem, features *nfdv1alpha1.Features) (bool, matchedFeatures, error) {
return evaluateFeatureMatcher(&e.MatchFeatures, features)
func evaluateMatchAnyElem(e *nfdv1alpha1.MatchAnyElem, features *nfdv1alpha1.Features, failFast bool) (bool, *MatchFeatureStatus, error) {
return evaluateFeatureMatcher(&e.MatchFeatures, features, failFast)
}
func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1.Features) (bool, matchedFeatures, error) {
matches := make(matchedFeatures, len(*m))
func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1.Features, failFast bool) (bool, *MatchFeatureStatus, error) {
var (
isMatch = true
isTermMatch = true
)
status := &MatchFeatureStatus{
MatchedFeatures: make(matchedFeatures, len(*m)),
}
// Logical AND over the terms
for _, term := range *m {
@ -207,14 +253,17 @@ func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1
dom := nameSplit[0]
nam := nameSplit[1]
if _, ok := matches[dom]; !ok {
matches[dom] = make(domainMatchedFeatures)
if _, ok := status.MatchedFeatures[dom]; !ok {
status.MatchedFeatures[dom] = make(domainMatchedFeatures)
}
var isMatch = true
var matchedElems []MatchedElement
var matchedExpressions *nfdv1alpha1.MatchExpressionSet
var err error
matchedFeatureTerm := nfdv1alpha1.FeatureMatcherTerm{
Feature: featureName,
}
fF, okF := features.Flags[featureName]
fA, okA := features.Attributes[featureName]
fI, okI := features.Instances[featureName]
@ -223,24 +272,37 @@ func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1
}
if term.MatchExpressions != nil {
isMatch, matchedElems, err = MatchMulti(term.MatchExpressions, fF.Elements, fA.Elements, fI.Elements)
isTermMatch, matchedElems, matchedExpressions, err = MatchMulti(term.MatchExpressions, fF.Elements, fA.Elements, fI.Elements, failFast)
matchedFeatureTerm.MatchExpressions = matchedExpressions
}
if err == nil && isMatch && term.MatchName != nil {
if err == nil && isTermMatch && term.MatchName != nil {
var meTmp []MatchedElement
isMatch, meTmp, err = MatchNamesMulti(term.MatchName, fF.Elements, fA.Elements, fI.Elements)
isTermMatch, meTmp, err = MatchNamesMulti(term.MatchName, fF.Elements, fA.Elements, fI.Elements)
matchedElems = append(matchedElems, meTmp...)
// MatchName has only one expression, in this case it's enough to check the isTermMatch flag
// to judge if the expression succeeded on the host.
if isTermMatch {
matchedFeatureTerm.MatchName = term.MatchName
}
}
matches[dom][nam] = append(matches[dom][nam], matchedElems...)
status.MatchedFeatures[dom][nam] = append(status.MatchedFeatures[dom][nam], matchedElems...)
if matchedFeatureTerm.MatchName != nil || (matchedFeatureTerm.MatchExpressions != nil && len(*matchedFeatureTerm.MatchExpressions) > 0) {
status.MatchedFeaturesTerms = append(status.MatchedFeaturesTerms, matchedFeatureTerm)
}
if err != nil {
return false, nil, err
} else if !isMatch {
return false, nil, nil
} else if !isTermMatch {
if !failFast {
isMatch = false
} else {
return false, status, nil
}
}
}
return true, matches, nil
return isMatch, status, nil
}
type templateHelper struct {

View file

@ -49,22 +49,22 @@ func TestRule(t *testing.T) {
}
// Test totally empty features
m, err := Execute(r1, f)
m, err := Execute(r1, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
_, err = Execute(r2, f)
_, err = Execute(r2, f, true)
assert.Error(t, err, "matching against a missing feature should have returned an error")
// Test properly initialized empty features
f = nfdv1alpha1.NewFeatures()
m, err = Execute(r1, f)
m, err = Execute(r1, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
assert.Empty(t, r1.Vars, "vars should be empty")
_, err = Execute(r2, f)
_, err = Execute(r2, f, true)
assert.Error(t, err, "matching against a missing feature type should have returned an error")
// Test empty feature sets
@ -72,11 +72,11 @@ func TestRule(t *testing.T) {
f.Attributes["domain-1.vf-1"] = nfdv1alpha1.NewAttributeFeatures(nil)
f.Instances["domain-1.if-1"] = nfdv1alpha1.NewInstanceFeatures()
m, err = Execute(r1, f)
m, err = Execute(r1, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
m, err = Execute(r2, f)
m, err = Execute(r2, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Nil(t, m.Labels, "unexpected match")
@ -85,7 +85,7 @@ func TestRule(t *testing.T) {
f.Attributes["domain-1.vf-1"].Elements["key-1"] = "val-x"
f.Instances["domain-1.if-1"] = nfdv1alpha1.NewInstanceFeatures(*nfdv1alpha1.NewInstanceFeature(map[string]string{"attr-1": "val-x"}))
m, err = Execute(r1, f)
m, err = Execute(r1, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
@ -96,17 +96,17 @@ func TestRule(t *testing.T) {
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{},
},
}
m, err = Execute(r1, f)
m, err = Execute(r1, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r1.Labels, m.Labels, "empty match expression set mathces anything")
// Match "key" features
m, err = Execute(r2, f)
m, err = Execute(r2, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Nil(t, m.Labels, "keys should not have matched")
f.Flags["domain-1.kf-1"].Elements["key-1"] = nfdv1alpha1.Nil{}
m, err = Execute(r2, f)
m, err = Execute(r2, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r2.Labels, m.Labels, "keys should have matched")
assert.Equal(t, r2.Vars, m.Vars, "vars should be present")
@ -123,12 +123,12 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Nil(t, m.Labels, "values should not have matched")
f.Attributes["domain-1.vf-1"].Elements["key-1"] = "val-1"
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "values should have matched")
@ -144,12 +144,12 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Nil(t, m.Labels, "instances should not have matched")
f.Instances["domain-1.if-1"].Elements[0].Attributes["attr-1"] = "val-1"
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "instances should have matched")
@ -173,7 +173,7 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f2)
m, err = Execute(r3, f2, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "key in multi-type feature should have matched")
@ -185,7 +185,7 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f2)
m, err = Execute(r3, f2, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "attribute in multi-type feature should have matched")
@ -197,7 +197,7 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f2)
m, err = Execute(r3, f2, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "attribute in multi-type feature should have matched")
@ -210,7 +210,7 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f2)
m, err = Execute(r3, f2, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "features in multi-type feature should have matched flags and attributes")
@ -222,7 +222,7 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f2)
m, err = Execute(r3, f2, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "features in multi-type feature should have matched instance")
@ -244,12 +244,12 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Nil(t, m.Labels, "instances should not have matched")
(*r3.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1")
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "instances should have matched")
@ -266,7 +266,7 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Nil(t, m.Labels, "instances should not have matched")
@ -282,7 +282,7 @@ func TestRule(t *testing.T) {
},
})
(*r3.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1")
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "instances should have matched")
}
@ -433,12 +433,12 @@ var-2=
"kf-foo": "true",
}
m, err := Execute(r1, f)
m, err := Execute(r1, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
assert.Equal(t, expectedVars, m.Vars, "instances should have matched")
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
assert.Equal(t, expectedVars, m.Vars, "instances should have matched")
@ -464,7 +464,7 @@ var-2=
"mf-key-d": "found",
}
expectedVars = map[string]string{}
m, err = Execute(r3, f)
m, err = Execute(r3, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
assert.Equal(t, expectedVars, m.Vars, "instances should have matched")
@ -486,32 +486,32 @@ var-2=
}
r2.LabelsTemplate = "foo=bar"
m, err = Execute(r2, f)
m, err = Execute(r2, f, true)
assert.Nil(t, err)
assert.Equal(t, map[string]string{"foo": "bar"}, m.Labels, "instances should have matched")
assert.Empty(t, m.Vars)
r2.LabelsTemplate = "foo"
_, err = Execute(r2, f)
_, err = Execute(r2, f, true)
assert.Error(t, err)
r2.LabelsTemplate = "{{"
_, err = Execute(r2, f)
_, err = Execute(r2, f, true)
assert.Error(t, err)
r2.LabelsTemplate = ""
r2.VarsTemplate = "bar=baz"
m, err = Execute(r2, f)
m, err = Execute(r2, f, true)
assert.Nil(t, err)
assert.Empty(t, m.Labels)
assert.Equal(t, map[string]string{"bar": "baz"}, m.Vars, "instances should have matched")
r2.VarsTemplate = "bar"
_, err = Execute(r2, f)
_, err = Execute(r2, f, true)
assert.Error(t, err)
r2.VarsTemplate = "{{"
_, err = Execute(r2, f)
_, err = Execute(r2, f, true)
assert.Error(t, err)
//
@ -535,7 +535,7 @@ var-2=
"key-5": "",
}
m, err = Execute(r4, f)
m, err = Execute(r4, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
@ -549,7 +549,7 @@ var-2=
},
}
m, err = Execute(r4, f)
m, err = Execute(r4, f, true)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, map[string]string(nil), m.Labels, "instances should have matched")
}

View file

@ -70,7 +70,7 @@ func processNodeFeatureRule(nodeFeatureRule nfdv1alpha1.NodeFeatureRule, nodeFea
for _, rule := range nodeFeatureRule.Spec.Rules {
fmt.Println("Processing rule: ", rule.Name)
ruleOut, err := nodefeaturerule.Execute(&rule, &nodeFeature.Features)
ruleOut, err := nodefeaturerule.Execute(&rule, &nodeFeature.Features, true)
if err != nil {
errs = append(errs, fmt.Errorf("failed to process rule: %q - %w", rule.Name, err))
continue

View file

@ -794,7 +794,7 @@ func (m *nfdMaster) nfdAPIUpdateNodeFeatureGroup(nfdClient nfdclientset.Interfac
nodeGroupValidator := make(map[string]bool)
for _, rule := range nodeFeatureGroup.Spec.Rules {
for _, feature := range nodeFeaturesList {
match, err := nodefeaturerule.ExecuteGroupRule(&rule, &feature.Spec.Features)
match, err := nodefeaturerule.ExecuteGroupRule(&rule, &feature.Spec.Features, true)
if err != nil {
klog.ErrorS(err, "failed to evaluate rule", "ruleName", rule.Name)
continue
@ -1018,7 +1018,7 @@ func (m *nfdMaster) processNodeFeatureRule(nodeName string, features *nfdv1alpha
klog.InfoS("executing NodeFeatureRule", "nodefeaturerule", klog.KObj(spec), "nodeName", nodeName)
}
for _, rule := range spec.Spec.Rules {
ruleOut, err := nodefeaturerule.Execute(&rule, features)
ruleOut, err := nodefeaturerule.Execute(&rule, features, true)
if err != nil {
klog.ErrorS(err, "failed to process rule", "ruleName", rule.Name, "nodefeaturerule", klog.KObj(spec), "nodeName", nodeName)
nfrProcessingErrors.Inc()

View file

@ -93,7 +93,7 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
klog.V(2).InfoS("resolving custom features", "configuration", utils.DelayedDumper(allFeatureConfig))
// Iterate over features
for _, rule := range allFeatureConfig {
ruleOut, err := nodefeaturerule.Execute(&rule, features)
ruleOut, err := nodefeaturerule.Execute(&rule, features, true)
if err != nil {
klog.ErrorS(err, "failed to execute rule")
continue