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

feat: support dynamic values for labels in the NodeFeatureRule

This PR aims to support the dynamic values for labels in the
NodeFeatureRule CRD, it would offer more flexible labeling for users.
To achieve this, we check whether label value starts with "@", and if
it's the case, we will get the value of the feature value, and update
the value of the label with the feature value.

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>
This commit is contained in:
AhmedGrati 2023-05-24 11:56:36 +01:00
parent d64398f85e
commit 08b9c3486e
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))