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:
parent
d64398f85e
commit
08b9c3486e
5 changed files with 113 additions and 29 deletions
|
@ -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
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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,27 +782,16 @@ 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)
|
|
||||||
}
|
|
||||||
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)
|
q, err := k8sQuantity.ParseQuantity(element)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid value %s (from %s): %w", element, value, err)
|
return "", fmt.Errorf("invalid value %s (from %s): %w", element, value, err)
|
||||||
}
|
}
|
||||||
return q.String(), nil
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Add table
Reference in a new issue