From c732bd97c579b7481703dd9a3dc86e1eaf5a1a08 Mon Sep 17 00:00:00 2001 From: Andrew Karpow Date: Wed, 17 Dec 2025 17:03:46 -0500 Subject: [PATCH] [decomission-controller] switch to maintenance-operator this switches the decomissioning-controller to use the terminating state of maintenance spec field to decomission the node. Also, the maintenance-operator will take care of removing the finalizer - thus removing the need to access nodes at all. --- api/v1/hypervisor_types.go | 9 +- .../crds/hypervisor-crd.yaml | 12 ++ internal/controller/aggregates_controller.go | 20 +-- internal/controller/decomission_controller.go | 160 +++++++----------- .../controller/decomission_controller_test.go | 88 ++++++---- internal/controller/hypervisor_controller.go | 14 +- internal/controller/onboarding_controller.go | 4 +- internal/controller/suite_test.go | 1 + internal/openstack/aggregates.go | 44 +++++ internal/utils/consts.go | 24 +++ internal/utils/lifecycle_enabled.go | 6 + 11 files changed, 216 insertions(+), 166 deletions(-) create mode 100644 internal/openstack/aggregates.go create mode 100644 internal/utils/consts.go diff --git a/api/v1/hypervisor_types.go b/api/v1/hypervisor_types.go index e38a2e37..f30f2bcd 100644 --- a/api/v1/hypervisor_types.go +++ b/api/v1/hypervisor_types.go @@ -57,10 +57,11 @@ const ( ConditionReasonReadyEvicted = "Evicted" // ConditionTypeOnboarding reasons - ConditionReasonInitial = "Initial" - ConditionReasonOnboarding = "Onboarding" - ConditionReasonTesting = "Testing" - ConditionReasonAborted = "Aborted" + ConditionReasonInitial = "Initial" + ConditionReasonOnboarding = "Onboarding" + ConditionReasonTesting = "Testing" + ConditionReasonAborted = "Aborted" + ConditionReasonDecommissioning = "Decommissioning" ) // HypervisorSpec defines the desired state of Hypervisor diff --git a/charts/openstack-hypervisor-operator/crds/hypervisor-crd.yaml b/charts/openstack-hypervisor-operator/crds/hypervisor-crd.yaml index ac60036d..fe36d98a 100644 --- a/charts/openstack-hypervisor-operator/crds/hypervisor-crd.yaml +++ b/charts/openstack-hypervisor-operator/crds/hypervisor-crd.yaml @@ -315,6 +315,14 @@ spec: firmwareVersion: description: FirmwareVersion type: string + gardenLinuxCommitID: + description: Represents the Garden Linux build commit id + type: string + gardenLinuxFeatures: + description: Represents the Garden Linux Feature Set + items: + type: string + type: array hardwareModel: description: HardwareModel type: string @@ -336,6 +344,10 @@ spec: prettyVersion: description: PrettyVersion type: string + variantID: + description: Identifying a specific variant or edition of the + operating system + type: string version: description: Represents the Operating System version. type: string diff --git a/internal/controller/aggregates_controller.go b/internal/controller/aggregates_controller.go index 8eb04790..9603b638 100644 --- a/internal/controller/aggregates_controller.go +++ b/internal/controller/aggregates_controller.go @@ -70,7 +70,7 @@ func (ac *AggregatesController) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, nil } - aggs, err := aggregatesByName(ctx, ac.computeClient) + aggs, err := openstack.GetAggregatesByName(ctx, ac.computeClient) if err != nil { err = fmt.Errorf("failed listing aggregates: %w", err) if err2 := ac.setErrorCondition(ctx, hv, err.Error()); err2 != nil { @@ -163,24 +163,6 @@ func (ac *AggregatesController) SetupWithManager(mgr ctrl.Manager) error { Complete(ac) } -func aggregatesByName(ctx context.Context, serviceClient *gophercloud.ServiceClient) (map[string]*aggregates.Aggregate, error) { - pages, err := aggregates.List(serviceClient).AllPages(ctx) - if err != nil { - return nil, fmt.Errorf("cannot list aggregates due to %w", err) - } - - aggs, err := aggregates.ExtractAggregates(pages) - if err != nil { - return nil, fmt.Errorf("cannot list aggregates due to %w", err) - } - - aggregateMap := make(map[string]*aggregates.Aggregate, len(aggs)) - for _, aggregate := range aggs { - aggregateMap[aggregate.Name] = &aggregate - } - return aggregateMap, nil -} - func addToAggregate(ctx context.Context, serviceClient *gophercloud.ServiceClient, aggs map[string]*aggregates.Aggregate, host, name, zone string) (err error) { aggregate, found := aggs[name] log := logger.FromContext(ctx) diff --git a/internal/controller/decomission_controller.go b/internal/controller/decomission_controller.go index 10a3f63c..eaaec0da 100644 --- a/internal/controller/decomission_controller.go +++ b/internal/controller/decomission_controller.go @@ -26,25 +26,24 @@ import ( "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/hypervisors" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/services" "github.com/gophercloud/gophercloud/v2/openstack/placement/v1/resourceproviders" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logger "sigs.k8s.io/controller-runtime/pkg/log" kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" "github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/openstack" + "github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/utils" ) const ( - decommissionFinalizerName = "cobaltcore.cloud.sap/decommission-hypervisor" - DecommissionControllerName = "nodeDecommission" + DecommissionControllerName = "decommission" ) type NodeDecommissionReconciler struct { @@ -57,97 +56,88 @@ type NodeDecommissionReconciler struct { // The counter-side in gardener is here: // https://github.com/gardener/machine-controller-manager/blob/rel-v0.56/pkg/util/provider/machinecontroller/machine.go#L646 -// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;patch;update -// +kubebuilder:rbac:groups="",resources=nodes/finalizers,verbs=update // +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch // +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors/status,verbs=get;list;watch;update;patch + func (r *NodeDecommissionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - hostname := req.Name - log := logger.FromContext(ctx).WithName(req.Name).WithValues("hostname", hostname) - ctx = logger.IntoContext(ctx, log) + log := logger.FromContext(ctx).WithName(req.Name) + hv := &kvmv1.Hypervisor{} + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := r.Get(ctx, req.NamespacedName, hv); err != nil { + // ignore not found errors, could be deleted + return k8sclient.IgnoreNotFound(err) + } - node := &corev1.Node{} - if err := r.Get(ctx, req.NamespacedName, node); err != nil { - return ctrl.Result{}, k8sclient.IgnoreNotFound(err) - } + setDecommissioningCondition := func(msg string) { + meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{ + Type: kvmv1.ConditionTypeReady, + Status: metav1.ConditionFalse, + Reason: kvmv1.ConditionReasonDecommissioning, + Message: msg, + }) + } - // Fetch HV to check if lifecycle management is enabled - hv := &kvmv1.Hypervisor{} - if err := r.Get(ctx, k8sclient.ObjectKey{Name: hostname}, hv); err != nil { - // ignore not found errors, could be deleted - return ctrl.Result{}, k8sclient.IgnoreNotFound(err) - } - if !hv.Spec.LifecycleEnabled { - // Get out of the way - return r.removeFinalizer(ctx, node) - } + if meta.IsStatusConditionTrue(hv.Status.Conditions, kvmv1.ConditionTypeReady) { + setDecommissioningCondition("Node is being decommissioned, removing host from nova") + return r.Status().Update(ctx, hv) + } - if !controllerutil.ContainsFinalizer(node, decommissionFinalizerName) { - return ctrl.Result{}, retry.RetryOnConflict(retry.DefaultRetry, func() error { - patch := k8sclient.MergeFrom(node.DeepCopy()) - controllerutil.AddFinalizer(node, decommissionFinalizerName) - if err := r.Patch(ctx, node, patch); err != nil { - return fmt.Errorf("failed to add finalizer due to %w", err) + hypervisor, err := openstack.GetHypervisorByName(ctx, r.computeClient, hv.Name, true) + if err != nil { + if errors.Is(err, openstack.ErrNoHypervisor) { + // We are (hopefully) done + setDecommissioningCondition("Node not registered in nova anymore, proceeding with deletion") + hv.Status.Evicted = true + return r.Status().Update(ctx, hv) } - log.Info("Added finalizer") - return nil - }) - } - // Not yet deleting hv, nothing more to do - if node.DeletionTimestamp.IsZero() { - return ctrl.Result{}, nil - } + setDecommissioningCondition(fmt.Sprintf("Failed to get %q from openstack: %v", hv.Name, err)) + return r.Status().Update(ctx, hv) + } - // Someone is just deleting the hv, without going through termination - // See: https://github.com/gardener/machine-controller-manager/blob/rel-v0.56/pkg/util/provider/machinecontroller/machine.go#L658-L659 - if !IsNodeConditionTrue(node.Status.Conditions, "Terminating") { - log.Info("removing finalizer since not terminating") - // So we just get out of the way for now - return r.removeFinalizer(ctx, node) - } + if err = r.doDecomission(ctx, hv, hypervisor); err != nil { + log.Error(err, "Failed to decomission node", "node", hv.Name) + setDecommissioningCondition(err.Error()) + return r.Status().Update(ctx, hv) + } - if meta.IsStatusConditionTrue(hv.Status.Conditions, kvmv1.ConditionTypeReady) { - return r.setDecommissioningCondition(ctx, hv, "Node is being decommissioned, removing host from nova") + // Decommissioning succeeded, proceed with deletion + hv.Status.Evicted = true + return r.Status().Update(ctx, hv) + }); err != nil { + return ctrl.Result{}, err } - log.Info("removing host from nova") - - hypervisor, err := openstack.GetHypervisorByName(ctx, r.computeClient, hostname, true) - if errors.Is(err, openstack.ErrNoHypervisor) { - // We are (hopefully) done - return r.removeFinalizer(ctx, node) - } + return ctrl.Result{RequeueAfter: utils.ShortRetryTime}, nil +} +func (r *NodeDecommissionReconciler) doDecomission(ctx context.Context, hv *kvmv1.Hypervisor, hypervisor *hypervisors.Hypervisor) error { // TODO: remove since RunningVMs is only available until micro-version 2.87, and also is updated asynchronously // so it might be not accurate if hypervisor.RunningVMs > 0 { // Still running VMs, cannot delete the service - msg := fmt.Sprintf("Node is being decommissioned, but still has %d running VMs", hypervisor.RunningVMs) - return r.setDecommissioningCondition(ctx, hv, msg) + return fmt.Errorf("node is being decommissioned, but still has %d running VMs", hypervisor.RunningVMs) } if hypervisor.Servers != nil && len(*hypervisor.Servers) > 0 { // Still VMs assigned to the host, cannot delete the service - msg := fmt.Sprintf("Node is being decommissioned, but still has %d assigned VMs, "+ - "check with `openstack server list --all-projects --host %s`", len(*hypervisor.Servers), hostname) - return r.setDecommissioningCondition(ctx, hv, msg) + return fmt.Errorf("node is being decommissioned, but still has %d assigned VMs, "+ + "check with `openstack server list --all-projects --host %s`", len(*hypervisor.Servers), hv.Name) } - // Before removing the service, first take the node out of the aggregates, - // so when the node comes back, it doesn't up with the old associations - aggs, err := aggregatesByName(ctx, r.computeClient) + // Before removing the service, first take the hypervisor out of the aggregates, + // so when the hypervisor comes back, it doesn't up with the old associations + aggs, err := openstack.GetAggregatesByName(ctx, r.computeClient) if err != nil { - return r.setDecommissioningCondition(ctx, hv, fmt.Sprintf("cannot list aggregates due to %v", err)) + return fmt.Errorf("cannot list aggregates due to: %w", err) } - host := node.Name + host := hv.Name for name, aggregate := range aggs { if slices.Contains(aggregate.Hosts, host) { opts := aggregates.RemoveHostOpts{Host: host} if err = aggregates.RemoveHost(ctx, r.computeClient, aggregate.ID, opts).Err; err != nil { - msg := fmt.Sprintf("failed to remove host %v from aggregate %v due to %v", name, host, err) - return r.setDecommissioningCondition(ctx, hv, msg) + return fmt.Errorf("failed to remove host %v from aggregate %v due to %w", name, host, err) } } } @@ -155,47 +145,19 @@ func (r *NodeDecommissionReconciler) Reconcile(ctx context.Context, req ctrl.Req // Deleting and evicted, so better delete the service err = services.Delete(ctx, r.computeClient, hypervisor.Service.ID).ExtractErr() if err != nil && !gophercloud.ResponseCodeIs(err, http.StatusNotFound) { - msg := fmt.Sprintf("cannot delete service %s due to %v", hypervisor.Service.ID, err) - return r.setDecommissioningCondition(ctx, hv, msg) + return fmt.Errorf("cannot delete service %s due to %w", hypervisor.Service.ID, err) } rp, err := resourceproviders.Get(ctx, r.placementClient, hypervisor.ID).Extract() if err != nil && !gophercloud.ResponseCodeIs(err, http.StatusNotFound) { - return r.setDecommissioningCondition(ctx, hv, fmt.Sprintf("cannot get resource provider: %v", err)) + return fmt.Errorf("cannot get resource provider %s due to %w", hypervisor.ID, err) } if err = openstack.CleanupResourceProvider(ctx, r.placementClient, rp); err != nil { - return r.setDecommissioningCondition(ctx, hv, fmt.Sprintf("cannot clean up resource provider: %v", err)) - } - - return r.removeFinalizer(ctx, node) -} - -func (r *NodeDecommissionReconciler) removeFinalizer(ctx context.Context, node *corev1.Node) (ctrl.Result, error) { - if !controllerutil.ContainsFinalizer(node, decommissionFinalizerName) { - return ctrl.Result{}, nil + return fmt.Errorf("cannot cleanup resource provider: %w", err) } - nodeBase := node.DeepCopy() - controllerutil.RemoveFinalizer(node, decommissionFinalizerName) - err := r.Patch(ctx, node, k8sclient.MergeFromWithOptions(nodeBase, - k8sclient.MergeFromWithOptimisticLock{}), k8sclient.FieldOwner(DecommissionControllerName)) - return ctrl.Result{}, err -} - -func (r *NodeDecommissionReconciler) setDecommissioningCondition(ctx context.Context, hv *kvmv1.Hypervisor, message string) (ctrl.Result, error) { - base := hv.DeepCopy() - meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{ - Type: kvmv1.ConditionTypeReady, - Status: metav1.ConditionFalse, - Reason: "Decommissioning", - Message: message, - }) - if err := r.Status().Patch(ctx, hv, k8sclient.MergeFromWithOptions(base, - k8sclient.MergeFromWithOptimisticLock{}), k8sclient.FieldOwner(DecommissionControllerName)); err != nil { - return ctrl.Result{}, fmt.Errorf("cannot update hypervisor status due to %w", err) - } - return ctrl.Result{RequeueAfter: shortRetryTime}, nil + return nil } // SetupWithManager sets up the controller with the Manager. @@ -217,6 +179,8 @@ func (r *NodeDecommissionReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). Named(DecommissionControllerName). - For(&corev1.Node{}). + For(&kvmv1.Hypervisor{}). + WithEventFilter(utils.HypervisorTerminationPredicate). + WithEventFilter(utils.LifecycleEnabledPredicate). Complete(r) } diff --git a/internal/controller/decomission_controller_test.go b/internal/controller/decomission_controller_test.go index 3eeb4a21..40d66140 100644 --- a/internal/controller/decomission_controller_test.go +++ b/internal/controller/decomission_controller_test.go @@ -19,10 +19,14 @@ package controller import ( "context" + "net/http" + "github.com/gophercloud/gophercloud/v2/testhelper" + osclient "github.com/gophercloud/gophercloud/v2/testhelper/client" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -41,12 +45,15 @@ var _ = Describe("Decommission Controller", func() { reconcileReq = ctrl.Request{ NamespacedName: nodeName, } + fakeServer testhelper.FakeServer ) BeforeEach(func(ctx SpecContext) { + fakeServer = testhelper.SetupHTTP() r = &NodeDecommissionReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + computeClient: osclient.ServiceClient(fakeServer), } By("creating the namespace for the reconciler") @@ -58,58 +65,65 @@ var _ = Describe("Decommission Controller", func() { }) By("creating the core resource for the Kind Node") - node := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: nodeName.Name, - Labels: map[string]string{labelEvictionRequired: "true"}, - }, - } - Expect(k8sClient.Create(ctx, node)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, node))).To(Succeed()) - }) - - By("Create the hypervisor resource with lifecycle enabled") - hypervisor := &kvmv1.Hypervisor{ + hv := &kvmv1.Hypervisor{ ObjectMeta: metav1.ObjectMeta{ Name: nodeName.Name, }, Spec: kvmv1.HypervisorSpec{ LifecycleEnabled: true, + Maintenance: kvmv1.MaintenanceTermination, }, } - Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed()) + Expect(k8sClient.Create(ctx, hv)).To(Succeed()) + + // set ready condition + meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{ + Type: kvmv1.ConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: kvmv1.ConditionReasonReadyReady, + Message: "Setting initial ready condition for testing", + }) + Expect(k8sClient.Status().Update(ctx, hv)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { - Expect(k8sClient.Delete(ctx, hypervisor)).To(Succeed()) + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, hv))).To(Succeed()) + fakeServer.Teardown() }) }) - AfterEach(func(ctx context.Context) { - node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName.Name}} - By("Cleanup the specific node and hypervisor resource") - Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, node))).To(Succeed()) + Context("When reconciling a hypervisor", func() { + It("It should set the ready status", func(ctx context.Context) { + By("reconciling the created resource") + _, err := r.Reconcile(ctx, reconcileReq) + Expect(err).NotTo(HaveOccurred()) - // Due to the decommissioning finalizer, we need to reconcile once more to delete the node completely - req := ctrl.Request{ - NamespacedName: types.NamespacedName{Name: nodeName.Name}, - } - _, err := r.Reconcile(ctx, req) - Expect(err).NotTo(HaveOccurred()) + By("checking that the ready condition is set to false with the decommissioning reason") + hv := &kvmv1.Hypervisor{} + Expect(k8sClient.Get(ctx, nodeName, hv)).To(Succeed()) + cond := meta.FindStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeReady) + Expect(cond).NotTo(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(kvmv1.ConditionReasonDecommissioning)) - nodelist := &corev1.NodeList{} - Expect(k8sClient.List(ctx, nodelist)).To(Succeed()) - Expect(nodelist.Items).To(BeEmpty()) - }) + fakeServer.Mux.HandleFunc("GET /os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) - Context("When reconciling a node", func() { - It("should set the finalizer", func(ctx context.Context) { + Expect(err).NotTo(HaveOccurred()) + }) By("reconciling the created resource") - _, err := r.Reconcile(ctx, reconcileReq) + _, err = r.Reconcile(ctx, reconcileReq) Expect(err).NotTo(HaveOccurred()) - node := &corev1.Node{} - Expect(k8sClient.Get(ctx, nodeName, node)).To(Succeed()) - Expect(node.Finalizers).To(ContainElement(decommissionFinalizerName)) + By("checking that the eviction succeeded") + hv = &kvmv1.Hypervisor{} + Expect(k8sClient.Get(ctx, nodeName, hv)).To(Succeed()) + Expect(hv.Status.Evicted).To(BeTrue()) + cond = meta.FindStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeReady) + Expect(cond).NotTo(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(kvmv1.ConditionReasonDecommissioning)) + Expect(cond.Message).To(Equal("Node not registered in nova anymore, proceeding with deletion")) }) }) }) diff --git a/internal/controller/hypervisor_controller.go b/internal/controller/hypervisor_controller.go index 37c80d6e..69093f8d 100644 --- a/internal/controller/hypervisor_controller.go +++ b/internal/controller/hypervisor_controller.go @@ -41,10 +41,11 @@ import ( ) const ( - labelLifecycleMode = "cobaltcore.cloud.sap/node-hypervisor-lifecycle" - annotationAggregates = "nova.openstack.cloud.sap/aggregates" - annotationCustomTraits = "nova.openstack.cloud.sap/custom-traits" - HypervisorControllerName = "hypervisor" + labelLifecycleMode = "cobaltcore.cloud.sap/node-hypervisor-lifecycle" + annotationAggregates = "nova.openstack.cloud.sap/aggregates" + annotationCustomTraits = "nova.openstack.cloud.sap/custom-traits" + decommissionFinalizerName = "cobaltcore.cloud.sap/decommission-hypervisor" + HypervisorControllerName = "hypervisor" ) var transferLabels = []string{ @@ -81,8 +82,9 @@ func (hv *HypervisorController) Reconcile(ctx context.Context, req ctrl.Request) nodeLabels := labels.Set(node.Labels) hypervisor := &kvmv1.Hypervisor{ ObjectMeta: metav1.ObjectMeta{ - Name: node.Name, - Labels: map[string]string{}, + Name: node.Name, + Labels: map[string]string{}, + Finalizers: []string{decommissionFinalizerName}, }, Spec: kvmv1.HypervisorSpec{ HighAvailability: true, diff --git a/internal/controller/onboarding_controller.go b/internal/controller/onboarding_controller.go index e882c44e..7d3e420a 100644 --- a/internal/controller/onboarding_controller.go +++ b/internal/controller/onboarding_controller.go @@ -197,7 +197,7 @@ func (r *OnboardingController) initialOnboarding(ctx context.Context, hv *kvmv1. return fmt.Errorf("cannot find availability-zone label %v on node", corev1.LabelTopologyZone) } - aggs, err := aggregatesByName(ctx, r.computeClient) + aggs, err := openstack.GetAggregatesByName(ctx, r.computeClient) if err != nil { return fmt.Errorf("cannot list aggregates %w", err) } @@ -345,7 +345,7 @@ func (r *OnboardingController) completeOnboarding(ctx context.Context, host stri return ctrl.Result{}, err } - aggs, err := aggregatesByName(ctx, r.computeClient) + aggs, err := openstack.GetAggregatesByName(ctx, r.computeClient) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to get aggregates %w", err) } diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index fa36940c..c443b4de 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -62,6 +62,7 @@ var _ = BeforeSuite(func() { testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, + DownloadBinaryAssets: true, // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the diff --git a/internal/openstack/aggregates.go b/internal/openstack/aggregates.go new file mode 100644 index 00000000..875aefb2 --- /dev/null +++ b/internal/openstack/aggregates.go @@ -0,0 +1,44 @@ +/* +SPDX-FileCopyrightText: Copyright 2024 SAP SE or an SAP affiliate company and cobaltcore-dev contributors +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openstack + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates" +) + +func GetAggregatesByName(ctx context.Context, serviceClient *gophercloud.ServiceClient) (map[string]*aggregates.Aggregate, error) { + pages, err := aggregates.List(serviceClient).AllPages(ctx) + if err != nil { + return nil, fmt.Errorf("cannot list aggregates due to %w", err) + } + + aggs, err := aggregates.ExtractAggregates(pages) + if err != nil { + return nil, fmt.Errorf("cannot list aggregates due to %w", err) + } + + aggregateMap := make(map[string]*aggregates.Aggregate, len(aggs)) + for _, aggregate := range aggs { + aggregateMap[aggregate.Name] = &aggregate + } + return aggregateMap, nil +} diff --git a/internal/utils/consts.go b/internal/utils/consts.go new file mode 100644 index 00000000..42568e83 --- /dev/null +++ b/internal/utils/consts.go @@ -0,0 +1,24 @@ +/* +SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import "time" + +const ( + ShortRetryTime = 1 * time.Second +) diff --git a/internal/utils/lifecycle_enabled.go b/internal/utils/lifecycle_enabled.go index 0934b0c5..8e7602d2 100644 --- a/internal/utils/lifecycle_enabled.go +++ b/internal/utils/lifecycle_enabled.go @@ -31,4 +31,10 @@ var ( } return true }) + HypervisorTerminationPredicate = predicate.NewPredicateFuncs(func(object client.Object) bool { + if hv, ok := object.(*kvmv1.Hypervisor); ok { + return hv.Spec.Maintenance == kvmv1.MaintenanceTermination + } + return false + }) )