blob: 4bbf38e7705ad722d2b8bdc2dff6873694315d19 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package cloudstack
import (
"fmt"
"reflect"
"sort"
"strings"
"testing"
"github.com/apache/cloudstack-go/v2/cloudstack"
"github.com/blang/semver/v4"
"go.uber.org/mock/gomock"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestCompareStringSlice(t *testing.T) {
tests := []struct {
name string
x []string
y []string
want bool
}{
{
name: "equal slices same order",
x: []string{"a", "b", "c"},
y: []string{"a", "b", "c"},
want: true,
},
{
name: "equal slices different order",
x: []string{"a", "b", "c"},
y: []string{"c", "a", "b"},
want: true,
},
{
name: "different lengths",
x: []string{"a", "b"},
y: []string{"a", "b", "c"},
want: false,
},
{
name: "same length different elements",
x: []string{"a", "b", "c"},
y: []string{"a", "b", "d"},
want: false,
},
{
name: "both empty",
x: []string{},
y: []string{},
want: true,
},
{
name: "both nil",
x: nil,
y: nil,
want: true,
},
{
name: "one nil one empty",
x: nil,
y: []string{},
want: true,
},
{
name: "one empty one non-empty",
x: []string{},
y: []string{"a"},
want: false,
},
{
name: "duplicate elements equal",
x: []string{"a", "a", "b"},
y: []string{"a", "b", "a"},
want: true,
},
{
name: "duplicate elements not equal - different counts",
x: []string{"a", "a", "b"},
y: []string{"a", "b", "b"},
want: false,
},
{
name: "single element equal",
x: []string{"a"},
y: []string{"a"},
want: true,
},
{
name: "single element not equal",
x: []string{"a"},
y: []string{"b"},
want: false,
},
{
name: "CIDR list comparison - typical use case",
x: []string{"10.0.0.0/8", "192.168.0.0/16"},
y: []string{"192.168.0.0/16", "10.0.0.0/8"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := compareStringSlice(tt.x, tt.y); got != tt.want {
t.Errorf("compareStringSlice(%v, %v) = %v, want %v", tt.x, tt.y, got, tt.want)
}
})
}
}
func TestSymmetricDifference(t *testing.T) {
tests := []struct {
name string
hostIDs []string
lbInstances []*cloudstack.VirtualMachine
wantAssign []string
wantRemove []string
}{
{
name: "no hosts no instances",
hostIDs: []string{},
lbInstances: []*cloudstack.VirtualMachine{},
wantAssign: nil,
wantRemove: nil,
},
{
name: "all new hosts",
hostIDs: []string{"host1", "host2", "host3"},
lbInstances: []*cloudstack.VirtualMachine{},
wantAssign: []string{"host1", "host2", "host3"},
wantRemove: nil,
},
{
name: "all hosts to remove",
hostIDs: []string{},
lbInstances: []*cloudstack.VirtualMachine{
{Id: "host1"},
{Id: "host2"},
},
wantAssign: nil,
wantRemove: []string{"host1", "host2"},
},
{
name: "exact match - nothing to do",
hostIDs: []string{"host1", "host2"},
lbInstances: []*cloudstack.VirtualMachine{
{Id: "host1"},
{Id: "host2"},
},
wantAssign: nil,
wantRemove: nil,
},
{
name: "partial overlap - some to add some to remove",
hostIDs: []string{"host1", "host3"},
lbInstances: []*cloudstack.VirtualMachine{
{Id: "host1"},
{Id: "host2"},
},
wantAssign: []string{"host3"},
wantRemove: []string{"host2"},
},
{
name: "add one host",
hostIDs: []string{"host1", "host2", "host3"},
lbInstances: []*cloudstack.VirtualMachine{
{Id: "host1"},
{Id: "host2"},
},
wantAssign: []string{"host3"},
wantRemove: nil,
},
{
name: "remove one host",
hostIDs: []string{"host1"},
lbInstances: []*cloudstack.VirtualMachine{
{Id: "host1"},
{Id: "host2"},
},
wantAssign: nil,
wantRemove: []string{"host2"},
},
{
name: "nil instances",
hostIDs: []string{"host1"},
lbInstances: nil,
wantAssign: []string{"host1"},
wantRemove: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotAssign, gotRemove := symmetricDifference(tt.hostIDs, tt.lbInstances)
// Sort slices for comparison since map iteration order is not guaranteed
sort.Strings(gotAssign)
sort.Strings(tt.wantAssign)
sort.Strings(gotRemove)
sort.Strings(tt.wantRemove)
if !compareStringSlice(gotAssign, tt.wantAssign) {
t.Errorf("symmetricDifference() assign = %v, want %v", gotAssign, tt.wantAssign)
}
if !compareStringSlice(gotRemove, tt.wantRemove) {
t.Errorf("symmetricDifference() remove = %v, want %v", gotRemove, tt.wantRemove)
}
})
}
}
func TestIsFirewallSupported(t *testing.T) {
tests := []struct {
name string
services []cloudstack.NetworkServiceInternal
want bool
}{
{
name: "empty services",
services: []cloudstack.NetworkServiceInternal{},
want: false,
},
{
name: "nil services",
services: nil,
want: false,
},
{
name: "firewall present",
services: []cloudstack.NetworkServiceInternal{
{Name: "Dhcp"},
{Name: "Firewall"},
{Name: "Dns"},
},
want: true,
},
{
name: "firewall not present",
services: []cloudstack.NetworkServiceInternal{
{Name: "Dhcp"},
{Name: "Dns"},
{Name: "Lb"},
},
want: false,
},
{
name: "only firewall",
services: []cloudstack.NetworkServiceInternal{
{Name: "Firewall"},
},
want: true,
},
{
name: "case sensitive - lowercase firewall",
services: []cloudstack.NetworkServiceInternal{
{Name: "firewall"},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isFirewallSupported(tt.services); got != tt.want {
t.Errorf("isFirewallSupported() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsNetworkACLSupported(t *testing.T) {
tests := []struct {
name string
services []cloudstack.NetworkServiceInternal
want bool
}{
{
name: "empty services",
services: []cloudstack.NetworkServiceInternal{},
want: false,
},
{
name: "nil services",
services: nil,
want: false,
},
{
name: "NetworkACL present",
services: []cloudstack.NetworkServiceInternal{
{Name: "Dhcp"},
{Name: "NetworkACL"},
{Name: "Dns"},
},
want: true,
},
{
name: "NetworkACL not present",
services: []cloudstack.NetworkServiceInternal{
{Name: "Dhcp"},
{Name: "Dns"},
{Name: "Firewall"},
},
want: false,
},
{
name: "only NetworkACL",
services: []cloudstack.NetworkServiceInternal{
{Name: "NetworkACL"},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isNetworkACLSupported(tt.services); got != tt.want {
t.Errorf("isNetworkACLSupported() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetStringFromServiceAnnotation(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
annotationKey string
defaultSetting string
want string
}{
{
name: "annotation present",
annotations: map[string]string{"key1": "value1"},
annotationKey: "key1",
defaultSetting: "default",
want: "value1",
},
{
name: "annotation not present - use default",
annotations: map[string]string{"other": "value"},
annotationKey: "key1",
defaultSetting: "default",
want: "default",
},
{
name: "annotation present but empty - return empty",
annotations: map[string]string{"key1": ""},
annotationKey: "key1",
defaultSetting: "default",
want: "",
},
{
name: "nil annotations - use default",
annotations: nil,
annotationKey: "key1",
defaultSetting: "default",
want: "default",
},
{
name: "empty default when not found",
annotations: map[string]string{},
annotationKey: "key1",
defaultSetting: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
Annotations: tt.annotations,
},
}
if got := getStringFromServiceAnnotation(service, tt.annotationKey, tt.defaultSetting); got != tt.want {
t.Errorf("getStringFromServiceAnnotation() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetBoolFromServiceAnnotation(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
annotationKey string
defaultSetting bool
want bool
}{
{
name: "annotation true",
annotations: map[string]string{"key1": "true"},
annotationKey: "key1",
defaultSetting: false,
want: true,
},
{
name: "annotation false",
annotations: map[string]string{"key1": "false"},
annotationKey: "key1",
defaultSetting: true,
want: false,
},
{
name: "annotation not present - use default true",
annotations: map[string]string{},
annotationKey: "key1",
defaultSetting: true,
want: true,
},
{
name: "annotation not present - use default false",
annotations: map[string]string{},
annotationKey: "key1",
defaultSetting: false,
want: false,
},
{
name: "invalid value - use default true",
annotations: map[string]string{"key1": "invalid"},
annotationKey: "key1",
defaultSetting: true,
want: true,
},
{
name: "invalid value - use default false",
annotations: map[string]string{"key1": "yes"},
annotationKey: "key1",
defaultSetting: false,
want: false,
},
{
name: "empty value - use default",
annotations: map[string]string{"key1": ""},
annotationKey: "key1",
defaultSetting: true,
want: true,
},
{
name: "nil annotations - use default",
annotations: nil,
annotationKey: "key1",
defaultSetting: true,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
Annotations: tt.annotations,
},
}
if got := getBoolFromServiceAnnotation(service, tt.annotationKey, tt.defaultSetting); got != tt.want {
t.Errorf("getBoolFromServiceAnnotation() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetCIDRList(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
want []string
wantErr bool
errContains string
expectEmpty bool
}{
{
name: "defaults to allow all when annotation missing",
annotations: nil,
want: []string{defaultAllowedCIDR},
},
{
name: "trims and splits cidrs",
annotations: map[string]string{
ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8, 192.168.0.0/16",
},
want: []string{"10.0.0.0/8", "192.168.0.0/16"},
},
{
name: "empty annotation returns empty list",
annotations: map[string]string{
ServiceAnnotationLoadBalancerSourceCidrs: "",
},
expectEmpty: true,
},
{
name: "invalid cidr returns error",
annotations: map[string]string{
ServiceAnnotationLoadBalancerSourceCidrs: "invalid-cidr",
},
wantErr: true,
errContains: "invalid CIDR",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lb := &loadBalancer{}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc",
Namespace: "default",
Annotations: tt.annotations,
},
}
got, err := lb.getCIDRList(svc)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Fatalf("error = %v, expected to contain %q", err, tt.errContains)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.expectEmpty {
if len(got) != 0 {
t.Fatalf("expected empty CIDR list, got %v", got)
}
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("getCIDRList() = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckLoadBalancerRule(t *testing.T) {
t.Run("rule not present returns nil", func(t *testing.T) {
lb := &loadBalancer{
rules: map[string]*cloudstack.LoadBalancerRule{},
}
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
service := &corev1.Service{}
rule, needsUpdate, err := lb.checkLoadBalancerRule("missing", port, LoadBalancerProtocolTCP, service, semver.Version{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rule != nil {
t.Fatalf("expected nil rule, got %v", rule)
}
if needsUpdate {
t.Fatalf("expected needsUpdate to be false")
}
})
t.Run("basic property mismatch deletes rule", 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-id").Return(deleteParams),
mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{}, nil),
)
lb := &loadBalancer{
CloudStackClient: &cloudstack.CloudStackClient{
LoadBalancer: mockLB,
},
ipAddr: "1.1.1.1",
rules: map[string]*cloudstack.LoadBalancerRule{
"rule": {
Id: "rule-id",
Name: "rule",
Publicip: "2.2.2.2",
Privateport: "30000",
Publicport: "80",
Cidrlist: defaultAllowedCIDR,
Algorithm: "roundrobin",
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
},
},
}
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
service := &corev1.Service{}
rule, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 21, Patch: 0})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rule != nil {
t.Fatalf("expected nil rule after deletion, got %v", rule)
}
if needsUpdate {
t.Fatalf("expected needsUpdate to be false")
}
if _, exists := lb.rules["rule"]; exists {
t.Fatalf("expected rule entry to be removed from map")
}
})
t.Run("cidr change triggers update on supported version", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
// No expectations on the mock; any delete call would fail the test.
mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
lbRule := &cloudstack.LoadBalancerRule{
Id: "rule-id",
Name: "rule",
Publicip: "1.1.1.1",
Privateport: "30000",
Publicport: "80",
Cidrlist: "10.0.0.0/8",
Algorithm: "roundrobin",
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
}
lb := &loadBalancer{
CloudStackClient: &cloudstack.CloudStackClient{
LoadBalancer: mockLB,
},
ipAddr: "1.1.1.1",
algorithm: "roundrobin",
rules: map[string]*cloudstack.LoadBalancerRule{
"rule": lbRule,
},
}
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, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rule != lbRule {
t.Fatalf("expected existing rule to be returned")
}
if !needsUpdate {
t.Fatalf("expected needsUpdate to be true due to CIDR change")
}
})
t.Run("cidr change triggers delete with older version", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
// No expectations on the mock; any delete or create call would fail the test.
mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{}
gomock.InOrder(
mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-id").Return(deleteParams),
mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{}, nil),
)
lbRule := &cloudstack.LoadBalancerRule{
Id: "rule-id",
Name: "rule",
Publicip: "1.1.1.1",
Privateport: "30000",
Publicport: "80",
Cidrlist: "10.0.0.0/8",
Algorithm: "roundrobin",
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
}
lb := &loadBalancer{
CloudStackClient: &cloudstack.CloudStackClient{
LoadBalancer: mockLB,
},
ipAddr: "1.1.1.1",
algorithm: "roundrobin",
rules: map[string]*cloudstack.LoadBalancerRule{
"rule": lbRule,
},
}
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, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 12, Patch: 0})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rule != nil {
t.Fatalf("expected nil rule after deletion, got %v", rule)
}
if needsUpdate {
t.Fatalf("expected needsUpdate to be false due to CIDR change with older version")
}
})
t.Run("invalid cidr returns error", func(t *testing.T) {
lb := &loadBalancer{
rules: map[string]*cloudstack.LoadBalancerRule{
"rule": {
Id: "rule-id",
Name: "rule",
Publicip: "1.1.1.1",
Privateport: "30000",
Publicport: "80",
Cidrlist: defaultAllowedCIDR,
Algorithm: "roundrobin",
Protocol: LoadBalancerProtocolTCP.CSProtocol(),
},
},
}
port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP}
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
ServiceAnnotationLoadBalancerSourceCidrs: "bad-cidr",
},
},
}
_, _, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
if err == nil {
t.Fatalf("expected error for invalid CIDR")
}
})
}
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)
}
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 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)
}
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")
}
})
}
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)
}
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("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)
}
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("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")
}
if lb.ipAddrID != "ip-123" {
t.Errorf("ipAddrID = %q, want %q", lb.ipAddrID, "ip-123")
}
if !lb.ipAssociatedByController {
t.Errorf("ipAssociatedByController = false, want true")
}
})
}
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")
}
})
}