From 04ec4a5904a18987a89ba317ac956e886c9736fb Mon Sep 17 00:00:00 2001 From: Andrea Panattoni Date: Tue, 6 May 2025 13:20:02 +0200 Subject: [PATCH 1/4] Support namespaced `{Sriov,SriovIB,OVS}Networks` SriovNetwork,IbSriovNetwork,OVSNetwork object (XNetwork resources from now on) must be created in the operator's namespace and the `.Spec.NetworkNamespace` field defines where the controller should create the NetworkAttachmentDefinition resource. This constraint can be a problem in clusters where applications are managed by non cluster administrators. These changes makes the `genericNetworkReconciler` to accept XNetwork resources in namespaces different than the operator's one. In such cases, the field `.Spec.NetworkNamespace` must be empty, and a validating webhook ensure this constraint. Signed-off-by: Andrea Panattoni --- controllers/generic_network_controller.go | 127 ++++++++++++++++---- controllers/sriovnetwork_controller_test.go | 106 ++++++++++++++++ controllers/suite_test.go | 1 + 3 files changed, 209 insertions(+), 25 deletions(-) diff --git a/controllers/generic_network_controller.go b/controllers/generic_network_controller.go index 66db878a36..af2d94e046 100644 --- a/controllers/generic_network_controller.go +++ b/controllers/generic_network_controller.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "fmt" netattdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" corev1 "k8s.io/api/core/v1" @@ -30,6 +31,7 @@ import ( "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" @@ -72,7 +74,6 @@ type genericNetworkReconciler struct { } func (r *genericNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - req.Namespace = vars.Namespace reqLogger := log.FromContext(ctx).WithValues(r.controller.Name(), req.NamespacedName) reqLogger.Info("Reconciling " + r.controller.Name()) @@ -91,37 +92,37 @@ func (r *genericNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque // Error reading the object - requeue the request. return reconcile.Result{}, err } - instanceFinalizers := instance.GetFinalizers() + + if instance == nil { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + + if instance.NetworkNamespace() != "" && instance.GetNamespace() != vars.Namespace { + reqLogger.Error( + fmt.Errorf("bad value for NetworkNamespace"), + ".spec.networkNamespace can't be specified if the resource belongs to a namespace other than the operator's", + "operatorNamespace", vars.Namespace, + ".metadata.namespace", instance.GetNamespace(), + ".spec.networkNamespace", instance.NetworkNamespace(), + ) + return reconcile.Result{}, nil + } + // examine DeletionTimestamp to determine if object is under deletion if instance.GetDeletionTimestamp().IsZero() { // The object is not being deleted, so if it does not have our finalizer, // then lets add the finalizer and update the object. This is equivalent // registering our finalizer. - if !sriovnetworkv1.StringInArray(sriovnetworkv1.NETATTDEFFINALIZERNAME, instanceFinalizers) { - instance.SetFinalizers(append(instanceFinalizers, sriovnetworkv1.NETATTDEFFINALIZERNAME)) - if err := r.Update(ctx, instance); err != nil { - return reconcile.Result{}, err - } + err = r.updateFinalizers(ctx, instance) + if err != nil { + return reconcile.Result{}, err } } else { // The object is being deleted - if sriovnetworkv1.StringInArray(sriovnetworkv1.NETATTDEFFINALIZERNAME, instanceFinalizers) { - // our finalizer is present, so lets handle any external dependency - reqLogger.Info("delete NetworkAttachmentDefinition CR", "Namespace", instance.NetworkNamespace(), "Name", instance.GetName()) - if err := r.deleteNetAttDef(ctx, instance); err != nil { - // if fail to delete the external dependency here, return with error - // so that it can be retried - return reconcile.Result{}, err - } - // remove our finalizer from the list and update it. - newFinalizers, found := sriovnetworkv1.RemoveString(sriovnetworkv1.NETATTDEFFINALIZERNAME, instanceFinalizers) - if found { - instance.SetFinalizers(newFinalizers) - if err := r.Update(ctx, instance); err != nil { - return reconcile.Result{}, err - } - } - } + err = r.cleanResourcesAndFinalizers(ctx, instance) return reconcile.Result{}, err } raw, err := instance.RenderNetAttDef() @@ -151,6 +152,14 @@ func (r *genericNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque return reconcile.Result{}, err } } + + if instance.GetNamespace() == netAttDef.Namespace { + // If the NetAttachDef is in the same namespace of the resource, then we can leverage the OwnerReference field for garbage collector + if err := controllerutil.SetOwnerReference(instance, netAttDef, r.Scheme); err != nil { + return reconcile.Result{}, err + } + } + // Check if this NetworkAttachmentDefinition already exists found := &netattdefv1.NetworkAttachmentDefinition{} err = r.Get(ctx, types.NamespacedName{Name: netAttDef.Name, Namespace: netAttDef.Namespace}, found) @@ -183,6 +192,7 @@ func (r *genericNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque if !equality.Semantic.DeepEqual(found.Spec, netAttDef.Spec) || !equality.Semantic.DeepEqual(found.GetAnnotations(), netAttDef.GetAnnotations()) { reqLogger.Info("Update NetworkAttachmentDefinition CR", "Namespace", netAttDef.Namespace, "Name", netAttDef.Name) netAttDef.SetResourceVersion(found.GetResourceVersion()) + err = r.Update(ctx, netAttDef) if err != nil { reqLogger.Error(err, "Couldn't update NetworkAttachmentDefinition CR", "Namespace", netAttDef.Namespace, "Name", netAttDef.Name) @@ -202,11 +212,37 @@ func (r *genericNetworkReconciler) SetupWithManager(mgr ctrl.Manager) error { } return ctrl.NewControllerManagedBy(mgr). For(r.controller.GetObject()). - Watches(&netattdefv1.NetworkAttachmentDefinition{}, &handler.EnqueueRequestForObject{}). + Watches(&netattdefv1.NetworkAttachmentDefinition{}, handler.EnqueueRequestsFromMapFunc(r.handleNetAttDef)). Watches(&corev1.Namespace{}, &namespaceHandler). Complete(r.controller) } +func (r *genericNetworkReconciler) handleNetAttDef(ctx context.Context, obj client.Object) []reconcile.Request { + ret := []reconcile.Request{} + instance := r.controller.GetObject() + nadNamespacedName := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()} + + err := r.Get(ctx, nadNamespacedName, instance) + if err == nil { + // Found a NetworkObject in the same namespace as the NetworkAttachmentDefinition, reconcile it + ret = append(ret, reconcile.Request{NamespacedName: nadNamespacedName}) + } else if !errors.IsNotFound(err) { + log.Log.WithName(r.controller.Name()+" handleNetAttDef").Error(err, "can't get object", "object", nadNamespacedName) + } + + // Not found, try to find the NetworkObject in the operator's namespace + operatorNamespacedName := types.NamespacedName{Namespace: vars.Namespace, Name: obj.GetName()} + err = r.Get(ctx, operatorNamespacedName, instance) + if err == nil { + // Found a NetworkObject in the operator's namespace, reconcile it + ret = append(ret, reconcile.Request{NamespacedName: operatorNamespacedName}) + } else if !errors.IsNotFound(err) { + log.Log.WithName(r.controller.Name()+" handleNetAttDef").Error(err, "can't get object", "object", operatorNamespacedName) + } + + return ret +} + func (r *genericNetworkReconciler) namespaceHandlerCreate(ctx context.Context, e event.TypedCreateEvent[client.Object], w workqueue.TypedRateLimitingInterface[reconcile.Request]) { networkList := r.controller.GetObjectList() err := r.List(ctx, @@ -252,3 +288,44 @@ func (r *genericNetworkReconciler) deleteNetAttDef(ctx context.Context, cr Netwo } return nil } + +func (r *genericNetworkReconciler) updateFinalizers(ctx context.Context, instance NetworkCRInstance) error { + if instance.GetNamespace() != vars.Namespace { + // If the resource is in a namespace different than the operator one, then the NetworkAttachmentDefinition will + // be created in the same namespace and its deletion can be handled by OwnerReferences. There is no need for finalizers + return nil + } + + instanceFinalizers := instance.GetFinalizers() + if !sriovnetworkv1.StringInArray(sriovnetworkv1.NETATTDEFFINALIZERNAME, instanceFinalizers) { + instance.SetFinalizers(append(instanceFinalizers, sriovnetworkv1.NETATTDEFFINALIZERNAME)) + if err := r.Update(ctx, instance); err != nil { + return err + } + } + + return nil +} + +func (r *genericNetworkReconciler) cleanResourcesAndFinalizers(ctx context.Context, instance NetworkCRInstance) error { + instanceFinalizers := instance.GetFinalizers() + + if sriovnetworkv1.StringInArray(sriovnetworkv1.NETATTDEFFINALIZERNAME, instanceFinalizers) { + // our finalizer is present, so lets handle any external dependency + log.FromContext(ctx).Info("delete NetworkAttachmentDefinition CR", "Namespace", instance.NetworkNamespace(), "Name", instance.GetName()) + if err := r.deleteNetAttDef(ctx, instance); err != nil { + // if fail to delete the external dependency here, return with error + // so that it can be retried + return err + } + // remove our finalizer from the list and update it. + newFinalizers, found := sriovnetworkv1.RemoveString(sriovnetworkv1.NETATTDEFFINALIZERNAME, instanceFinalizers) + if found { + instance.SetFinalizers(newFinalizers) + if err := r.Update(ctx, instance); err != nil { + return err + } + } + } + return nil +} diff --git a/controllers/sriovnetwork_controller_test.go b/controllers/sriovnetwork_controller_test.go index f2f7d2a077..df146ccea5 100644 --- a/controllers/sriovnetwork_controller_test.go +++ b/controllers/sriovnetwork_controller_test.go @@ -12,8 +12,10 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" dynclient "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/ginkgo/v2" @@ -342,6 +344,89 @@ var _ = Describe("SriovNetwork Controller", Ordered, func() { MustPassRepeatedly(10). Should(Succeed()) }) + + Context("When the SriovNetwork namespace is not equal to the operator one", func() { + BeforeAll(func() { + nsBlue := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-blue"}} + Expect(k8sClient.Create(context.Background(), nsBlue)).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + cleanNetworksInNamespace("ns-blue") + }) + + It("should create the NetAttachDefinition in the same namespace", func() { + cr := sriovnetworkv1.SriovNetwork{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sriovnet-blue", + Namespace: "ns-blue", + }, + Spec: sriovnetworkv1.SriovNetworkSpec{ + ResourceName: "resource_x", + }, + } + + err := k8sClient.Create(ctx, &cr) + Expect(err).NotTo(HaveOccurred()) + + netAttDef := &netattdefv1.NetworkAttachmentDefinition{} + err = util.WaitForNamespacedObject(netAttDef, k8sClient, "ns-blue", cr.GetName(), util.RetryInterval, util.Timeout) + Expect(err).NotTo(HaveOccurred()) + expectedOwnerReference := metav1.OwnerReference{ + Kind: "SriovNetwork", + APIVersion: sriovnetworkv1.GroupVersion.String(), + UID: cr.UID, + Name: cr.Name, + } + Expect(netAttDef.GetAnnotations()["k8s.v1.cni.cncf.io/resourceName"]).To(Equal("openshift.io/resource_x")) + + Expect(netAttDef.ObjectMeta.OwnerReferences).To(ContainElement(expectedOwnerReference)) + + // Patch the SriovNetwork + original := cr.DeepCopy() + cr.Spec.ResourceName = "resource_y" + err = k8sClient.Patch(ctx, &cr, dynclient.MergeFrom(original)) + Expect(err).NotTo(HaveOccurred()) + + // Check that the OwnerReference persists + netAttDef = &netattdefv1.NetworkAttachmentDefinition{} + + Eventually(func(g Gomega) { + netAttDef = &netattdefv1.NetworkAttachmentDefinition{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: cr.GetName(), Namespace: "ns-blue"}, netAttDef)).To(Succeed()) + g.Expect(netAttDef.GetAnnotations()["k8s.v1.cni.cncf.io/resourceName"]).To(Equal("openshift.io/resource_y")) + g.Expect(netAttDef.ObjectMeta.OwnerReferences).To(ContainElement(expectedOwnerReference)) + }).WithPolling(100 * time.Millisecond).WithTimeout(5 * time.Second).Should(Succeed()) + + // Delete the SriovNetwork + err = k8sClient.Delete(ctx, &cr) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should not create the NetAttachDefinition if the NetworkNamespace field is not empty", func() { + cr := sriovnetworkv1.SriovNetwork{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sriovnet-blue", + Namespace: "ns-blue", + }, + Spec: sriovnetworkv1.SriovNetworkSpec{ + NetworkNamespace: "default", + ResourceName: "resource_x", + }, + } + + err := k8sClient.Create(ctx, &cr) + Expect(err).NotTo(HaveOccurred()) + + Consistently(func(g Gomega) { + netAttDef := &netattdefv1.NetworkAttachmentDefinition{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: cr.GetName(), Namespace: "default"}, netAttDef) + g.Expect(err).To(HaveOccurred()) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }).WithPolling(100 * time.Millisecond).WithTimeout(1 * time.Second).Should(Succeed()) + }) + + }) }) }) @@ -390,3 +475,24 @@ func generateExpectedNetConfig(cr *sriovnetworkv1.SriovNetwork) string { } return configStr } + +func cleanNetworksInNamespace(namespace string) { + ctx := context.Background() + EventuallyWithOffset(1, func(g Gomega) { + err := k8sClient.DeleteAllOf(ctx, &sriovnetworkv1.SriovNetwork{}, client.InNamespace(namespace)) + g.Expect(err).NotTo(HaveOccurred()) + + k8sClient.DeleteAllOf(ctx, &netattdefv1.NetworkAttachmentDefinition{}, client.InNamespace(namespace)) + g.Expect(err).NotTo(HaveOccurred()) + + sriovNetworks := &sriovnetworkv1.SriovNetworkList{} + err = k8sClient.List(ctx, sriovNetworks, client.InNamespace(namespace)) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(sriovNetworks.Items).To(BeEmpty()) + + netAttachDefs := &netattdefv1.NetworkAttachmentDefinitionList{} + err = k8sClient.List(ctx, netAttachDefs, client.InNamespace(namespace)) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(netAttachDefs.Items).To(BeEmpty()) + }).WithPolling(100 * time.Millisecond).WithTimeout(10 * time.Second).Should(Succeed()) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index e759a0855a..e477142c8d 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -93,6 +93,7 @@ var _ = BeforeSuite(func() { logf.SetLogger(zap.New( zap.WriteTo(GinkgoWriter), + zap.Level(zapcore.Level(-2)), zap.UseDevMode(true), func(o *zap.Options) { o.TimeEncoder = zapcore.RFC3339NanoTimeEncoder From 5b30784db29e09da66365fb40022fbc9b351e7b2 Mon Sep 17 00:00:00 2001 From: Andrea Panattoni Date: Tue, 6 May 2025 18:15:29 +0200 Subject: [PATCH 2/4] Webhook validation for networks `.Spec.NetworkNamespace` field Signed-off-by: Andrea Panattoni --- .../operator-webhook/003-webhook.yaml | 12 +++ pkg/webhook/validate_networks.go | 43 +++++++++ pkg/webhook/validate_networks_test.go | 95 +++++++++++++++++++ pkg/webhook/webhook.go | 43 +++++++++ 4 files changed, 193 insertions(+) create mode 100644 pkg/webhook/validate_networks.go create mode 100644 pkg/webhook/validate_networks_test.go diff --git a/bindata/manifests/operator-webhook/003-webhook.yaml b/bindata/manifests/operator-webhook/003-webhook.yaml index 9181f1edff..a7768e1bc5 100644 --- a/bindata/manifests/operator-webhook/003-webhook.yaml +++ b/bindata/manifests/operator-webhook/003-webhook.yaml @@ -69,3 +69,15 @@ webhooks: apiGroups: [ "sriovnetwork.openshift.io" ] apiVersions: [ "v1" ] resources: [ "sriovnetworkpoolconfigs" ] + - operations: [ "CREATE", "UPDATE", ] + apiGroups: [ "sriovnetwork.openshift.io" ] + apiVersions: [ "v1" ] + resources: [ "sriovnetworks" ] + - operations: [ "CREATE", "UPDATE", ] + apiGroups: [ "sriovnetwork.openshift.io" ] + apiVersions: [ "v1" ] + resources: [ "sriovibnetworks" ] + - operations: [ "CREATE", "UPDATE", ] + apiGroups: [ "sriovnetwork.openshift.io" ] + apiVersions: [ "v1" ] + resources: [ "ovsnetworks" ] diff --git a/pkg/webhook/validate_networks.go b/pkg/webhook/validate_networks.go new file mode 100644 index 0000000000..358c38a97d --- /dev/null +++ b/pkg/webhook/validate_networks.go @@ -0,0 +1,43 @@ +package webhook + +import ( + "fmt" + + v1 "k8s.io/api/admission/v1" + + sriovnetworkv1 "github.com/k8snetworkplumbingwg/sriov-network-operator/api/v1" + "github.com/k8snetworkplumbingwg/sriov-network-operator/controllers" + "github.com/k8snetworkplumbingwg/sriov-network-operator/pkg/vars" +) + +func validateSriovNetwork(cr *sriovnetworkv1.SriovNetwork, operation v1.Operation) (bool, []string, error) { + err := validateNetworkNamespace(cr) + if err != nil { + return false, nil, err + } + return true, nil, nil +} + +func validateSriovIBNetwork(cr *sriovnetworkv1.SriovIBNetwork, operation v1.Operation) (bool, []string, error) { + err := validateNetworkNamespace(cr) + if err != nil { + return false, nil, err + } + return true, nil, nil +} + +func validateOVSNetwork(cr *sriovnetworkv1.OVSNetwork, operation v1.Operation) (bool, []string, error) { + err := validateNetworkNamespace(cr) + if err != nil { + return false, nil, err + } + return true, nil, nil +} + +func validateNetworkNamespace(cr controllers.NetworkCRInstance) error { + if cr.GetNamespace() != vars.Namespace && cr.NetworkNamespace() != "" { + return fmt.Errorf(".Spec.NetworkNamespace field can't be specified if the resource is not in the %s namespace", vars.Namespace) + } + + return nil +} diff --git a/pkg/webhook/validate_networks_test.go b/pkg/webhook/validate_networks_test.go new file mode 100644 index 0000000000..ee79af5946 --- /dev/null +++ b/pkg/webhook/validate_networks_test.go @@ -0,0 +1,95 @@ +package webhook + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/k8snetworkplumbingwg/sriov-network-operator/api/v1" + "github.com/k8snetworkplumbingwg/sriov-network-operator/controllers" + "github.com/k8snetworkplumbingwg/sriov-network-operator/pkg/vars" +) + +func TestValidate_NetworkNamespace(t *testing.T) { + defer func(previous string) { vars.Namespace = previous }(vars.Namespace) + vars.Namespace = "operator-namespace" + + testCases := []struct { + name string + network controllers.NetworkCRInstance + shouldFail bool + }{ + { + name: "SriovNetwork in operator namespace with empty NetworkNamespace", + network: &SriovNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "operator-namespace"}, Spec: SriovNetworkSpec{NetworkNamespace: ""}}, + shouldFail: false, + }, + { + name: "SriovNetwork in operator namespace with custom NetworkNamespace", + network: &SriovNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "operator-namespace"}, Spec: SriovNetworkSpec{NetworkNamespace: "xxx"}}, + shouldFail: false, + }, + { + name: "SriovNetwork in custom namespace with empty NetworkNamespace", + network: &SriovNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "xxx"}, Spec: SriovNetworkSpec{NetworkNamespace: ""}}, + shouldFail: false, + }, + { + name: "SriovIBNetwork in operator namespace with empty NetworkNamespace", + network: &SriovIBNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "operator-namespace"}, Spec: SriovIBNetworkSpec{NetworkNamespace: ""}}, + shouldFail: false, + }, + { + name: "SriovIBNetwork in operator namespace with custom NetworkNamespace", + network: &SriovIBNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "operator-namespace"}, Spec: SriovIBNetworkSpec{NetworkNamespace: "xxx"}}, + shouldFail: false, + }, + { + name: "SriovIBNetwork in custom namespace with empty NetworkNamespace", + network: &SriovIBNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "xxx"}, Spec: SriovIBNetworkSpec{NetworkNamespace: ""}}, + shouldFail: false, + }, + { + name: "OVSNetwork in operator namespace with empty NetworkNamespace", + network: &OVSNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "operator-namespace"}, Spec: OVSNetworkSpec{NetworkNamespace: ""}}, + shouldFail: false, + }, + { + name: "OVSNetwork in operator namespace with custom NetworkNamespace", + network: &OVSNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "operator-namespace"}, Spec: OVSNetworkSpec{NetworkNamespace: "xxx"}}, + shouldFail: false, + }, + { + name: "OVSNetwork in custom namespace with empty NetworkNamespace", + network: &OVSNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "xxx"}, Spec: OVSNetworkSpec{NetworkNamespace: ""}}, + shouldFail: false, + }, + { + name: "SriovNetwork in custom namespace with custom NetworkNamespace", + network: &SriovNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "xxx"}, Spec: SriovNetworkSpec{NetworkNamespace: "yyy"}}, + shouldFail: true, + }, + { + name: "SriovIBNetwork in custom namespace with custom NetworkNamespace", + network: &SriovIBNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "xxx"}, Spec: SriovIBNetworkSpec{NetworkNamespace: "yyy"}}, + shouldFail: true, + }, + { + name: "OVSNetwork in custom namespace with custom NetworkNamespace", + network: &OVSNetwork{ObjectMeta: metav1.ObjectMeta{Namespace: "xxx"}, Spec: OVSNetworkSpec{NetworkNamespace: "yyy"}}, + shouldFail: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := validateNetworkNamespace(tc.network) + if tc.shouldFail && err == nil { + t.Error("expected error but got none") + } + if !tc.shouldFail && err != nil { + t.Errorf("expected no error but got: %v", err) + } + }) + } +} diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index d560a66d1f..42df554094 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -97,6 +97,49 @@ func ValidateCustomResource(ar v1.AdmissionReview) *v1.AdmissionResponse { Reason: metav1.StatusReason(err.Error()), } } + + case "SriovNetwork": + network := sriovnetworkv1.SriovNetwork{} + + err = json.Unmarshal(raw, &network) + if err != nil { + log.Log.Error(err, "failed to unmarshal object") + return toV1AdmissionResponse(err) + } + + if reviewResponse.Allowed, reviewResponse.Warnings, err = validateSriovNetwork(&network, ar.Request.Operation); err != nil { + reviewResponse.Result = &metav1.Status{ + Reason: metav1.StatusReason(err.Error()), + } + } + case "SriovIBNetwork": + network := sriovnetworkv1.SriovIBNetwork{} + + err = json.Unmarshal(raw, &network) + if err != nil { + log.Log.Error(err, "failed to unmarshal object") + return toV1AdmissionResponse(err) + } + + if reviewResponse.Allowed, reviewResponse.Warnings, err = validateSriovIBNetwork(&network, ar.Request.Operation); err != nil { + reviewResponse.Result = &metav1.Status{ + Reason: metav1.StatusReason(err.Error()), + } + } + case "OVSNetwork": + network := sriovnetworkv1.OVSNetwork{} + + err = json.Unmarshal(raw, &network) + if err != nil { + log.Log.Error(err, "failed to unmarshal object") + return toV1AdmissionResponse(err) + } + + if reviewResponse.Allowed, reviewResponse.Warnings, err = validateOVSNetwork(&network, ar.Request.Operation); err != nil { + reviewResponse.Result = &metav1.Status{ + Reason: metav1.StatusReason(err.Error()), + } + } } return &reviewResponse From f131b4f58427d727256202f06c30d91fc1896891 Mon Sep 17 00:00:00 2001 From: Andrea Panattoni Date: Tue, 13 May 2025 09:57:42 +0200 Subject: [PATCH 3/4] webhook: Remove `klog.InitFlags(nil)` to avoid panic: ``` panic: /tmp/go-build1586768671/b001/exe/webhook flag redefined: alsologtostderr goroutine 1 [running]: flag.(*FlagSet).Var(0xc0000781c0, {0x3daf2d0, 0x5566969}, {0x394a710, 0xf}, {0x39e87ee, 0x49}) /usr/lib/golang/src/flag/flag.go:1029 +0x3b9 k8s.io/klog/v2.InitFlags.func1(0xc000234990?) /home/apanatto/go/pkg/mod/k8s.io/klog/v2@v2.130.1/klog.go:447 +0x31 flag.(*FlagSet).VisitAll(0xc0003f4c40?, 0xc000785de0) /usr/lib/golang/src/flag/flag.go:458 +0x42 k8s.io/klog/v2.InitFlags(0x3dc1aa0?) /home/apanatto/go/pkg/mod/k8s.io/klog/v2@v2.130.1/klog.go:446 +0x3c main.init.0() /home/apanatto/dev/github.com/k8snetworkplumbingwg/sriov-network-operator/cmd/webhook/main.go:27 +0x15 exit status 2 ``` Signed-off-by: Andrea Panattoni --- cmd/webhook/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index a43123cfcd..a83e083267 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -5,7 +5,6 @@ import ( "os" "github.com/spf13/cobra" - "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/log" snolog "github.com/k8snetworkplumbingwg/sriov-network-operator/pkg/log" @@ -24,7 +23,6 @@ var ( ) func init() { - klog.InitFlags(nil) snolog.BindFlags(flag.CommandLine) rootCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) } From ada185cb374f41dc50f31eed28842779b1b4a12c Mon Sep 17 00:00:00 2001 From: Andrea Panattoni Date: Mon, 12 May 2025 13:39:46 +0200 Subject: [PATCH 4/4] Namespaced network object end2end tests Move `[sriov] operator No SriovNetworkNodePolicy ...` test cases to its own test file. Implement test case to verify controller and webhook logic Signed-off-by: Andrea Panattoni --- test/conformance/tests/test_no_policy.go | 259 ++++++++++++++++++ test/conformance/tests/test_sriov_operator.go | 203 -------------- 2 files changed, 259 insertions(+), 203 deletions(-) create mode 100644 test/conformance/tests/test_no_policy.go diff --git a/test/conformance/tests/test_no_policy.go b/test/conformance/tests/test_no_policy.go new file mode 100644 index 0000000000..c965cf1867 --- /dev/null +++ b/test/conformance/tests/test_no_policy.go @@ -0,0 +1,259 @@ +package tests + +import ( + "context" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + netattdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + + sriovv1 "github.com/k8snetworkplumbingwg/sriov-network-operator/api/v1" + "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/cluster" + "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/discovery" + "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/namespaces" + "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/nodes" +) + +var _ = Describe("[sriov] operator", Ordered, ContinueOnFailure, func() { + Describe("No SriovNetworkNodePolicy", func() { + Context("SR-IOV network config daemon can be set by nodeselector", func() { + // 26186 + It("Should schedule the config daemon on selected nodes", func() { + if discovery.Enabled() { + Skip("Test unsuitable to be run in discovery mode") + } + + By("Checking that a daemon is scheduled on each worker node") + Eventually(func() bool { + return daemonsScheduledOnNodes("node-role.kubernetes.io/worker=") + }, 3*time.Minute, 1*time.Second).Should(Equal(true)) + + By("Labeling one worker node with the label needed for the daemon") + allNodes, err := clients.CoreV1Interface.Nodes().List(context.Background(), metav1.ListOptions{ + LabelSelector: "node-role.kubernetes.io/worker", + }) + Expect(err).ToNot(HaveOccurred()) + + selectedNodes, err := nodes.MatchingOptionalSelector(clients, allNodes.Items) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(selectedNodes)).To(BeNumerically(">", 0), "There must be at least one worker") + patch := []byte(`{"metadata":{"labels":{"sriovenabled":"true"}}}`) + candidate, err := clients.CoreV1Interface.Nodes().Patch(context.Background(), selectedNodes[0].Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) + Expect(err).ToNot(HaveOccurred()) + selectedNodes[0] = *candidate + + By("Setting the node selector for each daemon") + cfg := sriovv1.SriovOperatorConfig{} + err = clients.Get(context.TODO(), runtimeclient.ObjectKey{ + Name: "default", + Namespace: operatorNamespace, + }, &cfg) + Expect(err).ToNot(HaveOccurred()) + cfg.Spec.ConfigDaemonNodeSelector = map[string]string{ + "sriovenabled": "true", + } + Eventually(func() error { + return clients.Update(context.TODO(), &cfg) + }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) + + By("Checking that a daemon is scheduled only on selected node") + Eventually(func() bool { + return !daemonsScheduledOnNodes("sriovenabled!=true") && + daemonsScheduledOnNodes("sriovenabled=true") + }, 1*time.Minute, 1*time.Second).Should(Equal(true)) + + By("Restoring the node selector for daemons") + err = clients.Get(context.TODO(), runtimeclient.ObjectKey{ + Name: "default", + Namespace: operatorNamespace, + }, &cfg) + Expect(err).ToNot(HaveOccurred()) + cfg.Spec.ConfigDaemonNodeSelector = map[string]string{} + Eventually(func() error { + return clients.Update(context.TODO(), &cfg) + }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) + + By("Checking that a daemon is scheduled on each worker node") + Eventually(func() bool { + return daemonsScheduledOnNodes("node-role.kubernetes.io/worker") + }, 1*time.Minute, 1*time.Second).Should(Equal(true)) + }) + }) + + Context("LogLevel affects operator's logs", func() { + It("when set to 0 no lifecycle logs are present", func() { + if discovery.Enabled() { + Skip("Test unsuitable to be run in discovery mode") + } + + initialLogLevelValue := getOperatorConfigLogLevel() + DeferCleanup(func() { + By("Restore LogLevel to its initial value") + setOperatorConfigLogLevel(initialLogLevelValue) + }) + + initialDisableDrain, err := cluster.GetNodeDrainState(clients, operatorNamespace) + Expect(err).ToNot(HaveOccurred()) + + DeferCleanup(func() { + By("Restore DisableDrain to its initial value") + Eventually(func() error { + return cluster.SetDisableNodeDrainState(clients, operatorNamespace, initialDisableDrain) + }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) + }) + + By("Set operator LogLevel to 2") + setOperatorConfigLogLevel(2) + + By("Flip DisableDrain to trigger operator activity") + since := time.Now().Add(-10 * time.Second) + Eventually(func() error { + return cluster.SetDisableNodeDrainState(clients, operatorNamespace, !initialDisableDrain) + }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) + + By("Assert logs contains verbose output") + Eventually(func(g Gomega) { + logs := getOperatorLogs(since) + g.Expect(logs).To( + ContainElement(And( + ContainSubstring("Reconciling SriovOperatorConfig"), + )), + ) + + // Should contain verbose logging + g.Expect(logs).To( + ContainElement( + ContainSubstring("Start to sync webhook objects"), + ), + ) + }, 1*time.Minute, 5*time.Second).Should(Succeed()) + + By("Reduce operator LogLevel to 0") + setOperatorConfigLogLevel(0) + + By("Flip DisableDrain again to trigger operator activity") + since = time.Now().Add(-10 * time.Second) + Eventually(func() error { + return cluster.SetDisableNodeDrainState(clients, operatorNamespace, initialDisableDrain) + }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) + + By("Assert logs contains less operator activity") + Eventually(func(g Gomega) { + logs := getOperatorLogs(since) + + // time only contains sec, but we can have race here that in the same sec there was a sync + afterLogs := []string{} + found := false + for _, log := range logs { + if found { + afterLogs = append(afterLogs, log) + } + if strings.Contains(log, "{\"new-level\": 0, \"current-level\": 2}") { + found = true + } + } + g.Expect(found).To(BeTrue()) + g.Expect(afterLogs).To( + ContainElement(And( + ContainSubstring("Reconciling SriovOperatorConfig"), + )), + ) + + // Should not contain verbose logging + g.Expect(afterLogs).ToNot( + ContainElement( + ContainSubstring("Start to sync webhook objects"), + ), + ) + }, 3*time.Minute, 5*time.Second).Should(Succeed()) + }) + }) + + Context("SriovNetworkMetricsExporter", func() { + BeforeEach(func() { + if discovery.Enabled() { + Skip("Test unsuitable to be run in discovery mode") + } + + initialValue := isFeatureFlagEnabled("metricsExporter") + DeferCleanup(func() { + By("Restoring initial feature flag value") + setFeatureFlag("metricsExporter", initialValue) + }) + + By("Enabling `metricsExporter` feature flag") + setFeatureFlag("metricsExporter", true) + }) + + It("should be deployed if the feature gate is enabled", func() { + By("Checking that a daemon is scheduled on selected node") + Eventually(func() bool { + return isDaemonsetScheduledOnNodes("node-role.kubernetes.io/worker", "app=sriov-network-metrics-exporter") + }).WithTimeout(time.Minute).WithPolling(time.Second).Should(Equal(true)) + }) + + It("should deploy ServiceMonitor if the Prometheus operator is installed", func() { + _, err := clients.ServiceMonitors(operatorNamespace).List(context.Background(), metav1.ListOptions{}) + if k8serrors.IsNotFound(err) { + Skip("Prometheus operator not available in the cluster") + } + + By("Checking ServiceMonitor is deployed if needed") + Eventually(func(g Gomega) { + _, err := clients.ServiceMonitors(operatorNamespace).Get(context.Background(), "sriov-network-metrics-exporter", metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + }).WithTimeout(time.Minute).WithPolling(time.Second).Should(Succeed()) + }) + + It("should remove ServiceMonitor when the feature is turned off", func() { + setFeatureFlag("metricsExporter", false) + Eventually(func(g Gomega) { + _, err := clients.ServiceMonitors(operatorNamespace).Get(context.Background(), "sriov-network-metrics-exporter", metav1.GetOptions{}) + g.Expect(k8serrors.IsNotFound(err)).To(BeTrue()) + }).WithTimeout(time.Minute).WithPolling(time.Second).Should(Succeed()) + }) + }) + + Context("Namespaced network objects", func() { + DescribeTable("can be create in every namespaces", func(object runtimeclient.Object) { + err := clients.Create(context.Background(), object) + Expect(err).ToNot(HaveOccurred()) + + waitForNetAttachDef(object.GetName(), object.GetNamespace()) + + err = clients.Delete(context.Background(), object) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func() bool { + netAttDef := &netattdefv1.NetworkAttachmentDefinition{} + err := clients.Get(context.Background(), runtimeclient.ObjectKey{Name: object.GetName(), Namespace: object.GetNamespace()}, netAttDef) + return err != nil && k8serrors.IsNotFound(err) + }, 2*time.Minute, 10*time.Second).Should(BeTrue()) + }, + Entry("SriovNetwork", &sriovv1.SriovNetwork{ObjectMeta: metav1.ObjectMeta{Name: "sriovnet1", Namespace: namespaces.Test}}), + Entry("SriovIBNetwork", &sriovv1.SriovIBNetwork{ObjectMeta: metav1.ObjectMeta{Name: "sriovibnet1", Namespace: namespaces.Test}}), + Entry("OVSNetwork", &sriovv1.OVSNetwork{ObjectMeta: metav1.ObjectMeta{Name: "ovsnet1", Namespace: namespaces.Test}}), + ) + + DescribeTable("can NOT be in application namespace and have .Spec.NetworkNamespace != ''", func(object runtimeclient.Object) { + err := clients.Create(context.Background(), object) + Expect(err).To(HaveOccurred()) + Expect(string(k8serrors.ReasonForError(err))). + To(ContainSubstring(".Spec.NetworkNamespace field can't be specified if the resource is not in the ")) + }, + Entry("SriovNetwork", &sriovv1.SriovNetwork{ObjectMeta: metav1.ObjectMeta{Name: "sriovnet1", Namespace: namespaces.Test}, Spec: sriovv1.SriovNetworkSpec{NetworkNamespace: "default"}}), + Entry("SriovIBNetwork", &sriovv1.SriovIBNetwork{ObjectMeta: metav1.ObjectMeta{Name: "sriovibnet1", Namespace: namespaces.Test}, Spec: sriovv1.SriovIBNetworkSpec{NetworkNamespace: "default"}}), + Entry("OVSNetwork", &sriovv1.OVSNetwork{ObjectMeta: metav1.ObjectMeta{Name: "ovsnet1", Namespace: namespaces.Test}, Spec: sriovv1.OVSNetworkSpec{NetworkNamespace: "default"}}), + ) + }) + }) +}) diff --git a/test/conformance/tests/test_sriov_operator.go b/test/conformance/tests/test_sriov_operator.go index 52e594e095..0701023d24 100644 --- a/test/conformance/tests/test_sriov_operator.go +++ b/test/conformance/tests/test_sriov_operator.go @@ -23,7 +23,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,7 +33,6 @@ import ( "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/k8sreporter" "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/namespaces" "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/network" - "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/nodes" "github.com/k8snetworkplumbingwg/sriov-network-operator/test/util/pod" ) @@ -69,207 +67,6 @@ var _ = Describe("[sriov] operator", Ordered, func() { WaitForSRIOVStable() }) - Describe("No SriovNetworkNodePolicy", func() { - Context("SR-IOV network config daemon can be set by nodeselector", func() { - // 26186 - It("Should schedule the config daemon on selected nodes", func() { - if discovery.Enabled() { - Skip("Test unsuitable to be run in discovery mode") - } - - By("Checking that a daemon is scheduled on each worker node") - Eventually(func() bool { - return daemonsScheduledOnNodes("node-role.kubernetes.io/worker=") - }, 3*time.Minute, 1*time.Second).Should(Equal(true)) - - By("Labeling one worker node with the label needed for the daemon") - allNodes, err := clients.CoreV1Interface.Nodes().List(context.Background(), metav1.ListOptions{ - LabelSelector: "node-role.kubernetes.io/worker", - }) - Expect(err).ToNot(HaveOccurred()) - - selectedNodes, err := nodes.MatchingOptionalSelector(clients, allNodes.Items) - Expect(err).ToNot(HaveOccurred()) - - Expect(len(selectedNodes)).To(BeNumerically(">", 0), "There must be at least one worker") - patch := []byte(`{"metadata":{"labels":{"sriovenabled":"true"}}}`) - candidate, err := clients.CoreV1Interface.Nodes().Patch(context.Background(), selectedNodes[0].Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) - Expect(err).ToNot(HaveOccurred()) - selectedNodes[0] = *candidate - - By("Setting the node selector for each daemon") - cfg := sriovv1.SriovOperatorConfig{} - err = clients.Get(context.TODO(), runtimeclient.ObjectKey{ - Name: "default", - Namespace: operatorNamespace, - }, &cfg) - Expect(err).ToNot(HaveOccurred()) - cfg.Spec.ConfigDaemonNodeSelector = map[string]string{ - "sriovenabled": "true", - } - Eventually(func() error { - return clients.Update(context.TODO(), &cfg) - }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) - - By("Checking that a daemon is scheduled only on selected node") - Eventually(func() bool { - return !daemonsScheduledOnNodes("sriovenabled!=true") && - daemonsScheduledOnNodes("sriovenabled=true") - }, 1*time.Minute, 1*time.Second).Should(Equal(true)) - - By("Restoring the node selector for daemons") - err = clients.Get(context.TODO(), runtimeclient.ObjectKey{ - Name: "default", - Namespace: operatorNamespace, - }, &cfg) - Expect(err).ToNot(HaveOccurred()) - cfg.Spec.ConfigDaemonNodeSelector = map[string]string{} - Eventually(func() error { - return clients.Update(context.TODO(), &cfg) - }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) - - By("Checking that a daemon is scheduled on each worker node") - Eventually(func() bool { - return daemonsScheduledOnNodes("node-role.kubernetes.io/worker") - }, 1*time.Minute, 1*time.Second).Should(Equal(true)) - }) - }) - - Context("LogLevel affects operator's logs", func() { - It("when set to 0 no lifecycle logs are present", func() { - if discovery.Enabled() { - Skip("Test unsuitable to be run in discovery mode") - } - - initialLogLevelValue := getOperatorConfigLogLevel() - DeferCleanup(func() { - By("Restore LogLevel to its initial value") - setOperatorConfigLogLevel(initialLogLevelValue) - }) - - initialDisableDrain, err := cluster.GetNodeDrainState(clients, operatorNamespace) - Expect(err).ToNot(HaveOccurred()) - - DeferCleanup(func() { - By("Restore DisableDrain to its initial value") - Eventually(func() error { - return cluster.SetDisableNodeDrainState(clients, operatorNamespace, initialDisableDrain) - }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) - }) - - By("Set operator LogLevel to 2") - setOperatorConfigLogLevel(2) - - By("Flip DisableDrain to trigger operator activity") - since := time.Now().Add(-10 * time.Second) - Eventually(func() error { - return cluster.SetDisableNodeDrainState(clients, operatorNamespace, !initialDisableDrain) - }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) - - By("Assert logs contains verbose output") - Eventually(func(g Gomega) { - logs := getOperatorLogs(since) - g.Expect(logs).To( - ContainElement(And( - ContainSubstring("Reconciling SriovOperatorConfig"), - )), - ) - - // Should contain verbose logging - g.Expect(logs).To( - ContainElement( - ContainSubstring("Start to sync webhook objects"), - ), - ) - }, 1*time.Minute, 5*time.Second).Should(Succeed()) - - By("Reduce operator LogLevel to 0") - setOperatorConfigLogLevel(0) - - By("Flip DisableDrain again to trigger operator activity") - since = time.Now().Add(-10 * time.Second) - Eventually(func() error { - return cluster.SetDisableNodeDrainState(clients, operatorNamespace, initialDisableDrain) - }, 1*time.Minute, 5*time.Second).ShouldNot(HaveOccurred()) - - By("Assert logs contains less operator activity") - Eventually(func(g Gomega) { - logs := getOperatorLogs(since) - - // time only contains sec, but we can have race here that in the same sec there was a sync - afterLogs := []string{} - found := false - for _, log := range logs { - if found { - afterLogs = append(afterLogs, log) - } - if strings.Contains(log, "{\"new-level\": 0, \"current-level\": 2}") { - found = true - } - } - g.Expect(found).To(BeTrue()) - g.Expect(afterLogs).To( - ContainElement(And( - ContainSubstring("Reconciling SriovOperatorConfig"), - )), - ) - - // Should not contain verbose logging - g.Expect(afterLogs).ToNot( - ContainElement( - ContainSubstring("Start to sync webhook objects"), - ), - ) - }, 3*time.Minute, 5*time.Second).Should(Succeed()) - }) - }) - - Context("SriovNetworkMetricsExporter", func() { - BeforeEach(func() { - if discovery.Enabled() { - Skip("Test unsuitable to be run in discovery mode") - } - - initialValue := isFeatureFlagEnabled("metricsExporter") - DeferCleanup(func() { - By("Restoring initial feature flag value") - setFeatureFlag("metricsExporter", initialValue) - }) - - By("Enabling `metricsExporter` feature flag") - setFeatureFlag("metricsExporter", true) - }) - - It("should be deployed if the feature gate is enabled", func() { - By("Checking that a daemon is scheduled on selected node") - Eventually(func() bool { - return isDaemonsetScheduledOnNodes("node-role.kubernetes.io/worker", "app=sriov-network-metrics-exporter") - }).WithTimeout(time.Minute).WithPolling(time.Second).Should(Equal(true)) - }) - - It("should deploy ServiceMonitor if the Prometheus operator is installed", func() { - _, err := clients.ServiceMonitors(operatorNamespace).List(context.Background(), metav1.ListOptions{}) - if k8serrors.IsNotFound(err) { - Skip("Prometheus operator not available in the cluster") - } - - By("Checking ServiceMonitor is deployed if needed") - Eventually(func(g Gomega) { - _, err := clients.ServiceMonitors(operatorNamespace).Get(context.Background(), "sriov-network-metrics-exporter", metav1.GetOptions{}) - g.Expect(err).ToNot(HaveOccurred()) - }).WithTimeout(time.Minute).WithPolling(time.Second).Should(Succeed()) - }) - - It("should remove ServiceMonitor when the feature is turned off", func() { - setFeatureFlag("metricsExporter", false) - Eventually(func(g Gomega) { - _, err := clients.ServiceMonitors(operatorNamespace).Get(context.Background(), "sriov-network-metrics-exporter", metav1.GetOptions{}) - g.Expect(k8serrors.IsNotFound(err)).To(BeTrue()) - }).WithTimeout(time.Minute).WithPolling(time.Second).Should(Succeed()) - }) - }) - }) - Describe("Generic SriovNetworkNodePolicy", func() { numVfs := 5 resourceName := testResourceName