Repository: kubernetes-sigs/node-feature-discovery
Pull request #1788 (2024-07-12 06:51:09)

nfd-worker: change TestRun to use NodeFeature API
Run nfd-worker with NodeFeature API enabled (against a fake apiserver) instead of using the deprecated gRPC (against a nfd-master instance).

Expand the test to verify the features and labels that are advertised as a NodeFeature object.

Refs #1546


Patch:
diff --git a/pkg/nfd-worker/nfd-worker.go b/pkg/nfd-worker/nfd-worker.go
index 460041850..f0ed390bc 100644
--- a/pkg/nfd-worker/nfd-worker.go
+++ b/pkg/nfd-worker/nfd-worker.go
@@ -131,7 +131,7 @@ type nfdWorker struct {
 	grpcClient          pb.LabelerClient
 	healthServer        *grpc.Server
 	k8sClient           k8sclient.Interface
-	nfdClient           *nfdclient.Clientset
+	nfdClient           nfdclient.Interface
 	stop                chan struct{} // channel for signaling stop
 	featureSources      []source.FeatureSource
 	labelSources        []source.LabelSource
@@ -150,20 +150,26 @@ type NfdWorkerOption interface {
 
 // WithArgs is used for passing settings from command line arguments.
 func WithArgs(args *Args) NfdWorkerOption {
-	return &nfdMWorkerOpt{f: func(n *nfdWorker) { n.args = *args }}
+	return &nfdWorkerOpt{f: func(n *nfdWorker) { n.args = *args }}
 }
 
 // WithKuberneteClient forces to use the given kubernetes client, without
 // initializing one from kubeconfig.
 func WithKubernetesClient(cli k8sclient.Interface) NfdWorkerOption {
-	return &nfdMWorkerOpt{f: func(n *nfdWorker) { n.k8sClient = cli }}
+	return &nfdWorkerOpt{f: func(n *nfdWorker) { n.k8sClient = cli }}
 }
 
-type nfdMWorkerOpt struct {
+// WithNFDClient forces to use the given client for the NFD API, without
+// initializing one from kubeconfig.
+func WithNFDClient(cli nfdclient.Interface) NfdWorkerOption {
+	return &nfdWorkerOpt{f: func(n *nfdWorker) { n.nfdClient = cli }}
+}
+
+type nfdWorkerOpt struct {
 	f func(*nfdWorker)
 }
 
-func (f *nfdMWorkerOpt) apply(n *nfdWorker) {
+func (f *nfdWorkerOpt) apply(n *nfdWorker) {
 	f.f(n)
 }
 
@@ -836,7 +842,7 @@ func (m *nfdWorker) updateNodeFeatureObject(labels Labels) error {
 }
 
 // getNfdClient returns the clientset for using the nfd CRD api
-func (m *nfdWorker) getNfdClient() (*nfdclient.Clientset, error) {
+func (m *nfdWorker) getNfdClient() (nfdclient.Interface, error) {
 	if m.nfdClient != nil {
 		return m.nfdClient, nil
 	}


Test patch:
diff --git a/pkg/nfd-worker/nfd-worker_test.go b/pkg/nfd-worker/nfd-worker_test.go
index 42fe8b71b..6f9220568 100644
--- a/pkg/nfd-worker/nfd-worker_test.go
+++ b/pkg/nfd-worker/nfd-worker_test.go
@@ -24,9 +24,14 @@ import (
 	"time"
 
 	. "github.com/smartystreets/goconvey/convey"
+	"golang.org/x/net/context"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	fakeclient "k8s.io/client-go/kubernetes/fake"
 	"k8s.io/klog/v2"
 
+	fakenfdclient "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned/fake"
+	"sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/pkg/features"
 	master "sigs.k8s.io/node-feature-discovery/pkg/nfd-master"
 	worker "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
@@ -39,6 +44,13 @@ type testContext struct {
 	errs   chan error
 }
 
+func initializeFeatureGates() {
+	if err := features.NFDMutableFeatureGate.Add(features.DefaultNFDFeatureGates); err != nil {
+		klog.ErrorS(err, "failed to add default feature gates")
+		os.Exit(1)
+	}
+}
+
 func setupTest(args *master.Args) testContext {
 	// Fixed port and no-publish, for convenience
 	publish := true
@@ -48,10 +60,7 @@ func setupTest(args *master.Args) testContext {
 	}
 	args.Port = 8192
 	// Add FeatureGates flag
-	if err := features.NFDMutableFeatureGate.Add(features.DefaultNFDFeatureGates); err != nil {
-		klog.ErrorS(err, "failed to add default feature gates")
-		os.Exit(1)
-	}
+	initializeFeatureGates()
 	_ = features.NFDMutableFeatureGate.OverrideDefault(features.NodeFeatureAPI, false)
 	_ = features.NFDMutableFeatureGate.OverrideDefault(features.NodeFeatureGroupAPI, false)
 	m, err := master.NewNfdMaster(
@@ -106,21 +115,95 @@ func TestNewNfdWorker(t *testing.T) {
 }
 
 func TestRun(t *testing.T) {
-	ctx := setupTest(&master.Args{})
-	defer teardownTest(ctx)
-	Convey("When running nfd-worker against nfd-master", t, func() {
+	nfdCli := fakenfdclient.NewSimpleClientset()
+	initializeFeatureGates()
+	Convey("When running nfd-worker", t, func() {
 		Convey("When publishing features from fake source", func() {
+			os.Setenv("NODE_NAME", "fake-node")
+			os.Setenv("KUBERNETES_NAMESPACE", "fake-ns")
 			args := &worker.Args{
-				Server:    "localhost:8192",
-				Oneshot:   true,
-				Overrides: worker.ConfigOverrideArgs{LabelSources: &utils.StringSliceVal{"fake"}},
+				Oneshot: true,
+				Overrides: worker.ConfigOverrideArgs{
+					FeatureSources: &utils.StringSliceVal{"fake"},
+					LabelSources:   &utils.StringSliceVal{"fake"},
+				},
 			}
-			fooasdf, _ := worker.NewNfdWorker(worker.WithArgs(args),
-				worker.WithKubernetesClient(fakeclient.NewSimpleClientset()))
-			err := fooasdf.Run()
+			w, _ := worker.NewNfdWorker(
+				worker.WithArgs(args),
+				worker.WithKubernetesClient(fakeclient.NewSimpleClientset()),
+				worker.WithNFDClient(nfdCli),
+			)
+			err := w.Run()
 			Convey("No error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
+			Convey("NodeFeture object should be created", func() {
+				nf, err := nfdCli.NfdV1alpha1().NodeFeatures("fake-ns").Get(context.TODO(), "fake-node", metav1.GetOptions{})
+				So(err, ShouldBeNil)
+
+				nfExpected := &nfdv1alpha1.NodeFeature{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "fake-node",
+						Namespace: "fake-ns",
+						Labels: map[string]string{
+							"nfd.node.kubernetes.io/node-name": "fake-node",
+						},
+						Annotations: map[string]string{
+							"nfd.node.kubernetes.io/worker.version": "undefined",
+						},
+						OwnerReferences: []metav1.OwnerReference{},
+					},
+					Spec: nfdv1alpha1.NodeFeatureSpec{
+						Labels: map[string]string{
+							"feature.node.kubernetes.io/fake-fakefeature1": "true",
+							"feature.node.kubernetes.io/fake-fakefeature2": "true",
+							"feature.node.kubernetes.io/fake-fakefeature3": "true",
+						},
+						Features: v1alpha1.Features{
+							Flags: map[string]v1alpha1.FlagFeatureSet{
+								"fake.flag": {
+									Elements: map[string]v1alpha1.Nil{
+										"flag_1": {},
+										"flag_2": {},
+										"flag_3": {}},
+								},
+							},
+							Attributes: map[string]v1alpha1.AttributeFeatureSet{
+								"fake.attribute": {
+									Elements: map[string]string{
+										"attr_1": "true",
+										"attr_2": "false",
+										"attr_3": "10",
+									},
+								},
+							},
+							Instances: map[string]v1alpha1.InstanceFeatureSet{
+								"fake.instance": {
+									Elements: []v1alpha1.InstanceFeature{
+										{Attributes: map[string]string{
+											"name":   "instance_1",
+											"attr_1": "true",
+											"attr_2": "false",
+											"attr_3": "10",
+											"attr_4": "foobar",
+										}},
+										{Attributes: map[string]string{
+											"name":   "instance_2",
+											"attr_1": "true",
+											"attr_2": "true",
+											"attr_3": "100",
+										}},
+										{Attributes: map[string]string{
+											"name": "instance_3",
+										}},
+									},
+								},
+							},
+						},
+					},
+				}
+				So(nf, ShouldResemble, nfExpected)
+			})
 		})
 	})
 }