You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by vi...@apache.org on 2025/12/15 07:54:02 UTC
(cloudstack-kubernetes-provider) 01/01: add some unit tests
This is an automated email from the ASF dual-hosted git repository.
vishesh pushed a commit to branch add-unit-tests
in repository https://gitbox.apache.org/repos/asf/cloudstack-kubernetes-provider.git
commit 0874e77d50eedf2248c9409a546088376447ceae
Author: vishesh92 <vi...@gmail.com>
AuthorDate: Mon Dec 15 13:05:47 2025 +0530
add some unit tests
---
cloudstack_instances_test.go | 75 +
cloudstack_loadbalancer_test.go | 2924 +++++++++++++++++++++++++++++++++++++++
cloudstack_test.go | 40 +
3 files changed, 3039 insertions(+)
diff --git a/cloudstack_instances_test.go b/cloudstack_instances_test.go
index 4210e305..791d5617 100644
--- a/cloudstack_instances_test.go
+++ b/cloudstack_instances_test.go
@@ -174,3 +174,78 @@ func TestNodeAddresses(t *testing.T) {
})
}
}
+
+func TestGetProviderIDFromInstanceID(t *testing.T) {
+ cs := &CSCloud{}
+
+ tests := []struct {
+ name string
+ instanceID string
+ want string
+ }{
+ {
+ name: "valid instance ID",
+ instanceID: "vm-123",
+ want: "external-cloudstack://vm-123",
+ },
+ {
+ name: "empty instance ID",
+ instanceID: "",
+ want: "external-cloudstack://",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := cs.getProviderIDFromInstanceID(tt.instanceID)
+ if got != tt.want {
+ t.Errorf("getProviderIDFromInstanceID(%q) = %q, want %q", tt.instanceID, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestGetInstanceIDFromProviderID(t *testing.T) {
+ cs := &CSCloud{}
+
+ tests := []struct {
+ name string
+ providerID string
+ want string
+ }{
+ {
+ name: "full provider ID format",
+ providerID: "external-cloudstack://vm-123",
+ want: "vm-123",
+ },
+ {
+ name: "instance ID only - backward compatibility",
+ providerID: "vm-123",
+ want: "vm-123",
+ },
+ {
+ name: "empty string",
+ providerID: "",
+ want: "",
+ },
+ {
+ name: "invalid format - no separator",
+ providerID: "external-cloudstack-vm-123",
+ want: "external-cloudstack-vm-123",
+ },
+ {
+ name: "different provider prefix",
+ providerID: "aws://i-1234567890abcdef0",
+ want: "i-1234567890abcdef0",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := cs.getInstanceIDFromProviderID(tt.providerID)
+ if got != tt.want {
+ t.Errorf("getInstanceIDFromProviderID(%q) = %q, want %q", tt.providerID, got, tt.want)
+ }
+ })
+ }
+}
diff --git a/cloudstack_loadbalancer_test.go b/cloudstack_loadbalancer_test.go
index 847361a0..b863fc17 100644
--- a/cloudstack_loadbalancer_test.go
+++ b/cloudstack_loadbalancer_test.go
@@ -20,6 +20,7 @@
package cloudstack
import (
+ "fmt"
"reflect"
"sort"
"strings"
@@ -764,3 +765,2926 @@ func TestCheckLoadBalancerRule(t *testing.T) {
}
})
}
+
+func TestRuleToString(t *testing.T) {
+ tests := []struct {
+ name string
+ rule *cloudstack.FirewallRule
+ want string
+ }{
+ {
+ name: "TCP rule",
+ rule: &cloudstack.FirewallRule{
+ Protocol: "tcp",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.1",
+ Startport: 80,
+ Endport: 80,
+ },
+ want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}",
+ },
+ {
+ name: "UDP rule",
+ rule: &cloudstack.FirewallRule{
+ Protocol: "udp",
+ Cidrlist: "192.168.0.0/16",
+ Ipaddress: "203.0.113.2",
+ Startport: 53,
+ Endport: 53,
+ },
+ want: "{[192.168.0.0/16] -> 203.0.113.2:[53-53] (udp)}",
+ },
+ {
+ name: "TCP rule with port range",
+ rule: &cloudstack.FirewallRule{
+ Protocol: "tcp",
+ Cidrlist: "0.0.0.0/0",
+ Ipaddress: "203.0.113.3",
+ Startport: 8000,
+ Endport: 8999,
+ },
+ want: "{[0.0.0.0/0] -> 203.0.113.3:[8000-8999] (tcp)}",
+ },
+ {
+ name: "ICMP rule",
+ rule: &cloudstack.FirewallRule{
+ Protocol: "icmp",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.4",
+ Icmptype: 8,
+ Icmpcode: 0,
+ },
+ want: "{[10.0.0.0/8] -> 203.0.113.4 [8,0] (icmp)}",
+ },
+ {
+ name: "unknown protocol",
+ rule: &cloudstack.FirewallRule{
+ Protocol: "gre",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.6",
+ },
+ want: "{[10.0.0.0/8] -> 203.0.113.6 (gre)}",
+ },
+ {
+ name: "nil rule",
+ rule: nil,
+ want: "nil",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := ruleToString(tt.rule)
+ if got != tt.want {
+ t.Errorf("ruleToString() = %q, want %q", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestRulesToString(t *testing.T) {
+ tests := []struct {
+ name string
+ rules []*cloudstack.FirewallRule
+ want string
+ }{
+ {
+ name: "empty list",
+ rules: []*cloudstack.FirewallRule{},
+ want: "",
+ },
+ {
+ name: "single rule",
+ rules: []*cloudstack.FirewallRule{
+ {
+ Protocol: "tcp",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.1",
+ Startport: 80,
+ Endport: 80,
+ },
+ },
+ want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}",
+ },
+ {
+ name: "multiple rules",
+ rules: []*cloudstack.FirewallRule{
+ {
+ Protocol: "tcp",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.1",
+ Startport: 80,
+ Endport: 80,
+ },
+ {
+ Protocol: "udp",
+ Cidrlist: "192.168.0.0/16",
+ Ipaddress: "203.0.113.2",
+ Startport: 53,
+ Endport: 53,
+ },
+ {
+ Protocol: "icmp",
+ Cidrlist: "0.0.0.0/0",
+ Ipaddress: "203.0.113.3",
+ Icmptype: 8,
+ Icmpcode: 0,
+ },
+ },
+ want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}, {[192.168.0.0/16] -> 203.0.113.2:[53-53] (udp)}, {[0.0.0.0/0] -> 203.0.113.3 [8,0] (icmp)}",
+ },
+ {
+ name: "rules with nil rule",
+ rules: []*cloudstack.FirewallRule{
+ {
+ Protocol: "tcp",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.1",
+ Startport: 80,
+ Endport: 80,
+ },
+ nil,
+ {
+ Protocol: "udp",
+ Cidrlist: "192.168.0.0/16",
+ Ipaddress: "203.0.113.2",
+ Startport: 53,
+ Endport: 53,
+ },
+ },
+ want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}, nil, {[192.168.0.0/16] -> 203.0.113.2:[53-53] (udp)}",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := rulesToString(tt.rules)
+ if got != tt.want {
+ t.Errorf("rulesToString() = %q, want %q", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestRulesMapToString(t *testing.T) {
+ tests := []struct {
+ name string
+ rules map[*cloudstack.FirewallRule]bool
+ want string
+ }{
+ {
+ name: "empty map",
+ rules: map[*cloudstack.FirewallRule]bool{},
+ want: "",
+ },
+ {
+ name: "single rule",
+ rules: map[*cloudstack.FirewallRule]bool{
+ {
+ Protocol: "tcp",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.1",
+ Startport: 80,
+ Endport: 80,
+ }: true,
+ },
+ want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}",
+ },
+ {
+ name: "multiple rules",
+ rules: map[*cloudstack.FirewallRule]bool{
+ {
+ Protocol: "tcp",
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.1",
+ Startport: 80,
+ Endport: 80,
+ }: true,
+ {
+ Protocol: "udp",
+ Cidrlist: "192.168.0.0/16",
+ Ipaddress: "203.0.113.2",
+ Startport: 53,
+ Endport: 53,
+ }: false,
+ {
+ Protocol: "icmp",
+ Cidrlist: "0.0.0.0/0",
+ Ipaddress: "203.0.113.3",
+ Icmptype: 8,
+ Icmpcode: 0,
+ }: true,
+ },
+ // Note: Map iteration order is non-deterministic, so we need to check
+ // that all rules are present, not the exact order
+ want: "", // We'll check this differently
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := rulesMapToString(tt.rules)
+
+ if tt.want == "" {
+ // For maps, we can't predict order, so check that all rules are present
+ if len(tt.rules) == 0 {
+ if got != "" {
+ t.Errorf("rulesMapToString() = %q, want empty string", got)
+ }
+ return
+ }
+
+ // Check that all rules are present in the output
+ expectedRules := make([]string, 0, len(tt.rules))
+ for rule := range tt.rules {
+ expectedRules = append(expectedRules, ruleToString(rule))
+ }
+
+ // Split the output and check each rule is present
+ if got != "" {
+ parts := strings.Split(got, ", ")
+ if len(parts) != len(expectedRules) {
+ t.Errorf("rulesMapToString() returned %d rules, want %d", len(parts), len(expectedRules))
+ return
+ }
+
+ // Check that all expected rules are in the output
+ for _, expectedRule := range expectedRules {
+ found := false
+ for _, part := range parts {
+ if part == expectedRule {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("rulesMapToString() missing rule %q in output %q", expectedRule, got)
+ }
+ }
+ } else if len(expectedRules) > 0 {
+ t.Errorf("rulesMapToString() = empty string, want rules to be present")
+ }
+ } else {
+ if got != tt.want {
+ t.Errorf("rulesMapToString() = %q, want %q", got, tt.want)
+ }
+ }
+ })
+ }
+}
+
+func TestGetPublicIPAddress(t *testing.T) {
+ t.Run("IP found and allocated", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ resp := &cloudstack.ListPublicIpAddressesResponse{
+ Count: 1,
+ PublicIpAddresses: []*cloudstack.PublicIpAddress{
+ {
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ Allocated: "2023-01-01T00:00:00+0000",
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ }
+
+ err := lb.getPublicIPAddress("203.0.113.1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lb.ipAddr != "203.0.113.1" {
+ t.Errorf("ipAddr = %q, want %q", lb.ipAddr, "203.0.113.1")
+ }
+ if lb.ipAddrID != "ip-123" {
+ t.Errorf("ipAddrID = %q, want %q", lb.ipAddrID, "ip-123")
+ }
+ })
+
+ t.Run("IP found but not allocated - associates", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ resp := &cloudstack.ListPublicIpAddressesResponse{
+ Count: 1,
+ PublicIpAddresses: []*cloudstack.PublicIpAddress{
+ {
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ Allocated: "",
+ },
+ },
+ }
+
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ associateResp := &cloudstack.AssociateIpAddressResponse{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ err := lb.getPublicIPAddress("203.0.113.1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("IP not found", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ resp := &cloudstack.ListPublicIpAddressesResponse{
+ Count: 0,
+ PublicIpAddresses: []*cloudstack.PublicIpAddress{},
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ }
+
+ err := lb.getPublicIPAddress("203.0.113.1")
+ if err == nil {
+ t.Fatalf("expected error for IP not found")
+ }
+ if !strings.Contains(err.Error(), "could not find IP address") {
+ t.Errorf("error message = %q, want to contain 'could not find IP address'", err.Error())
+ }
+ })
+
+ t.Run("multiple IPs found", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ resp := &cloudstack.ListPublicIpAddressesResponse{
+ Count: 2,
+ PublicIpAddresses: []*cloudstack.PublicIpAddress{
+ {Id: "ip-1", Ipaddress: "203.0.113.1"},
+ {Id: "ip-2", Ipaddress: "203.0.113.1"},
+ },
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ }
+
+ err := lb.getPublicIPAddress("203.0.113.1")
+ if err == nil {
+ t.Fatalf("expected error for multiple IPs found")
+ }
+ if !strings.Contains(err.Error(), "Found 2 addresses") {
+ t.Errorf("error message = %q, want to contain 'Found 2 addresses'", err.Error())
+ }
+ })
+
+ t.Run("error retrieving IP", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ apiErr := fmt.Errorf("API error")
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ }
+
+ err := lb.getPublicIPAddress("203.0.113.1")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error retrieving IP address") {
+ t.Errorf("error message = %q, want to contain 'error retrieving IP address'", err.Error())
+ }
+ })
+
+ t.Run("project ID handling", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ resp := &cloudstack.ListPublicIpAddressesResponse{
+ Count: 1,
+ PublicIpAddresses: []*cloudstack.PublicIpAddress{
+ {
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ Allocated: "2023-01-01T00:00:00+0000",
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ projectID: "proj-123",
+ }
+
+ err := lb.getPublicIPAddress("203.0.113.1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+}
+
+func TestAssociatePublicIPAddress(t *testing.T) {
+ t.Run("associate IP for regular network", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ associateResp := &cloudstack.AssociateIpAddressResponse{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ }
+
+ err := lb.associatePublicIPAddress()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lb.ipAddr != "203.0.113.1" {
+ t.Errorf("ipAddr = %q, want %q", lb.ipAddr, "203.0.113.1")
+ }
+ if lb.ipAddrID != "ip-123" {
+ t.Errorf("ipAddrID = %q, want %q", lb.ipAddrID, "ip-123")
+ }
+ if !lb.ipAssociatedByController {
+ t.Errorf("ipAssociatedByController = false, want true")
+ }
+ })
+
+ t.Run("associate IP for VPC network", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "vpc-456",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ associateResp := &cloudstack.AssociateIpAddressResponse{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ }
+
+ err := lb.associatePublicIPAddress()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("associate specific IP address", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ associateResp := &cloudstack.AssociateIpAddressResponse{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.2",
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ ipAddr: "203.0.113.2",
+ }
+
+ err := lb.associatePublicIPAddress()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("error retrieving network", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ apiErr := fmt.Errorf("network API error")
+
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(nil, 1, apiErr)
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ }
+
+ err := lb.associatePublicIPAddress()
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error retrieving network") {
+ t.Errorf("error message = %q, want to contain 'error retrieving network'", err.Error())
+ }
+ })
+
+ t.Run("network not found", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(nil, 0, fmt.Errorf("not found"))
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ }
+
+ err := lb.associatePublicIPAddress()
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "could not find network") {
+ t.Errorf("error message = %q, want to contain 'could not find network'", err.Error())
+ }
+ })
+
+ t.Run("error associating IP", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ apiErr := fmt.Errorf("associate API error")
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ }
+
+ err := lb.associatePublicIPAddress()
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error associating new IP address") {
+ t.Errorf("error message = %q, want to contain 'error associating new IP address'", err.Error())
+ }
+ })
+
+ t.Run("project ID handling", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ associateResp := &cloudstack.AssociateIpAddressResponse{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ projectID: "proj-123",
+ }
+
+ err := lb.associatePublicIPAddress()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+}
+
+func TestReleaseLoadBalancerIP(t *testing.T) {
+ t.Run("successful release", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ disassociateParams := &cloudstack.DisassociateIpAddressParams{}
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewDisassociateIpAddressParams("ip-123").Return(disassociateParams),
+ mockAddress.EXPECT().DisassociateIpAddress(disassociateParams).Return(&cloudstack.DisassociateIpAddressResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ ipAddrID: "ip-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ err := lb.releaseLoadBalancerIP()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("error releasing IP", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ disassociateParams := &cloudstack.DisassociateIpAddressParams{}
+ apiErr := fmt.Errorf("disassociate API error")
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewDisassociateIpAddressParams("ip-123").Return(disassociateParams),
+ mockAddress.EXPECT().DisassociateIpAddress(disassociateParams).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ ipAddrID: "ip-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ err := lb.releaseLoadBalancerIP()
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error releasing load balancer IP") {
+ t.Errorf("error message = %q, want to contain 'error releasing load balancer IP'", err.Error())
+ }
+ })
+}
+
+func TestGetLoadBalancerIP(t *testing.T) {
+ t.Run("IP specified - retrieve existing", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ resp := &cloudstack.ListPublicIpAddressesResponse{
+ Count: 1,
+ PublicIpAddresses: []*cloudstack.PublicIpAddress{
+ {
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ Allocated: "2023-01-01T00:00:00+0000",
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ }
+
+ err := lb.getLoadBalancerIP("203.0.113.1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lb.ipAddr != "203.0.113.1" {
+ t.Errorf("ipAddr = %q, want %q", lb.ipAddr, "203.0.113.1")
+ }
+ })
+
+ t.Run("IP specified - associate unallocated", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ listParams := &cloudstack.ListPublicIpAddressesParams{}
+ resp := &cloudstack.ListPublicIpAddressesResponse{
+ Count: 1,
+ PublicIpAddresses: []*cloudstack.PublicIpAddress{
+ {
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ Allocated: "",
+ },
+ },
+ }
+
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ associateResp := &cloudstack.AssociateIpAddressResponse{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+ mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ err := lb.getLoadBalancerIP("203.0.113.1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("no IP specified - associate new", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Vpcid: "",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ associateParams := &cloudstack.AssociateIpAddressParams{}
+ associateResp := &cloudstack.AssociateIpAddressResponse{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123", gomock.Any()).Return(networkResp, 1, nil),
+ mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+ mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ networkID: "net-123",
+ }
+
+ err := lb.getLoadBalancerIP("")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lb.ipAddr != "203.0.113.1" {
+ t.Errorf("ipAddr = %q, want %q", lb.ipAddr, "203.0.113.1")
+ }
+ })
+}
+
+func TestCreateLoadBalancerRule(t *testing.T) {
+ t.Run("create rule with default CIDR", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+ createResp := &cloudstack.CreateLoadBalancerRuleResponse{
+ Id: "rule-123",
+ Algorithm: "roundrobin",
+ Cidrlist: defaultAllowedCIDR,
+ Name: "test-rule-tcp-80",
+ Networkid: "net-123",
+ Privateport: "30000",
+ Publicport: "80",
+ Publicip: "203.0.113.1",
+ Publicipid: "ip-123",
+ Protocol: "tcp",
+ }
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", "test-rule-tcp-80", 30000, 80).Return(createParams),
+ mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ networkID: "net-123",
+ ipAddrID: "ip-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ port := corev1.ServicePort{
+ Port: 80,
+ NodePort: 30000,
+ Protocol: corev1.ProtocolTCP,
+ }
+ service := &corev1.Service{}
+
+ rule, err := lb.createLoadBalancerRule("test-rule-tcp-80", port, LoadBalancerProtocolTCP, service)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if rule.Id != "rule-123" {
+ t.Errorf("rule.Id = %q, want %q", rule.Id, "rule-123")
+ }
+ if rule.Name != "test-rule-tcp-80" {
+ t.Errorf("rule.Name = %q, want %q", rule.Name, "test-rule-tcp-80")
+ }
+ })
+
+ t.Run("create rule with custom CIDR list", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+ createResp := &cloudstack.CreateLoadBalancerRuleResponse{
+ Id: "rule-123",
+ Algorithm: "roundrobin",
+ Cidrlist: "10.0.0.0/8,192.168.0.0/16",
+ Name: "test-rule-tcp-80",
+ Networkid: "net-123",
+ Privateport: "30000",
+ Publicport: "80",
+ Publicip: "203.0.113.1",
+ Publicipid: "ip-123",
+ Protocol: "tcp",
+ }
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", "test-rule-tcp-80", 30000, 80).Return(createParams),
+ mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ networkID: "net-123",
+ ipAddrID: "ip-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ port := corev1.ServicePort{
+ Port: 80,
+ NodePort: 30000,
+ Protocol: corev1.ProtocolTCP,
+ }
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8,192.168.0.0/16",
+ },
+ },
+ }
+
+ rule, err := lb.createLoadBalancerRule("test-rule-tcp-80", port, LoadBalancerProtocolTCP, service)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if rule.Cidrlist != "10.0.0.0/8,192.168.0.0/16" {
+ t.Errorf("rule.Cidrlist = %q, want %q", rule.Cidrlist, "10.0.0.0/8,192.168.0.0/16")
+ }
+ })
+
+ t.Run("create rule with proxy protocol", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+ createResp := &cloudstack.CreateLoadBalancerRuleResponse{
+ Id: "rule-123",
+ Algorithm: "roundrobin",
+ Cidrlist: defaultAllowedCIDR,
+ Name: "test-rule-tcp-proxy-80",
+ Networkid: "net-123",
+ Privateport: "30000",
+ Publicport: "80",
+ Publicip: "203.0.113.1",
+ Publicipid: "ip-123",
+ Protocol: "tcp-proxy",
+ }
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", "test-rule-tcp-proxy-80", 30000, 80).Return(createParams),
+ mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ networkID: "net-123",
+ ipAddrID: "ip-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ port := corev1.ServicePort{
+ Port: 80,
+ NodePort: 30000,
+ Protocol: corev1.ProtocolTCP,
+ }
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ ServiceAnnotationLoadBalancerProxyProtocol: "true",
+ },
+ },
+ }
+
+ rule, err := lb.createLoadBalancerRule("test-rule-tcp-proxy-80", port, LoadBalancerProtocolTCPProxy, service)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if rule.Protocol != "tcp-proxy" {
+ t.Errorf("rule.Protocol = %q, want %q", rule.Protocol, "tcp-proxy")
+ }
+ })
+
+ t.Run("error creating rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+ apiErr := fmt.Errorf("create rule API error")
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", "test-rule-tcp-80", 30000, 80).Return(createParams),
+ mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ networkID: "net-123",
+ ipAddrID: "ip-123",
+ ipAddr: "203.0.113.1",
+ }
+
+ port := corev1.ServicePort{
+ Port: 80,
+ NodePort: 30000,
+ Protocol: corev1.ProtocolTCP,
+ }
+ service := &corev1.Service{}
+
+ _, err := lb.createLoadBalancerRule("test-rule-tcp-80", port, LoadBalancerProtocolTCP, service)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error creating load balancer rule") {
+ t.Errorf("error message = %q, want to contain 'error creating load balancer rule'", err.Error())
+ }
+ })
+
+ t.Run("invalid CIDR in annotation", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", "test-rule-tcp-80", 30000, 80).Return(createParams),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ }
+
+ port := corev1.ServicePort{
+ Port: 80,
+ NodePort: 30000,
+ Protocol: corev1.ProtocolTCP,
+ }
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ ServiceAnnotationLoadBalancerSourceCidrs: "invalid-cidr",
+ },
+ },
+ }
+
+ _, err := lb.createLoadBalancerRule("test-rule-tcp-80", port, LoadBalancerProtocolTCP, service)
+ if err == nil {
+ t.Fatalf("expected error for invalid CIDR")
+ }
+ if !strings.Contains(err.Error(), "invalid CIDR") {
+ t.Errorf("error message = %q, want to contain 'invalid CIDR'", err.Error())
+ }
+ })
+}
+
+func TestUpdateLoadBalancerRule(t *testing.T) {
+ t.Run("update algorithm", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+ mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(&cloudstack.UpdateLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "source",
+ rules: map[string]*cloudstack.LoadBalancerRule{
+ "test-rule-tcp-80": {
+ Id: "rule-123",
+ Algorithm: "roundrobin",
+ Protocol: "tcp",
+ },
+ },
+ }
+
+ service := &corev1.Service{}
+
+ err := lb.updateLoadBalancerRule("test-rule-tcp-80", LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("update protocol", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+ mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(&cloudstack.UpdateLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ rules: map[string]*cloudstack.LoadBalancerRule{
+ "test-rule-tcp-80": {
+ Id: "rule-123",
+ Algorithm: "roundrobin",
+ Protocol: "tcp",
+ },
+ },
+ }
+
+ service := &corev1.Service{}
+
+ err := lb.updateLoadBalancerRule("test-rule-tcp-80", LoadBalancerProtocolTCPProxy, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("update CIDR list (CS >= 4.22)", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+ mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(&cloudstack.UpdateLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ rules: map[string]*cloudstack.LoadBalancerRule{
+ "test-rule-tcp-80": {
+ Id: "rule-123",
+ Algorithm: "roundrobin",
+ Protocol: "tcp",
+ Cidrlist: defaultAllowedCIDR,
+ },
+ },
+ }
+
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8",
+ },
+ },
+ }
+
+ err := lb.updateLoadBalancerRule("test-rule-tcp-80", LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("error updating rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+ apiErr := fmt.Errorf("update rule API error")
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+ mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ algorithm: "roundrobin",
+ rules: map[string]*cloudstack.LoadBalancerRule{
+ "test-rule-tcp-80": {
+ Id: "rule-123",
+ Algorithm: "roundrobin",
+ Protocol: "tcp",
+ },
+ },
+ }
+
+ service := &corev1.Service{}
+
+ err := lb.updateLoadBalancerRule("test-rule-tcp-80", LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if err != apiErr {
+ t.Errorf("error = %v, want %v", err, apiErr)
+ }
+ })
+}
+
+func TestDeleteLoadBalancerRule(t *testing.T) {
+ t.Run("successful deletion", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-123").Return(deleteParams),
+ mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ rules: map[string]*cloudstack.LoadBalancerRule{
+ "test-rule": {
+ Id: "rule-123",
+ Name: "test-rule",
+ },
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.deleteLoadBalancerRule(rule)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if _, exists := lb.rules["test-rule"]; exists {
+ t.Errorf("expected rule to be removed from map")
+ }
+ })
+
+ t.Run("error deleting rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{}
+ apiErr := fmt.Errorf("delete rule API error")
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-123").Return(deleteParams),
+ mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ rules: map[string]*cloudstack.LoadBalancerRule{
+ "test-rule": {
+ Id: "rule-123",
+ Name: "test-rule",
+ },
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.deleteLoadBalancerRule(rule)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error deleting load balancer rule") {
+ t.Errorf("error message = %q, want to contain 'error deleting load balancer rule'", err.Error())
+ }
+ })
+}
+
+func TestAssignHostsToRule(t *testing.T) {
+ t.Run("successful assignment", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ assignParams := &cloudstack.AssignToLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewAssignToLoadBalancerRuleParams("rule-123").Return(assignParams),
+ mockLB.EXPECT().AssignToLoadBalancerRule(gomock.Any()).Return(&cloudstack.AssignToLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.assignHostsToRule(rule, []string{"vm-1", "vm-2"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("error assigning hosts", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ assignParams := &cloudstack.AssignToLoadBalancerRuleParams{}
+ apiErr := fmt.Errorf("assign API error")
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewAssignToLoadBalancerRuleParams("rule-123").Return(assignParams),
+ mockLB.EXPECT().AssignToLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.assignHostsToRule(rule, []string{"vm-1"})
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error assigning hosts") {
+ t.Errorf("error message = %q, want to contain 'error assigning hosts'", err.Error())
+ }
+ })
+
+ t.Run("empty host list", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ assignParams := &cloudstack.AssignToLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewAssignToLoadBalancerRuleParams("rule-123").Return(assignParams),
+ mockLB.EXPECT().AssignToLoadBalancerRule(gomock.Any()).Return(&cloudstack.AssignToLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.assignHostsToRule(rule, []string{})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+}
+
+func TestRemoveHostsFromRule(t *testing.T) {
+ t.Run("successful removal", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ removeParams := &cloudstack.RemoveFromLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewRemoveFromLoadBalancerRuleParams("rule-123").Return(removeParams),
+ mockLB.EXPECT().RemoveFromLoadBalancerRule(gomock.Any()).Return(&cloudstack.RemoveFromLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.removeHostsFromRule(rule, []string{"vm-1", "vm-2"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+
+ t.Run("error removing hosts", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ removeParams := &cloudstack.RemoveFromLoadBalancerRuleParams{}
+ apiErr := fmt.Errorf("remove API error")
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewRemoveFromLoadBalancerRuleParams("rule-123").Return(removeParams),
+ mockLB.EXPECT().RemoveFromLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.removeHostsFromRule(rule, []string{"vm-1"})
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error removing hosts") {
+ t.Errorf("error message = %q, want to contain 'error removing hosts'", err.Error())
+ }
+ })
+
+ t.Run("empty host list", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ removeParams := &cloudstack.RemoveFromLoadBalancerRuleParams{}
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewRemoveFromLoadBalancerRuleParams("rule-123").Return(removeParams),
+ mockLB.EXPECT().RemoveFromLoadBalancerRule(gomock.Any()).Return(&cloudstack.RemoveFromLoadBalancerRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ rule := &cloudstack.LoadBalancerRule{
+ Id: "rule-123",
+ Name: "test-rule",
+ }
+
+ err := lb.removeHostsFromRule(rule, []string{})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
+}
+
+func TestUpdateFirewallRule(t *testing.T) {
+ t.Run("create new firewall rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 0,
+ FirewallRules: []*cloudstack.FirewallRule{},
+ }
+
+ createParams := &cloudstack.CreateFirewallRuleParams{}
+ createResp := &cloudstack.CreateFirewallRuleResponse{
+ Id: "fw-123",
+ }
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", "tcp").Return(createParams),
+ mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ ipAddr: "203.0.113.1",
+ }
+
+ updated, err := lb.updateFirewallRule("ip-123", 80, LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+
+ t.Run("rule already exists - no change", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 1,
+ FirewallRules: []*cloudstack.FirewallRule{
+ {
+ Id: "fw-123",
+ Protocol: "tcp",
+ Startport: 80,
+ Endport: 80,
+ Cidrlist: "10.0.0.0/8",
+ Ipaddress: "203.0.113.1",
+ Ipaddressid: "ip-123",
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ ipAddr: "203.0.113.1",
+ }
+
+ updated, err := lb.updateFirewallRule("ip-123", 80, LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+
+ t.Run("update existing rule - CIDR change", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 1,
+ FirewallRules: []*cloudstack.FirewallRule{
+ {
+ Id: "fw-123",
+ Protocol: "tcp",
+ Startport: 80,
+ Endport: 80,
+ Cidrlist: "192.168.0.0/16",
+ Ipaddress: "203.0.113.1",
+ Ipaddressid: "ip-123",
+ },
+ },
+ }
+
+ deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+ createParams := &cloudstack.CreateFirewallRuleParams{}
+ createResp := &cloudstack.CreateFirewallRuleResponse{
+ Id: "fw-124",
+ }
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+ mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(&cloudstack.DeleteFirewallRuleResponse{}, nil),
+ mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", "tcp").Return(createParams),
+ mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ ipAddr: "203.0.113.1",
+ }
+
+ updated, err := lb.updateFirewallRule("ip-123", 80, LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+
+ t.Run("default CIDR when empty list", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 0,
+ FirewallRules: []*cloudstack.FirewallRule{},
+ }
+
+ createParams := &cloudstack.CreateFirewallRuleParams{}
+ createResp := &cloudstack.CreateFirewallRuleResponse{
+ Id: "fw-123",
+ }
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", "tcp").Return(createParams),
+ mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ ipAddr: "203.0.113.1",
+ }
+
+ updated, err := lb.updateFirewallRule("ip-123", 80, LoadBalancerProtocolTCP, []string{})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+
+ t.Run("error listing rules", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ apiErr := fmt.Errorf("list API error")
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ ipAddr: "203.0.113.1",
+ }
+
+ _, err := lb.updateFirewallRule("ip-123", 80, LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error fetching firewall rules") {
+ t.Errorf("error message = %q, want to contain 'error fetching firewall rules'", err.Error())
+ }
+ })
+
+ t.Run("error creating rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 0,
+ FirewallRules: []*cloudstack.FirewallRule{},
+ }
+
+ createParams := &cloudstack.CreateFirewallRuleParams{}
+ apiErr := fmt.Errorf("create API error")
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", "tcp").Return(createParams),
+ mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ ipAddr: "203.0.113.1",
+ }
+
+ _, err := lb.updateFirewallRule("ip-123", 80, LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error creating new firewall rule") {
+ t.Errorf("error message = %q, want to contain 'error creating new firewall rule'", err.Error())
+ }
+ })
+
+ t.Run("error deleting rule - continues", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 1,
+ FirewallRules: []*cloudstack.FirewallRule{
+ {
+ Id: "fw-123",
+ Protocol: "tcp",
+ Startport: 80,
+ Endport: 80,
+ Cidrlist: "192.168.0.0/16",
+ Ipaddress: "203.0.113.1",
+ Ipaddressid: "ip-123",
+ },
+ },
+ }
+
+ deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+ deleteErr := fmt.Errorf("delete API error")
+ createParams := &cloudstack.CreateFirewallRuleParams{}
+ createResp := &cloudstack.CreateFirewallRuleResponse{
+ Id: "fw-124",
+ }
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+ mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(nil, deleteErr),
+ mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", "tcp").Return(createParams),
+ mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ ipAddr: "203.0.113.1",
+ }
+
+ updated, err := lb.updateFirewallRule("ip-123", 80, LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+ // Should still return true even if delete failed
+ if err != nil && !strings.Contains(err.Error(), "error creating") {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+}
+
+func TestDeleteFirewallRule(t *testing.T) {
+ t.Run("delete matching rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 1,
+ FirewallRules: []*cloudstack.FirewallRule{
+ {
+ Id: "fw-123",
+ Protocol: "tcp",
+ Startport: 80,
+ Endport: 80,
+ Ipaddressid: "ip-123",
+ },
+ },
+ }
+
+ deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+ mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(&cloudstack.DeleteFirewallRuleResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ }
+
+ deleted, err := lb.deleteFirewallRule("ip-123", 80, LoadBalancerProtocolTCP)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !deleted {
+ t.Errorf("deleted = false, want true")
+ }
+ })
+
+ t.Run("no matching rules", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 0,
+ FirewallRules: []*cloudstack.FirewallRule{},
+ }
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ }
+
+ deleted, err := lb.deleteFirewallRule("ip-123", 80, LoadBalancerProtocolTCP)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if deleted {
+ t.Errorf("deleted = true, want false")
+ }
+ })
+
+ t.Run("error listing rules", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ apiErr := fmt.Errorf("list API error")
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ }
+
+ _, err := lb.deleteFirewallRule("ip-123", 80, LoadBalancerProtocolTCP)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error fetching firewall rules") {
+ t.Errorf("error message = %q, want to contain 'error fetching firewall rules'", err.Error())
+ }
+ })
+
+ t.Run("error deleting rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+ listParams := &cloudstack.ListFirewallRulesParams{}
+ listResp := &cloudstack.ListFirewallRulesResponse{
+ Count: 1,
+ FirewallRules: []*cloudstack.FirewallRule{
+ {
+ Id: "fw-123",
+ Protocol: "tcp",
+ Startport: 80,
+ Endport: 80,
+ Ipaddressid: "ip-123",
+ },
+ },
+ }
+
+ deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+ deleteErr := fmt.Errorf("delete API error")
+
+ gomock.InOrder(
+ mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+ mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+ mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+ mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(nil, deleteErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Firewall: mockFirewall,
+ },
+ }
+
+ deleted, err := lb.deleteFirewallRule("ip-123", 80, LoadBalancerProtocolTCP)
+ // Should return false if deletion failed
+ if deleted {
+ t.Errorf("deleted = true, want false")
+ }
+ if err != deleteErr {
+ t.Errorf("error = %v, want %v", err, deleteErr)
+ }
+ })
+}
+
+func TestUpdateNetworkACL(t *testing.T) {
+ t.Run("create new ACL rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Aclid: "acl-456",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ aclListResp := &cloudstack.NetworkACLList{
+ Id: "acl-456",
+ Name: "custom-acl",
+ }
+
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ listResp := &cloudstack.ListNetworkACLsResponse{
+ Count: 0,
+ NetworkACLs: []*cloudstack.NetworkACL{},
+ }
+
+ createParams := &cloudstack.CreateNetworkACLParams{}
+ createResp := &cloudstack.CreateNetworkACLResponse{
+ Id: "acl-rule-123",
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+ mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, nil),
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+ mockNetworkACL.EXPECT().NewCreateNetworkACLParams("tcp").Return(createParams),
+ mockNetworkACL.EXPECT().CreateNetworkACL(gomock.Any()).Return(createResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ updated, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, "net-123")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+
+ t.Run("rule already exists", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Aclid: "acl-456",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ aclListResp := &cloudstack.NetworkACLList{
+ Id: "acl-456",
+ Name: "custom-acl",
+ }
+
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ listResp := &cloudstack.ListNetworkACLsResponse{
+ Count: 1,
+ NetworkACLs: []*cloudstack.NetworkACL{
+ {
+ Id: "acl-rule-123",
+ Protocol: "tcp",
+ Startport: "80",
+ Endport: "80",
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+ mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, nil),
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ updated, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, "net-123")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+
+ t.Run("default ACL - skip", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Aclid: "acl-456",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ aclListResp := &cloudstack.NetworkACLList{
+ Id: "acl-456",
+ Name: "default_allow",
+ }
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+ mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ updated, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, "net-123")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !updated {
+ t.Errorf("updated = false, want true")
+ }
+ })
+
+ t.Run("error fetching network", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ apiErr := fmt.Errorf("network API error")
+
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(nil, 1, apiErr)
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ },
+ }
+
+ _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, "net-123")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error fetching Network") {
+ t.Errorf("error message = %q, want to contain 'error fetching Network'", err.Error())
+ }
+ })
+
+ t.Run("error fetching ACL list", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Aclid: "acl-456",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ apiErr := fmt.Errorf("ACL list API error")
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+ mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(nil, 0, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, "net-123")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error fetching Network ACL List") {
+ t.Errorf("error message = %q, want to contain 'error fetching Network ACL List'", err.Error())
+ }
+ })
+
+ t.Run("network not found", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Aclid: "acl-456",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ aclListResp := &cloudstack.NetworkACLList{
+ Id: "acl-456",
+ Name: "custom-acl",
+ }
+
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ apiErr := fmt.Errorf("list ACL API error")
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+ mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, nil),
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, "net-123")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error fetching Network ACL") {
+ t.Errorf("error message = %q, want to contain 'error fetching Network ACL'", err.Error())
+ }
+ })
+
+ t.Run("error creating ACL rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Aclid: "acl-456",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ aclListResp := &cloudstack.NetworkACLList{
+ Id: "acl-456",
+ Name: "custom-acl",
+ }
+
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ listResp := &cloudstack.ListNetworkACLsResponse{
+ Count: 0,
+ NetworkACLs: []*cloudstack.NetworkACL{},
+ }
+
+ createParams := &cloudstack.CreateNetworkACLParams{}
+ apiErr := fmt.Errorf("create ACL API error")
+
+ gomock.InOrder(
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+ mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, nil),
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+ mockNetworkACL.EXPECT().NewCreateNetworkACLParams("tcp").Return(createParams),
+ mockNetworkACL.EXPECT().CreateNetworkACL(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ Network: mockNetwork,
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, "net-123")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error creating Network ACL") {
+ t.Errorf("error message = %q, want to contain 'error creating Network ACL'", err.Error())
+ }
+ })
+}
+
+func TestDeleteNetworkACLRule(t *testing.T) {
+ t.Run("delete matching rule", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ listResp := &cloudstack.ListNetworkACLsResponse{
+ Count: 1,
+ NetworkACLs: []*cloudstack.NetworkACL{
+ {
+ Id: "acl-rule-123",
+ Protocol: "tcp",
+ Startport: "80",
+ Endport: "80",
+ },
+ },
+ }
+
+ deleteParams := &cloudstack.DeleteNetworkACLParams{}
+
+ gomock.InOrder(
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+ mockNetworkACL.EXPECT().NewDeleteNetworkACLParams("acl-rule-123").Return(deleteParams),
+ mockNetworkACL.EXPECT().DeleteNetworkACL(deleteParams).Return(&cloudstack.DeleteNetworkACLResponse{}, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ deleted, err := lb.deleteNetworkACLRule(80, LoadBalancerProtocolTCP, "net-123")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !deleted {
+ t.Errorf("deleted = false, want true")
+ }
+ })
+
+ t.Run("no matching rules", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ listResp := &cloudstack.ListNetworkACLsResponse{
+ Count: 0,
+ NetworkACLs: []*cloudstack.NetworkACL{},
+ }
+
+ gomock.InOrder(
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ deleted, err := lb.deleteNetworkACLRule(80, LoadBalancerProtocolTCP, "net-123")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !deleted {
+ t.Errorf("deleted = false, want true")
+ }
+ })
+
+ t.Run("error listing ACLs", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ apiErr := fmt.Errorf("list ACL API error")
+
+ gomock.InOrder(
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(nil, apiErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ _, err := lb.deleteNetworkACLRule(80, LoadBalancerProtocolTCP, "net-123")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error fetching Network ACL rules") {
+ t.Errorf("error message = %q, want to contain 'error fetching Network ACL rules'", err.Error())
+ }
+ })
+
+ t.Run("error deleting ACL", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+ listParams := &cloudstack.ListNetworkACLsParams{}
+ listResp := &cloudstack.ListNetworkACLsResponse{
+ Count: 1,
+ NetworkACLs: []*cloudstack.NetworkACL{
+ {
+ Id: "acl-rule-123",
+ Protocol: "tcp",
+ Startport: "80",
+ Endport: "80",
+ },
+ },
+ }
+
+ deleteParams := &cloudstack.DeleteNetworkACLParams{}
+ deleteErr := fmt.Errorf("delete ACL API error")
+
+ gomock.InOrder(
+ mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+ mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+ mockNetworkACL.EXPECT().NewDeleteNetworkACLParams("acl-rule-123").Return(deleteParams),
+ mockNetworkACL.EXPECT().DeleteNetworkACL(deleteParams).Return(nil, deleteErr),
+ )
+
+ lb := &loadBalancer{
+ CloudStackClient: &cloudstack.CloudStackClient{
+ NetworkACL: mockNetworkACL,
+ },
+ }
+
+ deleted, err := lb.deleteNetworkACLRule(80, LoadBalancerProtocolTCP, "net-123")
+ if deleted {
+ t.Errorf("deleted = true, want false")
+ }
+ if err != deleteErr {
+ t.Errorf("error = %v, want %v", err, deleteErr)
+ }
+ })
+}
+
+func TestGetLoadBalancer(t *testing.T) {
+ t.Run("load balancer with existing rules", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ listParams := &cloudstack.ListLoadBalancerRulesParams{}
+ listResp := &cloudstack.ListLoadBalancerRulesResponse{
+ Count: 2,
+ LoadBalancerRules: []*cloudstack.LoadBalancerRule{
+ {
+ Id: "rule-1",
+ Name: "test-service-tcp-80",
+ Publicip: "203.0.113.1",
+ Publicipid: "ip-123",
+ Algorithm: "roundrobin",
+ Protocol: "tcp",
+ Publicport: "80",
+ Privateport: "30000",
+ },
+ {
+ Id: "rule-2",
+ Name: "test-service-tcp-443",
+ Publicip: "203.0.113.1",
+ Publicipid: "ip-123",
+ Algorithm: "roundrobin",
+ Protocol: "tcp",
+ Publicport: "443",
+ Privateport: "30443",
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewListLoadBalancerRulesParams().Return(listParams),
+ mockLB.EXPECT().ListLoadBalancerRules(gomock.Any()).Return(listResp, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-service",
+ Namespace: "default",
+ },
+ }
+
+ lb, err := cs.getLoadBalancer(service)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lb.ipAddr != "203.0.113.1" {
+ t.Errorf("ipAddr = %q, want %q", lb.ipAddr, "203.0.113.1")
+ }
+ if lb.ipAddrID != "ip-123" {
+ t.Errorf("ipAddrID = %q, want %q", lb.ipAddrID, "ip-123")
+ }
+ if len(lb.rules) != 2 {
+ t.Errorf("rules count = %d, want %d", len(lb.rules), 2)
+ }
+ })
+
+ t.Run("load balancer with no rules", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ listParams := &cloudstack.ListLoadBalancerRulesParams{}
+ listResp := &cloudstack.ListLoadBalancerRulesResponse{
+ Count: 0,
+ LoadBalancerRules: []*cloudstack.LoadBalancerRule{},
+ }
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewListLoadBalancerRulesParams().Return(listParams),
+ mockLB.EXPECT().ListLoadBalancerRules(gomock.Any()).Return(listResp, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-service",
+ Namespace: "default",
+ },
+ }
+
+ lb, err := cs.getLoadBalancer(service)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(lb.rules) != 0 {
+ t.Errorf("rules count = %d, want %d", len(lb.rules), 0)
+ }
+ if lb.ipAddr != "" {
+ t.Errorf("ipAddr = %q, want empty", lb.ipAddr)
+ }
+ })
+
+ t.Run("error retrieving rules", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+ listParams := &cloudstack.ListLoadBalancerRulesParams{}
+ apiErr := fmt.Errorf("list rules API error")
+
+ gomock.InOrder(
+ mockLB.EXPECT().NewListLoadBalancerRulesParams().Return(listParams),
+ mockLB.EXPECT().ListLoadBalancerRules(gomock.Any()).Return(nil, apiErr),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ LoadBalancer: mockLB,
+ },
+ }
+
+ service := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-service",
+ Namespace: "default",
+ },
+ }
+
+ _, err := cs.getLoadBalancer(service)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "error retrieving load balancer rules") {
+ t.Errorf("error message = %q, want to contain 'error retrieving load balancer rules'", err.Error())
+ }
+ })
+}
+
+func TestGetNetworkIDFromIPAddress(t *testing.T) {
+ t.Run("successful retrieval", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+ ipResp := &cloudstack.PublicIpAddress{
+ Id: "ip-123",
+ Ipaddress: "203.0.113.1",
+ Networkid: "net-123",
+ Associatednetworkid: "net-123",
+ }
+
+ networkResp := &cloudstack.Network{
+ Id: "net-123",
+ Service: []cloudstack.NetworkServiceInternal{},
+ }
+
+ gomock.InOrder(
+ mockAddress.EXPECT().GetPublicIpAddressByID("ip-123").Return(ipResp, 1, nil),
+ mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ Network: mockNetwork,
+ },
+ }
+
+ networkID, err := cs.getNetworkIDFromIPAddress("ip-123")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if networkID != "net-123" {
+ t.Errorf("networkID = %q, want %q", networkID, "net-123")
+ }
+ })
+
+ t.Run("IP not found", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+ apiErr := fmt.Errorf("IP not found")
+
+ mockAddress.EXPECT().GetPublicIpAddressByID("ip-123").Return(nil, 0, apiErr)
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ Address: mockAddress,
+ },
+ }
+
+ _, err := cs.getNetworkIDFromIPAddress("ip-123")
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if err != apiErr {
+ t.Errorf("error = %v, want %v", err, apiErr)
+ }
+ })
+}
+
+func TestVerifyHosts(t *testing.T) {
+ t.Run("all hosts in same network", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+ listParams := &cloudstack.ListVirtualMachinesParams{}
+ listResp := &cloudstack.ListVirtualMachinesResponse{
+ Count: 2,
+ VirtualMachines: []*cloudstack.VirtualMachine{
+ {
+ Id: "vm-1",
+ Name: "node-1",
+ Nic: []cloudstack.Nic{
+ {Networkid: "net-123"},
+ },
+ },
+ {
+ Id: "vm-2",
+ Name: "node-2",
+ Nic: []cloudstack.Nic{
+ {Networkid: "net-123"},
+ },
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+ mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ VirtualMachine: mockVM,
+ },
+ }
+
+ nodes := []*corev1.Node{
+ {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+ {ObjectMeta: metav1.ObjectMeta{Name: "node-2"}},
+ }
+
+ hostIDs, networkID, err := cs.verifyHosts(nodes)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(hostIDs) != 2 {
+ t.Errorf("hostIDs count = %d, want %d", len(hostIDs), 2)
+ }
+ if networkID != "net-123" {
+ t.Errorf("networkID = %q, want %q", networkID, "net-123")
+ }
+ })
+
+ t.Run("hosts in different networks", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+ listParams := &cloudstack.ListVirtualMachinesParams{}
+ listResp := &cloudstack.ListVirtualMachinesResponse{
+ Count: 2,
+ VirtualMachines: []*cloudstack.VirtualMachine{
+ {
+ Id: "vm-1",
+ Name: "node-1",
+ Nic: []cloudstack.Nic{
+ {Networkid: "net-123"},
+ },
+ },
+ {
+ Id: "vm-2",
+ Name: "node-2",
+ Nic: []cloudstack.Nic{
+ {Networkid: "net-456"},
+ },
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+ mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ VirtualMachine: mockVM,
+ },
+ }
+
+ nodes := []*corev1.Node{
+ {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+ {ObjectMeta: metav1.ObjectMeta{Name: "node-2"}},
+ }
+
+ _, _, err := cs.verifyHosts(nodes)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "different networks") {
+ t.Errorf("error message = %q, want to contain 'different networks'", err.Error())
+ }
+ })
+
+ t.Run("no matching hosts", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+ listParams := &cloudstack.ListVirtualMachinesParams{}
+ listResp := &cloudstack.ListVirtualMachinesResponse{
+ Count: 0,
+ VirtualMachines: []*cloudstack.VirtualMachine{},
+ }
+
+ gomock.InOrder(
+ mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+ mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ VirtualMachine: mockVM,
+ },
+ }
+
+ nodes := []*corev1.Node{
+ {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+ }
+
+ _, _, err := cs.verifyHosts(nodes)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ if !strings.Contains(err.Error(), "none of the hosts matched") {
+ t.Errorf("error message = %q, want to contain 'none of the hosts matched'", err.Error())
+ }
+ })
+
+ t.Run("FQDN node names", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+ listParams := &cloudstack.ListVirtualMachinesParams{}
+ listResp := &cloudstack.ListVirtualMachinesResponse{
+ Count: 1,
+ VirtualMachines: []*cloudstack.VirtualMachine{
+ {
+ Id: "vm-1",
+ Name: "node-1",
+ Nic: []cloudstack.Nic{
+ {Networkid: "net-123"},
+ },
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+ mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ VirtualMachine: mockVM,
+ },
+ }
+
+ nodes := []*corev1.Node{
+ {ObjectMeta: metav1.ObjectMeta{Name: "node-1.example.com"}},
+ }
+
+ hostIDs, networkID, err := cs.verifyHosts(nodes)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(hostIDs) != 1 {
+ t.Errorf("hostIDs count = %d, want %d", len(hostIDs), 1)
+ }
+ if networkID != "net-123" {
+ t.Errorf("networkID = %q, want %q", networkID, "net-123")
+ }
+ })
+
+ t.Run("case-insensitive matching", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ t.Cleanup(ctrl.Finish)
+
+ mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+ listParams := &cloudstack.ListVirtualMachinesParams{}
+ listResp := &cloudstack.ListVirtualMachinesResponse{
+ Count: 1,
+ VirtualMachines: []*cloudstack.VirtualMachine{
+ {
+ Id: "vm-1",
+ Name: "NODE-1",
+ Nic: []cloudstack.Nic{
+ {Networkid: "net-123"},
+ },
+ },
+ },
+ }
+
+ gomock.InOrder(
+ mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+ mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+ )
+
+ cs := &CSCloud{
+ client: &cloudstack.CloudStackClient{
+ VirtualMachine: mockVM,
+ },
+ }
+
+ nodes := []*corev1.Node{
+ {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+ }
+
+ hostIDs, networkID, err := cs.verifyHosts(nodes)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(hostIDs) != 1 {
+ t.Errorf("hostIDs count = %d, want %d", len(hostIDs), 1)
+ }
+ if networkID != "net-123" {
+ t.Errorf("networkID = %q, want %q", networkID, "net-123")
+ }
+ })
+}
diff --git a/cloudstack_test.go b/cloudstack_test.go
index a83b45b8..87ed02fd 100644
--- a/cloudstack_test.go
+++ b/cloudstack_test.go
@@ -275,3 +275,43 @@ func TestGetManagementServerVersion(t *testing.T) {
}
})
}
+
+func TestGetRegionFromZone(t *testing.T) {
+ tests := []struct {
+ name string
+ region string
+ zone string
+ want string
+ }{
+ {
+ name: "region configured in cloud config",
+ region: "us-east-1",
+ zone: "zone-1",
+ want: "us-east-1",
+ },
+ {
+ name: "region not configured, returns zone",
+ region: "",
+ zone: "zone-1",
+ want: "zone-1",
+ },
+ {
+ name: "region configured with empty zone",
+ region: "eu-central-1",
+ zone: "",
+ want: "eu-central-1",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cs := &CSCloud{
+ region: tt.region,
+ }
+ got := cs.getRegionFromZone(tt.zone)
+ if got != tt.want {
+ t.Errorf("getRegionFromZone(%q) with region=%q = %q, want %q", tt.zone, tt.region, got, tt.want)
+ }
+ })
+ }
+}