diff --git a/src/k8s/pkg/k8sd/features/metallb/chart.go b/src/k8s/pkg/k8sd/features/metallb/chart.go index dcb616b3d..a7bdffd2a 100644 --- a/src/k8s/pkg/k8sd/features/metallb/chart.go +++ b/src/k8s/pkg/k8sd/features/metallb/chart.go @@ -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"), @@ -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" diff --git a/src/k8s/pkg/k8sd/features/metallb/loadbalancer.go b/src/k8s/pkg/k8sd/features/metallb/loadbalancer.go index 47b583112..ef199d600 100644 --- a/src/k8s/pkg/k8sd/features/metallb/loadbalancer.go +++ b/src/k8s/pkg/k8sd/features/metallb/loadbalancer.go @@ -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" ) @@ -27,14 +27,14 @@ 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 } @@ -42,7 +42,7 @@ func ApplyLoadBalancer(ctx context.Context, snap snap.Snap, loadbalancer types.L err = fmt.Errorf("failed to enable LoadBalancer: %w", err) return types.FeatureStatus{ Enabled: false, - Version: controllerImageTag, + Version: ControllerImageTag, Message: fmt.Sprintf(deployFailedMsgTmpl, err), }, err } @@ -50,19 +50,19 @@ func ApplyLoadBalancer(ctx context.Context, snap snap.Snap, loadbalancer types.L 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 } @@ -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 @@ -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{ @@ -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) } @@ -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) } diff --git a/src/k8s/pkg/k8sd/features/metallb/loadbalancer_test.go b/src/k8s/pkg/k8sd/features/metallb/loadbalancer_test.go new file mode 100644 index 000000000..0bc1fb6f1 --- /dev/null +++ b/src/k8s/pkg/k8sd/features/metallb/loadbalancer_test.go @@ -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())) +} diff --git a/src/k8s/pkg/k8sd/features/metallb/register.go b/src/k8s/pkg/k8sd/features/metallb/register.go index 817f5408d..74cb0ed3e 100644 --- a/src/k8s/pkg/k8sd/features/metallb/register.go +++ b/src/k8s/pkg/k8sd/features/metallb/register.go @@ -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), )