From 1e46ca0cca81268ac7ccca0278d82c834fa3bdb0 Mon Sep 17 00:00:00 2001 From: Rahul Sharma Date: Thu, 26 Dec 2024 16:47:05 -0500 Subject: [PATCH] [test] : add unittests for node_controller (#284) * add unittests for node_controller * add check for env var as well --- cloud/linode/node_controller_test.go | 187 +++++++++++++++++++++++++++ cloud/linode/service_controller.go | 2 +- 2 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 cloud/linode/node_controller_test.go diff --git a/cloud/linode/node_controller_test.go b/cloud/linode/node_controller_test.go new file mode 100644 index 00000000..6ecdf318 --- /dev/null +++ b/cloud/linode/node_controller_test.go @@ -0,0 +1,187 @@ +package linode + +import ( + "context" + "errors" + "net" + "net/http" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/linode/linode-cloud-controller-manager/cloud/annotations" + "github.com/linode/linode-cloud-controller-manager/cloud/linode/client/mocks" + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/util/workqueue" +) + +func TestNodeController_processNext(t *testing.T) { + // Mock dependencies + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocks.NewMockClient(ctrl) + kubeClient := fake.NewSimpleClientset() + queue := workqueue.NewTypedDelayingQueueWithConfig(workqueue.TypedDelayingQueueConfig[any]{Name: "testQueue"}) + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + Spec: v1.NodeSpec{}, + } + + _, err := kubeClient.CoreV1().Nodes().Create(context.TODO(), node, metav1.CreateOptions{}) + assert.NoError(t, err, "expected no error during node creation") + + controller := &nodeController{ + kubeclient: kubeClient, + instances: newInstances(client), + queue: queue, + metadataLastUpdate: make(map[string]time.Time), + ttl: defaultMetadataTTL, + } + + t.Run("should return no error on unknown errors", func(t *testing.T) { + queue.Add(node) + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, errors.New("lookup failed")) + result := controller.processNext() + assert.True(t, result, "processNext should return true") + if queue.Len() != 0 { + t.Errorf("expected queue to be empty, got %d items", queue.Len()) + } + }) + + t.Run("should return no error if node exists", func(t *testing.T) { + queue.Add(node) + publicIP := net.ParseIP("172.234.31.123") + privateIP := net.ParseIP("192.168.159.135") + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{ + {ID: 111, Label: "test", IPv4: []*net.IP{&publicIP, &privateIP}, HostUUID: "111"}, + }, nil) + result := controller.processNext() + assert.True(t, result, "processNext should return true") + if queue.Len() != 0 { + t.Errorf("expected queue to be empty, got %d items", queue.Len()) + } + }) + + t.Run("should return no error if queued object is not of type Node", func(t *testing.T) { + queue.Add("abc") + result := controller.processNext() + assert.True(t, result, "processNext should return true") + if queue.Len() != 0 { + t.Errorf("expected queue to be empty, got %d items", queue.Len()) + } + }) + + t.Run("should return no error if node in k8s doesn't exist", func(t *testing.T) { + queue.Add(node) + controller.kubeclient = fake.NewSimpleClientset() + defer func() { controller.kubeclient = kubeClient }() + result := controller.processNext() + assert.True(t, result, "processNext should return true") + if queue.Len() != 0 { + t.Errorf("expected queue to be empty, got %d items", queue.Len()) + } + }) + + t.Run("should return error and requeue when it gets 429 from linode API", func(t *testing.T) { + queue = workqueue.NewTypedDelayingQueueWithConfig(workqueue.TypedDelayingQueueConfig[any]{Name: "testQueue1"}) + queue.Add(node) + controller.queue = queue + client := mocks.NewMockClient(ctrl) + controller.instances = newInstances(client) + retryInterval = 1 * time.Nanosecond + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, &linodego.Error{Code: http.StatusTooManyRequests, Message: "Too many requests"}) + result := controller.processNext() + time.Sleep(1 * time.Second) + assert.True(t, result, "processNext should return true") + if queue.Len() == 0 { + t.Errorf("expected queue to not be empty, got it empty") + } + }) + + t.Run("should return error and requeue when it gets error >= 500 from linode API", func(t *testing.T) { + queue = workqueue.NewTypedDelayingQueueWithConfig(workqueue.TypedDelayingQueueConfig[any]{Name: "testQueue2"}) + queue.Add(node) + controller.queue = queue + client := mocks.NewMockClient(ctrl) + controller.instances = newInstances(client) + retryInterval = 1 * time.Nanosecond + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, &linodego.Error{Code: http.StatusInternalServerError, Message: "Too many requests"}) + result := controller.processNext() + time.Sleep(1 * time.Second) + assert.True(t, result, "processNext should return true") + if queue.Len() == 0 { + t.Errorf("expected queue to not be empty, got it empty") + } + }) +} + +func TestNodeController_handleNode(t *testing.T) { + // Mock dependencies + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocks.NewMockClient(ctrl) + kubeClient := fake.NewSimpleClientset() + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + Spec: v1.NodeSpec{ProviderID: "linode://123"}, + } + _, err := kubeClient.CoreV1().Nodes().Create(context.TODO(), node, metav1.CreateOptions{}) + assert.NoError(t, err, "expected no error during node creation") + + instCache := newInstances(client) + + t.Setenv("LINODE_METADATA_TTL", "30") + nodeCtrl := newNodeController(kubeClient, client, nil, instCache) + assert.Equal(t, 30*time.Second, nodeCtrl.ttl, "expected ttl to be 30 seconds") + + // Test: Successful metadata update + publicIP := net.ParseIP("172.234.31.123") + privateIP := net.ParseIP("192.168.159.135") + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{ + {ID: 123, Label: "test-node", IPv4: []*net.IP{&publicIP, &privateIP}, HostUUID: "123"}, + }, nil) + err = nodeCtrl.handleNode(context.TODO(), node) + assert.NoError(t, err, "expected no error during handleNode") + + // Check metadataLastUpdate + lastUpdate := nodeCtrl.LastMetadataUpdate("test-node") + if time.Since(lastUpdate) > 5*time.Second { + t.Errorf("metadataLastUpdate was not updated correctly") + } + + // Annotations set, no update needed as ttl not reached + node.Labels[annotations.AnnLinodeHostUUID] = "123" + node.Annotations[annotations.AnnLinodeNodePrivateIP] = privateIP.String() + err = nodeCtrl.handleNode(context.TODO(), node) + assert.NoError(t, err, "expected no error during handleNode") + + // Lookup failure for linode instance + client = mocks.NewMockClient(ctrl) + nodeCtrl.instances = newInstances(client) + nodeCtrl.metadataLastUpdate["test-node"] = time.Now().Add(-2 * nodeCtrl.ttl) + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{}, errors.New("lookup failed")) + err = nodeCtrl.handleNode(context.TODO(), node) + assert.Error(t, err, "expected error during handleNode, got nil") + + // All fields already set + client = mocks.NewMockClient(ctrl) + nodeCtrl.instances = newInstances(client) + nodeCtrl.metadataLastUpdate["test-node"] = time.Now().Add(-2 * nodeCtrl.ttl) + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{ + {ID: 123, Label: "test-node", IPv4: []*net.IP{&publicIP, &privateIP}, HostUUID: "123"}, + }, nil) + err = nodeCtrl.handleNode(context.TODO(), node) + assert.NoError(t, err, "expected no error during handleNode") +} diff --git a/cloud/linode/service_controller.go b/cloud/linode/service_controller.go index ad936f87..684cac7e 100644 --- a/cloud/linode/service_controller.go +++ b/cloud/linode/service_controller.go @@ -15,7 +15,7 @@ import ( "k8s.io/klog/v2" ) -const retryInterval = time.Minute * 1 +var retryInterval = time.Minute * 1 type serviceController struct { loadbalancers *loadbalancers