mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-03-15 04:57:56 +00:00
Merge pull request #663 from marquiz/devel/rule-backrefs
Add variables to feature rule spec and support backrefs
This commit is contained in:
commit
ffe12cb1e4
12 changed files with 287 additions and 40 deletions
|
@ -111,3 +111,29 @@ spec:
|
||||||
- feature: cpu.cpuid
|
- feature: cpu.cpuid
|
||||||
matchExpressions:
|
matchExpressions:
|
||||||
AVX: {op: Exists}
|
AVX: {op: Exists}
|
||||||
|
|
||||||
|
# The following examples demonstrate vars field and back-referencing
|
||||||
|
# previous labels and vars
|
||||||
|
- name: "my dummy kernel rule"
|
||||||
|
labels:
|
||||||
|
"my.kernel.feature": "true"
|
||||||
|
matchFeatures:
|
||||||
|
- feature: kernel.version
|
||||||
|
matchExpressions:
|
||||||
|
major: {op: Gt, value: ["2"]}
|
||||||
|
|
||||||
|
- name: "my dummy rule with no labels"
|
||||||
|
vars:
|
||||||
|
"my.dummy.var": "1"
|
||||||
|
matchFeatures:
|
||||||
|
- feature: cpu.cpuid
|
||||||
|
matchExpressions: {}
|
||||||
|
|
||||||
|
- name: "my rule using backrefs"
|
||||||
|
labels:
|
||||||
|
"my.backref.feature": "true"
|
||||||
|
matchFeatures:
|
||||||
|
- feature: rule.matched
|
||||||
|
matchExpressions:
|
||||||
|
my.kernel.feature: {op: IsTrue}
|
||||||
|
my.dummy.var: {op: Gt, value: ["0"]}
|
||||||
|
|
|
@ -188,6 +188,21 @@ spec:
|
||||||
name:
|
name:
|
||||||
description: Name of the rule.
|
description: Name of the rule.
|
||||||
type: string
|
type: string
|
||||||
|
vars:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: 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.
|
||||||
|
type: object
|
||||||
|
varsTemplate:
|
||||||
|
description: 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.
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
|
|
|
@ -226,3 +226,29 @@
|
||||||
# - feature: cpu.cpuid
|
# - feature: cpu.cpuid
|
||||||
# matchExpressions:
|
# matchExpressions:
|
||||||
# AVX: {op: Exists}
|
# AVX: {op: Exists}
|
||||||
|
#
|
||||||
|
# # The following examples demonstrate vars field and back-referencing
|
||||||
|
# # previous labels and vars
|
||||||
|
# - name: "my dummy kernel rule"
|
||||||
|
# labels:
|
||||||
|
# "my.kernel.feature": "true"
|
||||||
|
# matchFeatures:
|
||||||
|
# - feature: kernel.version
|
||||||
|
# matchExpressions:
|
||||||
|
# major: {op: Gt, value: ["2"]}
|
||||||
|
#
|
||||||
|
# - name: "my dummy rule with no labels"
|
||||||
|
# vars:
|
||||||
|
# "my.dummy.var": "1"
|
||||||
|
# matchFeatures:
|
||||||
|
# - feature: cpu.cpuid
|
||||||
|
# matchExpressions: {}
|
||||||
|
#
|
||||||
|
# - name: "my rule using backrefs"
|
||||||
|
# labels:
|
||||||
|
# "my.backref.feature": "true"
|
||||||
|
# matchFeatures:
|
||||||
|
# - feature: rule.matched
|
||||||
|
# matchExpressions:
|
||||||
|
# my.kernel.feature: {op: IsTrue}
|
||||||
|
# my.dummy.var: {op: Gt, value: ["0"]}
|
||||||
|
|
|
@ -188,6 +188,21 @@ spec:
|
||||||
name:
|
name:
|
||||||
description: Name of the rule.
|
description: Name of the rule.
|
||||||
type: string
|
type: string
|
||||||
|
vars:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: 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.
|
||||||
|
type: object
|
||||||
|
varsTemplate:
|
||||||
|
description: 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.
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
|
|
|
@ -315,6 +315,32 @@ worker:
|
||||||
# - feature: cpu.cpuid
|
# - feature: cpu.cpuid
|
||||||
# matchExpressions:
|
# matchExpressions:
|
||||||
# AVX: {op: Exists}
|
# AVX: {op: Exists}
|
||||||
|
#
|
||||||
|
# # The following examples demonstrate vars field and back-referencing
|
||||||
|
# # previous labels and vars
|
||||||
|
# - name: "my dummy kernel rule"
|
||||||
|
# labels:
|
||||||
|
# "my.kernel.feature": "true"
|
||||||
|
# matchFeatures:
|
||||||
|
# - feature: kernel.version
|
||||||
|
# matchExpressions:
|
||||||
|
# major: {op: Gt, value: ["2"]}
|
||||||
|
#
|
||||||
|
# - name: "my dummy rule with no labels"
|
||||||
|
# vars:
|
||||||
|
# "my.dummy.var": "1"
|
||||||
|
# matchFeatures:
|
||||||
|
# - feature: cpu.cpuid
|
||||||
|
# matchExpressions: {}
|
||||||
|
#
|
||||||
|
# - name: "my rule using backrefs"
|
||||||
|
# labels:
|
||||||
|
# "my.backref.feature": "true"
|
||||||
|
# matchFeatures:
|
||||||
|
# - feature: rule.matched
|
||||||
|
# matchExpressions:
|
||||||
|
# my.kernel.feature: {op: IsTrue}
|
||||||
|
# my.dummy.var: {op: Gt, value: ["0"]}
|
||||||
### <NFD-WORKER-CONF-END-DO-NOT-REMOVE>
|
### <NFD-WORKER-CONF-END-DO-NOT-REMOVE>
|
||||||
|
|
||||||
podSecurityContext: {}
|
podSecurityContext: {}
|
||||||
|
|
|
@ -50,3 +50,18 @@ func NewInstanceFeature(attrs map[string]string) *InstanceFeature {
|
||||||
}
|
}
|
||||||
return &InstanceFeature{Attributes: attrs}
|
return &InstanceFeature{Attributes: attrs}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsertFeatureValues inserts new values into a specific feature.
|
||||||
|
func InsertFeatureValues(f Features, domain, feature string, values map[string]string) {
|
||||||
|
if _, ok := f[domain]; !ok {
|
||||||
|
f[domain] = NewDomainFeatures()
|
||||||
|
}
|
||||||
|
if _, ok := f[domain].Values[feature]; !ok {
|
||||||
|
f[domain].Values[feature] = NewValueFeatures(values)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
f[domain].Values[feature].Elements[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,26 +17,35 @@ limitations under the License.
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"bytes"
|
"k8s.io/klog/v2"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
|
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
|
||||||
"sigs.k8s.io/node-feature-discovery/pkg/utils"
|
"sigs.k8s.io/node-feature-discovery/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RuleOutput contains the output out rule execution.
|
||||||
|
// +k8s:deepcopy-gen=false
|
||||||
|
type RuleOutput struct {
|
||||||
|
Labels map[string]string
|
||||||
|
Vars map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the rule against a set of input features.
|
// Execute the rule against a set of input features.
|
||||||
func (r *Rule) Execute(features map[string]*feature.DomainFeatures) (map[string]string, error) {
|
func (r *Rule) Execute(features feature.Features) (RuleOutput, error) {
|
||||||
ret := make(map[string]string)
|
labels := make(map[string]string)
|
||||||
|
vars := make(map[string]string)
|
||||||
|
|
||||||
if len(r.MatchAny) > 0 {
|
if len(r.MatchAny) > 0 {
|
||||||
// Logical OR over the matchAny matchers
|
// Logical OR over the matchAny matchers
|
||||||
matched := false
|
matched := false
|
||||||
for _, matcher := range r.MatchAny {
|
for _, matcher := range r.MatchAny {
|
||||||
if m, err := matcher.match(features); err != nil {
|
if m, err := matcher.match(features); err != nil {
|
||||||
return nil, err
|
return RuleOutput{}, err
|
||||||
} else if m != nil {
|
} else if m != nil {
|
||||||
matched = true
|
matched = true
|
||||||
utils.KlogDump(4, "matches for matchAny "+r.Name, " ", m)
|
utils.KlogDump(4, "matches for matchAny "+r.Name, " ", m)
|
||||||
|
@ -46,33 +55,46 @@ func (r *Rule) Execute(features map[string]*feature.DomainFeatures) (map[string]
|
||||||
// produce the same labels)
|
// produce the same labels)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := r.executeLabelsTemplate(m, ret); err != nil {
|
if err := r.executeLabelsTemplate(m, labels); err != nil {
|
||||||
return nil, err
|
return RuleOutput{}, err
|
||||||
|
}
|
||||||
|
if err := r.executeVarsTemplate(m, vars); err != nil {
|
||||||
|
return RuleOutput{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !matched {
|
if !matched {
|
||||||
return nil, nil
|
klog.V(2).Infof("rule %q did not match", r.Name)
|
||||||
|
return RuleOutput{}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(r.MatchFeatures) > 0 {
|
if len(r.MatchFeatures) > 0 {
|
||||||
if m, err := r.MatchFeatures.match(features); err != nil {
|
if m, err := r.MatchFeatures.match(features); err != nil {
|
||||||
return nil, err
|
return RuleOutput{}, err
|
||||||
} else if m == nil {
|
} else if m == nil {
|
||||||
return nil, nil
|
klog.V(2).Infof("rule %q did not match", r.Name)
|
||||||
|
return RuleOutput{}, nil
|
||||||
} else {
|
} else {
|
||||||
utils.KlogDump(4, "matches for matchFeatures "+r.Name, " ", m)
|
utils.KlogDump(4, "matches for matchFeatures "+r.Name, " ", m)
|
||||||
if err := r.executeLabelsTemplate(m, ret); err != nil {
|
if err := r.executeLabelsTemplate(m, labels); err != nil {
|
||||||
return nil, err
|
return RuleOutput{}, err
|
||||||
|
}
|
||||||
|
if err := r.executeVarsTemplate(m, vars); err != nil {
|
||||||
|
return RuleOutput{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range r.Labels {
|
for k, v := range r.Labels {
|
||||||
ret[k] = v
|
labels[k] = v
|
||||||
}
|
}
|
||||||
|
for k, v := range r.Vars {
|
||||||
|
vars[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := RuleOutput{Labels: labels, Vars: vars}
|
||||||
|
utils.KlogDump(2, fmt.Sprintf("rule %q matched with: ", r.Name), " ", ret)
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
@ -100,6 +122,28 @@ func (r *Rule) executeLabelsTemplate(in matchedFeatures, out map[string]string)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Rule) executeVarsTemplate(in matchedFeatures, out map[string]string) error {
|
||||||
|
if r.VarsTemplate == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.varsTemplate == nil {
|
||||||
|
t, err := newTemplateHelper(r.VarsTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.varsTemplate = t
|
||||||
|
}
|
||||||
|
|
||||||
|
vars, err := r.varsTemplate.expandMap(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range vars {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type matchedFeatures map[string]domainMatchedFeatures
|
type matchedFeatures map[string]domainMatchedFeatures
|
||||||
|
|
||||||
type domainMatchedFeatures map[string]interface{}
|
type domainMatchedFeatures map[string]interface{}
|
||||||
|
|
|
@ -28,6 +28,7 @@ func TestRule(t *testing.T) {
|
||||||
r1 := Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}}
|
r1 := Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}}
|
||||||
r2 := Rule{
|
r2 := Rule{
|
||||||
Labels: map[string]string{"label-1": "label-val-1"},
|
Labels: map[string]string{"label-1": "label-val-1"},
|
||||||
|
Vars: map[string]string{"var-1": "var-val-1"},
|
||||||
MatchFeatures: FeatureMatcher{
|
MatchFeatures: FeatureMatcher{
|
||||||
FeatureMatcherTerm{
|
FeatureMatcherTerm{
|
||||||
Feature: "domain-1.kf-1",
|
Feature: "domain-1.kf-1",
|
||||||
|
@ -41,7 +42,7 @@ func TestRule(t *testing.T) {
|
||||||
// Test totally empty features
|
// Test totally empty features
|
||||||
m, err := r1.Execute(f)
|
m, err := r1.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r1.Labels, m, "empty matcher should have matched empty features")
|
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
|
||||||
|
|
||||||
_, err = r2.Execute(f)
|
_, err = r2.Execute(f)
|
||||||
assert.Error(t, err, "matching agains a missing domain should have returned an error")
|
assert.Error(t, err, "matching agains a missing domain should have returned an error")
|
||||||
|
@ -52,7 +53,8 @@ func TestRule(t *testing.T) {
|
||||||
|
|
||||||
m, err = r1.Execute(f)
|
m, err = r1.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r1.Labels, m, "empty matcher should have matched empty features")
|
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
|
||||||
|
assert.Empty(t, r1.Vars, "vars should be empty")
|
||||||
|
|
||||||
_, err = r2.Execute(f)
|
_, err = r2.Execute(f)
|
||||||
assert.Error(t, err, "matching agains a missing feature type should have returned an error")
|
assert.Error(t, err, "matching agains a missing feature type should have returned an error")
|
||||||
|
@ -64,11 +66,11 @@ func TestRule(t *testing.T) {
|
||||||
|
|
||||||
m, err = r1.Execute(f)
|
m, err = r1.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r1.Labels, m, "empty matcher should have matched empty features")
|
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
|
||||||
|
|
||||||
m, err = r2.Execute(f)
|
m, err = r2.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Nil(t, m, "unexpected match")
|
assert.Nil(t, m.Labels, "unexpected match")
|
||||||
|
|
||||||
// Test non-empty feature sets
|
// Test non-empty feature sets
|
||||||
d.Keys["kf-1"].Elements["key-x"] = feature.Nil{}
|
d.Keys["kf-1"].Elements["key-x"] = feature.Nil{}
|
||||||
|
@ -78,17 +80,18 @@ func TestRule(t *testing.T) {
|
||||||
|
|
||||||
m, err = r1.Execute(f)
|
m, err = r1.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r1.Labels, m, "empty matcher should have matched empty features")
|
assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
|
||||||
|
|
||||||
// Match "key" features
|
// Match "key" features
|
||||||
m, err = r2.Execute(f)
|
m, err = r2.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Nil(t, m, "keys should not have matched")
|
assert.Nil(t, m.Labels, "keys should not have matched")
|
||||||
|
|
||||||
d.Keys["kf-1"].Elements["key-1"] = feature.Nil{}
|
d.Keys["kf-1"].Elements["key-1"] = feature.Nil{}
|
||||||
m, err = r2.Execute(f)
|
m, err = r2.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r2.Labels, m, "keys should have matched")
|
assert.Equal(t, r2.Labels, m.Labels, "keys should have matched")
|
||||||
|
assert.Equal(t, r2.Vars, m.Vars, "vars should be present")
|
||||||
|
|
||||||
// Match "value" features
|
// Match "value" features
|
||||||
r3 := Rule{
|
r3 := Rule{
|
||||||
|
@ -104,12 +107,12 @@ func TestRule(t *testing.T) {
|
||||||
}
|
}
|
||||||
m, err = r3.Execute(f)
|
m, err = r3.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Nil(t, m, "values should not have matched")
|
assert.Nil(t, m.Labels, "values should not have matched")
|
||||||
|
|
||||||
d.Values["vf-1"].Elements["key-1"] = "val-1"
|
d.Values["vf-1"].Elements["key-1"] = "val-1"
|
||||||
m, err = r3.Execute(f)
|
m, err = r3.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r3.Labels, m, "values should have matched")
|
assert.Equal(t, r3.Labels, m.Labels, "values should have matched")
|
||||||
|
|
||||||
// Match "instance" features
|
// Match "instance" features
|
||||||
r4 := Rule{
|
r4 := Rule{
|
||||||
|
@ -125,12 +128,12 @@ func TestRule(t *testing.T) {
|
||||||
}
|
}
|
||||||
m, err = r4.Execute(f)
|
m, err = r4.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Nil(t, m, "instances should not have matched")
|
assert.Nil(t, m.Labels, "instances should not have matched")
|
||||||
|
|
||||||
d.Instances["if-1"].Elements[0].Attributes["attr-1"] = "val-1"
|
d.Instances["if-1"].Elements[0].Attributes["attr-1"] = "val-1"
|
||||||
m, err = r4.Execute(f)
|
m, err = r4.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r4.Labels, m, "instances should have matched")
|
assert.Equal(t, r4.Labels, m.Labels, "instances should have matched")
|
||||||
|
|
||||||
// Test multiple feature matchers
|
// Test multiple feature matchers
|
||||||
r5 := Rule{
|
r5 := Rule{
|
||||||
|
@ -152,12 +155,12 @@ func TestRule(t *testing.T) {
|
||||||
}
|
}
|
||||||
m, err = r5.Execute(f)
|
m, err = r5.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Nil(t, m, "instances should not have matched")
|
assert.Nil(t, m.Labels, "instances should not have matched")
|
||||||
|
|
||||||
r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
|
r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
|
||||||
m, err = r5.Execute(f)
|
m, err = r5.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r5.Labels, m, "instances should have matched")
|
assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
|
||||||
|
|
||||||
// Test MatchAny
|
// Test MatchAny
|
||||||
r5.MatchAny = []MatchAnyElem{
|
r5.MatchAny = []MatchAnyElem{
|
||||||
|
@ -174,7 +177,7 @@ func TestRule(t *testing.T) {
|
||||||
}
|
}
|
||||||
m, err = r5.Execute(f)
|
m, err = r5.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Nil(t, m, "instances should not have matched")
|
assert.Nil(t, m.Labels, "instances should not have matched")
|
||||||
|
|
||||||
r5.MatchAny = append(r5.MatchAny,
|
r5.MatchAny = append(r5.MatchAny,
|
||||||
MatchAnyElem{
|
MatchAnyElem{
|
||||||
|
@ -190,7 +193,7 @@ func TestRule(t *testing.T) {
|
||||||
r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
|
r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
|
||||||
m, err = r5.Execute(f)
|
m, err = r5.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, r5.Labels, m, "instances should have matched")
|
assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTemplating(t *testing.T) {
|
func TestTemplating(t *testing.T) {
|
||||||
|
@ -251,6 +254,12 @@ label-2=
|
||||||
{{range .domain_1.vf_1}}vf-{{.Name}}=vf-{{.Value}}
|
{{range .domain_1.vf_1}}vf-{{.Name}}=vf-{{.Value}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range .domain_1.if_1}}if-{{index . "attr-1"}}_{{index . "attr-2"}}=present
|
{{range .domain_1.if_1}}if-{{index . "attr-1"}}_{{index . "attr-2"}}=present
|
||||||
|
{{end}}`,
|
||||||
|
Vars: map[string]string{"var-1": "var-val-1"},
|
||||||
|
VarsTemplate: `
|
||||||
|
var-1=value-will-be-overridden-by-vars
|
||||||
|
var-2=
|
||||||
|
{{range .domain_1.kf_1}}kf-{{.Name}}=true
|
||||||
{{end}}`,
|
{{end}}`,
|
||||||
MatchFeatures: FeatureMatcher{
|
MatchFeatures: FeatureMatcher{
|
||||||
FeatureMatcherTerm{
|
FeatureMatcherTerm{
|
||||||
|
@ -294,10 +303,19 @@ label-2=
|
||||||
"if-1_val-2": "present",
|
"if-1_val-2": "present",
|
||||||
"if-10_val-20": "present",
|
"if-10_val-20": "present",
|
||||||
}
|
}
|
||||||
|
expectedVars := map[string]string{
|
||||||
|
"var-1": "var-val-1",
|
||||||
|
"var-2": "",
|
||||||
|
// From template
|
||||||
|
"kf-key-a": "true",
|
||||||
|
"kf-key-c": "true",
|
||||||
|
"kf-foo": "true",
|
||||||
|
}
|
||||||
|
|
||||||
m, err := r1.Execute(f)
|
m, err := r1.Execute(f)
|
||||||
assert.Nilf(t, err, "unexpected error: %v", err)
|
assert.Nilf(t, err, "unexpected error: %v", err)
|
||||||
assert.Equal(t, expectedLabels, m, "instances should have matched")
|
assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
|
||||||
|
assert.Equal(t, expectedVars, m.Vars, "instances should have matched")
|
||||||
|
|
||||||
//
|
//
|
||||||
// Test error cases
|
// Test error cases
|
||||||
|
@ -316,7 +334,8 @@ label-2=
|
||||||
r2.LabelsTemplate = "foo=bar"
|
r2.LabelsTemplate = "foo=bar"
|
||||||
m, err = r2.Execute(f)
|
m, err = r2.Execute(f)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, map[string]string{"foo": "bar"}, m, "instances should have matched")
|
assert.Equal(t, map[string]string{"foo": "bar"}, m.Labels, "instances should have matched")
|
||||||
|
assert.Empty(t, m.Vars)
|
||||||
|
|
||||||
r2.labelsTemplate = nil
|
r2.labelsTemplate = nil
|
||||||
r2.LabelsTemplate = "foo"
|
r2.LabelsTemplate = "foo"
|
||||||
|
@ -327,4 +346,23 @@ label-2=
|
||||||
r2.LabelsTemplate = "{{"
|
r2.LabelsTemplate = "{{"
|
||||||
_, err = r2.Execute(f)
|
_, err = r2.Execute(f)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
r2.labelsTemplate = nil
|
||||||
|
r2.LabelsTemplate = ""
|
||||||
|
r2.VarsTemplate = "bar=baz"
|
||||||
|
m, err = r2.Execute(f)
|
||||||
|
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 = nil
|
||||||
|
r2.VarsTemplate = "bar"
|
||||||
|
_, err = r2.Execute(f)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
r2.varsTemplate = nil
|
||||||
|
r2.VarsTemplate = "{{"
|
||||||
|
_, err = r2.Execute(f)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,19 @@ type Rule struct {
|
||||||
// +optional
|
// +optional
|
||||||
LabelsTemplate string `json:"labelsTemplate"`
|
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.
|
// MatchFeatures specifies a set of matcher terms all of which must match.
|
||||||
// +optional
|
// +optional
|
||||||
MatchFeatures FeatureMatcher `json:"matchFeatures"`
|
MatchFeatures FeatureMatcher `json:"matchFeatures"`
|
||||||
|
@ -177,3 +190,12 @@ const (
|
||||||
// expression must not have any values.
|
// expression must not have any values.
|
||||||
MatchIsFalse MatchOp = "IsFalse"
|
MatchIsFalse MatchOp = "IsFalse"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RuleBackrefDomain is the special feature domain for backreferencing
|
||||||
|
// output of preceding rules.
|
||||||
|
RuleBackrefDomain = "rule"
|
||||||
|
// RuleBackrefFeature is the special feature name for backreferencing
|
||||||
|
// output of preceding rules.
|
||||||
|
RuleBackrefFeature = "matched"
|
||||||
|
)
|
||||||
|
|
|
@ -308,6 +308,13 @@ func (in *Rule) DeepCopyInto(out *Rule) {
|
||||||
(*out)[key] = val
|
(*out)[key] = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if in.Vars != nil {
|
||||||
|
in, out := &in.Vars, &out.Vars
|
||||||
|
*out = make(map[string]string, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
(*out)[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
if in.MatchFeatures != nil {
|
if in.MatchFeatures != nil {
|
||||||
in, out := &in.MatchFeatures, &out.MatchFeatures
|
in, out := &in.MatchFeatures, &out.MatchFeatures
|
||||||
*out = make(FeatureMatcher, len(*in))
|
*out = make(FeatureMatcher, len(*in))
|
||||||
|
|
|
@ -43,7 +43,9 @@ import (
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
|
||||||
"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
|
"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
|
||||||
|
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
|
||||||
pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
|
pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
|
||||||
topologypb "sigs.k8s.io/node-feature-discovery/pkg/topologyupdater"
|
topologypb "sigs.k8s.io/node-feature-discovery/pkg/topologyupdater"
|
||||||
"sigs.k8s.io/node-feature-discovery/pkg/utils"
|
"sigs.k8s.io/node-feature-discovery/pkg/utils"
|
||||||
|
@ -499,6 +501,10 @@ func (m *nfdMaster) crLabels(r *pb.SetLabelsRequest) map[string]string {
|
||||||
|
|
||||||
l := make(map[string]string)
|
l := make(map[string]string)
|
||||||
ruleSpecs, err := m.nfdController.lister.List(labels.Everything())
|
ruleSpecs, err := m.nfdController.lister.List(labels.Everything())
|
||||||
|
sort.Slice(ruleSpecs, func(i, j int) bool {
|
||||||
|
return ruleSpecs[i].Name < ruleSpecs[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Errorf("failed to list LabelRule resources: %w", err)
|
klog.Errorf("failed to list LabelRule resources: %w", err)
|
||||||
return nil
|
return nil
|
||||||
|
@ -519,10 +525,14 @@ func (m *nfdMaster) crLabels(r *pb.SetLabelsRequest) map[string]string {
|
||||||
klog.Errorf("failed to process Rule %q: %w", rule.Name, err)
|
klog.Errorf("failed to process Rule %q: %w", rule.Name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for k, v := range ruleOut {
|
|
||||||
|
for k, v := range ruleOut.Labels {
|
||||||
l[k] = v
|
l[k] = v
|
||||||
}
|
}
|
||||||
utils.KlogDump(1, "", " ", ruleOut)
|
|
||||||
|
// Feed back rule output to features map for subsequent rules to match
|
||||||
|
feature.InsertFeatureValues(r.Features, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Labels)
|
||||||
|
feature.InsertFeatureValues(r.Features, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Vars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,31 +124,34 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for n, v := range ruleOut {
|
for n, v := range ruleOut.Labels {
|
||||||
labels[n] = v
|
labels[n] = v
|
||||||
}
|
}
|
||||||
|
// Feed back rule output to features map for subsequent rules to match
|
||||||
|
feature.InsertFeatureValues(domainFeatures, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Labels)
|
||||||
|
feature.InsertFeatureValues(domainFeatures, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Vars)
|
||||||
}
|
}
|
||||||
return labels, nil
|
return labels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CustomRule) execute(features map[string]*feature.DomainFeatures) (map[string]string, error) {
|
func (r *CustomRule) execute(features map[string]*feature.DomainFeatures) (nfdv1alpha1.RuleOutput, error) {
|
||||||
if r.LegacyRule != nil {
|
if r.LegacyRule != nil {
|
||||||
ruleOut, err := r.LegacyRule.execute(features)
|
ruleOut, err := r.LegacyRule.execute(features)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to execute legacy rule %s: %w", r.LegacyRule.Name, err)
|
return nfdv1alpha1.RuleOutput{}, fmt.Errorf("failed to execute legacy rule %s: %w", r.LegacyRule.Name, err)
|
||||||
}
|
}
|
||||||
return ruleOut, err
|
return nfdv1alpha1.RuleOutput{Labels: ruleOut}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Rule != nil {
|
if r.Rule != nil {
|
||||||
ruleOut, err := r.Rule.Execute(features)
|
ruleOut, err := r.Rule.Execute(features)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to execute rule %s: %w", r.Rule.Name, err)
|
return ruleOut, fmt.Errorf("failed to execute rule %s: %w", r.Rule.Name, err)
|
||||||
}
|
}
|
||||||
return ruleOut, err
|
return ruleOut, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("BUG: an empty rule, this really should not happen")
|
return nfdv1alpha1.RuleOutput{}, fmt.Errorf("BUG: an empty rule, this really should not happen")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LegacyRule) execute(features map[string]*feature.DomainFeatures) (map[string]string, error) {
|
func (r *LegacyRule) execute(features map[string]*feature.DomainFeatures) (map[string]string, error) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue