diff --git a/api/v1alpha1/tenantcontrolplane_types.go b/api/v1alpha1/tenantcontrolplane_types.go
index 100949c..9174a72 100644
--- a/api/v1alpha1/tenantcontrolplane_types.go
+++ b/api/v1alpha1/tenantcontrolplane_types.go
@@ -356,6 +356,15 @@ type ServiceSpec struct {
AdditionalPorts []AdditionalPort `json:"additionalPorts,omitempty"`
// ServiceType allows specifying how to expose the Tenant Control Plane.
ServiceType ServiceType `json:"serviceType"`
+ // AllocateLoadBalancerNodePorts defines whether NodePorts are automatically allocated
+ // for the Service when serviceType is LoadBalancer. It maps directly to the Service's
+ // spec.allocateLoadBalancerNodePorts. When nil, the Kubernetes default (true) applies,
+ // preserving existing behaviour. Set to false to expose the Tenant Control Plane only
+ // via the LoadBalancer IP and ClusterIP, without a per-node NodePort. This field is only
+ // valid when serviceType is LoadBalancer; setting it with any other serviceType is
+ // rejected by validation.
+ //+optional
+ AllocateLoadBalancerNodePorts *bool `json:"allocateLoadBalancerNodePorts,omitempty"`
}
// AddonSpec defines the spec for every addon.
@@ -483,6 +492,7 @@ type DataStoreOverride struct {
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreUsername) || has(self.dataStoreUsername)", message="unsetting the dataStoreUsername is not supported"
// +kubebuilder:validation:XValidation:rule="!has(self.networkProfile.loadBalancerSourceRanges) || (size(self.networkProfile.loadBalancerSourceRanges) == 0 || self.controlPlane.service.serviceType == 'LoadBalancer')", message="LoadBalancer source ranges are supported only with LoadBalancer service type"
// +kubebuilder:validation:XValidation:rule="!has(self.networkProfile.loadBalancerClass) || self.controlPlane.service.serviceType == 'LoadBalancer'", message="LoadBalancerClass is supported only with LoadBalancer service type"
+// +kubebuilder:validation:XValidation:rule="!has(self.controlPlane.service.allocateLoadBalancerNodePorts) || self.controlPlane.service.serviceType == 'LoadBalancer'", message="allocateLoadBalancerNodePorts is supported only with LoadBalancer service type"
// +kubebuilder:validation:XValidation:rule="self.controlPlane.service.serviceType != 'LoadBalancer' || (oldSelf.controlPlane.service.serviceType != 'LoadBalancer' && self.controlPlane.service.serviceType == 'LoadBalancer') || has(self.networkProfile.loadBalancerClass) == has(oldSelf.networkProfile.loadBalancerClass)",message="LoadBalancerClass cannot be set or unset at runtime"
type TenantControlPlaneSpec struct {
diff --git a/api/v1alpha1/tenantcontrolplane_types_test.go b/api/v1alpha1/tenantcontrolplane_types_test.go
index b07cc1f..33ccc65 100644
--- a/api/v1alpha1/tenantcontrolplane_types_test.go
+++ b/api/v1alpha1/tenantcontrolplane_types_test.go
@@ -10,6 +10,7 @@ import (
. "github.com/onsi/gomega"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/utils/ptr"
)
var _ = Describe("Cluster controller", func() {
@@ -118,4 +119,30 @@ var _ = Describe("Cluster controller", func() {
Expect(err.Error()).To(ContainSubstring("advertiseAddress must be a valid IP address"))
})
})
+
+ Context("AllocateLoadBalancerNodePorts", func() {
+ It("allows the field when service type is LoadBalancer", func() {
+ tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeLoadBalancer
+ tcp.Spec.ControlPlane.Service.AllocateLoadBalancerNodePorts = ptr.To(false)
+
+ err := k8sClient.Create(ctx, tcp)
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ It("allows creation when the field is unset and service type is not LoadBalancer", func() {
+ tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeNodePort
+
+ err := k8sClient.Create(ctx, tcp)
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ It("denies the field when service type is not LoadBalancer", func() {
+ tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeNodePort
+ tcp.Spec.ControlPlane.Service.AllocateLoadBalancerNodePorts = ptr.To(false)
+
+ err := k8sClient.Create(ctx, tcp)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("allocateLoadBalancerNodePorts is supported only with LoadBalancer service type"))
+ })
+ })
})
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 7335a7e..a83069e 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1564,6 +1564,16 @@ func (in *NetworkProfileSpec) DeepCopyInto(out *NetworkProfileSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
+ if in.ServiceCIDRs != nil {
+ in, out := &in.ServiceCIDRs, &out.ServiceCIDRs
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.PodCIDRs != nil {
+ in, out := &in.PodCIDRs, &out.PodCIDRs
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
if in.DNSServiceIPs != nil {
in, out := &in.DNSServiceIPs, &out.DNSServiceIPs
*out = make([]string, len(*in))
@@ -1724,6 +1734,11 @@ func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.AllocateLoadBalancerNodePorts != nil {
+ in, out := &in.AllocateLoadBalancerNodePorts, &out.AllocateLoadBalancerNodePorts
+ *out = new(bool)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec.
diff --git a/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml b/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
index 5304d13..360b750 100644
--- a/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
+++ b/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml
@@ -8339,6 +8339,16 @@ versions:
- targetPort
type: object
type: array
+ allocateLoadBalancerNodePorts:
+ description: |-
+ AllocateLoadBalancerNodePorts defines whether NodePorts are automatically allocated
+ for the Service when serviceType is LoadBalancer. It maps directly to the Service's
+ spec.allocateLoadBalancerNodePorts. When nil, the Kubernetes default (true) applies,
+ preserving existing behaviour. Set to false to expose the Tenant Control Plane only
+ via the LoadBalancer IP and ClusterIP, without a per-node NodePort. This field is only
+ valid when serviceType is LoadBalancer; setting it with any other serviceType is
+ rejected by validation.
+ type: boolean
serviceType:
description: ServiceType allows specifying how to expose the Tenant Control Plane.
enum:
@@ -8695,6 +8705,8 @@ versions:
rule: '!has(self.networkProfile.loadBalancerSourceRanges) || (size(self.networkProfile.loadBalancerSourceRanges) == 0 || self.controlPlane.service.serviceType == ''LoadBalancer'')'
- message: LoadBalancerClass is supported only with LoadBalancer service type
rule: '!has(self.networkProfile.loadBalancerClass) || self.controlPlane.service.serviceType == ''LoadBalancer'''
+ - message: allocateLoadBalancerNodePorts is supported only with LoadBalancer service type
+ rule: '!has(self.controlPlane.service.allocateLoadBalancerNodePorts) || self.controlPlane.service.serviceType == ''LoadBalancer'''
- message: LoadBalancerClass cannot be set or unset at runtime
rule: self.controlPlane.service.serviceType != 'LoadBalancer' || (oldSelf.controlPlane.service.serviceType != 'LoadBalancer' && self.controlPlane.service.serviceType == 'LoadBalancer') || has(self.networkProfile.loadBalancerClass) == has(oldSelf.networkProfile.loadBalancerClass)
status:
diff --git a/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml b/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
index 431bd97..0e4411a 100644
--- a/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
+++ b/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
@@ -8347,6 +8347,16 @@ spec:
- targetPort
type: object
type: array
+ allocateLoadBalancerNodePorts:
+ description: |-
+ AllocateLoadBalancerNodePorts defines whether NodePorts are automatically allocated
+ for the Service when serviceType is LoadBalancer. It maps directly to the Service's
+ spec.allocateLoadBalancerNodePorts. When nil, the Kubernetes default (true) applies,
+ preserving existing behaviour. Set to false to expose the Tenant Control Plane only
+ via the LoadBalancer IP and ClusterIP, without a per-node NodePort. This field is only
+ valid when serviceType is LoadBalancer; setting it with any other serviceType is
+ rejected by validation.
+ type: boolean
serviceType:
description: ServiceType allows specifying how to expose the Tenant Control Plane.
enum:
@@ -8703,6 +8713,8 @@ spec:
rule: '!has(self.networkProfile.loadBalancerSourceRanges) || (size(self.networkProfile.loadBalancerSourceRanges) == 0 || self.controlPlane.service.serviceType == ''LoadBalancer'')'
- message: LoadBalancerClass is supported only with LoadBalancer service type
rule: '!has(self.networkProfile.loadBalancerClass) || self.controlPlane.service.serviceType == ''LoadBalancer'''
+ - message: allocateLoadBalancerNodePorts is supported only with LoadBalancer service type
+ rule: '!has(self.controlPlane.service.allocateLoadBalancerNodePorts) || self.controlPlane.service.serviceType == ''LoadBalancer'''
- message: LoadBalancerClass cannot be set or unset at runtime
rule: self.controlPlane.service.serviceType != 'LoadBalancer' || (oldSelf.controlPlane.service.serviceType != 'LoadBalancer' && self.controlPlane.service.serviceType == 'LoadBalancer') || has(self.networkProfile.loadBalancerClass) == has(oldSelf.networkProfile.loadBalancerClass)
status:
diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md
index 1daec4f..d5b9be3 100644
--- a/docs/content/reference/api.md
+++ b/docs/content/reference/api.md
@@ -31169,6 +31169,19 @@ Defining the options for the Tenant Control Plane Service resource.
which targets the Tenant Control Plane pods.