Skip to content

Commit f1aaa2a

Browse files
committed
Add unit tests
1 parent 23ca9ed commit f1aaa2a

File tree

4 files changed

+444
-28
lines changed

4 files changed

+444
-28
lines changed

cloudstack_loadbalancer.go

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ func (lb *loadBalancer) checkLoadBalancerRule(lbRuleName string, port corev1.Ser
610610
lbRule.Privateport == strconv.Itoa(int(port.NodePort)) &&
611611
lbRule.Publicport == strconv.Itoa(int(port.Port))
612612

613-
cidrListChanged := len(cidrList) != len(lbRuleCidrList) || !setsEqual(cidrList, lbRuleCidrList)
613+
cidrListChanged := len(cidrList) != len(lbRuleCidrList) || !compareStringSlice(cidrList, lbRuleCidrList)
614614

615615
// Check if CIDR list also changed and version < 4.22, then we must recreate the rule.
616616
if !basicPropsMatch || (cidrListChanged && version.LT(semver.Version{Major: 4, Minor: 22, Patch: 0})) {
@@ -628,32 +628,6 @@ func (lb *loadBalancer) checkLoadBalancerRule(lbRuleName string, port corev1.Ser
628628
return lbRule, updateAlgo || updateProto || cidrListChanged, nil
629629
}
630630

631-
// setsEqual checks if two slices contain the exact same unique elements, regardless of order.
632-
func setsEqual(listA, listB []string) bool {
633-
createSet := func(list []string) map[string]bool {
634-
set := make(map[string]bool)
635-
for _, item := range list {
636-
set[item] = true
637-
}
638-
return set
639-
}
640-
641-
setA := createSet(listA)
642-
setB := createSet(listB)
643-
644-
if len(setA) != len(setB) {
645-
return false
646-
}
647-
648-
for item := range setA {
649-
if _, found := setB[item]; !found {
650-
return false
651-
}
652-
}
653-
654-
return true
655-
}
656-
657631
// updateLoadBalancerRule updates a load balancer rule.
658632
func (lb *loadBalancer) updateLoadBalancerRule(lbRuleName string, protocol LoadBalancerProtocol, service *corev1.Service, version semver.Version) error {
659633
lbRule := lb.rules[lbRuleName]

cloudstack_loadbalancer_test.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020
package cloudstack
2121

2222
import (
23+
"reflect"
2324
"sort"
25+
"strings"
2426
"testing"
2527

2628
"github.com/apache/cloudstack-go/v2/cloudstack"
29+
"github.com/blang/semver/v4"
30+
"go.uber.org/mock/gomock"
2731
corev1 "k8s.io/api/core/v1"
2832
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2933
)
@@ -478,3 +482,285 @@ func TestGetBoolFromServiceAnnotation(t *testing.T) {
478482
})
479483
}
480484
}
485+
486+
func TestGetCIDRList(t *testing.T) {
487+
tests := []struct {
488+
name string
489+
annotations map[string]string
490+
want []string
491+
wantErr bool
492+
errContains string
493+
expectEmpty bool
494+
}{
495+
{
496+
name: "defaults to allow all when annotation missing",
497+
annotations: nil,
498+
want: []string{defaultAllowedCIDR},
499+
},
500+
{
501+
name: "trims and splits cidrs",
502+
annotations: map[string]string{
503+
ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8, 192.168.0.0/16",
504+
},
505+
want: []string{"10.0.0.0/8", "192.168.0.0/16"},
506+
},
507+
{
508+
name: "empty annotation returns empty list",
509+
annotations: map[string]string{
510+
ServiceAnnotationLoadBalancerSourceCidrs: "",
511+
},
512+
expectEmpty: true,
513+
},
514+
{
515+
name: "invalid cidr returns error",
516+
annotations: map[string]string{
517+
ServiceAnnotationLoadBalancerSourceCidrs: "invalid-cidr",
518+
},
519+
wantErr: true,
520+
errContains: "invalid CIDR",
521+
},
522+
}
523+
524+
for _, tt := range tests {
525+
t.Run(tt.name, func(t *testing.T) {
526+
lb := &loadBalancer{}
527+
svc := &corev1.Service{
528+
ObjectMeta: metav1.ObjectMeta{
529+
Name: "svc",
530+
Namespace: "default",
531+
Annotations: tt.annotations,
532+
},
533+
}
534+
535+
got, err := lb.getCIDRList(svc)
536+
if tt.wantErr {
537+
if err == nil {
538+
t.Fatalf("expected error, got nil")
539+
}
540+
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
541+
t.Fatalf("error = %v, expected to contain %q", err, tt.errContains)
542+
}
543+
return
544+
}
545+
546+
if err != nil {
547+
t.Fatalf("unexpected error: %v", err)
548+
}
549+
550+
if tt.expectEmpty {
551+
if len(got) != 0 {
552+
t.Fatalf("expected empty CIDR list, got %v", got)
553+
}
554+
return
555+
}
556+
557+
if !reflect.DeepEqual(got, tt.want) {
558+
t.Fatalf("getCIDRList() = %v, want %v", got, tt.want)
559+
}
560+
})
561+
}
562+
}
563+
564+
func TestCheckLoadBalancerRule(t *testing.T) {
565+
t.Run("rule not present returns nil", func(t *testing.T) {
566+
lb := &loadBalancer{
567+
rules: map[string]*cloudstack.LoadBalancerRule{},
568+
}
569+
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
570+
service := &corev1.Service{}
571+
572+
rule, needsUpdate, err := lb.checkLoadBalancerRule("missing", port, LoadBalancerProtocolTCP, service, semver.Version{})
573+
if err != nil {
574+
t.Fatalf("unexpected error: %v", err)
575+
}
576+
if rule != nil {
577+
t.Fatalf("expected nil rule, got %v", rule)
578+
}
579+
if needsUpdate {
580+
t.Fatalf("expected needsUpdate to be false")
581+
}
582+
})
583+
584+
t.Run("basic property mismatch deletes rule", func(t *testing.T) {
585+
ctrl := gomock.NewController(t)
586+
t.Cleanup(ctrl.Finish)
587+
588+
mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
589+
deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{}
590+
591+
gomock.InOrder(
592+
mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-id").Return(deleteParams),
593+
mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{}, nil),
594+
)
595+
596+
lb := &loadBalancer{
597+
CloudStackClient: &cloudstack.CloudStackClient{
598+
LoadBalancer: mockLB,
599+
},
600+
ipAddr: "1.1.1.1",
601+
rules: map[string]*cloudstack.LoadBalancerRule{
602+
"rule": {
603+
Id: "rule-id",
604+
Name: "rule",
605+
Publicip: "2.2.2.2",
606+
Privateport: "30000",
607+
Publicport: "80",
608+
Cidrlist: defaultAllowedCIDR,
609+
Algorithm: "roundrobin",
610+
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
611+
},
612+
},
613+
}
614+
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
615+
service := &corev1.Service{}
616+
617+
rule, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 21, Patch: 0})
618+
if err != nil {
619+
t.Fatalf("unexpected error: %v", err)
620+
}
621+
if rule != nil {
622+
t.Fatalf("expected nil rule after deletion, got %v", rule)
623+
}
624+
if needsUpdate {
625+
t.Fatalf("expected needsUpdate to be false")
626+
}
627+
if _, exists := lb.rules["rule"]; exists {
628+
t.Fatalf("expected rule entry to be removed from map")
629+
}
630+
})
631+
632+
t.Run("cidr change triggers update on supported version", func(t *testing.T) {
633+
ctrl := gomock.NewController(t)
634+
t.Cleanup(ctrl.Finish)
635+
636+
// No expectations on the mock; any delete call would fail the test.
637+
mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
638+
639+
lbRule := &cloudstack.LoadBalancerRule{
640+
Id: "rule-id",
641+
Name: "rule",
642+
Publicip: "1.1.1.1",
643+
Privateport: "30000",
644+
Publicport: "80",
645+
Cidrlist: "10.0.0.0/8",
646+
Algorithm: "roundrobin",
647+
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
648+
}
649+
650+
lb := &loadBalancer{
651+
CloudStackClient: &cloudstack.CloudStackClient{
652+
LoadBalancer: mockLB,
653+
},
654+
ipAddr: "1.1.1.1",
655+
algorithm: "roundrobin",
656+
rules: map[string]*cloudstack.LoadBalancerRule{
657+
"rule": lbRule,
658+
},
659+
}
660+
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
661+
service := &corev1.Service{
662+
ObjectMeta: metav1.ObjectMeta{
663+
Annotations: map[string]string{
664+
ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8,192.168.0.0/16",
665+
},
666+
},
667+
}
668+
669+
rule, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
670+
if err != nil {
671+
t.Fatalf("unexpected error: %v", err)
672+
}
673+
if rule != lbRule {
674+
t.Fatalf("expected existing rule to be returned")
675+
}
676+
if !needsUpdate {
677+
t.Fatalf("expected needsUpdate to be true due to CIDR change")
678+
}
679+
})
680+
681+
t.Run("cidr change triggers delete with older version", func(t *testing.T) {
682+
ctrl := gomock.NewController(t)
683+
t.Cleanup(ctrl.Finish)
684+
685+
// No expectations on the mock; any delete or create call would fail the test.
686+
mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
687+
688+
deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{}
689+
690+
gomock.InOrder(
691+
mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-id").Return(deleteParams),
692+
mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{}, nil),
693+
)
694+
695+
lbRule := &cloudstack.LoadBalancerRule{
696+
Id: "rule-id",
697+
Name: "rule",
698+
Publicip: "1.1.1.1",
699+
Privateport: "30000",
700+
Publicport: "80",
701+
Cidrlist: "10.0.0.0/8",
702+
Algorithm: "roundrobin",
703+
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
704+
}
705+
706+
lb := &loadBalancer{
707+
CloudStackClient: &cloudstack.CloudStackClient{
708+
LoadBalancer: mockLB,
709+
},
710+
ipAddr: "1.1.1.1",
711+
algorithm: "roundrobin",
712+
rules: map[string]*cloudstack.LoadBalancerRule{
713+
"rule": lbRule,
714+
},
715+
}
716+
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
717+
service := &corev1.Service{
718+
ObjectMeta: metav1.ObjectMeta{
719+
Annotations: map[string]string{
720+
ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8,192.168.0.0/16",
721+
},
722+
},
723+
}
724+
725+
rule, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 12, Patch: 0})
726+
if err != nil {
727+
t.Fatalf("unexpected error: %v", err)
728+
}
729+
if rule != nil {
730+
t.Fatalf("expected nil rule after deletion, got %v", rule)
731+
}
732+
if needsUpdate {
733+
t.Fatalf("expected needsUpdate to be false due to CIDR change with older version")
734+
}
735+
})
736+
737+
t.Run("invalid cidr returns error", func(t *testing.T) {
738+
lb := &loadBalancer{
739+
rules: map[string]*cloudstack.LoadBalancerRule{
740+
"rule": {
741+
Id: "rule-id",
742+
Name: "rule",
743+
Publicip: "1.1.1.1",
744+
Privateport: "30000",
745+
Publicport: "80",
746+
Cidrlist: defaultAllowedCIDR,
747+
Algorithm: "roundrobin",
748+
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
749+
},
750+
},
751+
}
752+
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
753+
service := &corev1.Service{
754+
ObjectMeta: metav1.ObjectMeta{
755+
Annotations: map[string]string{
756+
ServiceAnnotationLoadBalancerSourceCidrs: "bad-cidr",
757+
},
758+
},
759+
}
760+
761+
_, _, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
762+
if err == nil {
763+
t.Fatalf("expected error for invalid CIDR")
764+
}
765+
})
766+
}

0 commit comments

Comments
 (0)