diff --git a/cmd/main.go b/cmd/main.go index aa8d6e3..0375247 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -236,14 +236,6 @@ func main() { os.Exit(1) } - if err = (&controller.NodeEvictionLabelReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Node") - os.Exit(1) - } - if err = (&controller.NodeDecommissionReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/internal/controller/gardener_node_lifecycle_controller.go b/internal/controller/gardener_node_lifecycle_controller.go index 91d04fb..c3a60da 100644 --- a/internal/controller/gardener_node_lifecycle_controller.go +++ b/internal/controller/gardener_node_lifecycle_controller.go @@ -32,7 +32,6 @@ import ( corev1ac "k8s.io/client-go/applyconfigurations/core/v1" v1 "k8s.io/client-go/applyconfigurations/meta/v1" policyv1ac "k8s.io/client-go/applyconfigurations/policy/v1" - "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/client/apiutil" @@ -72,40 +71,52 @@ func (r *GardenerNodeLifecycleController) Reconcile(ctx context.Context, req ctr return ctrl.Result{}, k8sclient.IgnoreNotFound(err) } - hv := kvmv1.Hypervisor{} - if err := r.Get(ctx, k8sclient.ObjectKey{Name: req.Name}, &hv); k8sclient.IgnoreNotFound(err) != nil { + hv := &kvmv1.Hypervisor{} + if err := r.Get(ctx, k8sclient.ObjectKey{Name: req.Name}, hv); k8sclient.IgnoreNotFound(err) != nil { return ctrl.Result{}, err } + if !hv.Spec.LifecycleEnabled { // Nothing to be done return ctrl.Result{}, nil } - if isTerminating(node) { - changed, err := setNodeLabels(ctx, r.Client, node, map[string]string{labelEvictionRequired: valueReasonTerminating}) - if changed || err != nil { - return ctrl.Result{}, err + // Only, if the maintenance controller is not active + if _, found := node.Labels["cloud.sap/maintenance-profile"]; !found { + // Sync the terminating status into the hypervisor spec + if isTerminating(node) && hv.Spec.Maintenance != kvmv1.MaintenanceTermination { + base := hv.DeepCopy() + hv.Spec.Maintenance = kvmv1.MaintenanceTermination + if err := r.Patch(ctx, hv, k8sclient.MergeFromWithOptions(base, k8sclient.MergeFromWithOptimisticLock{}), k8sclient.FieldOwner(MaintenanceControllerName)); err != nil { + return ctrl.Result{}, err + } } } // We do not care about the particular value, as long as it isn't an error var minAvailable int32 = 1 - evictionValue, found := node.Labels[labelEvictionApproved] - if found && evictionValue != "false" { + + // Onboarding is not in progress anymore, i.e. the host is onboarded + onboardingCompleted := meta.IsStatusConditionFalse(hv.Status.Conditions, kvmv1.ConditionTypeOnboarding) + // Evicting is not in progress anymore, i.e. the host is empty + evictionComplete := meta.IsStatusConditionFalse(hv.Status.Conditions, kvmv1.ConditionTypeEvicting) + + if evictionComplete { minAvailable = 0 + + if onboardingCompleted && isTerminating(node) { + // Onboarded & terminating & eviction complete -> disable HA + if err := disableInstanceHA(hv); err != nil { + return ctrl.Result{}, err + } + } } - if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - return r.ensureBlockingPodDisruptionBudget(ctx, node, minAvailable) - }); err != nil { + if err := r.ensureBlockingPodDisruptionBudget(ctx, node, minAvailable); err != nil { return ctrl.Result{}, err } - onboardingCompleted := meta.IsStatusConditionFalse(hv.Status.Conditions, kvmv1.ConditionTypeOnboarding) - - if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - return r.ensureSignallingDeployment(ctx, node, minAvailable, onboardingCompleted) - }); err != nil { + if err := r.ensureSignallingDeployment(ctx, node, minAvailable, onboardingCompleted); err != nil { return ctrl.Result{}, err } diff --git a/internal/controller/gardener_node_lifecycle_controller_test.go b/internal/controller/gardener_node_lifecycle_controller_test.go index 1af06de..f012cec 100644 --- a/internal/controller/gardener_node_lifecycle_controller_test.go +++ b/internal/controller/gardener_node_lifecycle_controller_test.go @@ -18,18 +18,29 @@ limitations under the License. package controller import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/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" - "sigs.k8s.io/controller-runtime/pkg/client" + + kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" ) var _ = Describe("Gardener Maintenance Controller", func() { const nodeName = "node-test" - var controller *GardenerNodeLifecycleController + var ( + controller *GardenerNodeLifecycleController + name = types.NamespacedName{Name: nodeName} + reconcileReq = ctrl.Request{NamespacedName: name} + maintenanceName = types.NamespacedName{Name: fmt.Sprintf("maint-%v", nodeName), Namespace: "kube-system"} + ) BeforeEach(func(ctx SpecContext) { controller = &GardenerNodeLifecycleController{ @@ -37,32 +48,107 @@ var _ = Describe("Gardener Maintenance Controller", func() { Scheme: k8sClient.Scheme(), } - By("creating the namespace for the reconciler") - ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "monsoon3"}} - Expect(client.IgnoreAlreadyExists(k8sClient.Create(ctx, ns))).To(Succeed()) - By("creating the core resource for the Kind Node") - resource := &corev1.Node{ + node := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ - Name: nodeName, - Labels: map[string]string{labelEvictionRequired: "true"}, + Name: nodeName, }, } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Create(ctx, node)).To(Succeed()) DeferCleanup(func(ctx SpecContext) { - Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, resource))).To(Succeed()) + By("Cleanup the specific node") + Expect(k8sClient.Delete(ctx, node)).To(Succeed()) + }) + + By("creating the core resource for the Kind hypervisor") + hypervisor := &kvmv1.Hypervisor{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + }, + Spec: kvmv1.HypervisorSpec{ + LifecycleEnabled: true, + }, + } + Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed()) + DeferCleanup(func(ctx SpecContext) { + Expect(k8sClient.Delete(ctx, hypervisor)).To(Succeed()) }) }) - Context("When reconciling a node", func() { - It("should successfully reconcile the resource", func(ctx SpecContext) { - req := ctrl.Request{ - NamespacedName: types.NamespacedName{Name: nodeName}, - } + Context("When reconciling a terminating node", func() { + BeforeEach(func(ctx SpecContext) { + By("Marking the node as terminating") + node := &corev1.Node{} + Expect(k8sClient.Get(ctx, name, node)).To(Succeed()) + node.Status.Conditions = append(node.Status.Conditions, corev1.NodeCondition{ + Type: "Terminating", + }) + Expect(k8sClient.Status().Update(ctx, node)).To(Succeed()) + }) + It("should successfully reconcile the resource", func(ctx SpecContext) { By("Reconciling the created resource") - _, err := controller.Reconcile(ctx, req) + _, err := controller.Reconcile(ctx, reconcileReq) Expect(err).NotTo(HaveOccurred()) + + hypervisor := &kvmv1.Hypervisor{} + Expect(k8sClient.Get(ctx, name, hypervisor)).To(Succeed()) + Expect(hypervisor.Spec.Maintenance).To(Equal(kvmv1.MaintenanceTermination)) }) }) + + Context("When reconciling a node", func() { + JustBeforeEach(func(ctx SpecContext) { + _, err := controller.Reconcile(ctx, reconcileReq) + Expect(err).NotTo(HaveOccurred()) + }) + It("should create a poddisruptionbudget", func(ctx SpecContext) { + pdb := &policyv1.PodDisruptionBudget{} + Expect(k8sClient.Get(ctx, maintenanceName, pdb)).To(Succeed()) + Expect(pdb.Spec.MinAvailable).To(HaveField("IntVal", BeNumerically("==", 1))) + }) + + It("should create a failing deployment to signal onboarding not being completed", func(ctx SpecContext) { + dep := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, maintenanceName, dep)).To(Succeed()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(dep.Spec.Template.Spec.Containers[0].StartupProbe.Exec.Command).To(Equal([]string{"/bin/false"})) + }) + + When("the node has been onboarded", func() { + BeforeEach(func(ctx SpecContext) { + hypervisor := &kvmv1.Hypervisor{} + Expect(k8sClient.Get(ctx, name, hypervisor)).To(Succeed()) + meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{ + Type: kvmv1.ConditionTypeOnboarding, + Status: metav1.ConditionFalse, + Reason: "dontcare", + Message: "dontcare", + }) + Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed()) + }) + + It("should create a deployment with onboarding completed", func(ctx SpecContext) { + dep := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, maintenanceName, dep)).To(Succeed()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(dep.Spec.Template.Spec.Containers[0].StartupProbe.Exec.Command).To(Equal([]string{"/bin/true"})) + }) + }) + + When("the node has been evicted", func() { + BeforeEach(func(ctx SpecContext) { + hypervisor := &kvmv1.Hypervisor{} + Expect(k8sClient.Get(ctx, name, hypervisor)).To(Succeed()) + meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{ + Type: kvmv1.ConditionTypeEvicting, + Status: metav1.ConditionFalse, + Reason: "dontcare", + Message: "dontcare", + }) + Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed()) + }) + }) + + }) }) diff --git a/internal/controller/node_eviction_label_controller.go b/internal/controller/node_eviction_label_controller.go deleted file mode 100644 index e31dc1e..0000000 --- a/internal/controller/node_eviction_label_controller.go +++ /dev/null @@ -1,196 +0,0 @@ -/* -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 controller - -import ( - "context" - "fmt" - "maps" - "strings" - - corev1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - 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" -) - -const ( - disabledSuffix = "-disabled" - labelMl2MechanismDriver = "neutron.openstack.cloud.sap/ml2-mechanism-driver" - NodeEvictionLabelControllerName = "nodeEvictionLabel" -) - -type NodeEvictionLabelReconciler struct { - k8sclient.Client - Scheme *runtime.Scheme -} - -// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;patch -// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=evictions,verbs=get;list;watch;create;update;patch;delete - -func (r *NodeEvictionLabelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := logger.FromContext(ctx).WithName(req.Name) - ctx = logger.IntoContext(ctx, log) - - node := &corev1.Node{} - if err := r.Get(ctx, req.NamespacedName, node); err != nil { - // ignore not found errors, could be deleted - return ctrl.Result{}, k8sclient.IgnoreNotFound(err) - } - - hostname, found := node.Labels[corev1.LabelHostname] - if !found { - // Should never happen (tm) - return ctrl.Result{}, nil - } - - maintenanceValue, found := node.Labels[labelEvictionRequired] - eviction := &kvmv1.Eviction{ - ObjectMeta: metav1.ObjectMeta{ - Name: hostname, - }, - } - - var err error - if !found { - newNode := node.DeepCopy() - permitAgentsLabels(newNode.Labels) - delete(newNode.Labels, labelEvictionApproved) - if !maps.Equal(newNode.Labels, node.Labels) { - err = r.Patch(ctx, newNode, k8sclient.MergeFrom(node)) - if err != nil { - return ctrl.Result{}, k8sclient.IgnoreNotFound(err) - } - } - err = r.Delete(ctx, eviction) - return ctrl.Result{}, k8sclient.IgnoreNotFound(err) - } - - var value string - hv := &kvmv1.Hypervisor{} - if err := r.Get(ctx, k8sclient.ObjectKey{Name: node.Name}, hv); err != nil { - return ctrl.Result{}, err - } - - if !HasStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeOnboarding) { - // Hasn't even started to onboard that node, so nothing to evict for sure - value = "true" - } else { - // check for existing eviction, else create it - value, err = r.reconcileEviction(ctx, eviction, hv, hostname, maintenanceValue) - if err != nil { - return ctrl.Result{}, err - } - } - - if value != "" { - err = disableInstanceHA(hv) - if err != nil { - return ctrl.Result{}, err - } - - newNode := node.DeepCopy() - if value == "true" { - evictAgentsLabels(newNode.Labels) - } - newNode.Labels[labelEvictionApproved] = maintenanceValue - - if !maps.Equal(newNode.Labels, node.Labels) { - err = r.Patch(ctx, newNode, k8sclient.MergeFrom(node)) - } - } - - return ctrl.Result{}, k8sclient.IgnoreNotFound(err) -} - -func (r *NodeEvictionLabelReconciler) reconcileEviction(ctx context.Context, eviction *kvmv1.Eviction, hypervisor *kvmv1.Hypervisor, hostname, maintenanceValue string) (string, error) { - log := logger.FromContext(ctx) - if err := r.Get(ctx, k8sclient.ObjectKeyFromObject(eviction), eviction); err != nil { - if !k8serrors.IsNotFound(err) { - return "", err - } - if err := controllerutil.SetOwnerReference(hypervisor, eviction, r.Scheme); err != nil { - return "", err - } - log.Info("Creating new eviction", "name", eviction.Name) - eviction.Spec = kvmv1.EvictionSpec{ - Hypervisor: hostname, - Reason: fmt.Sprintf("openstack-hypervisor-operator: label %v=%v", labelEvictionRequired, maintenanceValue), - } - - transportLabels(&hypervisor.ObjectMeta, &eviction.ObjectMeta) - if err = r.Create(ctx, eviction); err != nil { - return "", fmt.Errorf("failed to create eviction due to %w", err) - } - } - - // check if the eviction is already succeeded - var evictionState string - if status := meta.FindStatusCondition(eviction.Status.Conditions, kvmv1.ConditionTypeEvicting); status != nil { - evictionState = status.Reason - } - switch evictionState { - case "Succeeded": - return "true", nil - case "Failed": - return "false", nil - default: - return "", nil - } -} - -func evictAgentsLabels(labels map[string]string) { - hypervisorType, found := labels[labelHypervisor] - if found && !strings.HasSuffix(hypervisorType, disabledSuffix) { - labels[labelHypervisor] = hypervisorType + disabledSuffix - } - ml2MechanismDriver, found := labels[labelMl2MechanismDriver] - if found && !strings.HasSuffix(ml2MechanismDriver, disabledSuffix) { - labels[labelMl2MechanismDriver] = ml2MechanismDriver + disabledSuffix - } -} - -func permitAgentsLabels(labels map[string]string) { - hypervisorType, found := labels[labelHypervisor] - if found && strings.HasSuffix(hypervisorType, disabledSuffix) { - labels[labelHypervisor] = strings.TrimSuffix(hypervisorType, disabledSuffix) - } - ml2MechanismDriver, found := labels[labelMl2MechanismDriver] - if found && strings.HasSuffix(ml2MechanismDriver, disabledSuffix) { - labels[labelMl2MechanismDriver] = strings.TrimSuffix(ml2MechanismDriver, disabledSuffix) - } -} - -// SetupWithManager sets up the controller with the Manager. -func (r *NodeEvictionLabelReconciler) SetupWithManager(mgr ctrl.Manager) error { - ctx := context.Background() - _ = logger.FromContext(ctx) - - return ctrl.NewControllerManagedBy(mgr). - Named(NodeEvictionLabelControllerName). - For(&corev1.Node{}). // trigger the r.Reconcile whenever a node is created/updated/deleted. - Owns(&kvmv1.Eviction{}). // trigger the r.Reconcile whenever an Own-ed eviction is created/updated/deleted - Complete(r) -} diff --git a/internal/controller/node_eviction_label_controller_test.go b/internal/controller/node_eviction_label_controller_test.go deleted file mode 100644 index 6866a55..0000000 --- a/internal/controller/node_eviction_label_controller_test.go +++ /dev/null @@ -1,142 +0,0 @@ -/* -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 controller - -import ( - "os" - - "github.com/gophercloud/gophercloud/v2/testhelper" - . "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" - "sigs.k8s.io/controller-runtime/pkg/client" - - kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" -) - -var _ = Describe("Node Eviction Label Controller", func() { - const ( - nodeName = "test-node" - hostName = "test-hostname" - region = "region" - zone = "zone" - ) - var ( - reconciler *NodeEvictionLabelReconciler - req = ctrl.Request{NamespacedName: types.NamespacedName{Name: nodeName}} - fakeServer testhelper.FakeServer - reconcileNodeLoop = func(ctx SpecContext, steps int) (res ctrl.Result, err error) { - for range steps { - res, err = reconciler.Reconcile(ctx, req) - if err != nil { - return - } - } - return - } - ) - - BeforeEach(func(ctx SpecContext) { - fakeServer = testhelper.SetupHTTP() - Expect(os.Setenv("KVM_HA_SERVICE_URL", fakeServer.Endpoint())).To(Succeed()) - - DeferCleanup(func() { - Expect(os.Unsetenv("KVM_HA_SERVICE_URL")).To(Succeed()) - fakeServer.Teardown() - }) - - reconciler = &NodeEvictionLabelReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - } - - By("creating the namespace for the reconciler") - ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "monsoon3"}} - Expect(client.IgnoreAlreadyExists(k8sClient.Create(ctx, ns))).To(Succeed()) - - By("creating the node resource") - resource := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: nodeName, - Labels: map[string]string{ - corev1.LabelHostname: hostName, - corev1.LabelTopologyRegion: region, - corev1.LabelTopologyZone: zone, - labelEvictionRequired: "true", - }, - }, - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - - DeferCleanup(func(ctx SpecContext) { - By("Cleanup the specific node") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) - }) - - By("creating the hypervisor resource") - hypervisor := &kvmv1.Hypervisor{ - ObjectMeta: metav1.ObjectMeta{ - Name: nodeName, - Labels: map[string]string{ - corev1.LabelHostname: nodeName, - }, - }, - Spec: kvmv1.HypervisorSpec{ - LifecycleEnabled: true, - }, - } - Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed()) - DeferCleanup(func(ctx SpecContext) { - By("Cleanup the specific hypervisor") - Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, hypervisor))).To(Succeed()) - }) - - }) - - Context("When reconciling a node", func() { - BeforeEach(func(ctx SpecContext) { - hypervisor := &kvmv1.Hypervisor{} - Expect(k8sClient.Get(ctx, types.NamespacedName{Name: nodeName}, hypervisor)).To(Succeed()) - By("updating the hypervisor status sub-resource") - meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{ - Type: kvmv1.ConditionTypeOnboarding, - Status: metav1.ConditionTrue, - Reason: kvmv1.ConditionReasonInitial, - Message: "Initial onboarding", - }) - Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed()) - }) - - It("should successfully reconcile the resource", func(ctx SpecContext) { - By("ConditionType the created resource") - _, err := reconcileNodeLoop(ctx, 5) - Expect(err).NotTo(HaveOccurred()) - - // expect node controller to create an eviction for the node - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: hostName, - Namespace: "monsoon3", - }, &kvmv1.Eviction{}) - Expect(err).NotTo(HaveOccurred()) - }) - }) -}) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index d562a1b..75625a9 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -19,11 +19,9 @@ package controller import ( "bytes" - "context" "errors" "fmt" "io" - "maps" "net/http" "os" "slices" @@ -33,22 +31,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" v1ac "k8s.io/client-go/applyconfigurations/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" ) -// setNodeLabels sets the labels on the node. -func setNodeLabels(ctx context.Context, writer client.Writer, node *corev1.Node, labels map[string]string) (bool, error) { - newNode := node.DeepCopy() - maps.Copy(newNode.Labels, labels) - if maps.Equal(node.Labels, newNode.Labels) { - return false, nil - } - - return true, writer.Patch(ctx, newNode, client.MergeFrom(node)) -} - func InstanceHaUrl(region, zone, hostname string) string { if haURL, found := os.LookupEnv("KVM_HA_SERVICE_URL"); found { return haURL