diff --git a/cmd/nfd-master/main.go b/cmd/nfd-master/main.go index 5c78a38295..3f7fbe05c4 100644 --- a/cmd/nfd-master/main.go +++ b/cmd/nfd-master/main.go @@ -72,6 +72,18 @@ func main() { args.Overrides.ResyncPeriod = overrides.ResyncPeriod case "nfd-api-parallelism": args.Overrides.NfdApiParallelism = overrides.NfdApiParallelism + case "max-labels-per-cr": + args.Overrides.MaxLabelsPerCR = overrides.MaxLabelsPerCR + case "max-taints-per-cr": + args.Overrides.MaxTaintsPerCR = overrides.MaxTaintsPerCR + case "max-extended-resources-per-cr": + args.Overrides.MaxExtendedResourcesPerCR = overrides.MaxExtendedResourcesPerCR + case "allowed-namespaces": + args.Overrides.AllowedNamespaces = overrides.AllowedNamespaces + case "deny-node-feature-labels": + args.Overrides.DenyNodeFeatureLabels = overrides.DenyNodeFeatureLabels + case "overwrite-labels": + args.Overrides.OverwriteLabels = overrides.OverwriteLabels case "enable-nodefeature-api": klog.InfoS("-enable-nodefeature-api is deprecated, will be removed in a future release along with the deprecated gRPC API") case "ca-file": @@ -159,11 +171,12 @@ func initFlags(flagset *flag.FlagSet) (*master.Args, *master.ConfigOverrideArgs) args.Klog = klogutils.InitKlogFlags(flagset) overrides := &master.ConfigOverrideArgs{ - LabelWhiteList: &utils.RegexpVal{}, - DenyLabelNs: &utils.StringSetVal{}, - ExtraLabelNs: &utils.StringSetVal{}, - ResourceLabels: &utils.StringSetVal{}, - ResyncPeriod: &utils.DurationVal{Duration: time.Duration(1) * time.Hour}, + LabelWhiteList: &utils.RegexpVal{}, + DenyLabelNs: &utils.StringSetVal{}, + ExtraLabelNs: &utils.StringSetVal{}, + ResourceLabels: &utils.StringSetVal{}, + AllowedNamespaces: &utils.StringSliceVal{}, + ResyncPeriod: &utils.DurationVal{Duration: time.Duration(1) * time.Hour}, } flagset.Var(overrides.ExtraLabelNs, "extra-label-ns", "Comma separated list of allowed extra label namespaces") @@ -174,6 +187,9 @@ func initFlags(flagset *flag.FlagSet) (*master.Args, *master.ConfigOverrideArgs) "Enable node tainting feature") overrides.NoPublish = flagset.Bool("no-publish", false, "Do not publish feature labels") + overrides.DenyNodeFeatureLabels = flagset.Bool("deny-node-feature-labels", false, + "Deny third-party features from being created i.e. only create raw features") + overrides.OverwriteLabels = flagset.Bool("overwrite-labels", true, "Allow to overwrite labels") flagset.Var(overrides.DenyLabelNs, "deny-label-ns", "Comma separated list of denied label namespaces") flagset.Var(overrides.ResourceLabels, "resource-labels", @@ -184,5 +200,11 @@ func initFlags(flagset *flag.FlagSet) (*master.Args, *master.ConfigOverrideArgs) overrides.NfdApiParallelism = flagset.Int("nfd-api-parallelism", 10, "Defines the maximum number of goroutines responsible of updating nodes. "+ "Can be used for the throttling mechanism. It has effect only when -enable-nodefeature-api has been set.") + // Restrictions flags + overrides.MaxLabelsPerCR = flagset.Int("max-labels-per-cr", -1, "Defines the maximum number of labels that can be added by a single NodeFeature CR.") + overrides.MaxTaintsPerCR = flagset.Int("max-taints-per-cr", -1, "Defines the maximum number of taints that can be added by a single NodeFeature CR.") + overrides.MaxExtendedResourcesPerCR = flagset.Int("max-extended-resources-per-cr", -1, "Defines the maximum number of extended resources that can be added by a single NodeFeature CR.") + flagset.Var(overrides.AllowedNamespaces, "allowed-namespaces", + "Comma separated list of allowed Kubernetes namespaces to watch") return args, overrides } diff --git a/nfd-master.conf b/nfd-master.conf new file mode 100644 index 0000000000..629ea57281 --- /dev/null +++ b/nfd-master.conf @@ -0,0 +1,29 @@ +noPublish: false +autoDefaultNs: true +extraLabelNs: ["added.ns.io","added.kubernets.io"] +denyLabelNs: ["denied.ns.io","denied.kubernetes.io"] +resourceLabels: ["vendor-1.com/feature-1","vendor-2.io/feature-2"] +enableTaints: false +labelWhiteList: "" +resyncPeriod: "2h" +restrictions: + allowedNamespaces: ["default"] + denyNodeFeatureLabels: true +klog: + addDirHeader: false + alsologtostderr: false + logBacktraceAt: + logtostderr: true + skipHeaders: false + stderrthreshold: 2 + v: 0 + vmodule: + logDir: + logFile: + logFileMaxSize: 1800 + skipLogHeaders: false +leaderElection: + leaseDuration: 15s + renewDeadline: 10s + retryPeriod: 2s +nfdApiParallelism: 10 diff --git a/pkg/nfd-master/nfd-api-controller.go b/pkg/nfd-master/nfd-api-controller.go index 00c606eee8..ca8fa15034 100644 --- a/pkg/nfd-master/nfd-api-controller.go +++ b/pkg/nfd-master/nfd-api-controller.go @@ -35,8 +35,9 @@ import ( ) type nfdController struct { - featureLister nfdlisters.NodeFeatureLister - ruleLister nfdlisters.NodeFeatureRuleLister + featureLister nfdlisters.NodeFeatureLister + ruleLister nfdlisters.NodeFeatureRuleLister + allowedNamespaces []string stopChan chan struct{} @@ -47,6 +48,7 @@ type nfdController struct { type nfdApiControllerOptions struct { DisableNodeFeature bool ResyncPeriod time.Duration + AllowedNamespaces []string } func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiControllerOptions) (*nfdController, error) { @@ -54,6 +56,7 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC stopChan: make(chan struct{}, 1), updateAllNodesChan: make(chan struct{}, 1), updateOneNodeChan: make(chan string), + allowedNamespaces: nfdApiControllerOptions.AllowedNamespaces, } nfdClient := nfdclientset.NewForConfigOrDie(config) @@ -68,17 +71,23 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC AddFunc: func(obj interface{}) { nfr := obj.(*nfdv1alpha1.NodeFeature) klog.V(2).InfoS("NodeFeature added", "nodefeature", klog.KObj(nfr)) - c.updateOneNode("NodeFeature", nfr) + if c.isNamespaceAllowed(nfr.Namespace) { + c.updateOneNode("NodeFeature", nfr) + } }, UpdateFunc: func(oldObj, newObj interface{}) { nfr := newObj.(*nfdv1alpha1.NodeFeature) klog.V(2).InfoS("NodeFeature updated", "nodefeature", klog.KObj(nfr)) - c.updateOneNode("NodeFeature", nfr) + if c.isNamespaceAllowed(nfr.Namespace) { + c.updateOneNode("NodeFeature", nfr) + } }, DeleteFunc: func(obj interface{}) { nfr := obj.(*nfdv1alpha1.NodeFeature) klog.V(2).InfoS("NodeFeature deleted", "nodefeature", klog.KObj(nfr)) - c.updateOneNode("NodeFeature", nfr) + if c.isNamespaceAllowed(nfr.Namespace) { + c.updateOneNode("NodeFeature", nfr) + } }, }); err != nil { return nil, err @@ -129,6 +138,16 @@ func (c *nfdController) stop() { } } +func (c *nfdController) isNamespaceAllowed(namespace string) bool { + for _, ns := range c.allowedNamespaces { + if ns == namespace { + return true + } + } + + return false +} + func (c *nfdController) updateOneNode(typ string, obj metav1.Object) { nodeName, err := getNodeNameForObj(obj) if err != nil { diff --git a/pkg/nfd-master/nfd-api-controller_test.go b/pkg/nfd-master/nfd-api-controller_test.go index bffa9ffc20..1341847e08 100644 --- a/pkg/nfd-master/nfd-api-controller_test.go +++ b/pkg/nfd-master/nfd-api-controller_test.go @@ -42,3 +42,33 @@ func TestGetNodeNameForObj(t *testing.T) { assert.Nil(t, err) assert.Equal(t, n, "node-1") } + +func TestIsNamespaceAllowed(t *testing.T) { + c := &nfdController{} + + testcases := []struct { + name string + objectNamespace string + allowedNamespaces []string + expectedResult bool + }{ + { + name: "namespace not allowed", + objectNamespace: "ns3", + allowedNamespaces: []string{"ns1", "ns2"}, + expectedResult: false, + }, + { + name: "namespace is allowed", + objectNamespace: "ns1", + allowedNamespaces: []string{"ns2", "ns1"}, + expectedResult: false, + }, + } + + for _, tc := range testcases { + c.allowedNamespaces = tc.allowedNamespaces + res := c.isNamespaceAllowed(tc.name) + assert.Equal(t, res, tc.expectedResult) + } +} diff --git a/pkg/nfd-master/nfd-master-internal_test.go b/pkg/nfd-master/nfd-master-internal_test.go index 03f8703993..375dd1fd3e 100644 --- a/pkg/nfd-master/nfd-master-internal_test.go +++ b/pkg/nfd-master/nfd-master-internal_test.go @@ -500,15 +500,16 @@ func TestFilterLabels(t *testing.T) { func TestCreatePatches(t *testing.T) { Convey("When creating JSON patches", t, func() { existingItems := map[string]string{"key-1": "val-1", "key-2": "val-2", "key-3": "val-3"} + overwriteKeys := true jsonPath := "/root" - Convey("When when there are neither itmes to remoe nor to add or update", func() { - p := createPatches([]string{"foo", "bar"}, existingItems, map[string]string{}, jsonPath) + Convey("When there are neither itmes to remoe nor to add or update", func() { + p := createPatches([]string{"foo", "bar"}, existingItems, map[string]string{}, jsonPath, overwriteKeys) So(len(p), ShouldEqual, 0) }) - Convey("When when there are itmes to remoe but none to add or update", func() { - p := createPatches([]string{"key-2", "key-3", "foo"}, existingItems, map[string]string{}, jsonPath) + Convey("When there are itmes to remoe but none to add or update", func() { + p := createPatches([]string{"key-2", "key-3", "foo"}, existingItems, map[string]string{}, jsonPath, overwriteKeys) expected := []utils.JsonPatch{ utils.NewJsonPatch("remove", jsonPath, "key-2", ""), utils.NewJsonPatch("remove", jsonPath, "key-3", ""), @@ -516,9 +517,9 @@ func TestCreatePatches(t *testing.T) { So(sortJsonPatches(p), ShouldResemble, sortJsonPatches(expected)) }) - Convey("When when there are no itmes to remove but new items to add", func() { + Convey("When there are no itmes to remove but new items to add", func() { newItems := map[string]string{"new-key": "new-val", "key-1": "new-1"} - p := createPatches([]string{"key-1"}, existingItems, newItems, jsonPath) + p := createPatches([]string{"key-1"}, existingItems, newItems, jsonPath, overwriteKeys) expected := []utils.JsonPatch{ utils.NewJsonPatch("add", jsonPath, "new-key", newItems["new-key"]), utils.NewJsonPatch("replace", jsonPath, "key-1", newItems["key-1"]), @@ -526,9 +527,9 @@ func TestCreatePatches(t *testing.T) { So(sortJsonPatches(p), ShouldResemble, sortJsonPatches(expected)) }) - Convey("When when there are items to remove add and update", func() { + Convey("When there are items to remove add and update", func() { newItems := map[string]string{"new-key": "new-val", "key-2": "new-2", "key-4": "val-4"} - p := createPatches([]string{"key-1", "key-2", "key-3", "foo"}, existingItems, newItems, jsonPath) + p := createPatches([]string{"key-1", "key-2", "key-3", "foo"}, existingItems, newItems, jsonPath, overwriteKeys) expected := []utils.JsonPatch{ utils.NewJsonPatch("add", jsonPath, "new-key", newItems["new-key"]), utils.NewJsonPatch("add", jsonPath, "key-4", newItems["key-4"]), @@ -538,6 +539,16 @@ func TestCreatePatches(t *testing.T) { } So(sortJsonPatches(p), ShouldResemble, sortJsonPatches(expected)) }) + + Convey("When overwrite of keys is denied and there is already an existant key", func() { + overwriteKeys = false + newItems := map[string]string{"key-1": "new-2", "key-4": "val-4"} + p := createPatches([]string{}, existingItems, newItems, jsonPath, overwriteKeys) + expected := []utils.JsonPatch{ + utils.NewJsonPatch("add", jsonPath, "key-4", newItems["key-4"]), + } + So(sortJsonPatches(p), ShouldResemble, sortJsonPatches(expected)) + }) }) } @@ -623,6 +634,13 @@ leaderElection: leaseDuration: 20s renewDeadline: 4s retryPeriod: 30s +restrictions: + allowedNamespaces: ["default"] + denyNodeFeatureLabels: true + maxLabelsPerCr: 3 + maxTaintsPerCr: 4 + maxExtendedResourcesPerCr: 5 + overwriteLabels: true `) f.Close() So(err, ShouldBeNil) @@ -641,6 +659,12 @@ leaderElection: So(master.config.LeaderElection.LeaseDuration.Seconds(), ShouldEqual, float64(20)) So(master.config.LeaderElection.RenewDeadline.Seconds(), ShouldEqual, float64(4)) So(master.config.LeaderElection.RetryPeriod.Seconds(), ShouldEqual, float64(30)) + So(master.config.Restrictions.AllowedNamespaces.String(), ShouldEqual, "default") + So(master.config.Restrictions.DenyNodeFeatureLabels, ShouldEqual, true) + So(master.config.Restrictions.MaxLabelsPerCR, ShouldEqual, 3) + So(master.config.Restrictions.MaxTaintsPerCR, ShouldEqual, 4) + So(master.config.Restrictions.MaxExtendedResourcesPerCR, ShouldEqual, 5) + So(master.config.Restrictions.OverwriteLabels, ShouldEqual, true) }) }) @@ -884,3 +908,60 @@ func TestGetDynamicValue(t *testing.T) { }) } } + +func TestFilterTaints(t *testing.T) { + testcases := []struct { + name string + taints []corev1.Taint + maxTaints int + expectedResult []corev1.Taint + }{ + { + name: "no restriction on the number of taints", + taints: []corev1.Taint{ + { + Key: "feature.node.kubernetes.io/key1", + Value: "dummy", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + maxTaints: 0, + expectedResult: []corev1.Taint{ + { + Key: "feature.node.kubernetes.io/key1", + Value: "dummy", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + { + name: "max of 1 Taint should be generated", + taints: []corev1.Taint{ + { + Key: "feature.node.kubernetes.io/key1", + Value: "dummy", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "feature.node.kubernetes.io/key2", + Value: "dummy", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + maxTaints: 1, + expectedResult: []corev1.Taint{}, + }, + } + + mockMaster := newFakeMaster(nil) + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockMaster.config.Restrictions.MaxTaintsPerCR = tc.maxTaints + res := mockMaster.filterTaints(tc.taints) + Convey("The expected number of taints should be correct", t, func() { + So(len(res), ShouldEqual, len(tc.expectedResult)) + }) + }) + } +} diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go index e48d9c713e..83d83f71ab 100644 --- a/pkg/nfd-master/nfd-master.go +++ b/pkg/nfd-master/nfd-master.go @@ -70,6 +70,16 @@ type ExtendedResources map[string]string // Annotations are used for NFD-related node metadata type Annotations map[string]string +// Restrictions contains the restrictions on the NF and NFR Crs +type Restrictions struct { + MaxLabelsPerCR int + MaxTaintsPerCR int + MaxExtendedResourcesPerCR int + AllowedNamespaces utils.StringSliceVal + DenyNodeFeatureLabels bool + OverwriteLabels bool +} + // NFDConfig contains the configuration settings of NfdMaster. type NFDConfig struct { AutoDefaultNs bool @@ -83,6 +93,7 @@ type NFDConfig struct { LeaderElection LeaderElectionConfig NfdApiParallelism int Klog klogutils.KlogConfigOpts + Restrictions Restrictions } // LeaderElectionConfig contains the configuration for leader election @@ -94,14 +105,20 @@ type LeaderElectionConfig struct { // ConfigOverrideArgs are args that override config file options type ConfigOverrideArgs struct { - DenyLabelNs *utils.StringSetVal - ExtraLabelNs *utils.StringSetVal - LabelWhiteList *utils.RegexpVal - ResourceLabels *utils.StringSetVal - EnableTaints *bool - NoPublish *bool - ResyncPeriod *utils.DurationVal - NfdApiParallelism *int + DenyLabelNs *utils.StringSetVal + ExtraLabelNs *utils.StringSetVal + LabelWhiteList *utils.RegexpVal + ResourceLabels *utils.StringSetVal + EnableTaints *bool + NoPublish *bool + ResyncPeriod *utils.DurationVal + NfdApiParallelism *int + MaxLabelsPerCR *int + MaxTaintsPerCR *int + MaxExtendedResourcesPerCR *int + AllowedNamespaces *utils.StringSliceVal + DenyNodeFeatureLabels *bool + OverwriteLabels *bool } // Args holds command line arguments @@ -539,8 +556,7 @@ func (m *nfdMaster) updateMasterNode() error { p := createPatches([]string{m.instanceAnnotation(nfdv1alpha1.MasterVersionAnnotation)}, node.Annotations, nil, - "/metadata/annotations") - + "/metadata/annotations", true) err = m.patchNode(node.Name, p) if err != nil { return fmt.Errorf("failed to patch node annotations: %w", err) @@ -580,6 +596,16 @@ func (m *nfdMaster) filterFeatureLabels(labels Labels, features *nfdv1alpha1.Fea } } + if m.config.Restrictions.MaxLabelsPerCR > 0 && len(outLabels) > m.config.Restrictions.MaxLabelsPerCR { + klog.InfoS("number of labels in the request exceeds the restriction maximum labels per CR, skipping") + outLabels = Labels{} + } + + if m.config.Restrictions.MaxExtendedResourcesPerCR > 0 && len(extendedResources) > m.config.Restrictions.MaxExtendedResourcesPerCR { + klog.InfoS("number of extended resources in the request exceeds the restriction maximum extended resources per CR, skipping") + extendedResources = map[string]string{} + } + return outLabels, extendedResources } @@ -634,7 +660,7 @@ func getDynamicValue(value string, features *nfdv1alpha1.Features) (string, erro return element, nil } -func filterTaints(taints []corev1.Taint) []corev1.Taint { +func (m *nfdMaster) filterTaints(taints []corev1.Taint) []corev1.Taint { outTaints := []corev1.Taint{} for _, taint := range taints { @@ -645,6 +671,12 @@ func filterTaints(taints []corev1.Taint) []corev1.Taint { outTaints = append(outTaints, taint) } } + + if m.config.Restrictions.MaxTaintsPerCR > 0 && len(taints) > m.config.Restrictions.MaxTaintsPerCR { + klog.InfoS("number of taints in the request exceeds the restriction maximum taints per CR, skipping") + outTaints = []corev1.Taint{} + } + return outTaints } @@ -846,7 +878,13 @@ func (m *nfdMaster) refreshNodeFeatures(nodeName string, labels map[string]strin // Taints var taints []corev1.Taint if m.config.EnableTaints { - taints = filterTaints(crTaints) + taints = m.filterTaints(crTaints) + } + + // If we deny node feature labels, we'll empty the labels variable + if m.config.Restrictions.DenyNodeFeatureLabels { + klog.InfoS("node feature labels are denied, skipping...") + labels = map[string]string{} } err := m.updateNodeObject(nodeName, labels, annotations, extendedResources, taints) @@ -924,7 +962,7 @@ func (m *nfdMaster) setTaints(taints []corev1.Taint, nodeName string) error { newAnnotations[nfdv1alpha1.NodeTaintsAnnotation] = strings.Join(taintStrs, ",") } - patches := createPatches([]string{nfdv1alpha1.NodeTaintsAnnotation}, node.Annotations, newAnnotations, "/metadata/annotations") + patches := createPatches([]string{nfdv1alpha1.NodeTaintsAnnotation}, node.Annotations, newAnnotations, "/metadata/annotations", true) if len(patches) > 0 { err = m.patchNode(node.Name, patches) if err != nil { @@ -1071,7 +1109,7 @@ func (m *nfdMaster) updateNodeObject(nodeName string, labels Labels, featureAnno // Create JSON patches for changes in labels and annotations oldLabels := stringToNsNames(node.Annotations[m.instanceAnnotation(nfdv1alpha1.FeatureLabelsAnnotation)], nfdv1alpha1.FeatureLabelNs) oldAnnotations := stringToNsNames(node.Annotations[m.instanceAnnotation(nfdv1alpha1.FeatureAnnotationsTrackingAnnotation)], nfdv1alpha1.FeatureAnnotationNs) - patches := createPatches(oldLabels, node.Labels, labels, "/metadata/labels") + patches := createPatches(oldLabels, node.Labels, labels, "/metadata/labels", m.config.Restrictions.OverwriteLabels) oldAnnotations = append(oldAnnotations, []string{ m.instanceAnnotation(nfdv1alpha1.FeatureLabelsAnnotation), m.instanceAnnotation(nfdv1alpha1.ExtendedResourceAnnotation), @@ -1079,7 +1117,7 @@ func (m *nfdMaster) updateNodeObject(nodeName string, labels Labels, featureAnno // Clean up deprecated/stale nfd version annotations m.instanceAnnotation(nfdv1alpha1.MasterVersionAnnotation), m.instanceAnnotation(nfdv1alpha1.WorkerVersionAnnotation)}...) - patches = append(patches, createPatches(oldAnnotations, node.Annotations, annotations, "/metadata/annotations")...) + patches = append(patches, createPatches(oldAnnotations, node.Annotations, annotations, "/metadata/annotations", true)...) // patch node status with extended resource changes statusPatches := m.createExtendedResourcePatches(node, extendedResources) @@ -1111,7 +1149,7 @@ func (m *nfdMaster) updateNodeObject(nodeName string, labels Labels, featureAnno } // createPatches is a generic helper that returns json patch operations to perform -func createPatches(removeKeys []string, oldItems map[string]string, newItems map[string]string, jsonPath string) []utils.JsonPatch { +func createPatches(removeKeys []string, oldItems map[string]string, newItems map[string]string, jsonPath string, overwrite bool) []utils.JsonPatch { patches := []utils.JsonPatch{} // Determine items to remove @@ -1126,7 +1164,7 @@ func createPatches(removeKeys []string, oldItems map[string]string, newItems map // Determine items to add or replace for key, newVal := range newItems { if oldVal, ok := oldItems[key]; ok { - if newVal != oldVal { + if newVal != oldVal && overwrite { patches = append(patches, utils.NewJsonPatch("replace", jsonPath, key, newVal)) } } else { @@ -1232,7 +1270,6 @@ func (m *nfdMaster) configure(filepath string, overrides string) error { } m.config = c - if err := klogutils.MergeKlogConfiguration(m.args.Klog, c.Klog); err != nil { return err } diff --git a/test/e2e/node_feature_discovery_test.go b/test/e2e/node_feature_discovery_test.go index d1d51f72fa..8ca1087f6e 100644 --- a/test/e2e/node_feature_discovery_test.go +++ b/test/e2e/node_feature_discovery_test.go @@ -995,6 +995,91 @@ resyncPeriod: "1s" Expect(err).NotTo(HaveOccurred()) }) }) + + Context("allowed namespaces restriction is respected or not", func() { + BeforeEach(func(ctx context.Context) { + extraMasterPodSpecOpts = []testpod.SpecOption{ + testpod.SpecWithConfigMap("nfd-master-conf", "/etc/kubernetes/node-feature-discovery"), + } + cm := testutils.NewConfigMap("nfd-master-conf", "nfd-master.conf", ` +restrictions: + allowedNamespaces: ["non-existant-namespace"] +`) + fmt.Println(cm.Data) + _, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, cm, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + It("Nothing should be created", func(ctx context.Context) { + // deploy node feature object + if !useNodeFeatureApi { + Skip("NodeFeature API not enabled") + } + + nodes, err := getNonControlPlaneNodes(ctx, f.ClientSet) + Expect(err).NotTo(HaveOccurred()) + + targetNodeName := nodes[0].Name + Expect(targetNodeName).ToNot(BeEmpty(), "No suitable worker node found") + + // Apply Node Feature object + By("Creating NodeFeature object") + nodeFeatures, err := testutils.CreateOrUpdateNodeFeaturesFromFile(ctx, nfdClient, "nodefeature-1.yaml", f.Namespace.Name, targetNodeName) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying node labels from NodeFeature object #1 are not created") + // No labels should be created since the f.Namespace is not in the allowed Namespaces + expectedLabels := map[string]k8sLabels{} + eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes)) + + By("Deleting NodeFeature object") + err = nfdClient.NfdV1alpha1().NodeFeatures(f.Namespace.Name).Delete(ctx, nodeFeatures[0], metav1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("max labels, taints, extended resources restrictions should be respected", func() { + BeforeEach(func(ctx context.Context) { + extraMasterPodSpecOpts = []testpod.SpecOption{ + testpod.SpecWithConfigMap("nfd-master-conf", "/etc/kubernetes/node-feature-discovery"), + } + cm := testutils.NewConfigMap("nfd-master-conf", "nfd-master.conf", ` +restrictions: + maxLabelsPerCR: 1 + maxTaintsPerCR: 1 + maxExtendedResourcesPerCR: 1 +`) + _, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, cm, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + It("Nothing should be created", func(ctx context.Context) { + // deploy node feature object + if !useNodeFeatureApi { + Skip("NodeFeature API not enabled") + } + + nodes, err := getNonControlPlaneNodes(ctx, f.ClientSet) + Expect(err).NotTo(HaveOccurred()) + + targetNodeName := nodes[0].Name + Expect(targetNodeName).ToNot(BeEmpty(), "No suitable worker node found") + + // Apply Node Feature object + By("Creating NodeFeature object") + nodeFeatures, err := testutils.CreateOrUpdateNodeFeaturesFromFile(ctx, nfdClient, "nodefeature-1.yaml", f.Namespace.Name, targetNodeName) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying node labels from NodeFeature object #1 are not created") + // No labels should be created since the f.Namespace is not in the allowed Namespaces + expectedLabels := map[string]k8sLabels{} + expectedAnnotations := map[string]k8sAnnotations{} + eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchLabels(expectedLabels, nodes)) + eventuallyNonControlPlaneNodes(ctx, f.ClientSet).Should(MatchAnnotations(expectedAnnotations, nodes)) + + By("Deleting NodeFeature object") + err = nfdClient.NfdV1alpha1().NodeFeatures(f.Namespace.Name).Delete(ctx, nodeFeatures[0], metav1.DeleteOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + }) }) }