Skip to content

Commit

Permalink
add unittests for node_controller
Browse files Browse the repository at this point in the history
  • Loading branch information
rahulait committed Dec 23, 2024
1 parent b1b9af7 commit 0f96702
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 3 deletions.
2 changes: 1 addition & 1 deletion cloud/linode/node_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (s *nodeController) processNext() bool {
case *linodego.Error:
if deleteErr.Code >= http.StatusInternalServerError || deleteErr.Code == http.StatusTooManyRequests {
klog.Errorf("failed to add metadata for node (%s); retrying in 1 minute: %s", node.Name, err)
s.queue.AddAfter(node, retryInterval)
s.queue.AddAfter(node, defaultRetryInterval)
}

default:
Expand Down
164 changes: 164 additions & 0 deletions cloud/linode/node_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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{ProviderID: "linode://111"},
}

_, 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", 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 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)
defaultRetryInterval = 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)
defaultRetryInterval = 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)
nodeCtrl := newNodeController(kubeClient, client, nil, instCache)

// 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")
}
4 changes: 2 additions & 2 deletions cloud/linode/service_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"k8s.io/klog/v2"
)

const retryInterval = time.Minute * 1
var defaultRetryInterval = time.Minute * 1

type serviceController struct {
loadbalancers *loadbalancers
Expand Down Expand Up @@ -98,7 +98,7 @@ func (s *serviceController) processNextDeletion() bool {
case *linodego.Error:
if deleteErr.Code >= http.StatusInternalServerError || deleteErr.Code == http.StatusTooManyRequests {
klog.Errorf("failed to delete NodeBalancer for service (%s); retrying in 1 minute: %s", getServiceNn(service), err)
s.queue.AddAfter(service, retryInterval)
s.queue.AddAfter(service, defaultRetryInterval)

Check warning on line 101 in cloud/linode/service_controller.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/service_controller.go#L101

Added line #L101 was not covered by tests
}

default:
Expand Down

0 comments on commit 0f96702

Please sign in to comment.