From 6cbed379df00207e53e8688c2a92b144f8b66533 Mon Sep 17 00:00:00 2001 From: Markus Lehtonen Date: Thu, 4 Mar 2021 12:39:42 +0200 Subject: [PATCH] source/custom: implement matchAny directive Implement a new 'matchAny' directive in the new rule format, building on top of the previously implemented 'matchFeatures' matcher. MatchAny applies a logical OR over multiple matchFeatures directives. That is, it allows specifying multiple alternative matchers (at least one of which must match) in a single label rule. The configuration format for the new matchers is matchAny: - matchFeatures: - feature: . matchExpressions: : op: value: - - matchFeatures: ... A configuration example. In order to require a cpu feature, kernel module and one of two specific PCI devices (taking use of the shortform notation): - name: multi-device-test labels: multi-device-feature: "true" matchFeatures: - feature: kernel.loadedmodule matchExpressions: [driver-module] - feature: cpu.cpuid matchExpressions: [AVX512F] matchAny: - matchFeatures: - feature; pci.device matchExpressions: vendor: "8086" device: "1234" - matchFeatures: - feature: pci.device matchExpressions: vendor: "8086" device: "abcd" --- .../worker-config/nfd-worker.conf.example | 24 ++++++++++++++++ .../helm/node-feature-discovery/values.yaml | 24 ++++++++++++++++ source/custom/custom.go | 25 +++++++++++++++++ source/custom/custom_test.go | 28 +++++++++++++++++++ 4 files changed, 101 insertions(+) diff --git a/deployment/components/worker-config/nfd-worker.conf.example b/deployment/components/worker-config/nfd-worker.conf.example index c07c4147d..c37860736 100644 --- a/deployment/components/worker-config/nfd-worker.conf.example +++ b/deployment/components/worker-config/nfd-worker.conf.example @@ -160,3 +160,27 @@ # - feature: local.label # matchExpressions: # custom-feature-knob: {op: Gt, value: ["100"]} +# +# # The following feature demonstrates the capabilities of the matchAny +# - name: "my.ng.feature.2" +# labels: +# my-ng-feature-2: "my-value" +# # matchAny implements a logical IF over all elements (sub-matchers) in +# # the list (i.e. at least one feature matcher must match) +# matchAny: +# - matchFeatures: +# - feature: kernel.loadedmodule +# matchExpressions: +# driver-module-X: {op: Exists} +# - feature: pci.device +# matchExpressions: +# vendor: {op: In, value: ["8086"]} +# class: {op: In, value: ["0200"]} +# - matchFeatures: +# - feature: kernel.loadedmodule +# matchExpressions: +# driver-module-Y: {op: Exists} +# - feature: usb.device +# matchExpressions: +# vendor: {op: In, value: ["8086"]} +# class: {op: In, value: ["02"]} diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml index 0b7253fc5..5bb5b2150 100644 --- a/deployment/helm/node-feature-discovery/values.yaml +++ b/deployment/helm/node-feature-discovery/values.yaml @@ -246,6 +246,30 @@ worker: # - feature: local.label # matchExpressions: # custom-feature-knob: {op: Gt, value: ["100"]} + # + # # The following feature demonstrates the capabilities of the matchAny + # - name: "my.ng.feature.2" + # labels: + # my-ng-feature-2: "my-value" + # # matchAny implements a logical IF over all elements (sub-matchers) in + # # the list (i.e. at least one feature matcher must match) + # matchAny: + # - matchFeatures: + # - feature: kernel.loadedmodule + # matchExpressions: + # driver-module-X: {op: Exists} + # - feature: pci.device + # matchExpressions: + # vendor: {op: In, value: ["8086"]} + # class: {op: In, value: ["0200"]} + # - matchFeatures: + # - feature: kernel.loadedmodule + # matchExpressions: + # driver-module-Y: {op: Exists} + # - feature: usb.device + # matchExpressions: + # vendor: {op: In, value: ["8086"]} + # class: {op: In, value: ["02"]} ### podSecurityContext: {} diff --git a/source/custom/custom.go b/source/custom/custom.go index f86918a70..4ccc5f85a 100644 --- a/source/custom/custom.go +++ b/source/custom/custom.go @@ -54,6 +54,11 @@ type Rule struct { Name string `json:"name"` Labels map[string]string `json:"labels"` MatchFeatures FeatureMatcher `json:"matchFeatures"` + MatchAny []MatchAnyElem `json:"matchAny"` +} + +type MatchAnyElem struct { + MatchFeatures FeatureMatcher } type FeatureMatcher []FeatureMatcherTerm @@ -185,6 +190,22 @@ func (r *LegacyRule) execute(features map[string]*feature.DomainFeatures) (map[s } func (r *Rule) execute(features map[string]*feature.DomainFeatures) (map[string]string, error) { + if len(r.MatchAny) > 0 { + // Logical OR over the matchAny matchers + matched := false + for _, matcher := range r.MatchAny { + if m, err := matcher.match(features); err != nil { + return nil, err + } else if m { + matched = true + break + } + } + if !matched { + return nil, nil + } + } + if len(r.MatchFeatures) > 0 { if m, err := r.MatchFeatures.match(features); err != nil { return nil, err @@ -201,6 +222,10 @@ func (r *Rule) execute(features map[string]*feature.DomainFeatures) (map[string] return labels, nil } +func (e *MatchAnyElem) match(features map[string]*feature.DomainFeatures) (bool, error) { + return e.MatchFeatures.match(features) +} + func (m *FeatureMatcher) match(features map[string]*feature.DomainFeatures) (bool, error) { // Logical AND over the terms for _, term := range *m { diff --git a/source/custom/custom_test.go b/source/custom/custom_test.go index 13881d847..968413260 100644 --- a/source/custom/custom_test.go +++ b/source/custom/custom_test.go @@ -150,4 +150,32 @@ func TestRule(t *testing.T) { assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r5.Labels, m, "instances should have matched") + // Test MatchAny + r5.MatchAny = []MatchAnyElem{ + MatchAnyElem{ + MatchFeatures: FeatureMatcher{ + FeatureMatcherTerm{ + Feature: "domain-1.kf-1", + MatchExpressions: expression.MatchExpressionSet{"key-na": expression.MustCreateMatchExpression(expression.MatchExists)}, + }, + }, + }, + } + m, err = r5.execute(f) + assert.Nilf(t, err, "unexpected error: %v", err) + assert.Nil(t, m, "instances should not have matched") + + r5.MatchAny = append(r5.MatchAny, + MatchAnyElem{ + MatchFeatures: FeatureMatcher{ + FeatureMatcherTerm{ + Feature: "domain-1.kf-1", + MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchExists)}, + }, + }, + }) + r5.MatchFeatures[0].MatchExpressions["key-1"] = expression.MustCreateMatchExpression(expression.MatchIn, "val-1") + m, err = r5.execute(f) + assert.Nilf(t, err, "unexpected error: %v", err) + assert.Equal(t, r5.Labels, m, "instances should have matched") }