github.com/k8snetworkplumbingwg/sriov-network-operator@v1.2.1-0.20240408194816-2d2e5a45d453/controllers/sriovnetwork_controller_test.go (about)

     1  package controllers
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	netattdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
    12  	corev1 "k8s.io/api/core/v1"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	"k8s.io/apimachinery/pkg/types"
    16  	"k8s.io/client-go/util/retry"
    17  	dynclient "sigs.k8s.io/controller-runtime/pkg/client"
    18  
    19  	. "github.com/onsi/ginkgo/v2"
    20  	. "github.com/onsi/gomega"
    21  
    22  	sriovnetworkv1 "github.com/k8snetworkplumbingwg/sriov-network-operator/api/v1"
    23  	"github.com/k8snetworkplumbingwg/sriov-network-operator/test/util"
    24  )
    25  
    26  const (
    27  	on         = "on"
    28  	emptyCurls = "{}"
    29  )
    30  
    31  var _ = Describe("SriovNetwork Controller", Ordered, func() {
    32  	var cancel context.CancelFunc
    33  	var ctx context.Context
    34  
    35  	BeforeAll(func() {
    36  		By("Setup controller manager")
    37  		k8sManager, err := setupK8sManagerForTest()
    38  		Expect(err).ToNot(HaveOccurred())
    39  
    40  		err = (&SriovNetworkReconciler{
    41  			Client: k8sManager.GetClient(),
    42  			Scheme: k8sManager.GetScheme(),
    43  		}).SetupWithManager(k8sManager)
    44  		Expect(err).ToNot(HaveOccurred())
    45  
    46  		ctx, cancel = context.WithCancel(context.Background())
    47  
    48  		wg := sync.WaitGroup{}
    49  		wg.Add(1)
    50  		go func() {
    51  			defer wg.Done()
    52  			defer GinkgoRecover()
    53  			By("Start controller manager")
    54  			err := k8sManager.Start(ctx)
    55  			Expect(err).ToNot(HaveOccurred())
    56  		}()
    57  
    58  		DeferCleanup(func() {
    59  			By("Shutdown controller manager")
    60  			cancel()
    61  			wg.Wait()
    62  		})
    63  	})
    64  
    65  	Context("with SriovNetwork", func() {
    66  		specs := map[string]sriovnetworkv1.SriovNetworkSpec{
    67  			"test-0": {
    68  				ResourceName: "resource_1",
    69  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
    70  				Vlan:         100,
    71  				VlanQoS:      5,
    72  				VlanProto:    "802.1ad",
    73  			},
    74  			"test-1": {
    75  				ResourceName:     "resource_1",
    76  				IPAM:             `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
    77  				NetworkNamespace: "default",
    78  			},
    79  			"test-2": {
    80  				ResourceName: "resource_1",
    81  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
    82  				SpoofChk:     on,
    83  			},
    84  			"test-3": {
    85  				ResourceName: "resource_1",
    86  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
    87  				Trust:        on,
    88  			},
    89  			"test-4": {
    90  				ResourceName: "resource_1",
    91  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
    92  			},
    93  			"test-5": {
    94  				ResourceName: "resource_1",
    95  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
    96  				LogLevel:     "debug",
    97  				LogFile:      "/tmp/tmpfile",
    98  			},
    99  		}
   100  		sriovnets := util.GenerateSriovNetworkCRs(testNamespace, specs)
   101  		DescribeTable("should be possible to create/delete net-att-def",
   102  			func(cr sriovnetworkv1.SriovNetwork) {
   103  				var err error
   104  				expect := generateExpectedNetConfig(&cr)
   105  
   106  				By("Create the SriovNetwork Custom Resource")
   107  				// get global framework variables
   108  				err = k8sClient.Create(ctx, &cr)
   109  				Expect(err).NotTo(HaveOccurred())
   110  				ns := testNamespace
   111  				if cr.Spec.NetworkNamespace != "" {
   112  					ns = cr.Spec.NetworkNamespace
   113  				}
   114  				netAttDef := &netattdefv1.NetworkAttachmentDefinition{}
   115  				err = util.WaitForNamespacedObject(netAttDef, k8sClient, ns, cr.GetName(), util.RetryInterval, util.Timeout)
   116  				Expect(err).NotTo(HaveOccurred())
   117  				anno := netAttDef.GetAnnotations()
   118  
   119  				Expect(anno["k8s.v1.cni.cncf.io/resourceName"]).To(Equal("openshift.io/" + cr.Spec.ResourceName))
   120  				Expect(strings.TrimSpace(netAttDef.Spec.Config)).To(Equal(expect))
   121  
   122  				By("Delete the SriovNetwork Custom Resource")
   123  				found := &sriovnetworkv1.SriovNetwork{}
   124  				err = k8sClient.Get(ctx, types.NamespacedName{Namespace: cr.GetNamespace(), Name: cr.GetName()}, found)
   125  				Expect(err).NotTo(HaveOccurred())
   126  				err = k8sClient.Delete(ctx, found, []dynclient.DeleteOption{}...)
   127  				Expect(err).NotTo(HaveOccurred())
   128  
   129  				netAttDef = &netattdefv1.NetworkAttachmentDefinition{}
   130  				err = util.WaitForNamespacedObjectDeleted(netAttDef, k8sClient, ns, cr.GetName(), util.RetryInterval, util.Timeout)
   131  				Expect(err).NotTo(HaveOccurred())
   132  			},
   133  			Entry("with vlan, vlanQoS and vlanProto flag", sriovnets["test-0"]),
   134  			Entry("with networkNamespace flag", sriovnets["test-1"]),
   135  			Entry("with SpoofChk flag on", sriovnets["test-2"]),
   136  			Entry("with Trust flag on", sriovnets["test-3"]),
   137  			Entry("with LogLevel and LogFile", sriovnets["test-5"]),
   138  		)
   139  
   140  		newSpecs := map[string]sriovnetworkv1.SriovNetworkSpec{
   141  			"new-0": {
   142  				ResourceName: "resource_1",
   143  				IPAM:         `{"type":"dhcp"}`,
   144  				Vlan:         200,
   145  				VlanProto:    "802.1q",
   146  			},
   147  			"new-1": {
   148  				ResourceName: "resource_1",
   149  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
   150  			},
   151  			"new-2": {
   152  				ResourceName: "resource_1",
   153  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
   154  				SpoofChk:     on,
   155  			},
   156  			"new-3": {
   157  				ResourceName: "resource_1",
   158  				IPAM:         `{"type":"host-local","subnet":"10.56.217.0/24","rangeStart":"10.56.217.171","rangeEnd":"10.56.217.181","routes":[{"dst":"0.0.0.0/0"}],"gateway":"10.56.217.1"}`,
   159  				Trust:        on,
   160  			},
   161  		}
   162  		newsriovnets := util.GenerateSriovNetworkCRs(testNamespace, newSpecs)
   163  
   164  		DescribeTable("should be possible to update net-att-def",
   165  			func(old, new sriovnetworkv1.SriovNetwork) {
   166  				old.Name = new.GetName()
   167  				err := k8sClient.Create(ctx, &old)
   168  				defer func() {
   169  					// Cleanup the test resource
   170  					Expect(k8sClient.Delete(ctx, &old)).To(Succeed())
   171  				}()
   172  				Expect(err).NotTo(HaveOccurred())
   173  				found := &sriovnetworkv1.SriovNetwork{}
   174  				expect := generateExpectedNetConfig(&new)
   175  
   176  				retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
   177  					// Retrieve the latest version of SriovNetwork before attempting update
   178  					// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
   179  					getErr := k8sClient.Get(ctx, types.NamespacedName{Namespace: old.GetNamespace(), Name: old.GetName()}, found)
   180  					if getErr != nil {
   181  						io.WriteString(GinkgoWriter, fmt.Sprintf("Failed to get latest version of SriovNetwork: %v", getErr))
   182  					}
   183  					found.Spec = new.Spec
   184  					found.Annotations = new.Annotations
   185  					updateErr := k8sClient.Update(ctx, found)
   186  					if getErr != nil {
   187  						io.WriteString(GinkgoWriter, fmt.Sprintf("Failed to update latest version of SriovNetwork: %v", getErr))
   188  					}
   189  					return updateErr
   190  				})
   191  				if retryErr != nil {
   192  					Fail(fmt.Sprintf("Update failed: %v", retryErr))
   193  				}
   194  
   195  				ns := testNamespace
   196  				if new.Spec.NetworkNamespace != "" {
   197  					ns = new.Spec.NetworkNamespace
   198  				}
   199  
   200  				time.Sleep(time.Second * 2)
   201  				netAttDef := &netattdefv1.NetworkAttachmentDefinition{}
   202  				err = util.WaitForNamespacedObject(netAttDef, k8sClient, ns, old.GetName(), util.RetryInterval, util.Timeout)
   203  				Expect(err).NotTo(HaveOccurred())
   204  				anno := netAttDef.GetAnnotations()
   205  
   206  				Expect(anno["k8s.v1.cni.cncf.io/resourceName"]).To(Equal("openshift.io/" + new.Spec.ResourceName))
   207  				Expect(strings.TrimSpace(netAttDef.Spec.Config)).To(Equal(expect))
   208  			},
   209  			Entry("with vlan and proto flag and ipam updated", sriovnets["test-4"], newsriovnets["new-0"]),
   210  			Entry("with networkNamespace flag", sriovnets["test-4"], newsriovnets["new-1"]),
   211  			Entry("with SpoofChk flag on", sriovnets["test-4"], newsriovnets["new-2"]),
   212  			Entry("with Trust flag on", sriovnets["test-4"], newsriovnets["new-3"]),
   213  		)
   214  
   215  		Context("When a derived net-att-def CR is removed", func() {
   216  			It("should regenerate the net-att-def CR", func() {
   217  				cr := sriovnetworkv1.SriovNetwork{
   218  					TypeMeta: metav1.TypeMeta{
   219  						Kind:       "SriovNetwork",
   220  						APIVersion: "sriovnetwork.openshift.io/v1",
   221  					},
   222  					ObjectMeta: metav1.ObjectMeta{
   223  						Name:      "test-5",
   224  						Namespace: testNamespace,
   225  					},
   226  					Spec: sriovnetworkv1.SriovNetworkSpec{
   227  						NetworkNamespace: "default",
   228  						ResourceName:     "resource_1",
   229  						IPAM:             `{"type":"dhcp"}`,
   230  						Vlan:             200,
   231  					},
   232  				}
   233  				var err error
   234  				expect := generateExpectedNetConfig(&cr)
   235  
   236  				err = k8sClient.Create(ctx, &cr)
   237  				Expect(err).NotTo(HaveOccurred())
   238  				ns := testNamespace
   239  				if cr.Spec.NetworkNamespace != "" {
   240  					ns = cr.Spec.NetworkNamespace
   241  				}
   242  				netAttDef := &netattdefv1.NetworkAttachmentDefinition{}
   243  				err = util.WaitForNamespacedObject(netAttDef, k8sClient, ns, cr.GetName(), util.RetryInterval, util.Timeout)
   244  				Expect(err).NotTo(HaveOccurred())
   245  
   246  				err = k8sClient.Delete(ctx, netAttDef)
   247  				Expect(err).NotTo(HaveOccurred())
   248  				time.Sleep(3 * time.Second)
   249  				err = util.WaitForNamespacedObject(netAttDef, k8sClient, ns, cr.GetName(), util.RetryInterval, util.Timeout)
   250  				Expect(err).NotTo(HaveOccurred())
   251  				anno := netAttDef.GetAnnotations()
   252  				Expect(anno["k8s.v1.cni.cncf.io/resourceName"]).To(Equal("openshift.io/" + cr.Spec.ResourceName))
   253  				Expect(strings.TrimSpace(netAttDef.Spec.Config)).To(Equal(expect))
   254  
   255  				found := &sriovnetworkv1.SriovNetwork{}
   256  				err = k8sClient.Get(ctx, types.NamespacedName{Namespace: cr.GetNamespace(), Name: cr.GetName()}, found)
   257  				Expect(err).NotTo(HaveOccurred())
   258  				err = k8sClient.Delete(ctx, found, []dynclient.DeleteOption{}...)
   259  				Expect(err).NotTo(HaveOccurred())
   260  			})
   261  		})
   262  
   263  		Context("When the target NetworkNamespace doesn't exists", func() {
   264  			It("should create the NetAttachDef when the namespace is created", func() {
   265  				cr := sriovnetworkv1.SriovNetwork{
   266  					ObjectMeta: metav1.ObjectMeta{
   267  						Name:      "test-missing-namespace",
   268  						Namespace: testNamespace,
   269  					},
   270  					Spec: sriovnetworkv1.SriovNetworkSpec{
   271  						NetworkNamespace: "ns-xxx",
   272  						ResourceName:     "resource_missing_namespace",
   273  						IPAM:             `{"type":"dhcp"}`,
   274  						Vlan:             200,
   275  					},
   276  				}
   277  				var err error
   278  				expect := generateExpectedNetConfig(&cr)
   279  
   280  				err = k8sClient.Create(ctx, &cr)
   281  				Expect(err).NotTo(HaveOccurred())
   282  
   283  				DeferCleanup(func() {
   284  					err = k8sClient.Delete(ctx, &cr)
   285  					Expect(err).NotTo(HaveOccurred())
   286  				})
   287  
   288  				// Sleep 3 seconds to be sure the Reconcile loop has been invoked. This can be improved by exposing some information (e.g. the error)
   289  				// in the SriovNetwork.Status field.
   290  				time.Sleep(3 * time.Second)
   291  
   292  				netAttDef := &netattdefv1.NetworkAttachmentDefinition{}
   293  				err = k8sClient.Get(ctx, types.NamespacedName{Name: cr.GetName(), Namespace: "ns-xxx"}, netAttDef)
   294  				Expect(err).To(HaveOccurred())
   295  
   296  				// Create Namespace
   297  				nsObj := &corev1.Namespace{
   298  					ObjectMeta: metav1.ObjectMeta{Name: "ns-xxx"},
   299  				}
   300  				err = k8sClient.Create(ctx, nsObj)
   301  				Expect(err).NotTo(HaveOccurred())
   302  				DeferCleanup(func() {
   303  					err = k8sClient.Delete(ctx, nsObj)
   304  					Expect(err).NotTo(HaveOccurred())
   305  				})
   306  
   307  				// Check that net-attach-def has been created
   308  				err = util.WaitForNamespacedObject(netAttDef, k8sClient, "ns-xxx", cr.GetName(), util.RetryInterval, util.Timeout)
   309  				Expect(err).NotTo(HaveOccurred())
   310  
   311  				anno := netAttDef.GetAnnotations()
   312  				Expect(anno["k8s.v1.cni.cncf.io/resourceName"]).To(Equal("openshift.io/" + cr.Spec.ResourceName))
   313  				Expect(strings.TrimSpace(netAttDef.Spec.Config)).To(Equal(expect))
   314  			})
   315  		})
   316  
   317  	})
   318  })
   319  
   320  func generateExpectedNetConfig(cr *sriovnetworkv1.SriovNetwork) string {
   321  	spoofchk := ""
   322  	trust := ""
   323  	vlanProto := ""
   324  	logLevel := `"logLevel":"info",`
   325  	logFile := ""
   326  	ipam := emptyCurls
   327  
   328  	if cr.Spec.Trust == sriovnetworkv1.SriovCniStateOn {
   329  		trust = `"trust":"on",`
   330  	} else if cr.Spec.Trust == sriovnetworkv1.SriovCniStateOff {
   331  		trust = `"trust":"off",`
   332  	}
   333  
   334  	if cr.Spec.SpoofChk == sriovnetworkv1.SriovCniStateOn {
   335  		spoofchk = `"spoofchk":"on",`
   336  	} else if cr.Spec.SpoofChk == sriovnetworkv1.SriovCniStateOff {
   337  		spoofchk = `"spoofchk":"off",`
   338  	}
   339  
   340  	state := getLinkState(cr.Spec.LinkState)
   341  
   342  	if cr.Spec.IPAM != "" {
   343  		ipam = cr.Spec.IPAM
   344  	}
   345  	vlanQoS := cr.Spec.VlanQoS
   346  
   347  	if cr.Spec.VlanProto != "" {
   348  		vlanProto = fmt.Sprintf(`"vlanProto": "%s",`, cr.Spec.VlanProto)
   349  	}
   350  	if cr.Spec.LogLevel != "" {
   351  		logLevel = fmt.Sprintf(`"logLevel":"%s",`, cr.Spec.LogLevel)
   352  	}
   353  	if cr.Spec.LogFile != "" {
   354  		logFile = fmt.Sprintf(`"logFile":"%s",`, cr.Spec.LogFile)
   355  	}
   356  
   357  	configStr, err := formatJSON(fmt.Sprintf(
   358  		`{ "cniVersion":"0.3.1", "name":"%s","type":"sriov","vlan":%d,%s%s"vlanQoS":%d,%s%s%s%s"ipam":%s }`,
   359  		cr.GetName(), cr.Spec.Vlan, spoofchk, trust, vlanQoS, vlanProto, state, logLevel, logFile, ipam))
   360  	if err != nil {
   361  		panic(err)
   362  	}
   363  	return configStr
   364  }