1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-28 10:47:23 +00:00

Merge pull request #1184 from marquiz/devel/e2e

test/e2e: refactor matching of node properties
This commit is contained in:
Kubernetes Prow Robot 2023-05-04 23:45:12 -07:00 committed by GitHub
commit 5004574cbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 362 additions and 260 deletions

271
test/e2e/gomega.go Normal file
View file

@ -0,0 +1,271 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"context"
"fmt"
"strings"
"time"
. "github.com/onsi/gomega"
gomegatypes "github.com/onsi/gomega/types"
"golang.org/x/exp/maps"
taintutils "k8s.io/kubernetes/pkg/util/taints"
corev1 "k8s.io/api/core/v1"
clientset "k8s.io/client-go/kubernetes"
e2elog "k8s.io/kubernetes/test/e2e/framework"
)
type k8sLabels map[string]string
type k8sAnnotations map[string]string
// eventuallyNonControlPlaneNodes is a helper for asserting node properties
func eventuallyNonControlPlaneNodes(ctx context.Context, cli clientset.Interface) AsyncAssertion {
return Eventually(func(g Gomega, ctx context.Context) ([]corev1.Node, error) {
return getNonControlPlaneNodes(ctx, cli)
}).WithPolling(1 * time.Second).WithTimeout(10 * time.Second).WithContext(ctx)
}
// MatchLabels returns a specialized Gomega matcher for checking if a list of
// nodes are labeled as expected.
func MatchLabels(expectedNew map[string]k8sLabels, oldNodes []corev1.Node, ignoreUnexpected bool) gomegatypes.GomegaMatcher {
matcher := &nodeIterablePropertyMatcher[k8sLabels]{
propertyName: "labels",
ignoreUnexpected: ignoreUnexpected,
matchFunc: func(newNode, oldNode corev1.Node, expected k8sLabels) ([]string, []string, []string) {
expectedAll := maps.Clone(oldNode.Labels)
maps.Copy(expectedAll, expected)
return matchMap(newNode.Labels, expectedAll)
},
}
return &nodeListPropertyMatcher[k8sLabels]{
expected: expectedNew,
oldNodes: oldNodes,
matcher: matcher,
}
}
// MatchAnnotations returns a specialized Gomega matcher for checking if a list of
// nodes are annotated as expected.
func MatchAnnotations(expectedNew map[string]k8sAnnotations, oldNodes []corev1.Node, ignoreUnexpected bool) gomegatypes.GomegaMatcher {
matcher := &nodeIterablePropertyMatcher[k8sAnnotations]{
propertyName: "annotations",
ignoreUnexpected: ignoreUnexpected,
matchFunc: func(newNode, oldNode corev1.Node, expected k8sAnnotations) ([]string, []string, []string) {
expectedAll := maps.Clone(oldNode.Annotations)
maps.Copy(expectedAll, expected)
return matchMap(newNode.Annotations, expectedAll)
},
}
return &nodeListPropertyMatcher[k8sAnnotations]{
expected: expectedNew,
oldNodes: oldNodes,
matcher: matcher,
}
}
// MatchCapacity returns a specialized Gomega matcher for checking if a list of
// nodes have resource capacity as expected.
func MatchCapacity(expectedNew map[string]corev1.ResourceList, oldNodes []corev1.Node, ignoreUnexpected bool) gomegatypes.GomegaMatcher {
matcher := &nodeIterablePropertyMatcher[corev1.ResourceList]{
propertyName: "resource capacity",
ignoreUnexpected: ignoreUnexpected,
matchFunc: func(newNode, oldNode corev1.Node, expected corev1.ResourceList) ([]string, []string, []string) {
expectedAll := oldNode.Status.DeepCopy().Capacity
maps.Copy(expectedAll, expected)
return matchMap(newNode.Status.Capacity, expectedAll)
},
}
return &nodeListPropertyMatcher[corev1.ResourceList]{
expected: expectedNew,
oldNodes: oldNodes,
matcher: matcher,
}
}
// MatchTaints returns a specialized Gomega matcher for checking if a list of
// nodes are tainted as expected.
func MatchTaints(expectedNew map[string][]corev1.Taint, oldNodes []corev1.Node, ignoreUnexpected bool) gomegatypes.GomegaMatcher {
matcher := &nodeIterablePropertyMatcher[[]corev1.Taint]{
propertyName: "taints",
ignoreUnexpected: ignoreUnexpected,
matchFunc: func(newNode, oldNode corev1.Node, expected []corev1.Taint) (missing, invalid, unexpected []string) {
expectedAll := oldNode.Spec.DeepCopy().Taints
expectedAll = append(expectedAll, expected...)
taints := newNode.Spec.Taints
for _, expectedTaint := range expectedAll {
if !taintutils.TaintExists(taints, &expectedTaint) {
missing = append(missing, expectedTaint.ToString())
} else if ok, matched := taintWithValueExists(taints, &expectedTaint); !ok {
invalid = append(invalid, fmt.Sprintf("%s, expected value %s", matched.ToString(), expectedTaint.Value))
}
}
for _, taint := range taints {
if !taintutils.TaintExists(expectedAll, &taint) {
unexpected = append(unexpected, taint.ToString())
}
}
return missing, invalid, unexpected
},
}
return &nodeListPropertyMatcher[[]corev1.Taint]{
expected: expectedNew,
oldNodes: oldNodes,
matcher: matcher,
}
}
func taintWithValueExists(taints []corev1.Taint, taintToFind *corev1.Taint) (found bool, matched corev1.Taint) {
for _, taint := range taints {
if taint.Key == taintToFind.Key && taint.Effect == taintToFind.Effect {
matched = taint
if taint.Value == taintToFind.Value {
return true, matched
}
}
}
return false, matched
}
// nodeListPropertyMatcher is a generic Gomega matcher for asserting one property a group of nodes.
type nodeListPropertyMatcher[T any] struct {
expected map[string]T
oldNodes []corev1.Node
matcher nodePropertyMatcher[T]
}
// nodePropertyMatcher is a generic helper type for matching one node.
type nodePropertyMatcher[T any] interface {
match(newNode, oldNode corev1.Node, expected T) bool
message() string
negatedMessage() string
}
// Match method of the GomegaMatcher interface.
func (m *nodeListPropertyMatcher[T]) Match(actual interface{}) (bool, error) {
nodes, ok := actual.([]corev1.Node)
if !ok {
return false, fmt.Errorf("expected []corev1.Node, got: %T", actual)
}
for _, node := range nodes {
expected, ok := m.expected[node.Name]
if !ok {
if defaultExpected, ok := m.expected["*"]; ok {
expected = defaultExpected
} else {
e2elog.Logf("Skipping node %q as no expected was specified", node.Name)
continue
}
}
oldNode := getNode(m.oldNodes, node.Name)
if matched := m.matcher.match(node, oldNode, expected); !matched {
return false, nil
}
}
return true, nil
}
// FailureMessage method of the GomegaMatcher interface.
func (m *nodeListPropertyMatcher[T]) FailureMessage(actual interface{}) string {
return m.matcher.message()
}
// NegatedFailureMessage method of the GomegaMatcher interface.
func (m *nodeListPropertyMatcher[T]) NegatedFailureMessage(actual interface{}) string {
return m.matcher.negatedMessage()
}
// nodeIterablePropertyMatcher is a nodePropertyMatcher for matching iterable
// elements such as maps or lists.
type nodeIterablePropertyMatcher[T any] struct {
propertyName string
ignoreUnexpected bool
matchFunc func(newNode, oldNode corev1.Node, expected T) ([]string, []string, []string)
// TODO remove nolint when golangci-lint is able to cope with generics
node *corev1.Node //nolint:unused
missing []string //nolint:unused
invalidValue []string //nolint:unused
unexpected []string //nolint:unused
}
// TODO remove nolint when golangci-lint is able to cope with generics
//
//nolint:unused
func (m *nodeIterablePropertyMatcher[T]) match(newNode, oldNode corev1.Node, expected T) bool {
m.node = &newNode
m.missing, m.invalidValue, m.unexpected = m.matchFunc(newNode, oldNode, expected)
if m.ignoreUnexpected {
m.unexpected = nil
}
return len(m.missing) == 0 && len(m.invalidValue) == 0 && len(m.unexpected) == 0
}
// TODO remove nolint when golangci-lint is able to cope with generics
//
//nolint:unused
func (m *nodeIterablePropertyMatcher[T]) message() string {
msg := fmt.Sprintf("Node %q %s did not match:", m.node.Name, m.propertyName)
if len(m.missing) > 0 {
msg += fmt.Sprintf("\n missing:\n %s", strings.Join(m.missing, "\n "))
}
if len(m.invalidValue) > 0 {
msg += fmt.Sprintf("\n invalid value:\n %s", strings.Join(m.invalidValue, "\n "))
}
if len(m.unexpected) > 0 {
msg += fmt.Sprintf("\n unexpected:\n %s", strings.Join(m.unexpected, "\n "))
}
return msg
}
// TODO remove nolint when golangci-lint is able to cope with generics
//
//nolint:unused
func (m *nodeIterablePropertyMatcher[T]) negatedMessage() string {
return fmt.Sprintf("Node %q matched unexpectedly", m.node.Name)
}
// matchMap is a helper for matching map types
func matchMap[M ~map[K]V, K comparable, V comparable](actual, expected M) (missing, invalid, unexpected []string) {
for k, ve := range expected {
va, ok := actual[k]
if !ok {
missing = append(missing, fmt.Sprintf("%v", k))
} else if va != ve {
invalid = append(invalid, fmt.Sprintf("%v=%v, expected value %v", k, va, ve))
}
}
for k, v := range actual {
if _, ok := expected[k]; !ok {
unexpected = append(unexpected, fmt.Sprintf("%v=%v", k, v))
}
}
return missing, invalid, unexpected
}

View file

@ -24,10 +24,8 @@ import (
"strings"
"time"
"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"golang.org/x/exp/maps"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@ -275,11 +273,8 @@ var _ = SIGDescribe("NFD master and worker", func() {
//
Context("and a single worker pod with fake source enabled", func() {
It("it should decorate the node with the fake feature labels", func(ctx context.Context) {
fakeFeatureLabels := map[string]string{
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature1": "true",
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature2": "true",
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3": "true",
}
nodes, err := getNonControlPlaneNodes(ctx, f.ClientSet)
Expect(err).NotTo(HaveOccurred())
// Launch nfd-worker
By("Creating a nfd worker pod")
@ -289,7 +284,7 @@ var _ = SIGDescribe("NFD master and worker", func() {
testpod.SpecWithContainerExtraArgs("-oneshot", "-label-sources=fake"),
)
workerPod := testpod.NFDWorker(podSpecOpts...)
workerPod, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, workerPod, metav1.CreateOptions{})
workerPod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, workerPod, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
By("Waiting for the nfd-worker pod to succeed")
@ -298,20 +293,17 @@ var _ = SIGDescribe("NFD master and worker", func() {
Expect(err).NotTo(HaveOccurred())
By(fmt.Sprintf("Making sure '%s' was decorated with the fake feature labels", workerPod.Spec.NodeName))
node, err := f.ClientSet.CoreV1().Nodes().Get(ctx, workerPod.Spec.NodeName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
for k, v := range fakeFeatureLabels {
Expect(node.Labels[k]).To(Equal(v))
expectedLabels := map[string]k8sLabels{
workerPod.Spec.NodeName: {
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature1": "true",
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature2": "true",
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3": "true",
},
"*": {},
}
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
// Check that there are no unexpected NFD labels
for k := range node.Labels {
if strings.HasPrefix(k, nfdv1alpha1.FeatureLabelNs) {
Expect(fakeFeatureLabels).Should(HaveKey(k))
}
}
checkNodeFeatureObject(ctx, node.Name)
checkNodeFeatureObject(ctx, workerPod.Spec.NodeName)
By("Deleting the node-feature-discovery worker pod")
err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Delete(ctx, workerPod.Name, metav1.DeleteOptions{})
@ -435,30 +427,28 @@ var _ = SIGDescribe("NFD master and worker", func() {
targetLabelNameWildcard := "nodename-test-wildcard"
targetLabelValueWildcard := "customValue"
targetLabelNameNegative := "nodename-test-negative"
// create 2 configmaps
data1 := `
- name: ` + targetLabelName + `
data1 := fmt.Sprintf(`
- name: %s
matchOn:
# default value is true
- nodename:
- ` + targetNodeName
- "^%s$"`, targetLabelName, targetNodeName)
cm1 := testutils.NewConfigMap("custom-config-extra-1", "custom.conf", data1)
cm1, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, cm1, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
data2 := `
- name: ` + targetLabelNameWildcard + `
value: ` + targetLabelValueWildcard + `
data2 := fmt.Sprintf(`
- name: %s
value: %s
matchOn:
- nodename:
- ` + targetNodeNameWildcard + `
- name: ` + targetLabelNameNegative + `
- "^%s$"
- name: nodename-test-negative
matchOn:
- nodename:
- "thisNameShouldNeverMatch"`
- "thisNameShouldNeverMatch"`, targetLabelNameWildcard, targetLabelValueWildcard, targetNodeNameWildcard)
cm2 := testutils.NewConfigMap("custom-config-extra-2", "custom.conf", data2)
cm2, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, cm2, metav1.CreateOptions{})
@ -467,6 +457,7 @@ var _ = SIGDescribe("NFD master and worker", func() {
By("Creating nfd-worker daemonset with configmap mounted")
podSpecOpts := createPodSpecOpts(
testpod.SpecWithContainerImage(dockerImage()),
testpod.SpecWithContainerExtraArgs("-label-sources=custom"),
testpod.SpecWithConfigMap(cm1.Name, filepath.Join(custom.Directory, "cm1")),
testpod.SpecWithConfigMap(cm2.Name, filepath.Join(custom.Directory, "cm2")),
)
@ -478,32 +469,15 @@ var _ = SIGDescribe("NFD master and worker", func() {
By("Waiting for worker daemonset pods to be ready")
Expect(testpod.WaitForReady(ctx, f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 2)).NotTo(HaveOccurred())
By("Getting target node and checking labels")
targetNode, err := f.ClientSet.CoreV1().Nodes().Get(ctx, targetNodeName, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
labelFound := false
labelWildcardFound := false
labelNegativeFound := false
for k := range targetNode.Labels {
if strings.Contains(k, targetLabelName) {
if targetNode.Labels[k] == targetLabelValue {
labelFound = true
}
}
if strings.Contains(k, targetLabelNameWildcard) {
if targetNode.Labels[k] == targetLabelValueWildcard {
labelWildcardFound = true
}
}
if strings.Contains(k, targetLabelNameNegative) {
labelNegativeFound = true
}
By("Verifying node labels")
expectedLabels := map[string]k8sLabels{
targetNodeName: {
nfdv1alpha1.FeatureLabelNs + "/custom-" + targetLabelName: targetLabelValue,
nfdv1alpha1.FeatureLabelNs + "/custom-" + targetLabelNameWildcard: targetLabelValueWildcard,
},
"*": {},
}
Expect(labelFound).To(BeTrue(), "label not found!")
Expect(labelWildcardFound).To(BeTrue(), "label for wildcard nodename not found!")
Expect(labelNegativeFound).To(BeFalse(), "label for not existing nodename found!")
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Deleting nfd-worker daemonset")
err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Delete(ctx, workerDS.Name, metav1.DeleteOptions{})
@ -546,18 +520,16 @@ var _ = SIGDescribe("NFD master and worker", func() {
nfdv1alpha1.FeatureLabelNs + "/e2e-nodefeature-test-2": "obj-1",
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3": "overridden",
},
"*": {},
}
Expect(checkForNodeLabels(ctx, f.ClientSet,
expectedLabels, nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Deleting NodeFeature object")
err = nfdClient.NfdV1alpha1().NodeFeatures(f.Namespace.Name).Delete(ctx, nodeFeatures[0], metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
By("Verifying node labels from NodeFeature object were removed")
Expect(checkForNodeLabels(ctx, f.ClientSet,
nil, nodes,
)).NotTo(HaveOccurred())
expectedLabels[targetNodeName] = k8sLabels{}
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Creating nfd-worker daemonset")
podSpecOpts := createPodSpecOpts(
@ -579,9 +551,7 @@ var _ = SIGDescribe("NFD master and worker", func() {
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3": "true",
},
}
Expect(checkForNodeLabels(ctx, f.ClientSet,
expectedLabels, nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Re-creating NodeFeature object")
_, err = testutils.CreateOrUpdateNodeFeaturesFromFile(ctx, nfdClient, "nodefeature-1.yaml", f.Namespace.Name, targetNodeName)
@ -595,9 +565,7 @@ var _ = SIGDescribe("NFD master and worker", func() {
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature2": "true",
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3": "overridden",
}
Expect(checkForNodeLabels(ctx, f.ClientSet,
expectedLabels, nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Creating extra namespace")
extraNs, err := f.CreateNamespace(ctx, "node-feature-discvery-extra-ns", nil)
@ -610,7 +578,7 @@ var _ = SIGDescribe("NFD master and worker", func() {
By("Verifying node labels from NodeFeature object #2 are created")
expectedLabels[targetNodeName][nfdv1alpha1.FeatureLabelNs+"/e2e-nodefeature-test-1"] = "overridden-from-obj-2"
expectedLabels[targetNodeName][nfdv1alpha1.FeatureLabelNs+"/e2e-nodefeature-test-3"] = "obj-2"
Expect(checkForNodeLabels(ctx, f.ClientSet, expectedLabels, nodes)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Deleting NodeFeature object from the extra namespace")
err = nfdClient.NfdV1alpha1().NodeFeatures(extraNs.Name).Delete(ctx, nodeFeatures[0], metav1.DeleteOptions{})
@ -619,11 +587,7 @@ var _ = SIGDescribe("NFD master and worker", func() {
By("Verifying node labels from NodeFeature object were removed")
expectedLabels[targetNodeName][nfdv1alpha1.FeatureLabelNs+"/e2e-nodefeature-test-1"] = "obj-1"
delete(expectedLabels[targetNodeName], nfdv1alpha1.FeatureLabelNs+"/e2e-nodefeature-test-3")
Expect(checkForNodeLabels(ctx, f.ClientSet, expectedLabels, nodes)).NotTo(HaveOccurred())
Expect(checkForNodeLabels(ctx, f.ClientSet,
expectedLabels,
nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
})
It("denied labels should not be created by the NodeFeature object", func(ctx context.Context) {
@ -650,21 +614,14 @@ var _ = SIGDescribe("NFD master and worker", func() {
"custom.vendor.io/e2e-nodefeature-test-3": "vendor-ns",
},
}
Expect(checkForNodeLabels(ctx,
f.ClientSet,
expectedLabels,
nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Deleting NodeFeature object")
err = nfdClient.NfdV1alpha1().NodeFeatures(f.Namespace.Name).Delete(ctx, nodeFeatures[0], metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(checkForNodeLabels(ctx,
f.ClientSet,
nil,
nodes,
)).NotTo(HaveOccurred())
expectedLabels[targetNodeName] = k8sLabels{}
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
})
})
@ -728,7 +685,7 @@ core:
By("Waiting for worker daemonset pods to be ready")
Expect(testpod.WaitForReady(ctx, f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 2)).NotTo(HaveOccurred())
expected := map[string]k8sLabels{
expectedLabels := map[string]k8sLabels{
"*": {
nfdv1alpha1.FeatureLabelNs + "/e2e-flag-test-1": "true",
nfdv1alpha1.FeatureLabelNs + "/e2e-attribute-test-1": "true",
@ -740,57 +697,53 @@ core:
Expect(testutils.CreateNodeFeatureRulesFromFile(ctx, nfdClient, "nodefeaturerule-1.yaml")).NotTo(HaveOccurred())
By("Verifying node labels from NodeFeatureRules #1")
Expect(checkForNodeLabels(ctx,
f.ClientSet,
expected,
nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Creating NodeFeatureRules #2")
Expect(testutils.CreateNodeFeatureRulesFromFile(ctx, nfdClient, "nodefeaturerule-2.yaml")).NotTo(HaveOccurred())
// Add features from NodeFeatureRule #2
expected["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-matchany-test-1"] = "true"
expected["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_1"] = "found"
expected["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_2"] = "found"
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_2"] = "found"
By("Verifying node labels from NodeFeatureRules #1 and #2")
Expect(checkForNodeLabels(ctx,
f.ClientSet,
expected,
nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
// Add features from NodeFeatureRule #3
By("Creating NodeFeatureRules #3")
Expect(testutils.CreateNodeFeatureRulesFromFile(ctx, nfdClient, "nodefeaturerule-3.yaml")).NotTo(HaveOccurred())
By("Verifying node taints and annotation from NodeFeatureRules #3")
expectedTaints := []corev1.Taint{
{
Key: "feature.node.kubernetes.io/fake-special-node",
Value: "exists",
Effect: "PreferNoSchedule",
},
{
Key: "feature.node.kubernetes.io/fake-dedicated-node",
Value: "true",
Effect: "NoExecute",
},
{
Key: "feature.node.kubernetes.io/performance-optimized-node",
Value: "true",
Effect: "NoExecute",
expectedTaints := map[string][]corev1.Taint{
"*": {
{
Key: "feature.node.kubernetes.io/fake-special-node",
Value: "exists",
Effect: "PreferNoSchedule",
},
{
Key: "feature.node.kubernetes.io/fake-dedicated-node",
Value: "true",
Effect: "NoExecute",
},
{
Key: "feature.node.kubernetes.io/performance-optimized-node",
Value: "true",
Effect: "NoExecute",
},
},
}
expectedAnnotation := map[string]string{
"nfd.node.kubernetes.io/taints": "feature.node.kubernetes.io/fake-special-node=exists:PreferNoSchedule,feature.node.kubernetes.io/fake-dedicated-node=true:NoExecute,feature.node.kubernetes.io/performance-optimized-node=true:NoExecute"}
Expect(waitForNfdNodeTaints(ctx, f.ClientSet, expectedTaints, nodes)).NotTo(HaveOccurred())
Expect(waitForNfdNodeAnnotations(ctx, f.ClientSet, expectedAnnotation)).NotTo(HaveOccurred())
expectedAnnotations := map[string]k8sAnnotations{
"*": {
"nfd.node.kubernetes.io/taints": "feature.node.kubernetes.io/fake-special-node=exists:PreferNoSchedule,feature.node.kubernetes.io/fake-dedicated-node=true:NoExecute,feature.node.kubernetes.io/performance-optimized-node=true:NoExecute"},
}
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchTaints(expectedTaints, nodes, false))
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchAnnotations(expectedAnnotations, nodes, true))
By("Re-applying NodeFeatureRules #3 with updated taints")
Expect(testutils.UpdateNodeFeatureRulesFromFile(ctx, nfdClient, "nodefeaturerule-3-updated.yaml")).NotTo(HaveOccurred())
expectedTaintsUpdated := []corev1.Taint{
expectedTaints["*"] = []corev1.Taint{
{
Key: "feature.node.kubernetes.io/fake-special-node",
Value: "exists",
@ -802,41 +755,43 @@ core:
Effect: "NoExecute",
},
}
expectedAnnotationUpdated := map[string]string{
"nfd.node.kubernetes.io/taints": "feature.node.kubernetes.io/fake-special-node=exists:PreferNoSchedule,feature.node.kubernetes.io/foo=true:NoExecute"}
expectedAnnotations["*"]["nfd.node.kubernetes.io/taints"] = "feature.node.kubernetes.io/fake-special-node=exists:PreferNoSchedule,feature.node.kubernetes.io/foo=true:NoExecute"
By("Verifying updated node taints and annotation from NodeFeatureRules #3")
Expect(waitForNfdNodeTaints(ctx, f.ClientSet, expectedTaintsUpdated, nodes)).NotTo(HaveOccurred())
Expect(waitForNfdNodeAnnotations(ctx, f.ClientSet, expectedAnnotationUpdated)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchTaints(expectedTaints, nodes, false))
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchAnnotations(expectedAnnotations, nodes, true))
By("Deleting NodeFeatureRule object")
err = nfdClient.NfdV1alpha1().NodeFeatureRules().Delete(ctx, "e2e-test-3", metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
expectedTaints["*"] = []corev1.Taint{}
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchTaints(expectedTaints, nodes, false))
expectedERAnnotation := map[string]string{
"nfd.node.kubernetes.io/extended-resources": "nons,vendor.io/dynamic,vendor.io/static"}
expectedAnnotations["*"] = k8sAnnotations{"nfd.node.kubernetes.io/extended-resources": "nons,vendor.io/dynamic,vendor.io/static"}
expectedCapacity := corev1.ResourceList{
"feature.node.kubernetes.io/nons": resourcev1.MustParse("123"),
"vendor.io/dynamic": resourcev1.MustParse("10"),
"vendor.io/static": resourcev1.MustParse("123"),
expectedCapacity := map[string]corev1.ResourceList{
"*": {
"feature.node.kubernetes.io/nons": resourcev1.MustParse("123"),
"vendor.io/dynamic": resourcev1.MustParse("10"),
"vendor.io/static": resourcev1.MustParse("123"),
},
}
By("Creating NodeFeatureRules #4")
Expect(testutils.CreateNodeFeatureRulesFromFile(ctx, nfdClient, "nodefeaturerule-4.yaml")).NotTo(HaveOccurred())
By("Verifying node annotations from NodeFeatureRules #4")
Expect(waitForNfdNodeAnnotations(ctx, f.ClientSet, expectedERAnnotation)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchAnnotations(expectedAnnotations, nodes, true))
By("Verfiying node status capacity from NodeFeatureRules #4")
Expect(waitForCapacity(ctx, f.ClientSet, expectedCapacity, nodes)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchCapacity(expectedCapacity, nodes, false))
By("Deleting NodeFeatureRule object")
err = nfdClient.NfdV1alpha1().NodeFeatureRules().Delete(ctx, "e2e-extened-resource-test", metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
By("Verfiying node status capacity from NodeFeatureRules #4")
Expect(waitForCapacity(ctx, f.ClientSet, nil, nodes)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchCapacity(expectedCapacity, nodes, false))
By("Deleting nfd-worker daemonset")
err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Delete(ctx, workerDS.Name, metav1.DeleteOptions{})
@ -880,11 +835,7 @@ denyLabelNs: ["*.denied.ns","random.unwanted.ns"]
"custom.vendor.io/e2e-nodefeature-test-3": "vendor-ns",
},
}
Expect(checkForNodeLabels(ctx,
f.ClientSet,
expectedLabels,
nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Deleting NodeFeature object")
err = nfdClient.NfdV1alpha1().NodeFeatures(f.Namespace.Name).Delete(ctx, nodeFeatures[0], metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
@ -905,11 +856,7 @@ denyLabelNs: []
"random.unwanted.ns/e2e-nodefeature-test-2": "unwanted-ns",
},
}
Expect(checkForNodeLabels(ctx,
f.ClientSet,
expectedLabels,
nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
})
})
@ -948,10 +895,9 @@ resyncPeriod: "1s"
nfdv1alpha1.FeatureLabelNs + "/e2e-nodefeature-test-2": "obj-1",
nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3": "overridden",
},
"*": {},
}
Expect(checkForNodeLabels(ctx, f.ClientSet,
expectedLabels, nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
patches, err := json.Marshal(
[]apihelper.JsonPatch{
@ -968,11 +914,7 @@ resyncPeriod: "1s"
_, err = f.ClientSet.CoreV1().Nodes().Patch(ctx, targetNodeName, types.JSONPatchType, patches, metav1.PatchOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(checkForNodeLabels(ctx,
f.ClientSet,
expectedLabels,
nodes,
)).NotTo(HaveOccurred())
eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes, false))
By("Deleting NodeFeature object")
err = nfdClient.NfdV1alpha1().NodeFeatures(f.Namespace.Name).Delete(ctx, nodeFeatures[0], metav1.DeleteOptions{})
@ -993,117 +935,6 @@ resyncPeriod: "1s"
})
// simplePoll is a simple and stupid re-try loop
func simplePoll(poll func() error, wait time.Duration) error {
var err error
for retry := 0; retry < 3; retry++ {
if err = poll(); err == nil {
return nil
}
time.Sleep(wait * time.Second)
}
return err
}
// waitForCapacity waits for the capacity to be updated in the node status
func waitForCapacity(ctx context.Context, cli clientset.Interface, expectedNewERs corev1.ResourceList, oldNodes []corev1.Node) error {
poll := func() error {
nodes, err := getNonControlPlaneNodes(ctx, cli)
if err != nil {
return err
}
for _, node := range nodes {
oldNode := getNode(oldNodes, node.Name)
expected := oldNode.Status.DeepCopy().Capacity
for k, v := range expectedNewERs {
expected[k] = v
}
capacity := node.Status.Capacity
if !cmp.Equal(expected, capacity) {
return fmt.Errorf("node %q capacity does not match expected, diff (expected vs. received): %s", node.Name, cmp.Diff(expected, capacity))
}
}
return nil
}
return simplePoll(poll, 10)
}
// waitForNfdNodeAnnotations waits for node to be annotated as expected.
func waitForNfdNodeAnnotations(ctx context.Context, cli clientset.Interface, expected map[string]string) error {
poll := func() error {
nodes, err := getNonControlPlaneNodes(ctx, cli)
if err != nil {
return err
}
for _, node := range nodes {
for k, v := range expected {
if diff := cmp.Diff(v, node.Annotations[k]); diff != "" {
return fmt.Errorf("node %q annotation does not match expected, diff (expected vs. received): %s", node.Name, diff)
}
}
}
return nil
}
return simplePoll(poll, 2)
}
type k8sLabels map[string]string
// checkForNfdNodeLabels waits and checks that node is labeled as expected.
func checkForNodeLabels(ctx context.Context, cli clientset.Interface, expectedNewLabels map[string]k8sLabels, oldNodes []corev1.Node) error {
poll := func() error {
nodes, err := getNonControlPlaneNodes(ctx, cli)
if err != nil {
return err
}
for _, node := range nodes {
nodeExpected, ok := expectedNewLabels[node.Name]
if !ok {
nodeExpected = k8sLabels{}
if defaultExpected, ok := expectedNewLabels["*"]; ok {
nodeExpected = defaultExpected
}
}
oldLabels := getNode(oldNodes, node.Name).Labels
expectedNewLabels := maps.Clone(oldLabels)
maps.Copy(expectedNewLabels, nodeExpected)
if !cmp.Equal(node.Labels, expectedNewLabels) {
return fmt.Errorf("node %q labels do not match expected, diff (expected vs. received): %s", node.Name, cmp.Diff(expectedNewLabels, node.Labels))
}
}
return nil
}
return simplePoll(poll, 3)
}
// waitForNfdNodeTaints waits for node to be tainted as expected.
func waitForNfdNodeTaints(ctx context.Context, cli clientset.Interface, expectedNewTaints []corev1.Taint, oldNodes []corev1.Node) error {
poll := func() error {
nodes, err := getNonControlPlaneNodes(ctx, cli)
if err != nil {
return err
}
for _, node := range nodes {
oldNode := getNode(oldNodes, node.Name)
expected := oldNode.Spec.DeepCopy().Taints
expected = append(expected, expectedNewTaints...)
taints := node.Spec.Taints
if !cmp.Equal(expected, taints) {
return fmt.Errorf("node %q taints do not match expected, diff (expected vs. received): %s", node.Name, cmp.Diff(expected, taints))
}
}
return nil
}
return simplePoll(poll, 10)
}
// getNonControlPlaneNodes gets the nodes that are not tainted for exclusive control-plane usage
func getNonControlPlaneNodes(ctx context.Context, cli clientset.Interface) ([]corev1.Node, error) {
nodeList, err := cli.CoreV1().Nodes().List(ctx, metav1.ListOptions{})