From 7dce82c232b08c05eab8db38f726862a6204cb47 Mon Sep 17 00:00:00 2001 From: bilbo Date: Wed, 24 Jan 2024 13:15:22 +0100 Subject: [PATCH] Get floating IP for openstack machine from IPAM provider --- api/v1alpha5/zz_generated.conversion.go | 1 + api/v1alpha6/openstackmachine_conversion.go | 1 + api/v1alpha6/zz_generated.conversion.go | 1 + api/v1alpha7/openstackmachine_conversion.go | 1 + api/v1alpha7/zz_generated.conversion.go | 1 + api/v1beta1/conditions_consts.go | 9 + api/v1beta1/openstackmachine_types.go | 9 +- api/v1beta1/zz_generated.deepcopy.go | 5 + ...re.cluster.x-k8s.io_openstackclusters.yaml | 23 +++ ...er.x-k8s.io_openstackclustertemplates.yaml | 25 +++ ...re.cluster.x-k8s.io_openstackmachines.yaml | 23 +++ ...er.x-k8s.io_openstackmachinetemplates.yaml | 23 +++ config/rbac/role.yaml | 1 + controllers/openstackmachine_controller.go | 190 +++++++++++++++++- docs/book/src/api/v1beta1/api.md | 42 ++++ .../topics/crd-changes/v1alpha7-to-v1beta1.md | 18 ++ pkg/cloud/services/networking/port.go | 51 +++++ pkg/utils/names/names.go | 13 ++ 18 files changed, 429 insertions(+), 8 deletions(-) diff --git a/api/v1alpha5/zz_generated.conversion.go b/api/v1alpha5/zz_generated.conversion.go index 44dbc571e2..4d472de0be 100644 --- a/api/v1alpha5/zz_generated.conversion.go +++ b/api/v1alpha5/zz_generated.conversion.go @@ -1189,6 +1189,7 @@ func autoConvert_v1beta1_OpenStackMachineSpec_To_v1alpha5_OpenStackMachineSpec(i } else { out.IdentityRef = nil } + // WARNING: in.FloatingIPPoolRef requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1alpha6/openstackmachine_conversion.go b/api/v1alpha6/openstackmachine_conversion.go index 6720f99f90..2dcbec0f7b 100644 --- a/api/v1alpha6/openstackmachine_conversion.go +++ b/api/v1alpha6/openstackmachine_conversion.go @@ -161,6 +161,7 @@ func restorev1beta1MachineSpec(previous *infrav1.OpenStackMachineSpec, dst *infr dst.AdditionalBlockDevices = previous.AdditionalBlockDevices dst.ServerGroup = previous.ServerGroup dst.Image = previous.Image + dst.FloatingIPPoolRef = previous.FloatingIPPoolRef } func convertNetworksToPorts(networks []NetworkParam, s apiconversion.Scope) ([]infrav1.PortOpts, error) { diff --git a/api/v1alpha6/zz_generated.conversion.go b/api/v1alpha6/zz_generated.conversion.go index e3d5251fae..7530cc7dea 100644 --- a/api/v1alpha6/zz_generated.conversion.go +++ b/api/v1alpha6/zz_generated.conversion.go @@ -1224,6 +1224,7 @@ func autoConvert_v1beta1_OpenStackMachineSpec_To_v1alpha6_OpenStackMachineSpec(i } else { out.IdentityRef = nil } + // WARNING: in.FloatingIPPoolRef requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1alpha7/openstackmachine_conversion.go b/api/v1alpha7/openstackmachine_conversion.go index 575f8ad40b..0d21eb3617 100644 --- a/api/v1alpha7/openstackmachine_conversion.go +++ b/api/v1alpha7/openstackmachine_conversion.go @@ -137,6 +137,7 @@ func restorev1beta1MachineSpec(previous *infrav1.OpenStackMachineSpec, dst *infr restorev1beta1Port(&previous.Ports[i], &dst.Ports[i]) } } + dst.FloatingIPPoolRef = previous.FloatingIPPoolRef } func Convert_v1alpha7_OpenStackMachineSpec_To_v1beta1_OpenStackMachineSpec(in *OpenStackMachineSpec, out *infrav1.OpenStackMachineSpec, s apiconversion.Scope) error { diff --git a/api/v1alpha7/zz_generated.conversion.go b/api/v1alpha7/zz_generated.conversion.go index 5f364226bb..d22715b316 100644 --- a/api/v1alpha7/zz_generated.conversion.go +++ b/api/v1alpha7/zz_generated.conversion.go @@ -1424,6 +1424,7 @@ func autoConvert_v1beta1_OpenStackMachineSpec_To_v1alpha7_OpenStackMachineSpec(i } else { out.IdentityRef = nil } + // WARNING: in.FloatingIPPoolRef requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta1/conditions_consts.go b/api/v1beta1/conditions_consts.go index 288251636d..df2da634ec 100644 --- a/api/v1beta1/conditions_consts.go +++ b/api/v1beta1/conditions_consts.go @@ -53,3 +53,12 @@ const ( // FloatingIPErrorReason used when the floating ip could not be created or attached. FloatingIPErrorReason = "FloatingIPError" ) + +const ( + // FloatingAddressFromPoolReadyCondition reports on the current status of the Floating IPs from ipam pool. + FloatingAddressFromPoolReadyCondition clusterv1.ConditionType = "FloatingAddressFromPoolReady" + // WaitingForIpamProviderReason used when machine is waiting for ipam provider to be ready before proceeding. + FloatingAddressFromPoolWaitingForIpamProviderReason = "WaitingForIPAMProvider" + // FloatingAddressFromPoolErrorReason is used when there is an error attaching an IP from the pool to an machine. + FloatingAddressFromPoolErrorReason = "FloatingIPError" +) diff --git a/api/v1beta1/openstackmachine_types.go b/api/v1beta1/openstackmachine_types.go index 9e36fa9dd8..343063622d 100644 --- a/api/v1beta1/openstackmachine_types.go +++ b/api/v1beta1/openstackmachine_types.go @@ -27,7 +27,8 @@ import ( const ( // MachineFinalizer allows ReconcileOpenStackMachine to clean up OpenStack resources associated with OpenStackMachine before // removing it from the apiserver. - MachineFinalizer = "openstackmachine.infrastructure.cluster.x-k8s.io" + MachineFinalizer = "openstackmachine.infrastructure.cluster.x-k8s.io" + IPClaimMachineFinalizer = "openstackmachine.infrastructure.cluster.x-k8s.io/ip-claim" ) // OpenStackMachineSpec defines the desired state of OpenStackMachine. @@ -89,6 +90,12 @@ type OpenStackMachineSpec struct { // credentials specified in the cluster will be used. // +optional IdentityRef *OpenStackIdentityReference `json:"identityRef,omitempty"` + + // floatingIPPoolRef is a reference to a IPPool that will be assigned + // to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP + // will be assigned to the OpenStackMachine. + // +optional + FloatingIPPoolRef *corev1.TypedLocalObjectReference `json:"floatingIPPoolRef,omitempty"` } type ServerMetadata struct { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index aa1a30e5eb..00e173153a 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -910,6 +910,11 @@ func (in *OpenStackMachineSpec) DeepCopyInto(out *OpenStackMachineSpec) { *out = new(OpenStackIdentityReference) **out = **in } + if in.FloatingIPPoolRef != nil { + in, out := &in.FloatingIPPoolRef, &out.FloatingIPPoolRef + *out = new(v1.TypedLocalObjectReference) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackMachineSpec. 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 db67158b60..e86bfff42e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml @@ -4996,6 +4996,29 @@ spec: description: The flavor reference for the flavor for your server instance. type: string + floatingIPPoolRef: + description: |- + floatingIPPoolRef is a reference to a IPPool that will be assigned + to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP + will be assigned to the OpenStackMachine. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic identityRef: description: |- IdentityRef is a reference to a secret holding OpenStack credentials 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 5dc38e4098..56a34a5b19 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml @@ -2421,6 +2421,31 @@ spec: description: The flavor reference for the flavor for your server instance. type: string + floatingIPPoolRef: + description: |- + floatingIPPoolRef is a reference to a IPPool that will be assigned + to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP + will be assigned to the OpenStackMachine. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being + referenced + type: string + name: + description: Name is the name of resource being + referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic identityRef: description: |- IdentityRef is a reference to a secret holding OpenStack credentials diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml index 3bb1c4a4df..ef4dd7f27f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachines.yaml @@ -1784,6 +1784,29 @@ spec: flavor: description: The flavor reference for the flavor for your server instance. type: string + floatingIPPoolRef: + description: |- + floatingIPPoolRef is a reference to a IPPool that will be assigned + to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP + will be assigned to the OpenStackMachine. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic identityRef: description: |- IdentityRef is a reference to a secret holding OpenStack credentials diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml index 975c907650..f42138d667 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackmachinetemplates.yaml @@ -1457,6 +1457,29 @@ spec: description: The flavor reference for the flavor for your server instance. type: string + floatingIPPoolRef: + description: |- + floatingIPPoolRef is a reference to a IPPool that will be assigned + to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP + will be assigned to the OpenStackMachine. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic identityRef: description: |- IdentityRef is a reference to a secret holding OpenStack credentials diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 6a40632602..4790f87801 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -123,6 +123,7 @@ rules: - delete - get - list + - patch - update - watch - apiGroups: diff --git a/controllers/openstackmachine_controller.go b/controllers/openstackmachine_controller.go index 0195b007ee..5f10024af8 100644 --- a/controllers/openstackmachine_controller.go +++ b/controllers/openstackmachine_controller.go @@ -27,12 +27,14 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/record" "k8s.io/utils/pointer" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" capierrors "sigs.k8s.io/cluster-api/errors" + ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/annotations" "sigs.k8s.io/cluster-api/util/conditions" @@ -53,6 +55,7 @@ import ( "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/loadbalancer" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking" "sigs.k8s.io/cluster-api-provider-openstack/pkg/scope" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/names" ) // OpenStackMachineReconciler reconciles a OpenStackMachine object. @@ -73,6 +76,8 @@ const ( // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachines,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachines/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines;machines/status,verbs=get;list;watch +// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddressclaims;ipaddressclaims/status,verbs=get;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=ipam.cluster.x-k8s.io,resources=ipaddresses;ipaddresses/status,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=secrets;,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch @@ -238,6 +243,10 @@ func (r *OpenStackMachineReconciler) SetupWithManager(ctx context.Context, mgr c handler.EnqueueRequestsFromMapFunc(r.requeueOpenStackMachinesForUnpausedCluster(ctx)), builder.WithPredicates(predicates.ClusterUnpausedAndInfrastructureReady(ctrl.LoggerFrom(ctx))), ). + Watches( + &ipamv1.IPAddressClaim{}, + handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &infrav1.OpenStackMachine{}), + ). Complete(r) } @@ -320,6 +329,10 @@ func (r *OpenStackMachineReconciler) reconcileDelete(scope *scope.WithLogger, cl } } + if err := r.reconcileDeleteFloatingAddressFromPool(scope, openStackMachine); err != nil { + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(openStackMachine, infrav1.MachineFinalizer) scope.Logger().Info("Reconciled Machine delete successfully") return ctrl.Result{}, nil @@ -334,6 +347,136 @@ func GetPortIDs(ports []infrav1.PortStatus) []string { return portIDs } +// reconcileFloatingAddressFromPool reconciles the floating IP address from the pool. +// It returns the IPAddressClaim and a boolean indicating if the IPAddressClaim is ready. +func (r *OpenStackMachineReconciler) reconcileFloatingAddressFromPool(ctx context.Context, scope *scope.WithLogger, openStackMachine *infrav1.OpenStackMachine, openStackCluster *infrav1.OpenStackCluster) (*ipamv1.IPAddressClaim, bool, error) { + if openStackMachine.Spec.FloatingIPPoolRef == nil { + return nil, false, nil + } + var claim *ipamv1.IPAddressClaim + claim, err := r.getOrCreateIPAddressClaimForFloatingAddress(ctx, scope, openStackMachine, openStackCluster) + if err != nil { + conditions.MarkFalse(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition, infrav1.FloatingAddressFromPoolErrorReason, clusterv1.ConditionSeverityInfo, "Failed to reconcile floating IP claims: %v", err) + return nil, true, err + } + if claim.Status.AddressRef.Name == "" { + r.Recorder.Eventf(openStackMachine, corev1.EventTypeNormal, "WaitingForIPAddressClaim", "Waiting for IPAddressClaim %s/%s to be allocated", claim.Namespace, claim.Name) + return claim, true, nil + } + conditions.MarkTrue(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition) + return claim, false, nil +} + +// createIPAddressClaim creates IPAddressClaim for the FloatingAddressFromPool if it does not exist yet. +func (r *OpenStackMachineReconciler) getOrCreateIPAddressClaimForFloatingAddress(ctx context.Context, scope *scope.WithLogger, openStackMachine *infrav1.OpenStackMachine, openStackCluster *infrav1.OpenStackCluster) (*ipamv1.IPAddressClaim, error) { + var err error + + poolRef := openStackMachine.Spec.FloatingIPPoolRef + claimName := names.GetFloatingAddressClaimName(openStackMachine.Name) + claim := &ipamv1.IPAddressClaim{} + + err = r.Client.Get(ctx, client.ObjectKey{Namespace: openStackMachine.Namespace, Name: claimName}, claim) + if err == nil { + return claim, nil + } else if client.IgnoreNotFound(err) != nil { + return nil, err + } + + claim = &ipamv1.IPAddressClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Namespace: openStackMachine.Namespace, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: openStackCluster.Labels[clusterv1.ClusterNameLabel], + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: openStackMachine.APIVersion, + Kind: openStackMachine.Kind, + Name: openStackMachine.Name, + UID: openStackMachine.UID, + }, + }, + Finalizers: []string{infrav1.IPClaimMachineFinalizer}, + }, + Spec: ipamv1.IPAddressClaimSpec{ + PoolRef: *poolRef, + }, + } + + if err := r.Client.Create(ctx, claim); err != nil { + return nil, err + } + + r.Recorder.Eventf(openStackMachine, corev1.EventTypeNormal, "CreatingIPAddressClaim", "Creating IPAddressClaim %s/%s", claim.Namespace, claim.Name) + scope.Logger().Info("Created IPAddressClaim", "name", claim.Name) + return claim, nil +} + +func (r *OpenStackMachineReconciler) associateIPAddressFromIPAddressClaim(ctx context.Context, scope *scope.WithLogger, openStackMachine *infrav1.OpenStackMachine, instanceStatus *compute.InstanceStatus, instanceNS *compute.InstanceNetworkStatus, claim *ipamv1.IPAddressClaim) error { + address := &ipamv1.IPAddress{} + addressKey := client.ObjectKey{Namespace: openStackMachine.Namespace, Name: claim.Status.AddressRef.Name} + + if err := r.Client.Get(ctx, addressKey, address); err != nil { + return err + } + + instanceAddresses := instanceNS.Addresses() + for _, instanceAddress := range instanceAddresses { + if instanceAddress.Address == address.Spec.Address { + conditions.MarkTrue(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition) + return nil + } + } + + networkingService, err := networking.NewService(scope) + if err != nil { + return err + } + + fip, err := networkingService.GetFloatingIP(address.Spec.Address) + if err != nil { + return err + } + + if fip == nil { + conditions.MarkFalse(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition, infrav1.FloatingAddressFromPoolErrorReason, clusterv1.ConditionSeverityError, "floating IP does not exist") + return fmt.Errorf("floating IP %q does not exist", address.Spec.Address) + } + + port, err := networkingService.GetPortForExternalNetwork(instanceStatus.ID(), fip.FloatingNetworkID) + if err != nil { + return fmt.Errorf("get port for floating IP %q: %w", fip.FloatingIP, err) + } + + if port == nil { + conditions.MarkFalse(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition, infrav1.FloatingAddressFromPoolErrorReason, clusterv1.ConditionSeverityError, "Can't find port for floating IP %q on external network %s", fip.FloatingIP, fip.FloatingNetworkID) + return fmt.Errorf("port for floating IP %q on network %s does not exist", fip.FloatingIP, fip.FloatingNetworkID) + } + + if err = networkingService.AssociateFloatingIP(openStackMachine, fip, port.ID); err != nil { + return err + } + conditions.MarkTrue(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition) + return nil +} + +func (r *OpenStackMachineReconciler) reconcileDeleteFloatingAddressFromPool(scope *scope.WithLogger, openStackMachine *infrav1.OpenStackMachine) error { + log := scope.Logger().WithValues("openStackMachine", openStackMachine.Name) + log.Info("Reconciling Machine delete floating address from pool") + if openStackMachine.Spec.FloatingIPPoolRef == nil { + return nil + } + claimName := names.GetFloatingAddressClaimName(openStackMachine.Name) + claim := &ipamv1.IPAddressClaim{} + if err := r.Client.Get(context.Background(), client.ObjectKey{Namespace: openStackMachine.Namespace, Name: claimName}, claim); err != nil { + return client.IgnoreNotFound(err) + } + + controllerutil.RemoveFinalizer(claim, infrav1.IPClaimMachineFinalizer) + return r.Client.Update(context.Background(), claim) +} + func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope *scope.WithLogger, cluster *clusterv1.Cluster, openStackCluster *infrav1.OpenStackCluster, machine *clusterv1.Machine, openStackMachine *infrav1.OpenStackMachine) (_ ctrl.Result, reterr error) { var err error @@ -374,6 +517,11 @@ func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope return ctrl.Result{}, err } + floatingAddressClaim, waitingForFloatingAddress, err := r.reconcileFloatingAddressFromPool(ctx, scope, openStackMachine, openStackCluster) + if err != nil || waitingForFloatingAddress { + return ctrl.Result{}, err + } + networkingService, err := networking.NewService(scope) if err != nil { return ctrl.Result{}, err @@ -415,6 +563,14 @@ func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope }) openStackMachine.Status.Addresses = addresses + if floatingAddressClaim != nil { + if err := r.associateIPAddressFromIPAddressClaim(ctx, scope, openStackMachine, instanceStatus, instanceNS, floatingAddressClaim); err != nil { + conditions.MarkFalse(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition, infrav1.FloatingAddressFromPoolErrorReason, clusterv1.ConditionSeverityError, "Failed while associating ip from pool: %v", err) + return ctrl.Result{}, err + } + conditions.MarkTrue(openStackMachine, infrav1.FloatingAddressFromPoolReadyCondition) + } + switch instanceStatus.State() { case infrav1.InstanceStateActive: scope.Logger().Info("Machine instance state is ACTIVE", "id", instanceStatus.ID()) @@ -444,6 +600,7 @@ func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope // due to potential conflict or unexpected actions scope.Logger().Info("Waiting for instance to become ACTIVE", "id", instanceStatus.ID(), "status", instanceStatus.State()) conditions.MarkUnknown(openStackMachine, infrav1.InstanceReadyCondition, infrav1.InstanceNotReadyReason, "Instance state is not handled: %s", instanceStatus.State()) + return ctrl.Result{RequeueAfter: waitForInstanceBecomeActiveToReconcile}, nil } @@ -452,11 +609,32 @@ func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope return ctrl.Result{}, nil } + err = r.reconcileAPIServerLoadBalancer(scope, openStackCluster, openStackMachine, instanceStatus, instanceNS, clusterName) + if err != nil { + return ctrl.Result{}, err + } + conditions.MarkTrue(openStackMachine, infrav1.APIServerIngressReadyCondition) + scope.Logger().Info("Reconciled Machine create successfully") + return ctrl.Result{}, nil +} + +func (r *OpenStackMachineReconciler) reconcileAPIServerLoadBalancer(scope *scope.WithLogger, openStackCluster *infrav1.OpenStackCluster, openStackMachine *infrav1.OpenStackMachine, instanceStatus *compute.InstanceStatus, instanceNS *compute.InstanceNetworkStatus, clusterName string) error { + scope.Logger().Info("Reconciling APIServerLoadBalancer") + computeService, err := compute.NewService(scope) + if err != nil { + return err + } + + networkingService, err := networking.NewService(scope) + if err != nil { + return err + } + if openStackCluster.Spec.APIServerLoadBalancer.IsEnabled() { err = r.reconcileLoadBalancerMember(scope, openStackCluster, openStackMachine, instanceNS, clusterName) if err != nil { conditions.MarkFalse(openStackMachine, infrav1.APIServerIngressReadyCondition, infrav1.LoadBalancerMemberErrorReason, clusterv1.ConditionSeverityError, "Reconciling load balancer member failed: %v", err) - return ctrl.Result{}, fmt.Errorf("reconcile load balancer member: %w", err) + return fmt.Errorf("reconcile load balancer member: %w", err) } } else if !pointer.BoolDeref(openStackCluster.Spec.DisableAPIServerFloatingIP, false) { var floatingIPAddress *string @@ -469,12 +647,12 @@ func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope fp, err := networkingService.GetOrCreateFloatingIP(openStackMachine, openStackCluster, clusterName, floatingIPAddress) if err != nil { conditions.MarkFalse(openStackMachine, infrav1.APIServerIngressReadyCondition, infrav1.FloatingIPErrorReason, clusterv1.ConditionSeverityError, "Floating IP cannot be obtained or created: %v", err) - return ctrl.Result{}, fmt.Errorf("get or create floating IP %v: %w", floatingIPAddress, err) + return fmt.Errorf("get or create floating IP %v: %w", floatingIPAddress, err) } port, err := computeService.GetManagementPort(openStackCluster, instanceStatus) if err != nil { conditions.MarkFalse(openStackMachine, infrav1.APIServerIngressReadyCondition, infrav1.FloatingIPErrorReason, clusterv1.ConditionSeverityError, "Obtaining management port for control plane machine failed: %v", err) - return ctrl.Result{}, fmt.Errorf("get management port for control plane machine: %w", err) + return fmt.Errorf("get management port for control plane machine: %w", err) } if fp.PortID != "" { @@ -483,14 +661,12 @@ func (r *OpenStackMachineReconciler) reconcileNormal(ctx context.Context, scope err = networkingService.AssociateFloatingIP(openStackMachine, fp, port.ID) if err != nil { conditions.MarkFalse(openStackMachine, infrav1.APIServerIngressReadyCondition, infrav1.FloatingIPErrorReason, clusterv1.ConditionSeverityError, "Associating floating IP failed: %v", err) - return ctrl.Result{}, fmt.Errorf("associate floating IP %q with port %q: %w", fp.FloatingIP, port.ID, err) + return fmt.Errorf("associate floating IP %q with port %q: %w", fp.FloatingIP, port.ID, err) } } } conditions.MarkTrue(openStackMachine, infrav1.APIServerIngressReadyCondition) - - scope.Logger().Info("Reconciled Machine create successfully") - return ctrl.Result{}, nil + return nil } func getOrCreateMachinePorts(scope *scope.WithLogger, openStackCluster *infrav1.OpenStackCluster, machine *clusterv1.Machine, openStackMachine *infrav1.OpenStackMachine, networkingService *networking.Service, clusterName string) error { diff --git a/docs/book/src/api/v1beta1/api.md b/docs/book/src/api/v1beta1/api.md index 38003ddb7b..8f1731e094 100644 --- a/docs/book/src/api/v1beta1/api.md +++ b/docs/book/src/api/v1beta1/api.md @@ -739,6 +739,20 @@ to be used when reconciling this machine. If not specified, the credentials specified in the cluster will be used.

+ + +floatingIPPoolRef
+ +Kubernetes core/v1.TypedLocalObjectReference + + + +(Optional) +

floatingIPPoolRef is a reference to a IPPool that will be assigned +to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP +will be assigned to the OpenStackMachine.

+ + @@ -3186,6 +3200,20 @@ to be used when reconciling this machine. If not specified, the credentials specified in the cluster will be used.

+ + +floatingIPPoolRef
+ +Kubernetes core/v1.TypedLocalObjectReference + + + +(Optional) +

floatingIPPoolRef is a reference to a IPPool that will be assigned +to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP +will be assigned to the OpenStackMachine.

+ +

OpenStackMachineStatus @@ -3539,6 +3567,20 @@ to be used when reconciling this machine. If not specified, the credentials specified in the cluster will be used.

+ + +floatingIPPoolRef
+ +Kubernetes core/v1.TypedLocalObjectReference + + + +(Optional) +

floatingIPPoolRef is a reference to a IPPool that will be assigned +to an IPAddressClaim. Once the IPAddressClaim is fulfilled, the FloatingIP +will be assigned to the OpenStackMachine.

+ + diff --git a/docs/book/src/topics/crd-changes/v1alpha7-to-v1beta1.md b/docs/book/src/topics/crd-changes/v1alpha7-to-v1beta1.md index 7c98118078..9c12188e41 100644 --- a/docs/book/src/topics/crd-changes/v1alpha7-to-v1beta1.md +++ b/docs/book/src/topics/crd-changes/v1alpha7-to-v1beta1.md @@ -14,6 +14,7 @@ - [Change to image](#change-to-image) - [Removal of imageUUID](#removal-of-imageuuid) - [Changes to ports](#changes-to-ports) + - [Additon of floatingIPPoolRef](#additon-of-floatingippoolref) - [`OpenStackCluster`](#openstackcluster) - [Removal of cloudName](#removal-of-cloudname-1) - [identityRef is now required](#identityref-is-now-required) @@ -138,6 +139,23 @@ The following fields in `PortOpts` are renamed in order to keep them consistent * `hostId` becomes `hostID` * `allowedCidrs` becomes `allowedCIDRs` +#### Addition of floatingIPPoolRef + +A new field, FloatingIPPoolRef, has been introduced. It is important to note that this feature requires the existence of an IPPool to operate seamlessly. This new field references an IPPool whice will be used for floating IP allocation. +In additon to this field an IPPool resource called `OpenStackFloatingIPPool` has been added. Please note that this resource is new and still in alpha meaning its more likely to contain bugs and change in the future. + +When creating an OpenStackFloatingIPPool resource named MyOpenStackFloatingIPPool, you can reference it within your OpenStackMachine configuration YAML as follows: + +```yaml +spec: + template: + spec: + floatingIPPoolRef: + apiGroup: "infrastructure.cluster.x-k8s.io" + kind: "OpenStackFloatingIPPool" + name: "MyOpenStackFloatingIPPool" +``` + ### `OpenStackCluster` #### Removal of cloudName diff --git a/pkg/cloud/services/networking/port.go b/pkg/cloud/services/networking/port.go index e651ba992c..1f26739b91 100644 --- a/pkg/cloud/services/networking/port.go +++ b/pkg/cloud/services/networking/port.go @@ -58,6 +58,57 @@ func (s *Service) GetPortFromInstanceIP(instanceID string, ip string) ([]ports.P return s.client.ListPort(portOpts) } +func (s *Service) GetPortForExternalNetwork(instanceID string, externalNetworkID string) (*ports.Port, error) { + instancePortsOpts := ports.ListOpts{ + DeviceID: instanceID, + } + instancePorts, err := s.client.ListPort(instancePortsOpts) + if err != nil { + return nil, fmt.Errorf("lookup ports for server %s: %w", instanceID, err) + } + + for _, instancePort := range instancePorts { + networkPortsOpts := ports.ListOpts{ + NetworkID: instancePort.NetworkID, + DeviceOwner: "network:router_interface", + } + + networkPorts, err := s.client.ListPort(networkPortsOpts) + if err != nil { + return nil, fmt.Errorf("lookup ports for network %s: %w", instancePort.NetworkID, err) + } + + for _, networkPort := range networkPorts { + // Check if the instance port and the network port share a subnet + matchingSubnet := false + for _, fixedIP := range instancePort.FixedIPs { + for _, networkFixedIP := range networkPort.FixedIPs { + if fixedIP.SubnetID == networkFixedIP.SubnetID { + matchingSubnet = true + break + } + } + if matchingSubnet { + break + } + } + if !matchingSubnet { + continue + } + + router, err := s.client.GetRouter(networkPort.DeviceID) + if err != nil { + return nil, fmt.Errorf("lookup router %s: %w", networkPort.DeviceID, err) + } + + if router.GatewayInfo.NetworkID == externalNetworkID { + return &instancePort, nil + } + } + } + return nil, nil +} + func (s *Service) CreatePort(eventObject runtime.Object, clusterName string, portName string, portOpts *infrav1.PortOpts, instanceSecurityGroups []string, instanceTags []string) (*ports.Port, error) { var err error networkID := portOpts.Network.ID diff --git a/pkg/utils/names/names.go b/pkg/utils/names/names.go index e97ccc41c5..95c1bd455c 100644 --- a/pkg/utils/names/names.go +++ b/pkg/utils/names/names.go @@ -18,8 +18,21 @@ package names import ( "fmt" + "strings" +) + +const ( + FloatingAddressIPClaimNameSuffix = "floating-ip-address" ) func GetDescription(clusterName string) string { return fmt.Sprintf("Created by cluster-api-provider-openstack cluster %s", clusterName) } + +func GetFloatingAddressClaimName(openStackMachineName string) string { + return fmt.Sprintf("%s-%s", openStackMachineName, FloatingAddressIPClaimNameSuffix) +} + +func GetOpenStackMachineNameFromClaimName(claimName string) string { + return strings.TrimSuffix(claimName, fmt.Sprintf("-%s", FloatingAddressIPClaimNameSuffix)) +}