Repository: clastix/cluster-api-control-plane-provider-kamaji
Pull request #198: feat: add LoadBalancer configuration fields for KamajiControlPlane
Author: login: mridulgain
Created: 2025-05-02T14:07:21Z | Merged: 2025-05-06T15:44:52Z

Issue: https://github.com/clastix/cluster-api-control-plane-provider-kamaji/issues/196

Changes:
- Added missing `LoadBalancerSourceRanges` & `LoadBalancerClass` in a new parent field [LoadBalancerConfig](https://github.com/mridulgain/cluster-api-control-plane-provider-kamaji/blob/9b9aaec11ca68d607d4c4af07fecae155240e645/api/v1alpha1/kamajicontrolplane_types.go#L70) in the NetworkComponent struct in KamajiControlPlane types
- Added respective CEL conditional validations
- Updated clastix/kamaji dependency in go.mod to `edge-25.3.2` tag release


Diff:
diff --git a/api/v1alpha1/kamajicontrolplane_types.go b/api/v1alpha1/kamajicontrolplane_types.go
index 09c818c..eefb1a1 100644
--- a/api/v1alpha1/kamajicontrolplane_types.go
+++ b/api/v1alpha1/kamajicontrolplane_types.go
@@ -44,7 +44,30 @@ type IngressComponent struct {
 	ExtraAnnotations map[string]string `json:"extraAnnotations,omitempty"`
 }
 
+// LoadBalancerConfig is used when the KamajiControlPlane is exposed using a LoadBalancer service type.
+type LoadBalancerConfig struct {
+	// LoadBalancerSourceRanges restricts the IP ranges that can access
+	// the LoadBalancer type Service. This field defines a list of IP
+	// address ranges (in CIDR format) that are allowed to access the service.
+	// If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
+	// This feature is useful for restricting access to API servers or services
+	// to specific networks for security purposes.
+	// Example: {"192.168.1.0/24", "10.0.0.0/8"}
+	LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
+	// Specify the LoadBalancer class in case of multiple load balancer implementations.
+	// Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="LoadBalancerClass is immutable"
+	LoadBalancerClass *string `json:"loadBalancerClass,omitempty"`
+}
+
+// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerSourceRanges) || (size(self.loadBalancerConfig.loadBalancerSourceRanges) == 0 || self.serviceType == 'LoadBalancer')", message="LoadBalancerSourceRanges are supported only with LoadBalancer service type"
+// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerClass) || self.serviceType == 'LoadBalancer'", message="LoadBalancerClass is supported only with LoadBalancer service type"
+// +kubebuilder:validation:XValidation:rule="self.serviceType != 'LoadBalancer' || (oldSelf.serviceType != 'LoadBalancer' && self.serviceType == 'LoadBalancer') || !has(self.loadBalancerConfig) || has(self.loadBalancerConfig) && has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime"
+
 type NetworkComponent struct {
+	// Optional configuration for the LoadBalancer service that exposes the Kamaji control plane.
+	LoadBalancerConfig *LoadBalancerConfig `json:"loadBalancerConfig,omitempty"`
 	// When specified, the KamajiControlPlane will be reachable using an Ingress object
 	// deployed in the management cluster.
 	Ingress *IngressComponent `json:"ingress,omitempty"`
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 3be6f3c..e905c31 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -450,9 +450,39 @@ func (in *KineComponent) DeepCopy() *KineComponent {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancerConfig) DeepCopyInto(out *LoadBalancerConfig) {
+	*out = *in
+	if in.LoadBalancerSourceRanges != nil {
+		in, out := &in.LoadBalancerSourceRanges, &out.LoadBalancerSourceRanges
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.LoadBalancerClass != nil {
+		in, out := &in.LoadBalancerClass, &out.LoadBalancerClass
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerConfig.
+func (in *LoadBalancerConfig) DeepCopy() *LoadBalancerConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(LoadBalancerConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NetworkComponent) DeepCopyInto(out *NetworkComponent) {
 	*out = *in
+	if in.LoadBalancerConfig != nil {
+		in, out := &in.LoadBalancerConfig, &out.LoadBalancerConfig
+		*out = new(LoadBalancerConfig)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Ingress != nil {
 		in, out := &in.Ingress, &out.Ingress
 		*out = new(IngressComponent)
diff --git a/config/control-plane-components.yaml b/config/control-plane-components.yaml
index 9c88e1e..4c30d5f 100644
--- a/config/control-plane-components.yaml
+++ b/config/control-plane-components.yaml
@@ -6399,6 +6399,31 @@ spec:
                     required:
                     - hostname
                     type: object
+                  loadBalancerConfig:
+                    description: Optional configuration for the LoadBalancer service that exposes the Kamaji control plane.
+                    properties:
+                      loadBalancerClass:
+                        description: |-
+                          Specify the LoadBalancer class in case of multiple load balancer implementations.
+                          Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
+                        minLength: 1
+                        type: string
+                        x-kubernetes-validations:
+                        - message: LoadBalancerClass is immutable
+                          rule: self == oldSelf
+                      loadBalancerSourceRanges:
+                        description: |-
+                          LoadBalancerSourceRanges restricts the IP ranges that can access
+                          the LoadBalancer type Service. This field defines a list of IP
+                          address ranges (in CIDR format) that are allowed to access the service.
+                          If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
+                          This feature is useful for restricting access to API servers or services
+                          to specific networks for security purposes.
+                          Example: {"192.168.1.0/24", "10.0.0.0/8"}
+                        items:
+                          type: string
+                        type: array
+                    type: object
                   serviceAddress:
                     description: |-
                       This field can be used in case of pre-assigned address, such as a VIP,
@@ -6421,6 +6446,13 @@ spec:
                     - LoadBalancer
                     type: string
                 type: object
+                x-kubernetes-validations:
+                - message: LoadBalancerSourceRanges are supported only with LoadBalancer service type
+                  rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerSourceRanges) || (size(self.loadBalancerConfig.loadBalancerSourceRanges) == 0 || self.serviceType == ''LoadBalancer'')'
+                - message: LoadBalancerClass is supported only with LoadBalancer service type
+                  rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerClass) || self.serviceType == ''LoadBalancer'''
+                - message: LoadBalancerClass cannot be set or unset at runtime
+                  rule: self.serviceType != 'LoadBalancer' || (oldSelf.serviceType != 'LoadBalancer' && self.serviceType == 'LoadBalancer') || !has(self.loadBalancerConfig) || has(self.loadBalancerConfig) && has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)
               registry:
                 default: registry.k8s.io
                 description: |-
@@ -13079,6 +13111,31 @@ spec:
                             required:
                             - hostname
                             type: object
+                          loadBalancerConfig:
+                            description: Optional configuration for the LoadBalancer service that exposes the Kamaji control plane.
+                            properties:
+                              loadBalancerClass:
+                                description: |-
+                                  Specify the LoadBalancer class in case of multiple load balancer implementations.
+                                  Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
+                                minLength: 1
+                                type: string
+                                x-kubernetes-validations:
+                                - message: LoadBalancerClass is immutable
+                                  rule: self == oldSelf
+                              loadBalancerSourceRanges:
+                                description: |-
+                                  LoadBalancerSourceRanges restricts the IP ranges that can access
+                                  the LoadBalancer type Service. This field defines a list of IP
+                                  address ranges (in CIDR format) that are allowed to access the service.
+                                  If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
+                                  This feature is useful for restricting access to API servers or services
+                                  to specific networks for security purposes.
+                                  Example: {"192.168.1.0/24", "10.0.0.0/8"}
+                                items:
+                                  type: string
+                                type: array
+                            type: object
                           serviceAddress:
                             description: |-
                               This field can be used in case of pre-assigned address, such as a VIP,
@@ -13101,6 +13158,13 @@ spec:
                             - LoadBalancer
                             type: string
                         type: object
+                        x-kubernetes-validations:
+                        - message: LoadBalancerSourceRanges are supported only with LoadBalancer service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerSourceRanges) || (size(self.loadBalancerConfig.loadBalancerSourceRanges) == 0 || self.serviceType == ''LoadBalancer'')'
+                        - message: LoadBalancerClass is supported only with LoadBalancer service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerClass) || self.serviceType == ''LoadBalancer'''
+                        - message: LoadBalancerClass cannot be set or unset at runtime
+                          rule: self.serviceType != 'LoadBalancer' || (oldSelf.serviceType != 'LoadBalancer' && self.serviceType == 'LoadBalancer') || !has(self.loadBalancerConfig) || has(self.loadBalancerConfig) && has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)
                       registry:
                         default: registry.k8s.io
                         description: |-
diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml
index 1bff063..bdf5c09 100644
--- a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml
+++ b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml
@@ -6681,6 +6681,32 @@ spec:
                     required:
                     - hostname
                     type: object
+                  loadBalancerConfig:
+                    description: Optional configuration for the LoadBalancer service
+                      that exposes the Kamaji control plane.
+                    properties:
+                      loadBalancerClass:
+                        description: |-
+                          Specify the LoadBalancer class in case of multiple load balancer implementations.
+                          Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
+                        minLength: 1
+                        type: string
+                        x-kubernetes-validations:
+                        - message: LoadBalancerClass is immutable
+                          rule: self == oldSelf
+                      loadBalancerSourceRanges:
+                        description: |-
+                          LoadBalancerSourceRanges restricts the IP ranges that can access
+                          the LoadBalancer type Service. This field defines a list of IP
+                          address ranges (in CIDR format) that are allowed to access the service.
+                          If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
+                          This feature is useful for restricting access to API servers or services
+                          to specific networks for security purposes.
+                          Example: {"192.168.1.0/24", "10.0.0.0/8"}
+                        items:
+                          type: string
+                        type: array
+                    type: object
                   serviceAddress:
                     description: |-
                       This field can be used in case of pre-assigned address, such as a VIP,
@@ -6704,6 +6730,21 @@ spec:
                     - LoadBalancer
                     type: string
                 type: object
+                x-kubernetes-validations:
+                - message: LoadBalancerSourceRanges are supported only with LoadBalancer
+                    service type
+                  rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerSourceRanges)
+                    || (size(self.loadBalancerConfig.loadBalancerSourceRanges) ==
+                    0 || self.serviceType == ''LoadBalancer'')'
+                - message: LoadBalancerClass is supported only with LoadBalancer service
+                    type
+                  rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerClass)
+                    || self.serviceType == ''LoadBalancer'''
+                - message: LoadBalancerClass cannot be set or unset at runtime
+                  rule: self.serviceType != 'LoadBalancer' || (oldSelf.serviceType
+                    != 'LoadBalancer' && self.serviceType == 'LoadBalancer') || !has(self.loadBalancerConfig)
+                    || has(self.loadBalancerConfig) && has(self.loadBalancerConfig.loadBalancerClass)
+                    == has(oldSelf.loadBalancerConfig.loadBalancerClass)
               registry:
                 default: registry.k8s.io
                 description: |-
diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml
index 0081c40..15ef0c5 100644
--- a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml
+++ b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml
@@ -6762,6 +6762,32 @@ spec:
                             required:
                             - hostname
                             type: object
+                          loadBalancerConfig:
+                            description: Optional configuration for the LoadBalancer
+                              service that exposes the Kamaji control plane.
+                            properties:
+                              loadBalancerClass:
+                                description: |-
+                                  Specify the LoadBalancer class in case of multiple load balancer implementations.
+                                  Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
+                                minLength: 1
+                                type: string
+                                x-kubernetes-validations:
+                                - message: LoadBalancerClass is immutable
+                                  rule: self == oldSelf
+                              loadBalancerSourceRanges:
+                                description: |-
+                                  LoadBalancerSourceRanges restricts the IP ranges that can access
+                                  the LoadBalancer type Service. This field defines a list of IP
+                                  address ranges (in CIDR format) that are allowed to access the service.
+                                  If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
+                                  This feature is useful for restricting access to API servers or services
+                                  to specific networks for security purposes.
+                                  Example: {"192.168.1.0/24", "10.0.0.0/8"}
+                                items:
+                                  type: string
+                                type: array
+                            type: object
                           serviceAddress:
                             description: |-
                               This field can be used in case of pre-assigned address, such as a VIP,
@@ -6785,6 +6811,21 @@ spec:
                             - LoadBalancer
                             type: string
                         type: object
+                        x-kubernetes-validations:
+                        - message: LoadBalancerSourceRanges are supported only with
+                            LoadBalancer service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerSourceRanges)
+                            || (size(self.loadBalancerConfig.loadBalancerSourceRanges)
+                            == 0 || self.serviceType == ''LoadBalancer'')'
+                        - message: LoadBalancerClass is supported only with LoadBalancer
+                            service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerClass)
+                            || self.serviceType == ''LoadBalancer'''
+                        - message: LoadBalancerClass cannot be set or unset at runtime
+                          rule: self.serviceType != 'LoadBalancer' || (oldSelf.serviceType
+                            != 'LoadBalancer' && self.serviceType == 'LoadBalancer')
+                            || !has(self.loadBalancerConfig) || has(self.loadBalancerConfig)
+                            && has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)
                       registry:
                         default: registry.k8s.io
                         description: |-
diff --git a/controllers/kamajicontrolplane_controller_tcp.go b/controllers/kamajicontrolplane_controller_tcp.go
index 8e5b3c9..7f28201 100644
--- a/controllers/kamajicontrolplane_controller_tcp.go
+++ b/controllers/kamajicontrolplane_controller_tcp.go
@@ -13,6 +13,7 @@ import (
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/util/retry"
+	"k8s.io/utils/ptr"
 	capiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -25,7 +26,7 @@ var ErrUnsupportedCertificateSAN = errors.New("a certificate SAN must be made of
 
 //+kubebuilder:rbac:groups=kamaji.clastix.io,resources=tenantcontrolplanes,verbs=get;list;watch;create;update
 
-//nolint:funlen,gocognit,cyclop
+//nolint:funlen,gocognit,cyclop,maintidx
 func (r *KamajiControlPlaneReconciler) createOrUpdateTenantControlPlane(ctx context.Context, remoteClient client.Client, cluster capiv1beta1.Cluster, kcp kcpv1alpha1.KamajiControlPlane) (*kamajiv1alpha1.TenantControlPlane, error) {
 	tcp := &kamajiv1alpha1.TenantControlPlane{}
 	tcp.Name = kcp.GetName()
@@ -179,6 +180,17 @@ func (r *KamajiControlPlaneReconciler) createOrUpdateTenantControlPlane(ctx cont
 			} else {
 				tcp.Spec.ControlPlane.Ingress = nil
 			}
+			// LoadBalancer
+			if kcp.Spec.Network.LoadBalancerConfig != nil {
+				if lbClass := kcp.Spec.Network.LoadBalancerConfig.LoadBalancerClass; lbClass != nil {
+					tcp.Spec.NetworkProfile.LoadBalancerClass = ptr.To(*lbClass)
+				}
+
+				if srcRange := kcp.Spec.Network.LoadBalancerConfig.LoadBalancerSourceRanges; srcRange != nil {
+					tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = srcRange
+				}
+			}
+
 			// Deployment
 			tcp.Spec.ControlPlane.Deployment.NodeSelector = kcp.Spec.Deployment.NodeSelector
 			tcp.Spec.ControlPlane.Deployment.RuntimeClassName = kcp.Spec.Deployment.RuntimeClassName
diff --git a/go.mod b/go.mod
index 9f36af7..9a62fa6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/clastix/cluster-api-control-plane-provider-kamaji
 go 1.24.1
 
 require (
-	github.com/clastix/kamaji v1.0.1-0.20241021192153-9ca69e91f9c9
+	github.com/clastix/kamaji v1.0.1-0.20250321065512-09a5b05a9c3e
 	github.com/onsi/ginkgo/v2 v2.23.4
 	github.com/onsi/gomega v1.37.0
 	github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index e2bcb73..6508dca 100644
--- a/go.sum
+++ b/go.sum
@@ -22,8 +22,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/clastix/kamaji v1.0.1-0.20241021192153-9ca69e91f9c9 h1:HAZdvF+CFYeU0WRiqqh8ScmlS2RGnh597M3N2BafIvA=
-github.com/clastix/kamaji v1.0.1-0.20241021192153-9ca69e91f9c9/go.mod h1:RR+/jF13HXayXx85vif+pFFBWQLV8ks4dLj1AXTos6s=
+github.com/clastix/kamaji v1.0.1-0.20250321065512-09a5b05a9c3e h1:iUiJmUepY69i4BBP+EHuexmkRkjGnDCkNA6Lu7SthpQ=
+github.com/clastix/kamaji v1.0.1-0.20250321065512-09a5b05a9c3e/go.mod h1:296vxSRABLAeVo7J+RI/QnBShp8q5X9vj0ojmjon+Co=
 github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0=
 github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
 github.com/coredns/corefile-migration v1.0.26 h1:xiiEkVB1Dwolb24pkeDUDBfygV9/XsOSq79yFCrhptY=
@@ -160,8 +160,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=


Reviews:
user: login: prometherion
body: CELs seems right, I'll merge this as a feature, rather than a fix, but thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-03T16:30:06Z
user: login: prometherion
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-03T16:35:00Z
user: login: mridulgain
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T18:00:15Z
user: login: prometherion
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-05T05:56:21Z
user: login: mridulgain
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:44:32Z
user: login: mridulgain
body: Apart from this redundant nil check, other things looks good to me! 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:53:35Z
user: login: prometherion
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:20:24Z

Review comments:
diff_hunk: @@ -179,6 +179,11 @@ func (r *KamajiControlPlaneReconciler) createOrUpdateTenantControlPlane(ctx cont
 			} else {
 				tcp.Spec.ControlPlane.Ingress = nil
 			}
+			// LoadBalancer
+			if kcp.Spec.Network.LoadBalancerConfig != nil {
+				tcp.Spec.NetworkProfile.LoadBalancerClass = kcp.Spec.Network.LoadBalancerConfig.LoadBalancerClass
path: controllers/kamajicontrolplane_controller_tcp.go
user: login: prometherion
body: Maybe we should check if `kcp.Spec.Network.LoadBalancerConfig.LoadBalancerClass` is not nil and assign its value.

```suggestion
				if lbClass := kcp.Spec.Network.LoadBalancerConfig.LoadBalancerClass; lbClass != nil {
					tcp.Spec.NetworkProfile.LoadBalancerClass = 
ptr.To(*lbClass)
				}
```

Of course, we need to import `k8s.io/utils/ptr`
created_at: 2025-05-03T16:34:59Z
author_association: MEMBER
diff_hunk: @@ -179,6 +179,11 @@ func (r *KamajiControlPlaneReconciler) createOrUpdateTenantControlPlane(ctx cont
 			} else {
 				tcp.Spec.ControlPlane.Ingress = nil
 			}
+			// LoadBalancer
+			if kcp.Spec.Network.LoadBalancerConfig != nil {
+				tcp.Spec.NetworkProfile.LoadBalancerClass = kcp.Spec.Network.LoadBalancerConfig.LoadBalancerClass
path: controllers/kamajicontrolplane_controller_tcp.go
user: login: mridulgain
body: Yes! good point. Addressed.
created_at: 2025-05-03T18:00:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +44,30 @@ type IngressComponent struct {
 	ExtraAnnotations map[string]string `json:"extraAnnotations,omitempty"`
 }
 
+// LoadBalancerConfig is used when the KamajiControlPlane is exposed using a LoadBalancer service type.
+type LoadBalancerConfig struct {
+	// LoadBalancerSourceRanges restricts the IP ranges that can access
+	// the LoadBalancer type Service. This field defines a list of IP
+	// address ranges (in CIDR format) that are allowed to access the service.
+	// If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
+	// This feature is useful for restricting access to API servers or services
+	// to specific networks for security purposes.
+	// Example: {"192.168.1.0/24", "10.0.0.0/8"}
+	LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
+	// Specify the LoadBalancer class in case of multiple load balancer implementations.
+	// Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="LoadBalancerClass is immutable"
+	LoadBalancerClass *string `json:"loadBalancerClass,omitempty"`
+}
+
+// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerConfig.loadBalancerSourceRanges) || (size(self.loadBalancerConfig.loadBalancerSourceRanges) == 0 || self.serviceType == 'LoadBalancer')", message="LoadBalancer source ranges are supported only with LoadBalancer service type"
+// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerConfig.loadBalancerClass) || self.serviceType == 'LoadBalancer'", message="LoadBalancerClass is supported only with LoadBalancer service type"
+// +kubebuilder:validation:XValidation:rule="self.serviceType != 'LoadBalancer' || (oldSelf.serviceType != 'LoadBalancer' && self.serviceType == 'LoadBalancer') || has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime"
path: api/v1alpha1/kamajicontrolplane_types.go
user: login: prometherion
body: We're facing [CI error](https://github.com/clastix/cluster-api-control-plane-provider-kamaji/actions/runs/14824985961/job/41628568025?pr=198) due to the `has` helpers here.

The root cause is `*LoadBalancerConfig`: being a pointer, CEL doesn't have any object to traverse. I'd try avoiding a pointer since underlying fields are already omitempty, as the parent one.
created_at: 2025-05-05T05:56:17Z
author_association: MEMBER
diff_hunk: @@ -44,7 +44,30 @@ type IngressComponent struct {
 	ExtraAnnotations map[string]string `json:"extraAnnotations,omitempty"`
 }
 
+// LoadBalancerConfig is used when the KamajiControlPlane is exposed using a LoadBalancer service type.
+type LoadBalancerConfig struct {
+	// LoadBalancerSourceRanges restricts the IP ranges that can access
+	// the LoadBalancer type Service. This field defines a list of IP
+	// address ranges (in CIDR format) that are allowed to access the service.
+	// If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
+	// This feature is useful for restricting access to API servers or services
+	// to specific networks for security purposes.
+	// Example: {"192.168.1.0/24", "10.0.0.0/8"}
+	LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
+	// Specify the LoadBalancer class in case of multiple load balancer implementations.
+	// Field supported only for Tenant Control Plane instances exposed using a LoadBalancer Service.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="LoadBalancerClass is immutable"
+	LoadBalancerClass *string `json:"loadBalancerClass,omitempty"`
+}
+
+// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerConfig.loadBalancerSourceRanges) || (size(self.loadBalancerConfig.loadBalancerSourceRanges) == 0 || self.serviceType == 'LoadBalancer')", message="LoadBalancer source ranges are supported only with LoadBalancer service type"
+// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerConfig.loadBalancerClass) || self.serviceType == 'LoadBalancer'", message="LoadBalancerClass is supported only with LoadBalancer service type"
+// +kubebuilder:validation:XValidation:rule="self.serviceType != 'LoadBalancer' || (oldSelf.serviceType != 'LoadBalancer' && self.serviceType == 'LoadBalancer') || has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime"
path: api/v1alpha1/kamajicontrolplane_types.go
user: login: mridulgain
body: I see.. The last e2e run passed as `config/control-plane-components.yaml` didn't have the new CEL validations.
I should have added a nil check. Implementing your suggestion as well. 

created_at: 2025-05-05T13:44:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6785,6 +6811,21 @@ spec:
                             - LoadBalancer
                             type: string
                         type: object
+                        x-kubernetes-validations:
+                        - message: LoadBalancerSourceRanges are supported only with
+                            LoadBalancer service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerSourceRanges)
+                            || (size(self.loadBalancerConfig.loadBalancerSourceRanges)
+                            == 0 || self.serviceType == ''LoadBalancer'')'
+                        - message: LoadBalancerClass is supported only with LoadBalancer
+                            service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerClass)
+                            || self.serviceType == ''LoadBalancer'''
+                        - message: LoadBalancerClass cannot be set or unset at runtime
+                          rule: self.serviceType != 'LoadBalancer' || (oldSelf.serviceType
+                            != 'LoadBalancer' && self.serviceType == 'LoadBalancer')
+                            || !has(self.loadBalancerConfig) || has(self.loadBalancerConfig)
+                            && has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)
path: config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml
user: login: mridulgain
body: I also added a nil check in my last commit. We can remove any one of these.
created_at: 2025-05-06T13:52:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6785,6 +6811,21 @@ spec:
                             - LoadBalancer
                             type: string
                         type: object
+                        x-kubernetes-validations:
+                        - message: LoadBalancerSourceRanges are supported only with
+                            LoadBalancer service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerSourceRanges)
+                            || (size(self.loadBalancerConfig.loadBalancerSourceRanges)
+                            == 0 || self.serviceType == ''LoadBalancer'')'
+                        - message: LoadBalancerClass is supported only with LoadBalancer
+                            service type
+                          rule: '!has(self.loadBalancerConfig) || !has(self.loadBalancerConfig.loadBalancerClass)
+                            || self.serviceType == ''LoadBalancer'''
+                        - message: LoadBalancerClass cannot be set or unset at runtime
+                          rule: self.serviceType != 'LoadBalancer' || (oldSelf.serviceType
+                            != 'LoadBalancer' && self.serviceType == 'LoadBalancer')
+                            || !has(self.loadBalancerConfig) || has(self.loadBalancerConfig)
+                            && has(self.loadBalancerConfig.loadBalancerClass) == has(oldSelf.loadBalancerConfig.loadBalancerClass)
path: config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml
user: login: prometherion
body: It's not redundant, it's required:

```
2025-05-06T15:16:04Z    ERROR   Reconciler error        {"controller": "kamajicontrolplane", "controllerGroup": "controlplane.cluster.x-k8s.io", "controllerKind": "KamajiControlPlane", "KamajiControlPlane": {"name":"capi-quickstart-kubevirt","namespace":"default"}, "namespace": "default", "name": "capi-quickstart-kubevirt", "reconcileID": "8ac0b8a5-caae-46b8-b966-31b3c256f30e", "error": "cannot update KamajiControlPlane with ControlPlaneEndpoint: KamajiControlPlane.controlplane.cluster.x-k8s.io \"capi-quickstart-kubevirt\" is invalid: spec.network: Invalid value: \"object\": no such key: loadBalancerConfig evaluating rule: LoadBalancerClass cannot be set or unset at runtime", "errorVerbose": "KamajiControlPlane.controlplane.cluster.x-k8s.io \"capi-quickstart-kubevirt\" is invalid: spec.network: Invalid value: \"object\": no such key: loadBalancerConfig evaluating rule: LoadBalancerClass cannot be set or unset at runtime\ncannot update KamajiControlPlane with ControlPlaneEndpoint\ngithub.com/clastix/cluster-api-control-plane-provider-kamaji/controllers.(*KamajiControlPlaneReconciler).patchControlPlaneEndpoint\n\t/workspace/controllers/kamajicontrolplane_controller_cluster_patch.go:70\ngithub.com/clastix/cluster-api-control-plane-provider-kamaji/controllers.(*KamajiControlPlaneReconciler).Reconcile.func5\n\t/workspace/controllers/kamajicontrolplane_controller.go:187\ngithub.com/clastix/cluster-api-control-plane-provider-kamaji/controllers.TrackConditionType\n\t/workspace/controllers/conditions.go:26\ngithub.com/clastix/cluster-api-control-plane-provider-kamaji/controllers.(*KamajiControlPlaneReconciler).Reconcile\n\t/workspace/controllers/kamajicontrolplane_controller.go:186\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Reconcile\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.20.4/pkg/internal/controller/controller.go:119\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).reconcileHandler\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.20.4/pkg/internal/controller/controller.go:334\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).processNextWorkItem\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.20.4/pkg/internal/controller/controller.go:294\nsigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller[...]).Start.func2.2\n\t/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.20.4/pkg/internal/controller/controller.go:255\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1700"}
```

When I add it:

```
2025-05-06T15:19:53Z    INFO    reconciliation completed        {"controller": "kamajicontrolplane", "controllerGroup": "controlplane.cluster.x-k8s.io", "controllerKind": "KamajiControlPlane", "KamajiControlPlane": {"name":"capi-quickstart-kubevirt","namespace":"default"}, "namespace": "default", "name": "capi-quickstart-kubevirt", "reconcileID": "8cfb4aae-10ab-4062-a6d8-69bf7a21aa49", "duration": "61.496133ms"}
```
created_at: 2025-05-06T15:20:24Z
author_association: MEMBER

PR comments:
user: login: prometherion
created_at: 2025-05-04T06:27:58Z
body: We can ignore the commitlint job given I'll squash this PR, however, the other two are mandatory.

Just issue `make release` and commits the generated files, and add a `nolint:maintidx` comment on the function `golanci-lint` is reporting.
author_association: MEMBER
user: login: mridulgain
created_at: 2025-05-05T17:47:48Z
body: > error: timed out waiting for the condition on clusters/capi-quickstart
> Error: Process completed with exit code 1.

Is this an intermittent issue at `Deploy target cluster` step of e2e test workflow?
author_association: CONTRIBUTOR
user: login: prometherion
created_at: 2025-05-05T20:08:41Z
body: No, it used to work.

I was expecting you were testing changed locally, better do dig this to understand what's broken.
author_association: MEMBER
user: login: prometherion
created_at: 2025-05-06T13:15:19Z
body: @mridulgain I just force-pushed changes to find a solution: tl;dr; CEL is a pain and we need a pointer to avoid unsetting values when updating the `KamajiControlPlane` resource with the Tenant Control Plane LB address.

Please, review my last commit and if it's ok, let's get this merged.
author_association: MEMBER
user: login: prometherion
created_at: 2025-05-06T15:44:31Z
body: I'm getting this merged, just asking next time to give a spin to changes rather than offload that to e2e, although CI is already doing a sort of guardrailing for contributors.
author_association: MEMBER