Skip to content

Commit

Permalink
Add unit tests for metallb load balancer (#649)
Browse files Browse the repository at this point in the history
  • Loading branch information
HomayoonAlimohammadi authored Sep 5, 2024
1 parent 9b9b79b commit b90ee72
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 20 deletions.
12 changes: 6 additions & 6 deletions src/k8s/pkg/k8sd/features/metallb/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import (
)

var (
// chartMetalLB represents manifests to deploy MetalLB speaker and controller.
chartMetalLB = helm.InstallableChart{
// ChartMetalLB represents manifests to deploy MetalLB speaker and controller.
ChartMetalLB = helm.InstallableChart{
Name: "metallb",
Namespace: "metallb-system",
ManifestPath: filepath.Join("charts", "metallb-0.14.5.tgz"),
}

// chartMetalLBLoadBalancer represents manifests to deploy MetalLB L2 or BGP resources.
chartMetalLBLoadBalancer = helm.InstallableChart{
// ChartMetalLBLoadBalancer represents manifests to deploy MetalLB L2 or BGP resources.
ChartMetalLBLoadBalancer = helm.InstallableChart{
Name: "metallb-loadbalancer",
Namespace: "metallb-system",
ManifestPath: filepath.Join("charts", "ck-loadbalancer"),
Expand All @@ -24,8 +24,8 @@ var (
// controllerImageRepo is the image to use for metallb-controller.
controllerImageRepo = "ghcr.io/canonical/k8s-snap/metallb/controller"

// controllerImageTag is the tag to use for metallb-controller.
controllerImageTag = "v0.14.5"
// ControllerImageTag is the tag to use for metallb-controller.
ControllerImageTag = "v0.14.5"

// speakerImageRepo is the image to use for metallb-speaker.
speakerImageRepo = "ghcr.io/canonical/k8s-snap/metallb/speaker"
Expand Down
26 changes: 13 additions & 13 deletions src/k8s/pkg/k8sd/features/metallb/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

const (
enabledMsgTmpl = "enabled, %s mode"
disabledMsg = "disabled"
DisabledMsg = "disabled"
deleteFailedMsgTmpl = "Failed to delete MetalLB, the error was: %v"
deployFailedMsgTmpl = "Failed to deploy MetalLB, the error was: %v"
)
Expand All @@ -27,42 +27,42 @@ func ApplyLoadBalancer(ctx context.Context, snap snap.Snap, loadbalancer types.L
err = fmt.Errorf("failed to disable LoadBalancer: %w", err)
return types.FeatureStatus{
Enabled: false,
Version: controllerImageTag,
Version: ControllerImageTag,
Message: fmt.Sprintf(deleteFailedMsgTmpl, err),
}, err
}
return types.FeatureStatus{
Enabled: false,
Version: controllerImageTag,
Message: disabledMsg,
Version: ControllerImageTag,
Message: DisabledMsg,
}, nil
}

if err := enableLoadBalancer(ctx, snap, loadbalancer, network); err != nil {
err = fmt.Errorf("failed to enable LoadBalancer: %w", err)
return types.FeatureStatus{
Enabled: false,
Version: controllerImageTag,
Version: ControllerImageTag,
Message: fmt.Sprintf(deployFailedMsgTmpl, err),
}, err
}

if loadbalancer.GetBGPMode() {
return types.FeatureStatus{
Enabled: true,
Version: controllerImageTag,
Version: ControllerImageTag,
Message: fmt.Sprintf(enabledMsgTmpl, "BGP"),
}, nil
} else if loadbalancer.GetL2Mode() {
return types.FeatureStatus{
Enabled: true,
Version: controllerImageTag,
Version: ControllerImageTag,
Message: fmt.Sprintf(enabledMsgTmpl, "L2"),
}, nil
} else {
return types.FeatureStatus{
Enabled: true,
Version: controllerImageTag,
Version: ControllerImageTag,
Message: fmt.Sprintf(enabledMsgTmpl, "Unknown"),
}, nil
}
Expand All @@ -71,11 +71,11 @@ func ApplyLoadBalancer(ctx context.Context, snap snap.Snap, loadbalancer types.L
func disableLoadBalancer(ctx context.Context, snap snap.Snap, network types.Network) error {
m := snap.HelmClient()

if _, err := m.Apply(ctx, chartMetalLBLoadBalancer, helm.StateDeleted, nil); err != nil {
if _, err := m.Apply(ctx, ChartMetalLBLoadBalancer, helm.StateDeleted, nil); err != nil {
return fmt.Errorf("failed to uninstall MetalLB LoadBalancer chart: %w", err)
}

if _, err := m.Apply(ctx, chartMetalLB, helm.StateDeleted, nil); err != nil {
if _, err := m.Apply(ctx, ChartMetalLB, helm.StateDeleted, nil); err != nil {
return fmt.Errorf("failed to uninstall MetalLB chart: %w", err)
}
return nil
Expand All @@ -88,7 +88,7 @@ func enableLoadBalancer(ctx context.Context, snap snap.Snap, loadbalancer types.
"controller": map[string]any{
"image": map[string]any{
"repository": controllerImageRepo,
"tag": controllerImageTag,
"tag": ControllerImageTag,
},
},
"speaker": map[string]any{
Expand All @@ -107,7 +107,7 @@ func enableLoadBalancer(ctx context.Context, snap snap.Snap, loadbalancer types.
},
},
}
if _, err := m.Apply(ctx, chartMetalLB, helm.StatePresent, metalLBValues); err != nil {
if _, err := m.Apply(ctx, ChartMetalLB, helm.StatePresent, metalLBValues); err != nil {
return fmt.Errorf("failed to apply MetalLB configuration: %w", err)
}

Expand Down Expand Up @@ -145,7 +145,7 @@ func enableLoadBalancer(ctx context.Context, snap snap.Snap, loadbalancer types.
},
}

if _, err := m.Apply(ctx, chartMetalLBLoadBalancer, helm.StatePresent, values); err != nil {
if _, err := m.Apply(ctx, ChartMetalLBLoadBalancer, helm.StatePresent, values); err != nil {
return fmt.Errorf("failed to apply MetalLB LoadBalancer configuration: %w", err)
}

Expand Down
208 changes: 208 additions & 0 deletions src/k8s/pkg/k8sd/features/metallb/loadbalancer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package metallb_test

import (
"context"
"errors"
"testing"

. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/utils/ptr"

"github.com/canonical/k8s/pkg/client/helm"
helmmock "github.com/canonical/k8s/pkg/client/helm/mock"
"github.com/canonical/k8s/pkg/client/kubernetes"
"github.com/canonical/k8s/pkg/k8sd/features/metallb"
"github.com/canonical/k8s/pkg/k8sd/types"
snapmock "github.com/canonical/k8s/pkg/snap/mock"
)

func TestDisabled(t *testing.T) {
t.Run("HelmApplyFails", func(t *testing.T) {
g := NewWithT(t)

applyErr := errors.New("failed to apply")
helmM := &helmmock.Mock{
ApplyErr: applyErr,
}
snapM := &snapmock.Snap{
Mock: snapmock.Mock{
HelmClient: helmM,
},
}
lbCfg := types.LoadBalancer{
Enabled: ptr.To(false),
}

status, err := metallb.ApplyLoadBalancer(context.Background(), snapM, lbCfg, types.Network{}, nil)

g.Expect(err).To(MatchError(applyErr))
g.Expect(status.Enabled).To(BeFalse())
g.Expect(status.Message).To(ContainSubstring(applyErr.Error()))
g.Expect(status.Version).To(Equal(metallb.ControllerImageTag))
g.Expect(helmM.ApplyCalledWith).To(HaveLen(1))

callArgs := helmM.ApplyCalledWith[0]
g.Expect(callArgs.Chart).To(Equal(metallb.ChartMetalLBLoadBalancer))
g.Expect(callArgs.State).To(Equal(helm.StateDeleted))
g.Expect(callArgs.Values).To(BeNil())
})
t.Run("Success", func(t *testing.T) {
g := NewWithT(t)

helmM := &helmmock.Mock{}
snapM := &snapmock.Snap{
Mock: snapmock.Mock{
HelmClient: helmM,
},
}
lbCfg := types.LoadBalancer{
Enabled: ptr.To(false),
}

status, err := metallb.ApplyLoadBalancer(context.Background(), snapM, lbCfg, types.Network{}, nil)

g.Expect(err).ToNot(HaveOccurred())
g.Expect(status.Enabled).To(BeFalse())
g.Expect(status.Message).To(Equal(metallb.DisabledMsg))
g.Expect(status.Version).To(Equal(metallb.ControllerImageTag))
g.Expect(helmM.ApplyCalledWith).To(HaveLen(2))

firstCallArgs := helmM.ApplyCalledWith[0]
g.Expect(firstCallArgs.Chart).To(Equal(metallb.ChartMetalLBLoadBalancer))
g.Expect(firstCallArgs.State).To(Equal(helm.StateDeleted))
g.Expect(firstCallArgs.Values).To(BeNil())

secondCallArgs := helmM.ApplyCalledWith[1]
g.Expect(secondCallArgs.Chart).To(Equal(metallb.ChartMetalLB))
g.Expect(secondCallArgs.State).To(Equal(helm.StateDeleted))
g.Expect(secondCallArgs.Values).To(BeNil())
})
}

func TestEnabled(t *testing.T) {
t.Run("HelmApplyFails", func(t *testing.T) {
g := NewWithT(t)

applyErr := errors.New("failed to apply")
helmM := &helmmock.Mock{
ApplyErr: applyErr,
}
snapM := &snapmock.Snap{
Mock: snapmock.Mock{
HelmClient: helmM,
},
}
lbCfg := types.LoadBalancer{
Enabled: ptr.To(true),
}

status, err := metallb.ApplyLoadBalancer(context.Background(), snapM, lbCfg, types.Network{}, nil)

g.Expect(err).To(MatchError(applyErr))
g.Expect(status.Enabled).To(BeFalse())
g.Expect(status.Message).To(ContainSubstring(applyErr.Error()))
g.Expect(status.Version).To(Equal(metallb.ControllerImageTag))
g.Expect(helmM.ApplyCalledWith).To(HaveLen(1))

callArgs := helmM.ApplyCalledWith[0]
g.Expect(callArgs.Chart).To(Equal(metallb.ChartMetalLB))
g.Expect(callArgs.State).To(Equal(helm.StatePresent))
// we don't validate values since it's just a static struct
// and won't be changed by configurations
g.Expect(callArgs.Values).ToNot(BeNil())
})
t.Run("Success", func(t *testing.T) {
g := NewWithT(t)

helmM := &helmmock.Mock{}
clientset := fake.NewSimpleClientset()
fd, ok := clientset.Discovery().(*fakediscovery.FakeDiscovery)
g.Expect(ok).To(BeTrue())
fd.Resources = []*metav1.APIResourceList{
{
GroupVersion: "metallb.io/v1beta1",
APIResources: []metav1.APIResource{
{Name: "ipaddresspools"},
{Name: "l2advertisements"},
{Name: "bgpadvertisements"},
},
},
{
GroupVersion: "metallb.io/v1beta2",
APIResources: []metav1.APIResource{
{Name: "bgppeers"},
},
},
}
snapM := &snapmock.Snap{
Mock: snapmock.Mock{
HelmClient: helmM,
KubernetesClient: &kubernetes.Client{
Interface: clientset,
},
},
}
lbCfg := types.LoadBalancer{
Enabled: ptr.To(true),
// setting both modes to true for testing purposes
L2Mode: ptr.To(true),
L2Interfaces: ptr.To([]string{"eth0", "eth1"}),
BGPMode: ptr.To(true),
BGPLocalASN: ptr.To(64512),
BGPPeerAddress: ptr.To("10.0.0.1/32"),
BGPPeerASN: ptr.To(64513),
BGPPeerPort: ptr.To(179),
CIDRs: ptr.To([]string{"192.0.2.0/24"}),
IPRanges: ptr.To([]types.LoadBalancer_IPRange{
{Start: "20.0.20.100", Stop: "20.0.20.200"},
}),
}

status, err := metallb.ApplyLoadBalancer(context.Background(), snapM, lbCfg, types.Network{}, nil)

g.Expect(err).ToNot(HaveOccurred())
g.Expect(status.Enabled).To(BeTrue())
g.Expect(status.Version).To(Equal(metallb.ControllerImageTag))
g.Expect(helmM.ApplyCalledWith).To(HaveLen(2))

firstCallArgs := helmM.ApplyCalledWith[0]
g.Expect(firstCallArgs.Chart).To(Equal(metallb.ChartMetalLB))
g.Expect(firstCallArgs.State).To(Equal(helm.StatePresent))
// we don't validate values since it's just a static struct
// and won't be changed by configurations
g.Expect(firstCallArgs.Values).ToNot(BeNil())

secondCallArgs := helmM.ApplyCalledWith[1]
g.Expect(secondCallArgs.Chart).To(Equal(metallb.ChartMetalLBLoadBalancer))
g.Expect(secondCallArgs.State).To(Equal(helm.StatePresent))
validateLoadBalancerValues(g, secondCallArgs.Values, lbCfg)
})
}

func validateLoadBalancerValues(g Gomega, values map[string]interface{}, lbCfg types.LoadBalancer) {
l2 := values["l2"].(map[string]any)
g.Expect(l2["enabled"]).To(Equal(lbCfg.GetL2Mode()))
g.Expect(l2["interfaces"]).To(Equal(lbCfg.GetL2Interfaces()))

ipPoolCIDRs := values["ipPool"].(map[string]any)["cidrs"].([]map[string]any)
g.Expect(ipPoolCIDRs).To(HaveLen(len(lbCfg.GetCIDRs()) + len(lbCfg.GetIPRanges())))
for _, cidr := range lbCfg.GetCIDRs() {
g.Expect(ipPoolCIDRs).To(ContainElement(map[string]any{"cidr": cidr}))
}
for _, ipRange := range lbCfg.GetIPRanges() {
g.Expect(ipPoolCIDRs).To(ContainElement(map[string]any{"start": ipRange.Start, "stop": ipRange.Stop}))
}

bgp := values["bgp"].(map[string]any)
g.Expect(bgp["enabled"]).To(Equal(lbCfg.GetBGPMode()))
g.Expect(bgp["localASN"]).To(Equal(lbCfg.GetBGPLocalASN()))
neighbors := bgp["neighbors"].([]map[string]any)
g.Expect(neighbors).To(HaveLen(1))
neighbor := neighbors[0]
g.Expect(neighbor["peerAddress"]).To(Equal(lbCfg.GetBGPPeerAddress()))
g.Expect(neighbor["peerASN"]).To(Equal(lbCfg.GetBGPPeerASN()))
g.Expect(neighbor["peerPort"]).To(Equal(lbCfg.GetBGPPeerPort()))
}
2 changes: 1 addition & 1 deletion src/k8s/pkg/k8sd/features/metallb/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func init() {
images.Register(
fmt.Sprintf("%s:%s", controllerImageRepo, controllerImageTag),
fmt.Sprintf("%s:%s", controllerImageRepo, ControllerImageTag),
fmt.Sprintf("%s:%s", speakerImageRepo, speakerImageTag),
fmt.Sprintf("%s:%s", frrImageRepo, frrImageTag),
)
Expand Down

0 comments on commit b90ee72

Please sign in to comment.