diff --git a/api/v1alpha1/addon_types.go b/api/v1alpha1/addon_types.go index ccc376c5e..1fab8f983 100644 --- a/api/v1alpha1/addon_types.go +++ b/api/v1alpha1/addon_types.go @@ -24,6 +24,8 @@ const ( VirtualIPProviderKubeVIP = "KubeVIP" + ServiceLoadBalancerProviderMetalLB = "MetalLB" + AddonStrategyClusterResourceSet AddonStrategy = "ClusterResourceSet" AddonStrategyHelmAddon AddonStrategy = "HelmAddon" @@ -69,6 +71,9 @@ type Addons struct { // +optional CSIProviders *CSI `json:"csi,omitempty"` + + // +optional + ServiceLoadBalancer *ServiceLoadBalancer `json:"serviceLoadBalancer,omitempty"` } type AddonStrategy string @@ -160,3 +165,10 @@ type CCM struct { // +optional Credentials *corev1.LocalObjectReference `json:"credentials,omitempty"` } + +type ServiceLoadBalancer struct { + // The LoadBalancer-type Service provider to deploy. Not required in infrastructures where + // the CCM acts as the provider. + // +kubebuilder:validation:Enum=MetalLB + Provider string `json:"provider"` +} diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index de4b8b1f6..9f4ea4d95 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -14,4 +14,6 @@ const ( AWSVariableName = "aws" // NutanixVariableName is the Nutanix config patch variable name. NutanixVariableName = "nutanix" + // ServiceLoadBalancerName is the Service LoadBalancer config patch variable name. + ServiceLoadBalancerVariableName = "serviceLoadBalancer" ) diff --git a/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml index 14ec91fc4..0d9087ec8 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_awsclusterconfigs.yaml @@ -201,6 +201,18 @@ spec: required: - strategy type: object + serviceLoadBalancer: + properties: + provider: + description: |- + The LoadBalancer-type Service provider to deploy. Not required in infrastructures where + the CCM acts as the provider. + enum: + - MetalLB + type: string + required: + - provider + type: object type: object aws: description: AWS cluster configuration. diff --git a/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml index 0086d1894..60025cfb4 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_dockerclusterconfigs.yaml @@ -202,6 +202,18 @@ spec: required: - strategy type: object + serviceLoadBalancer: + properties: + provider: + description: |- + The LoadBalancer-type Service provider to deploy. Not required in infrastructures where + the CCM acts as the provider. + enum: + - MetalLB + type: string + required: + - provider + type: object type: object controlPlane: description: DockerNodeConfigSpec defines the desired state of DockerNodeSpec. diff --git a/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml b/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml index 632aa9193..318799881 100644 --- a/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml +++ b/api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml @@ -202,6 +202,18 @@ spec: required: - strategy type: object + serviceLoadBalancer: + properties: + provider: + description: |- + The LoadBalancer-type Service provider to deploy. Not required in infrastructures where + the CCM acts as the provider. + enum: + - MetalLB + type: string + required: + - provider + type: object type: object controlPlane: description: NutanixNodeSpec defines the desired state of NutanixNodeSpec. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9986f1357..bbb8c1734 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -350,6 +350,11 @@ func (in *Addons) DeepCopyInto(out *Addons) { *out = new(CSI) (*in).DeepCopyInto(*out) } + if in.ServiceLoadBalancer != nil { + in, out := &in.ServiceLoadBalancer, &out.ServiceLoadBalancer + *out = new(ServiceLoadBalancer) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Addons. @@ -1119,6 +1124,21 @@ func (in *SecurityGroup) DeepCopy() *SecurityGroup { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceLoadBalancer) DeepCopyInto(out *ServiceLoadBalancer) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceLoadBalancer. +func (in *ServiceLoadBalancer) DeepCopy() *ServiceLoadBalancer { + if in == nil { + return nil + } + out := new(ServiceLoadBalancer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StorageClassConfig) DeepCopyInto(out *StorageClassConfig) { *out = *in diff --git a/charts/cluster-api-runtime-extensions-nutanix/README.md b/charts/cluster-api-runtime-extensions-nutanix/README.md index 4c63dd9bd..03d53d671 100644 --- a/charts/cluster-api-runtime-extensions-nutanix/README.md +++ b/charts/cluster-api-runtime-extensions-nutanix/README.md @@ -62,6 +62,8 @@ A Helm chart for cluster-api-runtime-extensions-nutanix | hooks.nfd.crsStrategy.defaultInstallationConfigMap.name | string | `"node-feature-discovery"` | | | hooks.nfd.helmAddonStrategy.defaultValueTemplateConfigMap.create | bool | `true` | | | hooks.nfd.helmAddonStrategy.defaultValueTemplateConfigMap.name | string | `"default-nfd-helm-values-template"` | | +| hooks.serviceLoadBalancer.metalLB.defaultValueTemplateConfigMap.create | bool | `true` | | +| hooks.serviceLoadBalancer.metalLB.defaultValueTemplateConfigMap.name | string | `"default-metallb-helm-values-template"` | | | hooks.virtualIP.kubeVip.defaultTemplateConfigMap.create | bool | `true` | | | hooks.virtualIP.kubeVip.defaultTemplateConfigMap.name | string | `"default-kube-vip-template"` | | | image.pullPolicy | string | `"IfNotPresent"` | | diff --git a/charts/cluster-api-runtime-extensions-nutanix/templates/helm-config.yaml b/charts/cluster-api-runtime-extensions-nutanix/templates/helm-config.yaml index 84a4292e6..62ae1ad10 100644 --- a/charts/cluster-api-runtime-extensions-nutanix/templates/helm-config.yaml +++ b/charts/cluster-api-runtime-extensions-nutanix/templates/helm-config.yaml @@ -15,6 +15,10 @@ data: ChartName: cluster-autoscaler ChartVersion: 9.35.0 RepositoryURL: https://kubernetes.github.io/autoscaler + metallb: | + ChartName: metallb + ChartVersion: v0.14.5 + RepositoryURL: https://metallb.github.io/metallb nfd: | ChartName: node-feature-discovery ChartVersion: 0.15.2 diff --git a/charts/cluster-api-runtime-extensions-nutanix/templates/serviceloadbalancer/metallb/helm-addon-installation.yaml b/charts/cluster-api-runtime-extensions-nutanix/templates/serviceloadbalancer/metallb/helm-addon-installation.yaml new file mode 100644 index 000000000..c0bc2e238 --- /dev/null +++ b/charts/cluster-api-runtime-extensions-nutanix/templates/serviceloadbalancer/metallb/helm-addon-installation.yaml @@ -0,0 +1,31 @@ +# Copyright 2023 D2iQ, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{{- if .Values.hooks.serviceLoadBalancer.metalLB.defaultValueTemplateConfigMap.create }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: '{{ .Values.hooks.serviceLoadBalancer.metalLB.defaultValueTemplateConfigMap.name }}' +data: + values.yaml: |- + controller: + tolerations: + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + operator: Exists + - key: CriticalAddonsOnly + operator: Exists + - effect: NoExecute + operator: Exists + tolerationSeconds: 300 + speaker: + tolerations: + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + operator: Exists + - key: CriticalAddonsOnly + operator: Exists + - effect: NoExecute + operator: Exists + tolerationSeconds: 300 +{{- end -}} diff --git a/charts/cluster-api-runtime-extensions-nutanix/values.yaml b/charts/cluster-api-runtime-extensions-nutanix/values.yaml index dfc59c36c..4db472071 100644 --- a/charts/cluster-api-runtime-extensions-nutanix/values.yaml +++ b/charts/cluster-api-runtime-extensions-nutanix/values.yaml @@ -71,7 +71,11 @@ hooks: defaultValueTemplateConfigMap: create: true name: default-cluster-autoscaler-helm-values-template - + serviceLoadBalancer: + metalLB: + defaultValueTemplateConfigMap: + create: true + name: default-metallb-helm-values-template virtualIP: kubeVip: defaultTemplateConfigMap: diff --git a/docs/content/addons/serviceloadbalancer.md b/docs/content/addons/serviceloadbalancer.md new file mode 100644 index 000000000..cc767c146 --- /dev/null +++ b/docs/content/addons/serviceloadbalancer.md @@ -0,0 +1,40 @@ ++++ +title = "Service LoadBalancer" ++++ + +When an application running in a cluster needs to be exposed outside of the cluster, one option is +to use an [external load balancer], by creating a Kubernetes Service of the +`LoadBalancer` type. + +The Service Load Balancer is the component that backs this Kubernetes Service, either by creating +a Virtual IP, creating a machine that runs load balancer software, by delegating to APIs, such as +the underlying infrastructure, or a hardware load balancer. + +CAREN currently supports the following Service Load Balancers: + +- [MetalLB] + +## Example + +To enable deployment of MetalLB on a cluster, specify the following values: + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: +spec: + topology: + variables: + - name: clusterConfig + value: + addons: + serviceLoadBalancer: + provider: MetalLB +``` + +See [MetalLB documentation] for details on configuration. + +[external load balancer]: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/ +[MetalLB]: https://metallb.org +[MetalLB documentation]: https://metallb.org/configuration/ diff --git a/hack/addons/kustomize/metallb/kustomization.yaml.tmpl b/hack/addons/kustomize/metallb/kustomization.yaml.tmpl new file mode 100644 index 000000000..4c36e3ffd --- /dev/null +++ b/hack/addons/kustomize/metallb/kustomization.yaml.tmpl @@ -0,0 +1,27 @@ +# Copyright 2024 D2iQ, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# NOTE This file is used by the tool in `hack/tools/helm-cm` to add +# metallb chart metadata to the "helm-addons" ConfigMap. The tool takes +# a kustomization as input. We do not use this file with kustomize. + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +metadata: + name: metallb + +sortOptions: + order: fifo + +helmCharts: +- name: metallb + repo: https://metallb.github.io/metallb + releaseName: metallb + version: ${METALLB_CHART_VERSION} + valuesFile: helm-values.yaml + includeCRDs: true + skipTests: true + namespace: metallb-system + +namespace: metallb-system diff --git a/make/addons.mk b/make/addons.mk index e4124eaa3..907fa882d 100644 --- a/make/addons.mk +++ b/make/addons.mk @@ -21,6 +21,8 @@ export NUTANIX_CCM_CHART_VERSION := 0.3.3 export KUBE_VIP_VERSION := v0.8.0 +export METALLB_CHART_VERSION := v0.14.5 + .PHONY: addons.sync addons.sync: $(addprefix update-addon.,calico cilium nfd cluster-autoscaler aws-ebs-csi aws-ccm.127 aws-ccm.128 aws-ccm.129 kube-vip) diff --git a/pkg/handlers/aws/mutation/controlplaneloadbalancer/inject_test.go b/pkg/handlers/aws/mutation/controlplaneloadbalancer/inject_test.go index 030d5fb7b..161221045 100644 --- a/pkg/handlers/aws/mutation/controlplaneloadbalancer/inject_test.go +++ b/pkg/handlers/aws/mutation/controlplaneloadbalancer/inject_test.go @@ -34,7 +34,7 @@ var _ = Describe("Generate AWS ControlPlane LoadBalancer patches", func() { Name: "unset variable", }, { - Name: "ControlPlaneLoadbalancer scheme set to internet-facing", + Name: "ControlPlaneLoadBalancer scheme set to internet-facing", Vars: []runtimehooksv1.Variable{ capitest.VariableWithValue( clusterconfig.MetaVariableName, @@ -55,7 +55,7 @@ var _ = Describe("Generate AWS ControlPlane LoadBalancer patches", func() { }}, }, { - Name: "ControlPlaneLoadbalancer scheme set to internal", + Name: "ControlPlaneLoadBalancer scheme set to internal", Vars: []runtimehooksv1.Variable{ capitest.VariableWithValue( clusterconfig.MetaVariableName, diff --git a/pkg/handlers/generic/lifecycle/config/cm.go b/pkg/handlers/generic/lifecycle/config/cm.go index 89b44fa20..6aad57e9e 100644 --- a/pkg/handlers/generic/lifecycle/config/cm.go +++ b/pkg/handlers/generic/lifecycle/config/cm.go @@ -24,6 +24,7 @@ const ( NutanixStorageCSI Component = "nutanix-storage-csi" NutanixSnapshotCSI Component = "nutanix-snapshot-csi" NutanixCCM Component = "nutanix-ccm" + MetalLB Component = "metallb" ) type HelmChartGetter struct { diff --git a/pkg/handlers/generic/lifecycle/handlers.go b/pkg/handlers/generic/lifecycle/handlers.go index d34ffcf5a..97f891986 100644 --- a/pkg/handlers/generic/lifecycle/handlers.go +++ b/pkg/handlers/generic/lifecycle/handlers.go @@ -21,6 +21,8 @@ import ( nutanixcsi "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/csi/nutanix-csi" "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/nfd" "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/servicelbgc" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/serviceloadbalancer" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/serviceloadbalancer/metallb" "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options" ) @@ -34,6 +36,7 @@ type Handlers struct { nutnaixCSIConfig *nutanixcsi.NutanixCSIConfig awsccmConfig *awsccm.AWSCCMConfig nutanixCCMConfig *nutanixccm.Config + metalLBConfig *metallb.Config } func New( @@ -51,6 +54,7 @@ func New( awsccmConfig: &awsccm.AWSCCMConfig{GlobalOptions: globalOptions}, nutnaixCSIConfig: &nutanixcsi.NutanixCSIConfig{GlobalOptions: globalOptions}, nutanixCCMConfig: &nutanixccm.Config{GlobalOptions: globalOptions}, + metalLBConfig: &metallb.Config{GlobalOptions: globalOptions}, } } @@ -76,6 +80,13 @@ func (h *Handlers) AllHandlers(mgr manager.Manager) []handlers.Named { helmChartInfoGetter, ), } + serviceLoadBalancerHandlers := map[string]serviceloadbalancer.ServiceLoadBalancerProvider{ + v1alpha1.ServiceLoadBalancerProviderMetalLB: metallb.New( + mgr.GetClient(), + h.metalLBConfig, + helmChartInfoGetter, + ), + } return []handlers.Named{ calico.New(mgr.GetClient(), h.calicoCNIConfig, helmChartInfoGetter), cilium.New(mgr.GetClient(), h.ciliumCNIConfig, helmChartInfoGetter), @@ -84,6 +95,7 @@ func (h *Handlers) AllHandlers(mgr manager.Manager) []handlers.Named { servicelbgc.New(mgr.GetClient()), csi.New(mgr.GetClient(), csiHandlers), ccm.New(mgr.GetClient(), ccmHandlers), + serviceloadbalancer.New(mgr.GetClient(), serviceLoadBalancerHandlers), } } @@ -96,4 +108,5 @@ func (h *Handlers) AddFlags(flagSet *pflag.FlagSet) { h.awsccmConfig.AddFlags("awsccm", pflag.CommandLine) h.nutnaixCSIConfig.AddFlags("nutanixcsi", flagSet) h.nutanixCCMConfig.AddFlags("nutanixccm", flagSet) + h.metalLBConfig.AddFlags("metallb", flagSet) } diff --git a/pkg/handlers/generic/lifecycle/serviceloadbalancer/handler.go b/pkg/handlers/generic/lifecycle/serviceloadbalancer/handler.go new file mode 100644 index 000000000..bc377f5a9 --- /dev/null +++ b/pkg/handlers/generic/lifecycle/serviceloadbalancer/handler.go @@ -0,0 +1,131 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package serviceloadbalancer + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + commonhandlers "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/lifecycle" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/clusterconfig" +) + +type ServiceLoadBalancerProvider interface { + Apply( + ctx context.Context, + cluster *clusterv1.Cluster, + log logr.Logger, + ) error +} + +type ServiceLoadBalancerHandler struct { + client ctrlclient.Client + variableName string + variablePath []string + ProviderHandler map[string]ServiceLoadBalancerProvider +} + +var ( + _ commonhandlers.Named = &ServiceLoadBalancerHandler{} + _ lifecycle.AfterControlPlaneInitialized = &ServiceLoadBalancerHandler{} +) + +func New( + c ctrlclient.Client, + handlers map[string]ServiceLoadBalancerProvider, +) *ServiceLoadBalancerHandler { + return &ServiceLoadBalancerHandler{ + client: c, + variableName: clusterconfig.MetaVariableName, + variablePath: []string{"addons", v1alpha1.ServiceLoadBalancerVariableName}, + ProviderHandler: handlers, + } +} + +func (c *ServiceLoadBalancerHandler) Name() string { + return "ServiceLoadBalancerHandler" +} + +func (c *ServiceLoadBalancerHandler) AfterControlPlaneInitialized( + ctx context.Context, + req *runtimehooksv1.AfterControlPlaneInitializedRequest, + resp *runtimehooksv1.AfterControlPlaneInitializedResponse, +) { + clusterKey := ctrlclient.ObjectKeyFromObject(&req.Cluster) + + log := ctrl.LoggerFrom(ctx).WithValues( + "cluster", + clusterKey, + ) + + varMap := variables.ClusterVariablesToVariablesMap(req.Cluster.Spec.Topology.Variables) + + slb, err := variables.Get[v1alpha1.ServiceLoadBalancer]( + varMap, + c.variableName, + c.variablePath...) + if err != nil { + log.Error( + err, + "failed to read ServiceLoadBalancer provider from cluster definition", + ) + resp.SetStatus(runtimehooksv1.ResponseStatusFailure) + resp.SetMessage( + fmt.Sprintf("failed to read ServiceLoadBalancer provider from cluster definition: %v", + err, + ), + ) + return + } + + handler, ok := c.ProviderHandler[slb.Provider] + if !ok { + err = fmt.Errorf("unknown ServiceLoadBalancer Provider") + log.Error(err, "provider", slb.Provider) + resp.SetStatus(runtimehooksv1.ResponseStatusFailure) + resp.SetMessage( + fmt.Sprintf("%s %s", err, slb.Provider), + ) + return + } + + log.Info(fmt.Sprintf("Deploying ServiceLoadBalancer provider %s", slb.Provider)) + err = handler.Apply( + ctx, + &req.Cluster, + log, + ) + if err != nil { + log.Error( + err, + fmt.Sprintf( + "failed to deploy ServiceLoadBalancer provider %s", + slb.Provider, + ), + ) + resp.SetStatus(runtimehooksv1.ResponseStatusFailure) + resp.SetMessage( + fmt.Sprintf( + "failed to deploy ServiceLoadBalancer provider: %v", + err, + ), + ) + } + + resp.SetStatus(runtimehooksv1.ResponseStatusSuccess) + resp.SetMessage( + fmt.Sprintf( + "deployed ServiceLoadBalancer provider %s", + slb.Provider), + ) +} diff --git a/pkg/handlers/generic/lifecycle/serviceloadbalancer/metallb/handler.go b/pkg/handlers/generic/lifecycle/serviceloadbalancer/metallb/handler.go new file mode 100644 index 000000000..5f81981d8 --- /dev/null +++ b/pkg/handlers/generic/lifecycle/serviceloadbalancer/metallb/handler.go @@ -0,0 +1,155 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package metallb + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/controllers/remote" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + caaphv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/sigs.k8s.io/cluster-api-addon-provider-helm/api/v1alpha1" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/k8s/client" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/config" + lifecycleutils "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/lifecycle/utils" + "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options" +) + +const ( + defaultHelmReleaseName = "metallb" + defaultHelmReleaseNamespace = "metallb-system" +) + +// These labels allow the MetalLB speaker pod to obtain elevated permissions, +// which it requires in order to perform its network functionalities. +var podSecurityReleaseNamespaceLabels = map[string]string{ + "pod-security.kubernetes.io/enforce": "privileged", + "pod-security.kubernetes.io/audit": "privileged", + "pod-security.kubernetes.io/warn": "privileged", +} + +type Config struct { + *options.GlobalOptions + + defaultValuesTemplateConfigMapName string +} + +func (c *Config) AddFlags(prefix string, flags *pflag.FlagSet) { + flags.StringVar( + &c.defaultValuesTemplateConfigMapName, + prefix+".default-values-template-configmap-name", + "default-metallb-helm-values-template", + "default values ConfigMap name", + ) +} + +type MetalLB struct { + client ctrlclient.Client + config *Config + helmChartInfoGetter *config.HelmChartGetter +} + +func New( + c ctrlclient.Client, + cfg *Config, + helmChartInfoGetter *config.HelmChartGetter, +) *MetalLB { + return &MetalLB{ + client: c, + config: cfg, + helmChartInfoGetter: helmChartInfoGetter, + } +} + +func (n *MetalLB) Apply( + ctx context.Context, + cluster *clusterv1.Cluster, + log logr.Logger, +) error { + log.Info("Applying MetalLB installation") + + values, err := lifecycleutils.RetrieveValuesTemplate( + ctx, + n.client, + n.config.defaultValuesTemplateConfigMapName, + n.config.DefaultsNamespace(), + ) + if err != nil { + return fmt.Errorf( + "failed to retrieve MetalLB installation values template ConfigMap for cluster: %w", + err, + ) + } + + remoteClient, err := remote.NewClusterClient( + ctx, + "", + n.client, + ctrlclient.ObjectKeyFromObject(cluster), + ) + if err != nil { + return fmt.Errorf("error creating remote cluster client: %w", err) + } + + err = lifecycleutils.EnsureNamespaceWithMetadata( + ctx, + remoteClient, + defaultHelmReleaseNamespace, + podSecurityReleaseNamespaceLabels, + nil, + ) + if err != nil { + return fmt.Errorf( + "failed to ensure release namespace %q exists: %w", + defaultHelmReleaseName, + err, + ) + } + + helmChartInfo, err := n.helmChartInfoGetter.For(ctx, log, config.MetalLB) + if err != nil { + return fmt.Errorf("failed to get MetalLB helm chart: %w", err) + } + + hcp := &caaphv1.HelmChartProxy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: caaphv1.GroupVersion.String(), + Kind: "HelmChartProxy", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: cluster.Namespace, + Name: "metallb-" + cluster.Name, + }, + Spec: caaphv1.HelmChartProxySpec{ + RepoURL: helmChartInfo.Repository, + ChartName: helmChartInfo.Name, + ClusterSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{clusterv1.ClusterNameLabel: cluster.Name}, + }, + ReleaseNamespace: defaultHelmReleaseNamespace, + ReleaseName: defaultHelmReleaseName, + Version: helmChartInfo.Version, + ValuesTemplate: values, + }, + } + + if err = controllerutil.SetOwnerReference(cluster, hcp, n.client.Scheme()); err != nil { + return fmt.Errorf( + "failed to set owner reference on MetalLB installation HelmChartProxy: %w", + err, + ) + } + + if err = client.ServerSideApply(ctx, n.client, hcp); err != nil { + return fmt.Errorf("failed to apply MetalLB installation HelmChartProxy: %w", err) + } + + return nil +} diff --git a/pkg/handlers/generic/lifecycle/utils/utils.go b/pkg/handlers/generic/lifecycle/utils/utils.go index 251b0d069..3ae03f7f1 100644 --- a/pkg/handlers/generic/lifecycle/utils/utils.go +++ b/pkg/handlers/generic/lifecycle/utils/utils.go @@ -108,6 +108,29 @@ func EnsureNamespaceWithName(ctx context.Context, c ctrlclient.Client, name stri return EnsureNamespace(ctx, c, ns) } +// EnsureNamespaceWithMetadata will create the namespace with the specified name, +// labels, and/or annotations, if it does not exist. +func EnsureNamespaceWithMetadata(ctx context.Context, + c ctrlclient.Client, + name string, + labels, + annotations map[string]string, +) error { + ns := &corev1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Annotations: annotations, + Labels: labels, + }, + } + + return EnsureNamespace(ctx, c, ns) +} + // EnsureNamespace will create the namespace if it does not exist. func EnsureNamespace(ctx context.Context, c ctrlclient.Client, ns *corev1.Namespace) error { if ns.TypeMeta.APIVersion == "" {