From e2897fb1fe495a2c7697c6e926717da7264868ab Mon Sep 17 00:00:00 2001
From: Ondrej Blazek
Date: Mon, 4 Mar 2024 15:01:26 +0100
Subject: [PATCH] feat: add configurable loadbalancer network
Previously when loadbalacer was created it used the same network/subnet as the
control plane nodes for the VIP. This was not always the right assumption as some
users might want to be able to customize this according to their env.
This commit fixes the above by adding two fields into
OpenStackClusterSpec/Status two fields `network` and `subnets` under
`APIServerLoadBalancer` so that user can define which network/subnet
to use for allocation of the loadbalancer.
Signed-off-by: Ondrej Blazek
---
api/v1alpha5/zz_generated.conversion.go | 3 +
api/v1alpha6/openstackcluster_conversion.go | 45 +++-
api/v1alpha6/zz_generated.conversion.go | 48 ++--
api/v1alpha7/openstackcluster_conversion.go | 24 ++
api/v1alpha7/zz_generated.conversion.go | 68 +++---
api/v1beta1/types.go | 17 ++
api/v1beta1/zz_generated.deepcopy.go | 17 ++
...re.cluster.x-k8s.io_openstackclusters.yaml | 216 ++++++++++++++++++
...er.x-k8s.io_openstackclustertemplates.yaml | 174 ++++++++++++++
controllers/openstackcluster_controller.go | 57 +++++
docs/book/src/api/v1beta1/api.md | 51 +++++
.../services/loadbalancer/loadbalancer.go | 42 +++-
.../loadbalancer/loadbalancer_test.go | 50 +++-
13 files changed, 738 insertions(+), 74 deletions(-)
diff --git a/api/v1alpha5/zz_generated.conversion.go b/api/v1alpha5/zz_generated.conversion.go
index cc9d3ff4cc..999c555fb4 100644
--- a/api/v1alpha5/zz_generated.conversion.go
+++ b/api/v1alpha5/zz_generated.conversion.go
@@ -456,6 +456,8 @@ func autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha5_APIServerLoadBalancer
out.AdditionalPorts = *(*[]int)(unsafe.Pointer(&in.AdditionalPorts))
out.AllowedCIDRs = *(*[]string)(unsafe.Pointer(&in.AllowedCIDRs))
// WARNING: in.Provider requires manual conversion: does not exist in peer-type
+ // WARNING: in.Network requires manual conversion: does not exist in peer-type
+ // WARNING: in.Subnets requires manual conversion: does not exist in peer-type
return nil
}
@@ -597,6 +599,7 @@ func autoConvert_v1beta1_LoadBalancer_To_v1alpha5_LoadBalancer(in *v1beta1.LoadB
out.InternalIP = in.InternalIP
out.AllowedCIDRs = *(*[]string)(unsafe.Pointer(&in.AllowedCIDRs))
// WARNING: in.Tags requires manual conversion: does not exist in peer-type
+ // WARNING: in.LoadBalancerNetwork requires manual conversion: does not exist in peer-type
return nil
}
diff --git a/api/v1alpha6/openstackcluster_conversion.go b/api/v1alpha6/openstackcluster_conversion.go
index d9c7070f66..16375c2c07 100644
--- a/api/v1alpha6/openstackcluster_conversion.go
+++ b/api/v1alpha6/openstackcluster_conversion.go
@@ -210,6 +210,13 @@ func restorev1beta1ClusterSpec(previous *infrav1.OpenStackClusterSpec, dst *infr
dst.APIServerLoadBalancer.Enabled = previous.APIServerLoadBalancer.Enabled
}
optional.RestoreString(&previous.APIServerLoadBalancer.Provider, &dst.APIServerLoadBalancer.Provider)
+
+ if previous.APIServerLoadBalancer.Network != nil {
+ dst.APIServerLoadBalancer.Network = previous.APIServerLoadBalancer.Network
+ }
+ if previous.APIServerLoadBalancer.Subnets != nil {
+ dst.APIServerLoadBalancer.Subnets = previous.APIServerLoadBalancer.Subnets
+ }
}
if dst.APIServerLoadBalancer.IsZero() {
dst.APIServerLoadBalancer = previous.APIServerLoadBalancer
@@ -294,6 +301,18 @@ func Convert_v1alpha6_OpenStackClusterSpec_To_v1beta1_OpenStackClusterSpec(in *O
return nil
}
+func Convert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer(in *infrav1.APIServerLoadBalancer, out *APIServerLoadBalancer, s apiconversion.Scope) error {
+ return autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer(in, out, s)
+}
+
+func Convert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer(in *infrav1.LoadBalancer, out *LoadBalancer, s apiconversion.Scope) error {
+ return autoConvert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer(in, out, s)
+}
+
+func Convert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in *APIServerLoadBalancer, out *infrav1.APIServerLoadBalancer, s apiconversion.Scope) error {
+ return autoConvert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in, out, s)
+}
+
func Convert_v1beta1_OpenStackClusterSpec_To_v1alpha6_OpenStackClusterSpec(in *infrav1.OpenStackClusterSpec, out *OpenStackClusterSpec, s apiconversion.Scope) error {
err := autoConvert_v1beta1_OpenStackClusterSpec_To_v1alpha6_OpenStackClusterSpec(in, out, s)
if err != nil {
@@ -375,6 +394,10 @@ func restorev1beta1ClusterStatus(previous *infrav1.OpenStackClusterStatus, dst *
dst.BastionSecurityGroup = previous.BastionSecurityGroup
restorev1beta1BastionStatus(previous.Bastion, dst.Bastion)
+
+ if previous.APIServerLoadBalancer != nil {
+ dst.APIServerLoadBalancer = previous.APIServerLoadBalancer
+ }
}
func Convert_v1beta1_OpenStackClusterStatus_To_v1alpha6_OpenStackClusterStatus(in *infrav1.OpenStackClusterStatus, out *OpenStackClusterStatus, s apiconversion.Scope) error {
@@ -390,7 +413,16 @@ func Convert_v1beta1_OpenStackClusterStatus_To_v1alpha6_OpenStackClusterStatus(i
}
out.Network.Router = (*Router)(in.Router)
- out.Network.APIServerLoadBalancer = (*LoadBalancer)(in.APIServerLoadBalancer)
+ if in.APIServerLoadBalancer != nil {
+ out.Network.APIServerLoadBalancer = &LoadBalancer{
+ Name: in.APIServerLoadBalancer.Name,
+ ID: in.APIServerLoadBalancer.ID,
+ IP: in.APIServerLoadBalancer.IP,
+ InternalIP: in.APIServerLoadBalancer.InternalIP,
+ AllowedCIDRs: in.APIServerLoadBalancer.AllowedCIDRs,
+ Tags: in.APIServerLoadBalancer.Tags,
+ }
+ }
}
return nil
@@ -405,7 +437,16 @@ func Convert_v1alpha6_OpenStackClusterStatus_To_v1beta1_OpenStackClusterStatus(i
// Router and APIServerLoadBalancer have been moved out of Network in v1beta1
if in.Network != nil {
out.Router = (*infrav1.Router)(in.Network.Router)
- out.APIServerLoadBalancer = (*infrav1.LoadBalancer)(in.Network.APIServerLoadBalancer)
+ if in.Network.APIServerLoadBalancer != nil {
+ out.APIServerLoadBalancer = &infrav1.LoadBalancer{
+ Name: in.Network.APIServerLoadBalancer.Name,
+ ID: in.Network.APIServerLoadBalancer.ID,
+ IP: in.Network.APIServerLoadBalancer.IP,
+ InternalIP: in.Network.APIServerLoadBalancer.InternalIP,
+ AllowedCIDRs: in.Network.APIServerLoadBalancer.AllowedCIDRs,
+ Tags: in.Network.APIServerLoadBalancer.Tags,
+ }
+ }
}
return nil
diff --git a/api/v1alpha6/zz_generated.conversion.go b/api/v1alpha6/zz_generated.conversion.go
index 046dfa05fe..93dfea061a 100644
--- a/api/v1alpha6/zz_generated.conversion.go
+++ b/api/v1alpha6/zz_generated.conversion.go
@@ -41,16 +41,6 @@ func init() {
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
- if err := s.AddGeneratedConversionFunc((*APIServerLoadBalancer)(nil), (*v1beta1.APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
- return Convert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(a.(*APIServerLoadBalancer), b.(*v1beta1.APIServerLoadBalancer), scope)
- }); err != nil {
- return err
- }
- if err := s.AddGeneratedConversionFunc((*v1beta1.APIServerLoadBalancer)(nil), (*APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
- return Convert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer(a.(*v1beta1.APIServerLoadBalancer), b.(*APIServerLoadBalancer), scope)
- }); err != nil {
- return err
- }
if err := s.AddGeneratedConversionFunc((*AddressPair)(nil), (*v1beta1.AddressPair)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha6_AddressPair_To_v1beta1_AddressPair(a.(*AddressPair), b.(*v1beta1.AddressPair), scope)
}); err != nil {
@@ -86,11 +76,6 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
- if err := s.AddGeneratedConversionFunc((*v1beta1.LoadBalancer)(nil), (*LoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
- return Convert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer(a.(*v1beta1.LoadBalancer), b.(*LoadBalancer), scope)
- }); err != nil {
- return err
- }
if err := s.AddGeneratedConversionFunc((*OpenStackCluster)(nil), (*v1beta1.OpenStackCluster)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha6_OpenStackCluster_To_v1beta1_OpenStackCluster(a.(*OpenStackCluster), b.(*v1beta1.OpenStackCluster), scope)
}); err != nil {
@@ -246,6 +231,11 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
+ if err := s.AddConversionFunc((*APIServerLoadBalancer)(nil), (*v1beta1.APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(a.(*APIServerLoadBalancer), b.(*v1beta1.APIServerLoadBalancer), scope)
+ }); err != nil {
+ return err
+ }
if err := s.AddConversionFunc((*Bastion)(nil), (*v1beta1.Bastion)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha6_Bastion_To_v1beta1_Bastion(a.(*Bastion), b.(*v1beta1.Bastion), scope)
}); err != nil {
@@ -341,6 +331,11 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
+ if err := s.AddConversionFunc((*v1beta1.APIServerLoadBalancer)(nil), (*APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer(a.(*v1beta1.APIServerLoadBalancer), b.(*APIServerLoadBalancer), scope)
+ }); err != nil {
+ return err
+ }
if err := s.AddConversionFunc((*v1beta1.BastionStatus)(nil), (*Instance)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_BastionStatus_To_v1alpha6_Instance(a.(*v1beta1.BastionStatus), b.(*Instance), scope)
}); err != nil {
@@ -351,6 +346,11 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
+ if err := s.AddConversionFunc((*v1beta1.LoadBalancer)(nil), (*LoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer(a.(*v1beta1.LoadBalancer), b.(*LoadBalancer), scope)
+ }); err != nil {
+ return err
+ }
if err := s.AddConversionFunc((*v1beta1.NetworkFilter)(nil), (*NetworkFilter)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_NetworkFilter_To_v1alpha6_NetworkFilter(a.(*v1beta1.NetworkFilter), b.(*NetworkFilter), scope)
}); err != nil {
@@ -461,11 +461,6 @@ func autoConvert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer
return nil
}
-// Convert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer is an autogenerated conversion function.
-func Convert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in *APIServerLoadBalancer, out *v1beta1.APIServerLoadBalancer, s conversion.Scope) error {
- return autoConvert_v1alpha6_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in, out, s)
-}
-
func autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer(in *v1beta1.APIServerLoadBalancer, out *APIServerLoadBalancer, s conversion.Scope) error {
if err := v1.Convert_Pointer_bool_To_bool(&in.Enabled, &out.Enabled, s); err != nil {
return err
@@ -475,14 +470,11 @@ func autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer
if err := optional.Convert_optional_String_To_string(&in.Provider, &out.Provider, s); err != nil {
return err
}
+ // WARNING: in.Network requires manual conversion: does not exist in peer-type
+ // WARNING: in.Subnets requires manual conversion: does not exist in peer-type
return nil
}
-// Convert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer is an autogenerated conversion function.
-func Convert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer(in *v1beta1.APIServerLoadBalancer, out *APIServerLoadBalancer, s conversion.Scope) error {
- return autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha6_APIServerLoadBalancer(in, out, s)
-}
-
func autoConvert_v1alpha6_AddressPair_To_v1beta1_AddressPair(in *AddressPair, out *v1beta1.AddressPair, s conversion.Scope) error {
out.IPAddress = in.IPAddress
if err := optional.Convert_string_To_optional_String(&in.MACAddress, &out.MACAddress, s); err != nil {
@@ -622,14 +614,10 @@ func autoConvert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer(in *v1beta1.LoadB
out.InternalIP = in.InternalIP
out.AllowedCIDRs = *(*[]string)(unsafe.Pointer(&in.AllowedCIDRs))
out.Tags = *(*[]string)(unsafe.Pointer(&in.Tags))
+ // WARNING: in.LoadBalancerNetwork requires manual conversion: does not exist in peer-type
return nil
}
-// Convert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer is an autogenerated conversion function.
-func Convert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer(in *v1beta1.LoadBalancer, out *LoadBalancer, s conversion.Scope) error {
- return autoConvert_v1beta1_LoadBalancer_To_v1alpha6_LoadBalancer(in, out, s)
-}
-
func autoConvert_v1alpha6_NetworkFilter_To_v1beta1_NetworkFilter(in *NetworkFilter, out *v1beta1.NetworkFilter, s conversion.Scope) error {
out.Name = in.Name
out.Description = in.Description
diff --git a/api/v1alpha7/openstackcluster_conversion.go b/api/v1alpha7/openstackcluster_conversion.go
index 09d641f5ab..f273d98760 100644
--- a/api/v1alpha7/openstackcluster_conversion.go
+++ b/api/v1alpha7/openstackcluster_conversion.go
@@ -216,6 +216,14 @@ func restorev1beta1ClusterSpec(previous *infrav1.OpenStackClusterSpec, dst *infr
dst.APIServerLoadBalancer.Enabled = previous.APIServerLoadBalancer.Enabled
}
optional.RestoreString(&previous.APIServerLoadBalancer.Provider, &dst.APIServerLoadBalancer.Provider)
+
+ if previous.APIServerLoadBalancer.Network != nil {
+ dst.APIServerLoadBalancer.Network = previous.APIServerLoadBalancer.Network
+ }
+
+ if previous.APIServerLoadBalancer.Subnets != nil {
+ dst.APIServerLoadBalancer.Subnets = previous.APIServerLoadBalancer.Subnets
+ }
}
if dst.APIServerLoadBalancer.IsZero() {
dst.APIServerLoadBalancer = previous.APIServerLoadBalancer
@@ -300,6 +308,18 @@ func Convert_v1alpha7_OpenStackClusterSpec_To_v1beta1_OpenStackClusterSpec(in *O
return nil
}
+func Convert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(in *infrav1.LoadBalancer, out *LoadBalancer, s apiconversion.Scope) error {
+ return autoConvert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(in, out, s)
+}
+
+func Convert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer(in *infrav1.APIServerLoadBalancer, out *APIServerLoadBalancer, s apiconversion.Scope) error {
+ return autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer(in, out, s)
+}
+
+func Convert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in *APIServerLoadBalancer, out *infrav1.APIServerLoadBalancer, s apiconversion.Scope) error {
+ return autoConvert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in, out, s)
+}
+
func Convert_v1beta1_OpenStackClusterSpec_To_v1alpha7_OpenStackClusterSpec(in *infrav1.OpenStackClusterSpec, out *OpenStackClusterSpec, s apiconversion.Scope) error {
err := autoConvert_v1beta1_OpenStackClusterSpec_To_v1alpha7_OpenStackClusterSpec(in, out, s)
if err != nil {
@@ -362,6 +382,10 @@ func restorev1beta1ClusterStatus(previous *infrav1.OpenStackClusterStatus, dst *
}
restorev1beta1BastionStatus(previous.Bastion, dst.Bastion)
+
+ if previous.APIServerLoadBalancer != nil {
+ dst.APIServerLoadBalancer = previous.APIServerLoadBalancer
+ }
}
func Convert_v1beta1_OpenStackClusterStatus_To_v1alpha7_OpenStackClusterStatus(in *infrav1.OpenStackClusterStatus, out *OpenStackClusterStatus, s apiconversion.Scope) error {
diff --git a/api/v1alpha7/zz_generated.conversion.go b/api/v1alpha7/zz_generated.conversion.go
index ca0e36a024..407c862a75 100644
--- a/api/v1alpha7/zz_generated.conversion.go
+++ b/api/v1alpha7/zz_generated.conversion.go
@@ -41,16 +41,6 @@ func init() {
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
- if err := s.AddGeneratedConversionFunc((*APIServerLoadBalancer)(nil), (*v1beta1.APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
- return Convert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(a.(*APIServerLoadBalancer), b.(*v1beta1.APIServerLoadBalancer), scope)
- }); err != nil {
- return err
- }
- if err := s.AddGeneratedConversionFunc((*v1beta1.APIServerLoadBalancer)(nil), (*APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
- return Convert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer(a.(*v1beta1.APIServerLoadBalancer), b.(*APIServerLoadBalancer), scope)
- }); err != nil {
- return err
- }
if err := s.AddGeneratedConversionFunc((*AdditionalBlockDevice)(nil), (*v1beta1.AdditionalBlockDevice)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha7_AdditionalBlockDevice_To_v1beta1_AdditionalBlockDevice(a.(*AdditionalBlockDevice), b.(*v1beta1.AdditionalBlockDevice), scope)
}); err != nil {
@@ -131,11 +121,6 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
- if err := s.AddGeneratedConversionFunc((*v1beta1.LoadBalancer)(nil), (*LoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
- return Convert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(a.(*v1beta1.LoadBalancer), b.(*LoadBalancer), scope)
- }); err != nil {
- return err
- }
if err := s.AddGeneratedConversionFunc((*NetworkStatus)(nil), (*v1beta1.NetworkStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha7_NetworkStatus_To_v1beta1_NetworkStatus(a.(*NetworkStatus), b.(*v1beta1.NetworkStatus), scope)
}); err != nil {
@@ -316,6 +301,11 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
+ if err := s.AddConversionFunc((*APIServerLoadBalancer)(nil), (*v1beta1.APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(a.(*APIServerLoadBalancer), b.(*v1beta1.APIServerLoadBalancer), scope)
+ }); err != nil {
+ return err
+ }
if err := s.AddConversionFunc((*Bastion)(nil), (*v1beta1.Bastion)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha7_Bastion_To_v1beta1_Bastion(a.(*Bastion), b.(*v1beta1.Bastion), scope)
}); err != nil {
@@ -391,6 +381,11 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
+ if err := s.AddConversionFunc((*v1beta1.APIServerLoadBalancer)(nil), (*APIServerLoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer(a.(*v1beta1.APIServerLoadBalancer), b.(*APIServerLoadBalancer), scope)
+ }); err != nil {
+ return err
+ }
if err := s.AddConversionFunc((*v1beta1.BastionStatus)(nil), (*BastionStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_BastionStatus_To_v1alpha7_BastionStatus(a.(*v1beta1.BastionStatus), b.(*BastionStatus), scope)
}); err != nil {
@@ -401,6 +396,11 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
+ if err := s.AddConversionFunc((*v1beta1.LoadBalancer)(nil), (*LoadBalancer)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(a.(*v1beta1.LoadBalancer), b.(*LoadBalancer), scope)
+ }); err != nil {
+ return err
+ }
if err := s.AddConversionFunc((*v1beta1.NetworkFilter)(nil), (*NetworkFilter)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_NetworkFilter_To_v1alpha7_NetworkFilter(a.(*v1beta1.NetworkFilter), b.(*NetworkFilter), scope)
}); err != nil {
@@ -496,11 +496,6 @@ func autoConvert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer
return nil
}
-// Convert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer is an autogenerated conversion function.
-func Convert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in *APIServerLoadBalancer, out *v1beta1.APIServerLoadBalancer, s conversion.Scope) error {
- return autoConvert_v1alpha7_APIServerLoadBalancer_To_v1beta1_APIServerLoadBalancer(in, out, s)
-}
-
func autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer(in *v1beta1.APIServerLoadBalancer, out *APIServerLoadBalancer, s conversion.Scope) error {
if err := v1.Convert_Pointer_bool_To_bool(&in.Enabled, &out.Enabled, s); err != nil {
return err
@@ -510,14 +505,11 @@ func autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer
if err := optional.Convert_optional_String_To_string(&in.Provider, &out.Provider, s); err != nil {
return err
}
+ // WARNING: in.Network requires manual conversion: does not exist in peer-type
+ // WARNING: in.Subnets requires manual conversion: does not exist in peer-type
return nil
}
-// Convert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer is an autogenerated conversion function.
-func Convert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer(in *v1beta1.APIServerLoadBalancer, out *APIServerLoadBalancer, s conversion.Scope) error {
- return autoConvert_v1beta1_APIServerLoadBalancer_To_v1alpha7_APIServerLoadBalancer(in, out, s)
-}
-
func autoConvert_v1alpha7_AdditionalBlockDevice_To_v1beta1_AdditionalBlockDevice(in *AdditionalBlockDevice, out *v1beta1.AdditionalBlockDevice, s conversion.Scope) error {
out.Name = in.Name
out.SizeGiB = in.SizeGiB
@@ -786,14 +778,10 @@ func autoConvert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(in *v1beta1.LoadB
out.InternalIP = in.InternalIP
out.AllowedCIDRs = *(*[]string)(unsafe.Pointer(&in.AllowedCIDRs))
out.Tags = *(*[]string)(unsafe.Pointer(&in.Tags))
+ // WARNING: in.LoadBalancerNetwork requires manual conversion: does not exist in peer-type
return nil
}
-// Convert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer is an autogenerated conversion function.
-func Convert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(in *v1beta1.LoadBalancer, out *LoadBalancer, s conversion.Scope) error {
- return autoConvert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(in, out, s)
-}
-
func autoConvert_v1alpha7_NetworkFilter_To_v1beta1_NetworkFilter(in *NetworkFilter, out *v1beta1.NetworkFilter, s conversion.Scope) error {
out.Name = in.Name
out.Description = in.Description
@@ -1075,7 +1063,15 @@ func autoConvert_v1alpha7_OpenStackClusterStatus_To_v1beta1_OpenStackClusterStat
out.Network = (*v1beta1.NetworkStatusWithSubnets)(unsafe.Pointer(in.Network))
out.ExternalNetwork = (*v1beta1.NetworkStatus)(unsafe.Pointer(in.ExternalNetwork))
out.Router = (*v1beta1.Router)(unsafe.Pointer(in.Router))
- out.APIServerLoadBalancer = (*v1beta1.LoadBalancer)(unsafe.Pointer(in.APIServerLoadBalancer))
+ if in.APIServerLoadBalancer != nil {
+ in, out := &in.APIServerLoadBalancer, &out.APIServerLoadBalancer
+ *out = new(v1beta1.LoadBalancer)
+ if err := Convert_v1alpha7_LoadBalancer_To_v1beta1_LoadBalancer(*in, *out, s); err != nil {
+ return err
+ }
+ } else {
+ out.APIServerLoadBalancer = nil
+ }
out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains))
if in.ControlPlaneSecurityGroup != nil {
in, out := &in.ControlPlaneSecurityGroup, &out.ControlPlaneSecurityGroup
@@ -1128,7 +1124,15 @@ func autoConvert_v1beta1_OpenStackClusterStatus_To_v1alpha7_OpenStackClusterStat
out.Network = (*NetworkStatusWithSubnets)(unsafe.Pointer(in.Network))
out.ExternalNetwork = (*NetworkStatus)(unsafe.Pointer(in.ExternalNetwork))
out.Router = (*Router)(unsafe.Pointer(in.Router))
- out.APIServerLoadBalancer = (*LoadBalancer)(unsafe.Pointer(in.APIServerLoadBalancer))
+ if in.APIServerLoadBalancer != nil {
+ in, out := &in.APIServerLoadBalancer, &out.APIServerLoadBalancer
+ *out = new(LoadBalancer)
+ if err := Convert_v1beta1_LoadBalancer_To_v1alpha7_LoadBalancer(*in, *out, s); err != nil {
+ return err
+ }
+ } else {
+ out.APIServerLoadBalancer = nil
+ }
out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains))
if in.ControlPlaneSecurityGroup != nil {
in, out := &in.ControlPlaneSecurityGroup, &out.ControlPlaneSecurityGroup
diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go
index 9bf90d826d..b141d2731b 100644
--- a/api/v1beta1/types.go
+++ b/api/v1beta1/types.go
@@ -632,6 +632,12 @@ type LoadBalancer struct {
AllowedCIDRs []string `json:"allowedCIDRs,omitempty"`
//+optional
Tags []string `json:"tags,omitempty"`
+ // LoadBalancerNetwork contains information about network and/or subnets which the
+ // loadbalancer is allocated on.
+ // If subnets are specified within the LoadBalancerNetwork currently only the first
+ // subnet in the list is taken into account.
+ // +optional
+ LoadBalancerNetwork *NetworkStatusWithSubnets `json:"loadBalancerNetwork,omitempty"`
}
// SecurityGroupStatus represents the basic information of the associated
@@ -802,6 +808,17 @@ type APIServerLoadBalancer struct {
// specified.
// +optional
Provider optional.String `json:"provider,omitempty"`
+
+ // Network defines which network should the load balancer be allocated on.
+ //+optional
+ Network *NetworkParam `json:"network,omitempty"`
+ // Subnets define which subnets should the load balancer be allocated on.
+ // It is expected that subnets are located on the network specified in this resource.
+ // Only the first element is taken into account.
+ // +optional
+ // +listType=atomic
+ // kubebuilder:validation:MaxLength:=2
+ Subnets []SubnetParam `json:"subnets,omitempty"`
}
func (s *APIServerLoadBalancer) IsZero() bool {
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index c75e06db9e..c1a66427d4 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -50,6 +50,18 @@ func (in *APIServerLoadBalancer) DeepCopyInto(out *APIServerLoadBalancer) {
*out = new(string)
**out = **in
}
+ if in.Network != nil {
+ in, out := &in.Network, &out.Network
+ *out = new(NetworkParam)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Subnets != nil {
+ in, out := &in.Subnets, &out.Subnets
+ *out = make([]SubnetParam, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerLoadBalancer.
@@ -372,6 +384,11 @@ func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) {
*out = make([]string, len(*in))
copy(*out, *in)
}
+ if in.LoadBalancerNetwork != nil {
+ in, out := &in.LoadBalancerNetwork, &out.LoadBalancerNetwork
+ *out = new(NetworkStatusWithSubnets)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancer.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml
index 6211a5157b..37e4f575e3 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml
@@ -4893,12 +4893,185 @@ spec:
API server loadbalancer, omit the APIServerLoadBalancer field in the
cluster spec instead.
type: boolean
+ network:
+ description: Network defines which network should the load balancer
+ be allocated on.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select an OpenStack
+ network. If provided, cannot be empty.
+ minProperties: 1
+ properties:
+ description:
+ type: string
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the ID of the network to use. If ID is
+ provided, the other filters cannot be provided. Must be
+ in UUID format.
+ format: uuid
+ type: string
+ type: object
provider:
description: |-
Provider specifies name of a specific Octavia provider to use for the
API load balancer. The Octavia default will be used if it is not
specified.
type: string
+ subnets:
+ description: |-
+ Subnets define which subnets should the load balancer be allocated on.
+ It is expected that subnets are located on the network specified in this resource.
+ Only the first element is taken into account.
+ kubebuilder:validation:MaxLength:=2
+ items:
+ description: SubnetParam specifies an OpenStack subnet to use.
+ It may be specified by either ID or filter, but not both.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select the subnet.
+ It must match exactly one subnet.
+ minProperties: 1
+ properties:
+ cidr:
+ type: string
+ description:
+ type: string
+ gatewayIP:
+ type: string
+ ipVersion:
+ type: integer
+ ipv6AddressMode:
+ type: string
+ ipv6RAMode:
+ type: string
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the uuid of the subnet. It will not be
+ validated.
+ format: uuid
+ type: string
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
required:
- enabled
type: object
@@ -6355,6 +6528,49 @@ spec:
type: string
ip:
type: string
+ loadBalancerNetwork:
+ description: |-
+ LoadBalancerNetwork contains information about network and/or subnets which the
+ loadbalancer is allocated on.
+ If subnets are specified within the LoadBalancerNetwork currently only the first
+ subnet in the list is taken into account.
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ subnets:
+ description: Subnets is a list of subnets associated with
+ the default cluster network. Machines which use the default
+ cluster network will get an address from all of these subnets.
+ items:
+ description: Subnet represents basic information about the
+ associated OpenStack Neutron Subnet.
+ properties:
+ cidr:
+ type: string
+ id:
+ type: string
+ name:
+ type: string
+ tags:
+ items:
+ type: string
+ type: array
+ required:
+ - cidr
+ - id
+ - name
+ type: object
+ type: array
+ tags:
+ items:
+ type: string
+ type: array
+ required:
+ - id
+ - name
+ type: object
name:
type: string
tags:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml
index ccc9da38b0..cebc2b2a04 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml
@@ -2317,12 +2317,186 @@ spec:
API server loadbalancer, omit the APIServerLoadBalancer field in the
cluster spec instead.
type: boolean
+ network:
+ description: Network defines which network should the
+ load balancer be allocated on.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select an
+ OpenStack network. If provided, cannot be empty.
+ minProperties: 1
+ properties:
+ description:
+ type: string
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the ID of the network to use. If
+ ID is provided, the other filters cannot be provided.
+ Must be in UUID format.
+ format: uuid
+ type: string
+ type: object
provider:
description: |-
Provider specifies name of a specific Octavia provider to use for the
API load balancer. The Octavia default will be used if it is not
specified.
type: string
+ subnets:
+ description: |-
+ Subnets define which subnets should the load balancer be allocated on.
+ It is expected that subnets are located on the network specified in this resource.
+ Only the first element is taken into account.
+ kubebuilder:validation:MaxLength:=2
+ items:
+ description: SubnetParam specifies an OpenStack subnet
+ to use. It may be specified by either ID or filter,
+ but not both.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: Filter specifies a filter to select
+ the subnet. It must match exactly one subnet.
+ minProperties: 1
+ properties:
+ cidr:
+ type: string
+ description:
+ type: string
+ gatewayIP:
+ type: string
+ ipVersion:
+ type: integer
+ ipv6AddressMode:
+ type: string
+ ipv6RAMode:
+ type: string
+ name:
+ type: string
+ notTags:
+ description: |-
+ NotTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ NotTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ projectID:
+ type: string
+ tags:
+ description: |-
+ Tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ TagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ description: |-
+ NeutronTag represents a tag on a Neutron resource.
+ It may not be empty and may not contain commas.
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ type: object
+ id:
+ description: ID is the uuid of the subnet. It will
+ not be validated.
+ format: uuid
+ type: string
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
required:
- enabled
type: object
diff --git a/controllers/openstackcluster_controller.go b/controllers/openstackcluster_controller.go
index 3823b6ec3a..56831c714a 100644
--- a/controllers/openstackcluster_controller.go
+++ b/controllers/openstackcluster_controller.go
@@ -619,6 +619,57 @@ func bastionHashHasChanged(computeHash string, clusterAnnotations map[string]str
return latestHash != computeHash
}
+func resolveLoadBalancerNetwork(openStackCluster *infrav1.OpenStackCluster, networkingService *networking.Service) error {
+ lbSpec := openStackCluster.Spec.APIServerLoadBalancer
+ if lbSpec.IsEnabled() {
+ lbStatus := openStackCluster.Status.APIServerLoadBalancer
+ if lbStatus == nil {
+ lbStatus = &infrav1.LoadBalancer{}
+ openStackCluster.Status.APIServerLoadBalancer = lbStatus
+ }
+
+ lbNetStatus := lbStatus.LoadBalancerNetwork
+ if lbNetStatus == nil {
+ lbNetStatus = &infrav1.NetworkStatusWithSubnets{
+ NetworkStatus: infrav1.NetworkStatus{},
+ }
+ }
+
+ if lbSpec.Network != nil {
+ lbNet, err := networkingService.GetNetworkByParam(lbSpec.Network)
+ if err != nil {
+ handleUpdateOSCError(openStackCluster, fmt.Errorf("failed to find loadbalancer network: %w", err))
+ return fmt.Errorf("failed to find network: %w", err)
+ }
+
+ lbNetStatus.Name = lbNet.Name
+ lbNetStatus.ID = lbNet.ID
+ lbNetStatus.Tags = lbNet.Tags
+
+ // Filter out only relevant subnets specified by the spec
+ lbNetStatus.Subnets = []infrav1.Subnet{}
+ for _, s := range lbSpec.Subnets {
+ matchFound := false
+ for _, subnetID := range lbNet.Subnets {
+ if s.ID != nil && subnetID == *s.ID {
+ matchFound = true
+ lbNetStatus.Subnets = append(
+ lbNetStatus.Subnets, infrav1.Subnet{
+ ID: *s.ID,
+ })
+ }
+ }
+ if !matchFound {
+ handleUpdateOSCError(openStackCluster, fmt.Errorf("no subnet match was found in the specified network (specified subnet: %v, available subnets: %v)", s, lbNet.Subnets))
+ return fmt.Errorf("no subnet match was found in the specified network (specified subnet: %v, available subnets: %v)", s, lbNet.Subnets)
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
func reconcileNetworkComponents(scope *scope.WithLogger, cluster *clusterv1.Cluster, openStackCluster *infrav1.OpenStackCluster) error {
clusterResourceName := names.ClusterResourceName(cluster)
@@ -647,6 +698,12 @@ func reconcileNetworkComponents(scope *scope.WithLogger, cluster *clusterv1.Clus
return fmt.Errorf("failed to reconcile network: ManagedSubnets only supports one element, %d provided", len(openStackCluster.Spec.ManagedSubnets))
}
+ err = resolveLoadBalancerNetwork(openStackCluster, networkingService)
+ if err != nil {
+ handleUpdateOSCError(openStackCluster, fmt.Errorf("failed to reconcile loadbalancer network: %w", err))
+ return fmt.Errorf("failed to reconcile loadbalancer network: %w", err)
+ }
+
err = networkingService.ReconcileSecurityGroups(openStackCluster, clusterResourceName)
if err != nil {
handleUpdateOSCError(openStackCluster, fmt.Errorf("failed to reconcile security groups: %w", err))
diff --git a/docs/book/src/api/v1beta1/api.md b/docs/book/src/api/v1beta1/api.md
index 51eccf2f6c..087efc5310 100644
--- a/docs/book/src/api/v1beta1/api.md
+++ b/docs/book/src/api/v1beta1/api.md
@@ -902,6 +902,37 @@ API load balancer. The Octavia default will be used if it is not
specified.
+
+
+network
+
+
+NetworkParam
+
+
+ |
+
+(Optional)
+ Network defines which network should the load balancer be allocated on.
+ |
+
+
+
+subnets
+
+
+[]SubnetParam
+
+
+ |
+
+(Optional)
+ Subnets define which subnets should the load balancer be allocated on.
+It is expected that subnets are located on the network specified in this resource.
+Only the first element is taken into account.
+kubebuilder:validation:MaxLength:=2
+ |
+
AdditionalBlockDevice
@@ -1750,6 +1781,23 @@ string
(Optional)
+
+
+loadBalancerNetwork
+
+
+NetworkStatusWithSubnets
+
+
+ |
+
+(Optional)
+ LoadBalancerNetwork contains information about network and/or subnets which the
+loadbalancer is allocated on.
+If subnets are specified within the LoadBalancerNetwork currently only the first
+subnet in the list is taken into account.
+ |
+
MachineResources
@@ -1905,6 +1953,7 @@ FilterByNeutronTags
(Appears on:
+APIServerLoadBalancer,
OpenStackClusterSpec,
PortOpts)
@@ -2002,6 +2051,7 @@ string
(Appears on:
+LoadBalancer,
OpenStackClusterStatus)
@@ -5044,6 +5094,7 @@ FilterByNeutronTags
(Appears on:
+APIServerLoadBalancer,
ExternalRouterIPParam,
FixedIP,
OpenStackClusterSpec)
diff --git a/pkg/cloud/services/loadbalancer/loadbalancer.go b/pkg/cloud/services/loadbalancer/loadbalancer.go
index 2de72c81d6..cd309221b4 100644
--- a/pkg/cloud/services/loadbalancer/loadbalancer.go
+++ b/pkg/cloud/services/loadbalancer/loadbalancer.go
@@ -270,9 +270,34 @@ func (s *Service) getOrCreateAPILoadBalancer(openStackCluster *infrav1.OpenStack
return nil, fmt.Errorf("network is not yet available in OpenStackCluster.Status")
}
- // Create the VIP on the first cluster subnet
- subnetID := openStackCluster.Status.Network.Subnets[0].ID
- s.scope.Logger().Info("Creating load balancer in subnet", "subnetID", subnetID, "name", loadBalancerName)
+ if openStackCluster.Status.APIServerLoadBalancer == nil {
+ return nil, fmt.Errorf("apiserver loadbalancer network is not yet available in OpenStackCluster.Status")
+ }
+
+ lbNetwork := openStackCluster.Status.APIServerLoadBalancer.LoadBalancerNetwork
+ if lbNetwork == nil {
+ lbNetwork = &infrav1.NetworkStatusWithSubnets{}
+ openStackCluster.Status.APIServerLoadBalancer.LoadBalancerNetwork = lbNetwork
+ }
+
+ var vipNetworkID, vipSubnetID string
+ if lbNetwork.ID != "" {
+ vipNetworkID = lbNetwork.ID
+ }
+
+ if len(lbNetwork.Subnets) > 0 {
+ // Currently only the first subnet is taken into account.
+ // This can be fixed as soon as we switch over to gophercloud release that
+ // contains AdditionalVips field.
+ vipSubnetID = lbNetwork.Subnets[0].ID
+ }
+
+ if vipNetworkID == "" && vipSubnetID == "" {
+ // keep the default and create the VIP on the first cluster subnet
+ vipSubnetID = openStackCluster.Status.Network.Subnets[0].ID
+ }
+
+ s.scope.Logger().Info("Creating load balancer in subnet", "subnetID", vipSubnetID, "name", loadBalancerName)
providers, err := s.loadbalancerClient.ListLoadBalancerProviders()
if err != nil {
@@ -300,11 +325,12 @@ func (s *Service) getOrCreateAPILoadBalancer(openStackCluster *infrav1.OpenStack
}
lbCreateOpts := loadbalancers.CreateOpts{
- Name: loadBalancerName,
- VipSubnetID: subnetID,
- Description: names.GetDescription(clusterResourceName),
- Provider: lbProvider,
- Tags: openStackCluster.Spec.Tags,
+ Name: loadBalancerName,
+ VipSubnetID: vipSubnetID,
+ VipNetworkID: vipNetworkID,
+ Description: names.GetDescription(clusterResourceName),
+ Provider: lbProvider,
+ Tags: openStackCluster.Spec.Tags,
}
if vipAddress != nil {
lbCreateOpts.VipAddress = *vipAddress
diff --git a/pkg/cloud/services/loadbalancer/loadbalancer_test.go b/pkg/cloud/services/loadbalancer/loadbalancer_test.go
index ce46235d82..3768884adb 100644
--- a/pkg/cloud/services/loadbalancer/loadbalancer_test.go
+++ b/pkg/cloud/services/loadbalancer/loadbalancer_test.go
@@ -475,17 +475,63 @@ func Test_getOrCreateAPILoadBalancer(t *testing.T) {
{ID: "aaaaaaaa-bbbb-cccc-dddd-333333333333"},
},
},
+ APIServerLoadBalancer: &infrav1.LoadBalancer{
+ LoadBalancerNetwork: nil,
+ },
},
},
expectLoadBalancer: func(m *mock.MockLbClientMockRecorder) {
m.ListLoadBalancers(gomock.Any()).Return([]loadbalancers.LoadBalancer{}, nil)
m.ListLoadBalancerProviders().Return(octaviaProviders, nil)
m.CreateLoadBalancer(gomock.Any()).Return(&loadbalancers.LoadBalancer{
- ID: "AAAAA",
+ ID: "AAAAA",
+ VipSubnetID: "aaaaaaaa-bbbb-cccc-dddd-222222222222",
}, nil)
},
want: &loadbalancers.LoadBalancer{
- ID: "AAAAA",
+ ID: "AAAAA",
+ VipSubnetID: "aaaaaaaa-bbbb-cccc-dddd-222222222222",
+ },
+ },
+ {
+ name: "loadbalancer on a specific network created",
+ openStackCluster: &infrav1.OpenStackCluster{
+ Status: infrav1.OpenStackClusterStatus{
+ Network: &infrav1.NetworkStatusWithSubnets{
+ Subnets: []infrav1.Subnet{
+ {ID: "aaaaaaaa-bbbb-cccc-dddd-222222222222"},
+ },
+ },
+ APIServerLoadBalancer: &infrav1.LoadBalancer{
+ LoadBalancerNetwork: &infrav1.NetworkStatusWithSubnets{
+ NetworkStatus: infrav1.NetworkStatus{
+ Name: "VIPNET",
+ ID: "VIPNET",
+ },
+ Subnets: []infrav1.Subnet{
+ {
+ Name: "vip-subnet",
+ CIDR: "10.0.0.0/24",
+ ID: "VIPSUBNET",
+ },
+ },
+ },
+ },
+ },
+ },
+ expectLoadBalancer: func(m *mock.MockLbClientMockRecorder) {
+ m.ListLoadBalancers(gomock.Any()).Return([]loadbalancers.LoadBalancer{}, nil)
+ m.ListLoadBalancerProviders().Return(octaviaProviders, nil)
+ m.CreateLoadBalancer(gomock.Any()).Return(&loadbalancers.LoadBalancer{
+ ID: "AAAAA",
+ VipSubnetID: "VIPSUBNET",
+ VipNetworkID: "VIPNET",
+ }, nil)
+ },
+ want: &loadbalancers.LoadBalancer{
+ ID: "AAAAA",
+ VipSubnetID: "VIPSUBNET",
+ VipNetworkID: "VIPNET",
},
},
}