mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-03-14 20:56:42 +00:00
apis/nfd: fix multiple matcher terms targeting the same feature
Fix NodeFeatureRule templating in cases where multiple matchFeatures terms are targeting the same feature. Previously, only matched feature elements from the last matcher terms were used as the input to the template. However, the input should contain all matched elements from all matcher terms. For example, consider the example rule snippet below: ... labelsTemplate: | {{ range .pci.device }}vendor.io/pci-device.{{ .class }}-{{ .device }}=exists {{ end }} matchFeatures: - feature: pci.device matchExpressions: class: {op: InRegexp, value: ["^03"]} vendor: {op: In, value: ["1234"]} - feature: pci.device matchExpressions: class: {op: InRegexp, value: ["^12"]} This rule matches if both a pci device of class 03 from vendor 1234 exists and a pci device of class 12 (from any vendor) exists. Previously, the template would only generate labels from the devices in class 12 (as that's the last term). With this patch the template creates device labels from devices in both classes 03 and 12.
This commit is contained in:
parent
62870968cd
commit
63c22551df
5 changed files with 48 additions and 101 deletions
|
@ -288,16 +288,15 @@ func (m *MatchExpressionSet) MatchKeys(keys map[string]Nil) (bool, error) {
|
|||
return matched, err
|
||||
}
|
||||
|
||||
// MatchedKey holds one matched key.
|
||||
type MatchedKey struct {
|
||||
Name string
|
||||
}
|
||||
// MatchedElement holds one matched Instance.
|
||||
// +k8s:deepcopy-gen=false
|
||||
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 (m *MatchExpressionSet) MatchGetKeys(keys map[string]Nil) (bool, []MatchedKey, error) {
|
||||
ret := make([]MatchedKey, 0, len(*m))
|
||||
func (m *MatchExpressionSet) MatchGetKeys(keys map[string]Nil) (bool, []MatchedElement, error) {
|
||||
ret := make([]MatchedElement, 0, len(*m))
|
||||
|
||||
for n, e := range *m {
|
||||
match, err := e.MatchKeys(n, keys)
|
||||
|
@ -307,10 +306,8 @@ func (m *MatchExpressionSet) MatchGetKeys(keys map[string]Nil) (bool, []MatchedK
|
|||
if !match {
|
||||
return false, nil, nil
|
||||
}
|
||||
ret = append(ret, MatchedKey{Name: n})
|
||||
ret = append(ret, MatchedElement{"Name": n})
|
||||
}
|
||||
// Sort for reproducible output
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
|
||||
return true, ret, nil
|
||||
}
|
||||
|
||||
|
@ -320,17 +317,11 @@ func (m *MatchExpressionSet) MatchValues(values map[string]string) (bool, error)
|
|||
return matched, err
|
||||
}
|
||||
|
||||
// MatchedValue holds one matched key-value pair.
|
||||
type MatchedValue struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// 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 (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []MatchedValue, error) {
|
||||
ret := make([]MatchedValue, 0, len(*m))
|
||||
func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []MatchedElement, error) {
|
||||
ret := make([]MatchedElement, 0, len(*m))
|
||||
|
||||
for n, e := range *m {
|
||||
match, err := e.MatchValues(n, values)
|
||||
|
@ -340,10 +331,8 @@ func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []M
|
|||
if !match {
|
||||
return false, nil, nil
|
||||
}
|
||||
ret = append(ret, MatchedValue{Name: n, Value: values[n]})
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -355,15 +344,12 @@ func (m *MatchExpressionSet) MatchInstances(instances []InstanceFeature) (bool,
|
|||
return len(v) > 0, err
|
||||
}
|
||||
|
||||
// MatchedInstance holds one matched Instance.
|
||||
type MatchedInstance map[string]string
|
||||
|
||||
// MatchGetInstances evaluates the MatchExpressionSet against a set of instance
|
||||
// features, each of which is an individual set of key-value pairs
|
||||
// (attributes). A slice containing all matching instances is returned. An
|
||||
// empty (non-nil) slice is returned if no matching instances were found.
|
||||
func (m *MatchExpressionSet) MatchGetInstances(instances []InstanceFeature) ([]MatchedInstance, error) {
|
||||
ret := []MatchedInstance{}
|
||||
func (m *MatchExpressionSet) MatchGetInstances(instances []InstanceFeature) ([]MatchedElement, error) {
|
||||
ret := []MatchedElement{}
|
||||
|
||||
for _, i := range instances {
|
||||
if match, err := m.MatchValues(i.Attributes); err != nil {
|
||||
|
|
|
@ -290,8 +290,8 @@ func TestMatchValues(t *testing.T) {
|
|||
|
||||
func TestMESMatchKeys(t *testing.T) {
|
||||
type I = map[string]api.Nil
|
||||
type MK = api.MatchedKey
|
||||
type O = []MK
|
||||
type ME = api.MatchedElement
|
||||
type O = []ME
|
||||
type TC struct {
|
||||
mes string
|
||||
input I
|
||||
|
@ -312,7 +312,7 @@ foo: { op: DoesNotExist }
|
|||
bar: { op: Exists }
|
||||
`,
|
||||
input: I{"bar": {}, "baz": {}, "buzz": {}},
|
||||
output: O{MK{Name: "bar"}, MK{Name: "foo"}},
|
||||
output: O{ME{"Name": "bar"}, ME{"Name": "foo"}},
|
||||
result: assert.Truef, err: assert.Nilf},
|
||||
|
||||
{mes: `
|
||||
|
@ -351,8 +351,8 @@ bar: { op: Exists }
|
|||
|
||||
func TestMESMatchValues(t *testing.T) {
|
||||
type I = map[string]string
|
||||
type MV = api.MatchedValue
|
||||
type O = []MV
|
||||
type ME = api.MatchedElement
|
||||
type O = []ME
|
||||
type TC struct {
|
||||
mes string
|
||||
input I
|
||||
|
@ -382,7 +382,7 @@ bar: { op: In, value: ["val", "wal"] }
|
|||
baz: { op: Gt, value: ["10"] }
|
||||
`,
|
||||
input: I{"foo": "1", "bar": "val", "baz": "123", "buzz": "light"},
|
||||
output: O{MV{Name: "bar", Value: "val"}, MV{Name: "baz", Value: "123"}, MV{Name: "foo", Value: "1"}},
|
||||
output: O{ME{"Name": "bar", "Value": "val"}, ME{"Name": "baz", "Value": "123"}, ME{"Name": "foo", "Value": "1"}},
|
||||
result: assert.Truef, err: assert.Nilf},
|
||||
|
||||
{mes: `
|
||||
|
@ -413,8 +413,8 @@ baz: { op: Gt, value: ["10"] }
|
|||
|
||||
func TestMESMatchInstances(t *testing.T) {
|
||||
type I = api.InstanceFeature
|
||||
type MI = api.MatchedInstance
|
||||
type O = []MI
|
||||
type ME = api.MatchedElement
|
||||
type O = []ME
|
||||
type A = map[string]string
|
||||
type TC struct {
|
||||
mes string
|
||||
|
|
|
@ -155,7 +155,7 @@ func (r *Rule) executeVarsTemplate(in matchedFeatures, out map[string]string) er
|
|||
|
||||
type matchedFeatures map[string]domainMatchedFeatures
|
||||
|
||||
type domainMatchedFeatures map[string]interface{}
|
||||
type domainMatchedFeatures map[string][]MatchedElement
|
||||
|
||||
func (e *MatchAnyElem) match(features *Features) (bool, matchedFeatures, error) {
|
||||
return e.MatchFeatures.match(features)
|
||||
|
@ -175,30 +175,26 @@ func (m *FeatureMatcher) match(features *Features) (bool, matchedFeatures, error
|
|||
nameSplit = []string{featureName, ""}
|
||||
}
|
||||
|
||||
if _, ok := matches[nameSplit[0]]; !ok {
|
||||
matches[nameSplit[0]] = make(domainMatchedFeatures)
|
||||
dom := nameSplit[0]
|
||||
nam := nameSplit[1]
|
||||
if _, ok := matches[dom]; !ok {
|
||||
matches[dom] = make(domainMatchedFeatures)
|
||||
}
|
||||
|
||||
var isMatch bool
|
||||
var matchedElems []MatchedElement
|
||||
var err error
|
||||
if f, ok := features.Flags[featureName]; ok {
|
||||
m, v, e := term.MatchExpressions.MatchGetKeys(f.Elements)
|
||||
isMatch = m
|
||||
err = e
|
||||
matches[nameSplit[0]][nameSplit[1]] = v
|
||||
isMatch, matchedElems, err = term.MatchExpressions.MatchGetKeys(f.Elements)
|
||||
} else if f, ok := features.Attributes[featureName]; ok {
|
||||
m, v, e := term.MatchExpressions.MatchGetValues(f.Elements)
|
||||
isMatch = m
|
||||
err = e
|
||||
matches[nameSplit[0]][nameSplit[1]] = v
|
||||
isMatch, matchedElems, err = term.MatchExpressions.MatchGetValues(f.Elements)
|
||||
} else if f, ok := features.Instances[featureName]; ok {
|
||||
v, e := term.MatchExpressions.MatchGetInstances(f.Elements)
|
||||
isMatch = len(v) > 0
|
||||
err = e
|
||||
matches[nameSplit[0]][nameSplit[1]] = v
|
||||
matchedElems, err = term.MatchExpressions.MatchGetInstances(f.Elements)
|
||||
isMatch = len(matchedElems) > 0
|
||||
} else {
|
||||
return false, nil, fmt.Errorf("feature %q not available", featureName)
|
||||
}
|
||||
matches[dom][nam] = append(matches[dom][nam], matchedElems...)
|
||||
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
|
|
|
@ -246,6 +246,13 @@ func TestTemplating(t *testing.T) {
|
|||
"attr-2": "val-200",
|
||||
},
|
||||
},
|
||||
{
|
||||
Attributes: map[string]string{
|
||||
"attr-1": "1000",
|
||||
"attr-2": "val-2000",
|
||||
"attr-3": "3000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -290,6 +297,14 @@ var-2=
|
|||
"attr-1": MustCreateMatchExpression(MatchLt, "100"),
|
||||
},
|
||||
},
|
||||
FeatureMatcherTerm{
|
||||
Feature: "domain_1.if_1",
|
||||
MatchExpressions: MatchExpressionSet{
|
||||
"attr-1": MustCreateMatchExpression(MatchExists),
|
||||
"attr-2": MustCreateMatchExpression(MatchExists),
|
||||
"attr-3": MustCreateMatchExpression(MatchExists),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -309,8 +324,9 @@ var-2=
|
|||
"vf-key-1": "vf-val-1",
|
||||
"vf-bar": "vf-",
|
||||
// From if_1 template
|
||||
"if-1_val-2": "present",
|
||||
"if-10_val-20": "present",
|
||||
"if-1_val-2": "present",
|
||||
"if-10_val-20": "present",
|
||||
"if-1000_val-2000": "present",
|
||||
}
|
||||
expectedVars := map[string]string{
|
||||
"var-1": "var-val-1",
|
||||
|
|
|
@ -276,57 +276,6 @@ func (in MatchValue) DeepCopy() MatchValue {
|
|||
return *out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in MatchedInstance) DeepCopyInto(out *MatchedInstance) {
|
||||
{
|
||||
in := &in
|
||||
*out = make(MatchedInstance, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchedInstance.
|
||||
func (in MatchedInstance) DeepCopy() MatchedInstance {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(MatchedInstance)
|
||||
in.DeepCopyInto(out)
|
||||
return *out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *MatchedKey) DeepCopyInto(out *MatchedKey) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchedKey.
|
||||
func (in *MatchedKey) DeepCopy() *MatchedKey {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(MatchedKey)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *MatchedValue) DeepCopyInto(out *MatchedValue) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchedValue.
|
||||
func (in *MatchedValue) DeepCopy() *MatchedValue {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(MatchedValue)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Nil) DeepCopyInto(out *Nil) {
|
||||
*out = *in
|
||||
|
|
Loading…
Add table
Reference in a new issue