@@ -13,6 +13,7 @@ import (
1313 "os"
1414 "reflect"
1515 "regexp"
16+ "slices"
1617 "strconv"
1718 "strings"
1819 "testing"
@@ -297,6 +298,10 @@ func TestCCMLoadBalancers(t *testing.T) {
297298 name : "Update Load Balancer - No Nodes" ,
298299 f : testUpdateLoadBalancerNoNodes ,
299300 },
301+ {
302+ name : "Update Load Balancer - Node excluded by annotation" ,
303+ f : testUpdateLoadBalancerNodeExcludedByAnnotation ,
304+ },
300305 {
301306 name : "Create Load Balancer - Very long Service name" ,
302307 f : testVeryLongServiceName ,
@@ -4356,6 +4361,28 @@ func testMakeLoadBalancerStatusEnvVar(t *testing.T, client *linodego.Client, _ *
43564361 os .Unsetenv ("LINODE_HOSTNAME_ONLY_INGRESS" )
43574362}
43584363
4364+ func getLatestNbNodesForService (t * testing.T , client * linodego.Client , svc * v1.Service , lb * loadbalancers ) []linodego.NodeBalancerNode {
4365+ t .Helper ()
4366+ nb , err := lb .getNodeBalancerByStatus (t .Context (), svc )
4367+ if err != nil {
4368+ t .Fatalf ("expected no error got %v" , err )
4369+ }
4370+ cfgs , errConfigs := client .ListNodeBalancerConfigs (t .Context (), nb .ID , nil )
4371+ if errConfigs != nil {
4372+ t .Fatalf ("expected no error getting configs, got %v" , errConfigs )
4373+ }
4374+ slices .SortFunc (cfgs , func (a , b linodego.NodeBalancerConfig ) int {
4375+ return a .ID - b .ID
4376+ })
4377+
4378+ // Verify nodes were created correctly (only non-excluded nodes)
4379+ nodeBalancerNodes , err := client .ListNodeBalancerNodes (t .Context (), nb .ID , cfgs [0 ].ID , nil )
4380+ if err != nil {
4381+ t .Fatalf ("expected no error got %v" , err )
4382+ }
4383+ return nodeBalancerNodes
4384+ }
4385+
43594386func testCleanupDoesntCall (t * testing.T , client * linodego.Client , fakeAPI * fakeAPI ) {
43604387 t .Helper ()
43614388
@@ -4409,6 +4436,246 @@ func testCleanupDoesntCall(t *testing.T, client *linodego.Client, fakeAPI *fakeA
44094436 })
44104437}
44114438
4439+ func testUpdateLoadBalancerNodeExcludedByAnnotation (t * testing.T , client * linodego.Client , _ * fakeAPI ) {
4440+ t .Helper ()
4441+ svc := & v1.Service {
4442+ ObjectMeta : metav1.ObjectMeta {
4443+ Name : randString (),
4444+ UID : "foobar123" ,
4445+ Annotations : map [string ]string {},
4446+ },
4447+ Spec : v1.ServiceSpec {
4448+ Ports : []v1.ServicePort {
4449+ {
4450+ Name : randString (),
4451+ Protocol : "http" ,
4452+ Port : int32 (80 ),
4453+ NodePort : int32 (8080 ),
4454+ },
4455+ },
4456+ },
4457+ }
4458+
4459+ lb , assertion := newLoadbalancers (client , "us-west" ).(* loadbalancers )
4460+ if ! assertion {
4461+ t .Error ("type assertion failed" )
4462+ }
4463+ defer func () {
4464+ _ = lb .EnsureLoadBalancerDeleted (t .Context (), "linodelb" , svc )
4465+ }()
4466+
4467+ fakeClientset := fake .NewSimpleClientset ()
4468+ lb .kubeClient = fakeClientset
4469+
4470+ nodeBalancer , err := client .CreateNodeBalancer (t .Context (), linodego.NodeBalancerCreateOptions {
4471+ Region : lb .zone ,
4472+ })
4473+ if err != nil {
4474+ t .Fatalf ("failed to create NodeBalancer: %s" , err )
4475+ }
4476+ svc .Status .LoadBalancer = * makeLoadBalancerStatus (svc , nodeBalancer )
4477+ stubService (fakeClientset , svc )
4478+ svc .SetAnnotations (map [string ]string {
4479+ annotations .AnnLinodeNodeBalancerID : strconv .Itoa (nodeBalancer .ID ),
4480+ })
4481+
4482+ // setup done, test ensure/update
4483+ nodes := []* v1.Node {
4484+ {
4485+ ObjectMeta : metav1.ObjectMeta {
4486+ Name : "node-1" ,
4487+ },
4488+ Status : v1.NodeStatus {
4489+ Addresses : []v1.NodeAddress {
4490+ {
4491+ Type : v1 .NodeInternalIP ,
4492+ Address : "127.0.0.1" ,
4493+ },
4494+ },
4495+ },
4496+ },
4497+ {
4498+ ObjectMeta : metav1.ObjectMeta {
4499+ Name : "node-2" ,
4500+ Annotations : map [string ]string {
4501+ annotations .AnnExcludeNodeFromNb : "true" ,
4502+ },
4503+ },
4504+ Status : v1.NodeStatus {
4505+ Addresses : []v1.NodeAddress {
4506+ {
4507+ Type : v1 .NodeInternalIP ,
4508+ Address : "127.0.0.2" ,
4509+ },
4510+ },
4511+ },
4512+ },
4513+ {
4514+ ObjectMeta : metav1.ObjectMeta {
4515+ Name : "node-3" ,
4516+ },
4517+ Status : v1.NodeStatus {
4518+ Addresses : []v1.NodeAddress {
4519+ {
4520+ Type : v1 .NodeInternalIP ,
4521+ Address : "127.0.0.3" ,
4522+ },
4523+ },
4524+ },
4525+ },
4526+ }
4527+
4528+ // Test initial creation - should only create nodes that aren't excluded
4529+ _ , err = lb .EnsureLoadBalancer (t .Context (), "linodelb" , svc , nodes )
4530+ if err != nil {
4531+ t .Fatalf ("expected no error got %v" , err )
4532+ }
4533+ nodeBalancerNodes := getLatestNbNodesForService (t , client , svc , lb )
4534+ // Should have only 2 nodes (node-1 and node-3), since node-2 is excluded
4535+ if len (nodeBalancerNodes ) != 2 {
4536+ t .Errorf ("expected 2 nodes, got %d" , len (nodeBalancerNodes ))
4537+ }
4538+
4539+ // Verify excluded node is not present
4540+ for _ , nbNode := range nodeBalancerNodes {
4541+ if strings .Contains (nbNode .Label , "node-2" ) {
4542+ t .Errorf ("excluded node 'node-2' should not be present in nodeBalancer nodes" )
4543+ }
4544+ }
4545+
4546+ // Test Update operation
4547+ updatedNodes := []* v1.Node {
4548+ {
4549+ ObjectMeta : metav1.ObjectMeta {
4550+ Name : "node-1" ,
4551+ Annotations : map [string ]string {
4552+ annotations .AnnExcludeNodeFromNb : "true" , // Now exclude node-1
4553+ },
4554+ },
4555+ Status : v1.NodeStatus {
4556+ Addresses : []v1.NodeAddress {
4557+ {
4558+ Type : v1 .NodeInternalIP ,
4559+ Address : "127.0.0.1" ,
4560+ },
4561+ },
4562+ },
4563+ },
4564+ {
4565+ ObjectMeta : metav1.ObjectMeta {
4566+ Name : "node-2" ,
4567+ Annotations : map [string ]string {
4568+ annotations .AnnExcludeNodeFromNb : "true" , // Still excluded
4569+ },
4570+ },
4571+ Status : v1.NodeStatus {
4572+ Addresses : []v1.NodeAddress {
4573+ {
4574+ Type : v1 .NodeInternalIP ,
4575+ Address : "127.0.0.2" ,
4576+ },
4577+ },
4578+ },
4579+ },
4580+ {
4581+ ObjectMeta : metav1.ObjectMeta {
4582+ Name : "node-3" ,
4583+ },
4584+ Status : v1.NodeStatus {
4585+ Addresses : []v1.NodeAddress {
4586+ {
4587+ Type : v1 .NodeInternalIP ,
4588+ Address : "127.0.0.3" ,
4589+ },
4590+ },
4591+ },
4592+ },
4593+ }
4594+
4595+ // Update the load balancer with updated nodes
4596+ if err = lb .UpdateLoadBalancer (t .Context (), "linodelb" , svc , updatedNodes ); err != nil {
4597+ t .Fatalf ("unexpected error updating LoadBalancer: %v" , err )
4598+ }
4599+
4600+ // Verify nodes were updated correctly
4601+ nodeBalancerNodesAfterUpdate := getLatestNbNodesForService (t , client , svc , lb )
4602+
4603+ // Should have only 1 node (node-3), since both node-1 and node-2 are excluded
4604+ if len (nodeBalancerNodesAfterUpdate ) != 1 {
4605+ t .Errorf ("expected 1 node after update, got %d" , len (nodeBalancerNodesAfterUpdate ))
4606+ }
4607+
4608+ // Verify excluded nodes are not present
4609+ for _ , nbNode := range nodeBalancerNodesAfterUpdate {
4610+ if strings .Contains (nbNode .Label , "node-1" ) || strings .Contains (nbNode .Label , "node-2" ) {
4611+ t .Errorf ("excluded nodes should not be present in nodeBalancer nodes after update" )
4612+ }
4613+ }
4614+
4615+ // Test edge case: all nodes excluded
4616+ allExcludedNodes := []* v1.Node {
4617+ {
4618+ ObjectMeta : metav1.ObjectMeta {
4619+ Name : "node-1" ,
4620+ Annotations : map [string ]string {
4621+ annotations .AnnExcludeNodeFromNb : "true" ,
4622+ },
4623+ },
4624+ Status : v1.NodeStatus {
4625+ Addresses : []v1.NodeAddress {
4626+ {
4627+ Type : v1 .NodeInternalIP ,
4628+ Address : "127.0.0.1" ,
4629+ },
4630+ },
4631+ },
4632+ },
4633+ {
4634+ ObjectMeta : metav1.ObjectMeta {
4635+ Name : "node-2" ,
4636+ Annotations : map [string ]string {
4637+ annotations .AnnExcludeNodeFromNb : "true" ,
4638+ },
4639+ },
4640+ Status : v1.NodeStatus {
4641+ Addresses : []v1.NodeAddress {
4642+ {
4643+ Type : v1 .NodeInternalIP ,
4644+ Address : "127.0.0.2" ,
4645+ },
4646+ },
4647+ },
4648+ },
4649+ {
4650+ ObjectMeta : metav1.ObjectMeta {
4651+ Name : "node-3" ,
4652+ Annotations : map [string ]string {
4653+ annotations .AnnExcludeNodeFromNb : "true" ,
4654+ },
4655+ },
4656+ Status : v1.NodeStatus {
4657+ Addresses : []v1.NodeAddress {
4658+ {
4659+ Type : v1 .NodeInternalIP ,
4660+ Address : "127.0.0.3" ,
4661+ },
4662+ },
4663+ },
4664+ },
4665+ }
4666+
4667+ if err = lb .UpdateLoadBalancer (t .Context (), "linodelb" , svc , allExcludedNodes ); err != nil {
4668+ t .Errorf ("expected no error when all nodes are excluded, got %v" , err )
4669+ }
4670+
4671+ // Verify nodes were updated correctly
4672+ nodeBalancerNodesAfterUpdate = getLatestNbNodesForService (t , client , svc , lb )
4673+ // Should have only 0 node (node-3), since all nodes are excluded
4674+ if len (nodeBalancerNodesAfterUpdate ) != 0 {
4675+ t .Errorf ("expected 0 nodes after update, got %d" , len (nodeBalancerNodesAfterUpdate ))
4676+ }
4677+ }
4678+
44124679func testUpdateLoadBalancerNoNodes (t * testing.T , client * linodego.Client , _ * fakeAPI ) {
44134680 t .Helper ()
44144681
0 commit comments