1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-31 04:04:51 +00:00

Merge pull request #1226 from AhmedGrati/feat-support-dynamic-values-nfr-labels

feat: support dynamic values for labels in the NodeFeatureRule
This commit is contained in:
Kubernetes Prow Robot 2023-06-01 06:17:46 -07:00 committed by GitHub
commit 4dea63c9fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 113 additions and 29 deletions

View file

@ -475,6 +475,38 @@ The `.name` field is required and used as an identifier of the rule.
The `.labels` is a map of the node labels to create if the rule matches. The `.labels` is a map of the node labels to create if the rule matches.
Take this rule as a referential example:
```yaml
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureRule
metadata:
name: my-sample-rule-object
spec:
rules:
- name: "my dynamic label value rule"
labels:
linux-lsm-enabled: "@kernel.config.LSM"
custom-label: "customlabel"
```
Label `linux-lsm-enabled` uses the `@` notation for dynamic values.
The value of the label will be the value of the attribute `LSM`
of the feature `kernel.config`.
The `@<feature-name>.<element-name>` format can be used to inject values of
detected features to the label. See
[available features](#available-features) for possible values to use.
This will yield into the following node label:
```yaml
labels:
...
feature.node.kubernetes.io/linux-lsm-enabled: apparmor
feature.node.kubernetes.io/custom-label: "customlabel"
```
#### Labels template #### Labels template
The `.labelsTemplate` field specifies a text template for dynamically creating The `.labelsTemplate` field specifies a text template for dynamically creating

View file

@ -36,6 +36,7 @@ import (
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sclient "k8s.io/client-go/kubernetes" k8sclient "k8s.io/client-go/kubernetes"
"sigs.k8s.io/node-feature-discovery/pkg/apihelper" "sigs.k8s.io/node-feature-discovery/pkg/apihelper"
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
"sigs.k8s.io/node-feature-discovery/pkg/labeler" "sigs.k8s.io/node-feature-discovery/pkg/labeler"
"sigs.k8s.io/node-feature-discovery/pkg/utils" "sigs.k8s.io/node-feature-discovery/pkg/utils"
@ -438,6 +439,34 @@ func TestSetLabels(t *testing.T) {
}) })
} }
func TestFilterLabels(t *testing.T) {
mockHelper := &apihelper.MockAPIHelpers{}
mockMaster := newMockMaster(mockHelper)
Convey("When using dynamic values", t, func() {
labelName := "testLabel"
labelValue := "@test.feature.LSM"
features := nfdv1alpha1.Features{
Attributes: map[string]nfdv1alpha1.AttributeFeatureSet{
"test.feature": v1alpha1.AttributeFeatureSet{
Elements: map[string]string{
"LSM": "123",
},
},
},
}
labelValue, err := mockMaster.filterFeatureLabel(labelName, labelValue, &features)
Convey("Operation should succeed", func() {
So(err, ShouldBeNil)
})
Convey("Label value should change", func() {
So(labelValue, ShouldEqual, "123")
})
})
}
func TestCreatePatches(t *testing.T) { func TestCreatePatches(t *testing.T) {
Convey("When creating JSON patches", t, func() { Convey("When creating JSON patches", t, func() {
existingItems := map[string]string{"key-1": "val-1", "key-2": "val-2", "key-3": "val-3"} existingItems := map[string]string{"key-1": "val-1", "key-2": "val-2", "key-3": "val-3"}

View file

@ -483,13 +483,13 @@ func (m *nfdMaster) updateMasterNode() error {
// into extended resources. This function also handles proper namespacing of // into extended resources. This function also handles proper namespacing of
// labels and ERs, i.e. adds the possibly missing default namespace for labels // labels and ERs, i.e. adds the possibly missing default namespace for labels
// arriving through the gRPC API. // arriving through the gRPC API.
func (m *nfdMaster) filterFeatureLabels(labels Labels) (Labels, ExtendedResources) { func (m *nfdMaster) filterFeatureLabels(labels Labels, features *nfdv1alpha1.Features) (Labels, ExtendedResources) {
outLabels := Labels{} outLabels := Labels{}
for name, value := range labels { for name, value := range labels {
// Add possibly missing default ns // Add possibly missing default ns
name := addNs(name, nfdv1alpha1.FeatureLabelNs) name := addNs(name, nfdv1alpha1.FeatureLabelNs)
if err := m.filterFeatureLabel(name, value); err != nil { if value, err := m.filterFeatureLabel(name, value, features); err != nil {
klog.ErrorS(err, "ignoring label", "labelKey", name, "labelValue", value) klog.ErrorS(err, "ignoring label", "labelKey", name, "labelValue", value)
} else { } else {
outLabels[name] = value outLabels[name] = value
@ -515,10 +515,11 @@ func (m *nfdMaster) filterFeatureLabels(labels Labels) (Labels, ExtendedResource
return outLabels, extendedResources return outLabels, extendedResources
} }
func (m *nfdMaster) filterFeatureLabel(name, value string) error { func (m *nfdMaster) filterFeatureLabel(name, value string, features *nfdv1alpha1.Features) (string, error) {
//Validate label name //Validate label name
if errs := k8svalidation.IsQualifiedName(name); len(errs) > 0 { if errs := k8svalidation.IsQualifiedName(name); len(errs) > 0 {
return fmt.Errorf("invalid name %q: %s", name, strings.Join(errs, "; ")) return "", fmt.Errorf("invalid name %q: %s", name, strings.Join(errs, "; "))
} }
// Check label namespace, filter out if ns is not whitelisted // Check label namespace, filter out if ns is not whitelisted
@ -528,22 +529,53 @@ func (m *nfdMaster) filterFeatureLabel(name, value string) error {
// If the namespace is denied, and not present in the extraLabelNs, label will be ignored // If the namespace is denied, and not present in the extraLabelNs, label will be ignored
if isNamespaceDenied(ns, m.deniedNs.wildcard, m.deniedNs.normal) { if isNamespaceDenied(ns, m.deniedNs.wildcard, m.deniedNs.normal) {
if _, ok := m.config.ExtraLabelNs[ns]; !ok { if _, ok := m.config.ExtraLabelNs[ns]; !ok {
return fmt.Errorf("namespace %q is not allowed", ns) return "", fmt.Errorf("namespace %q is not allowed", ns)
} }
} }
} }
// Skip if label doesn't match labelWhiteList // Skip if label doesn't match labelWhiteList
if !m.config.LabelWhiteList.Regexp.MatchString(base) { if !m.config.LabelWhiteList.Regexp.MatchString(base) {
return fmt.Errorf("%s (%s) does not match the whitelist (%s)", base, name, m.config.LabelWhiteList.Regexp.String()) return "", fmt.Errorf("%s (%s) does not match the whitelist (%s)", base, name, m.config.LabelWhiteList.Regexp.String())
}
var filteredLabel string
// Dynamic Value
if strings.HasPrefix(value, "@") {
dynamicValue, err := getDynamicValue(value, features)
if err != nil {
return "", err
}
filteredLabel = dynamicValue
} else {
filteredLabel = value
} }
// Validate the label value // Validate the label value
if errs := k8svalidation.IsValidLabelValue(value); len(errs) > 0 { if errs := k8svalidation.IsValidLabelValue(filteredLabel); len(errs) > 0 {
return fmt.Errorf("invalid value %q: %s", value, strings.Join(errs, "; ")) return "", fmt.Errorf("invalid value %q: %s", filteredLabel, strings.Join(errs, "; "))
} }
return filteredLabel, nil
}
return nil func getDynamicValue(value string, features *nfdv1alpha1.Features) (string, error) {
// value is a string in the form of attribute.featureset.elements
split := strings.SplitN(value[1:], ".", 3)
if len(split) != 3 {
return "", fmt.Errorf("value %s is not in the form of '@domain.feature.element'", value)
}
featureName := split[0] + "." + split[1]
elementName := split[2]
attrFeatureSet, ok := features.Attributes[featureName]
if !ok {
return "", fmt.Errorf("feature %s not found", featureName)
}
element, ok := attrFeatureSet.Elements[elementName]
if !ok {
return "", fmt.Errorf("element %s not found on feature %s", elementName, featureName)
}
return element, nil
} }
func filterTaints(taints []corev1.Taint) []corev1.Taint { func filterTaints(taints []corev1.Taint) []corev1.Taint {
@ -750,26 +782,15 @@ func filterExtendedResource(name, value string, features *nfdv1alpha1.Features)
// Dynamic Value // Dynamic Value
if strings.HasPrefix(value, "@") { if strings.HasPrefix(value, "@") {
// value is a string in the form of attribute.featureset.elements if element, err := getDynamicValue(value, features); err != nil {
split := strings.SplitN(value[1:], ".", 3) return "", err
if len(split) != 3 { } else {
return "", fmt.Errorf("value %s is not in the form of '@domain.feature.element'", value) q, err := k8sQuantity.ParseQuantity(element)
if err != nil {
return "", fmt.Errorf("invalid value %s (from %s): %w", element, value, err)
}
return q.String(), nil
} }
featureName := split[0] + "." + split[1]
elementName := split[2]
attrFeatureSet, ok := features.Attributes[featureName]
if !ok {
return "", fmt.Errorf("feature %s not found", featureName)
}
element, ok := attrFeatureSet.Elements[elementName]
if !ok {
return "", fmt.Errorf("element %s not found on feature %s", elementName, featureName)
}
q, err := k8sQuantity.ParseQuantity(element)
if err != nil {
return "", fmt.Errorf("invalid value %s (from %s): %w", element, value, err)
}
return q.String(), nil
} }
// Static Value (Pre-Defined at the NodeFeatureRule) // Static Value (Pre-Defined at the NodeFeatureRule)
q, err := k8sQuantity.ParseQuantity(value) q, err := k8sQuantity.ParseQuantity(value)
@ -794,7 +815,7 @@ func (m *nfdMaster) refreshNodeFeatures(cli *kubernetes.Clientset, nodeName stri
// Remove labels which are intended to be extended resources via // Remove labels which are intended to be extended resources via
// -resource-labels or their NS is not whitelisted // -resource-labels or their NS is not whitelisted
labels, extendedResources := m.filterFeatureLabels(labels) labels, extendedResources := m.filterFeatureLabels(labels, features)
// Mix in CR-originated extended resources with -resource-labels // Mix in CR-originated extended resources with -resource-labels
for k, v := range crExtendedResources { for k, v := range crExtendedResources {

View file

@ -10,6 +10,7 @@ spec:
- name: "e2e-matchany-test-1" - name: "e2e-matchany-test-1"
labels: labels:
e2e-matchany-test-1: "true" e2e-matchany-test-1: "true"
dynamic-label: "@rule.matched.e2e-attribute-test-1"
vars: vars:
e2e-instance-test-1.not: "false" e2e-instance-test-1.not: "false"
matchFeatures: matchFeatures:

View file

@ -710,6 +710,7 @@ core:
expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-matchany-test-1"] = "true" expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-matchany-test-1"] = "true"
expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_1"] = "found" expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_1"] = "found"
expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_2"] = "found" expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_2"] = "found"
expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/dynamic-label"] = "true"
By("Verifying node labels from NodeFeatureRules #1 and #2") By("Verifying node labels from NodeFeatureRules #1 and #2")
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false)) eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))