Repository: kedacore/keda

Issue:
ScaledObject created with transfer-hpa-ownership annotation does not set .status.hpaName
### Report

When creating a ScaledObject with the `scaledobject.keda.sh/transfer-hpa-ownership` annotation, it does not set `.status.hpaName`. 

### Expected Behavior

Adopted/transferred-ownership ScaledObject resources should have `.status.hpaName` set to the name of the HPA referenced in `.spec.advanced.horizontalPodAutoscalerConfig.name`

### Actual Behavior

`.status.hpaName` is unset in all cases except when KEDA was responsible for the initial creation of the managed HPA. 

### Steps to Reproduce the Problem

1. Create an HPA.
2. Create a ScaledObject with the `transfer-hpa-ownership` annotation combined with `.spec.advanced` to have KEDA take over the HPA.
3. Observe that `.status.hpaName` never gets set on the ScaledObject.


### Logs from KEDA operator

_No response_

### KEDA Version

2.14.0

### Kubernetes Version

1.29

### Platform

Any

### Scaler Details

_No response_

### Anything else?

I think that this may be an issue for any ScaledObject where the first reconcile loop doesn't go through [this block](https://github.com/kedacore/keda/blob/6681d5ecbc82293626cab693625c9cc9fb453381/controllers/keda/hpa.go#L57-L65), tracing through the code this seems to be the only place where status.HpaName is set and it's only called when HPAs are initially created.

Discussion:
@SpiritZhou: Are you willing to fix this bug? :)@SriRamanujam: @SpiritZhou I'm happy to take a crack at it, but I wanted to confirm that this is in fact a bug and not intended behavior or otherwise load-bearing :D @SpiritZhou: I believe this is a bug. but we can ask @zroubalik @JorTurFer @wozniakjan to double check@JorTurFer: It looks as a bug, yeah@stale: This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions.
@stale: This issue has been automatically closed due to inactivity.
@justinmir: I've added a fix for this issue here: https://github.com/kedacore/keda/pull/6860. Notably, this issue includes more than `transfer-hpa-ownership`, if the hpaName is cleared from the status for any reason from a ScaledObject, it will never be reset. Can we reopen this issue and push forward with the above PR?

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef6b74216..2da66e463 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -79,6 +79,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
 
 - **General**: Fix prefixes on envFrom elements in a deployment spec aren't being interpreted and Environment variables are not prefixed with the prefix ([#6728](https://github.com/kedacore/keda/issues/6728))
 - **General**: Remove klogr dependency and replace with zap ([#5732](https://github.com/kedacore/keda/issues/5732))
+- **General**: Sets hpaName in Status when ScaledObject adopts/finds an existing HPA ([#6336](https://github.com/kedacore/keda/issues/6336))
 - **Temporal Scaler**: Fix Temporal Scaler TLS version ([#6707](https://github.com/kedacore/keda/pull/6707))
 
 ### Deprecations
diff --git a/controllers/keda/hpa.go b/controllers/keda/hpa.go
index 5fb2c835e..8ecebc775 100644
--- a/controllers/keda/hpa.go
+++ b/controllers/keda/hpa.go
@@ -38,6 +38,19 @@ import (
 	version "github.com/kedacore/keda/v2/version"
 )
 
+// storeHpaNameInStatus updates the ScaledObject status subresource with the hpaName.
+func (r *ScaledObjectReconciler) storeHpaNameInStatus(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject, hpaName string) error {
+	status := scaledObject.Status.DeepCopy()
+	status.HpaName = hpaName
+
+	err := kedastatus.UpdateScaledObjectStatus(ctx, r.Client, logger, scaledObject, status)
+	if err != nil {
+		logger.Error(err, "Failed to update scaledObject status with used hpaName")
+		return err
+	}
+	return nil
+}
+
 // createAndDeployNewHPA creates and deploy HPA in the cluster for specified ScaledObject
 func (r *ScaledObjectReconciler) createAndDeployNewHPA(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject, gvkr *kedav1alpha1.GroupVersionKindResource) error {
 	hpaName := getHPAName(scaledObject)
@@ -54,17 +67,7 @@ func (r *ScaledObjectReconciler) createAndDeployNewHPA(ctx context.Context, logg
 		return err
 	}
 
-	// store hpaName in the ScaledObject
-	status := scaledObject.Status.DeepCopy()
-	status.HpaName = hpaName
-
-	err = kedastatus.UpdateScaledObjectStatus(ctx, r.Client, logger, scaledObject, status)
-	if err != nil {
-		logger.Error(err, "Failed to update scaledObject status with used hpaName")
-		return err
-	}
-
-	return nil
+	return r.storeHpaNameInStatus(ctx, logger, scaledObject, hpaName)
 }
 
 // newHPAForScaledObject returns HPA as it is specified in ScaledObject
diff --git a/controllers/keda/scaledobject_controller.go b/controllers/keda/scaledobject_controller.go
index 68355eef3..b6b6d4486 100755
--- a/controllers/keda/scaledobject_controller.go
+++ b/controllers/keda/scaledobject_controller.go
@@ -479,6 +479,14 @@ func (r *ScaledObjectReconciler) ensureHPAForScaledObjectExists(ctx context.Cont
 		return false, err
 	}
 
+	// If the HPA name does not match the one in ScaledObject status, we need to update the status
+	if scaledObject.Status.HpaName != hpaName {
+		err = r.storeHpaNameInStatus(ctx, logger, scaledObject, hpaName)
+		if err != nil {
+			return false, err
+		}
+	}
+
 	return false, nil
 }
 



Test patch:
diff --git a/controllers/keda/scaledobject_controller_test.go b/controllers/keda/scaledobject_controller_test.go
index 059025fc2..ae04c90f6 100644
--- a/controllers/keda/scaledobject_controller_test.go
+++ b/controllers/keda/scaledobject_controller_test.go
@@ -349,6 +349,62 @@ var _ = Describe("ScaledObjectController", func() {
 			Expect(errors.IsNotFound(err)).To(Equal(true))
 		})
 
+		It("sets the hpaName in status if not set and HPA already exists", func() {
+			// Create the scaling target.
+			deploymentName := "hpa-name-update"
+			soName := "so-" + deploymentName
+			err := k8sClient.Create(context.Background(), generateDeployment(deploymentName))
+			Expect(err).ToNot(HaveOccurred())
+
+			// Create the ScaledObject without specifying name.
+			so := &kedav1alpha1.ScaledObject{
+				ObjectMeta: metav1.ObjectMeta{Name: soName, Namespace: "default"},
+				Spec: kedav1alpha1.ScaledObjectSpec{
+					ScaleTargetRef: &kedav1alpha1.ScaleTarget{
+						Name: deploymentName,
+					},
+					Advanced: &kedav1alpha1.AdvancedConfig{
+						HorizontalPodAutoscalerConfig: &kedav1alpha1.HorizontalPodAutoscalerConfig{},
+					},
+					Triggers: []kedav1alpha1.ScaleTriggers{
+						{
+							Type: "cron",
+							Metadata: map[string]string{
+								"timezone":        "UTC",
+								"start":           "0 * * * *",
+								"end":             "1 * * * *",
+								"desiredReplicas": "1",
+							},
+						},
+					},
+				},
+			}
+			err = k8sClient.Create(context.Background(), so)
+			Expect(err).ToNot(HaveOccurred())
+
+			// Get and confirm the HPA.
+			hpa := &autoscalingv2.HorizontalPodAutoscaler{}
+			Eventually(func() error {
+				return k8sClient.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("keda-hpa-%s", soName), Namespace: "default"}, hpa)
+			}).ShouldNot(HaveOccurred())
+			Expect(hpa.Name).To(Equal(fmt.Sprintf("keda-hpa-%s", soName)))
+
+			// Remove the HPA name from the ScaledObject.
+			Eventually(func() error {
+				err = k8sClient.Get(context.Background(), types.NamespacedName{Name: soName, Namespace: "default"}, so)
+				Expect(err).ToNot(HaveOccurred())
+				so.Status.HpaName = ""
+				return k8sClient.Status().Update(context.Background(), so)
+			}).ShouldNot(HaveOccurred())
+
+			// Wait until the hpaName is updated in the scaled object.
+			Eventually(func() string {
+				err = k8sClient.Get(context.Background(), types.NamespacedName{Name: soName, Namespace: "default"}, so)
+				Expect(err).ToNot(HaveOccurred())
+				return so.Status.HpaName
+			}).Should(Equal(fmt.Sprintf("keda-hpa-%s", soName)))
+		})
+
 		//https://github.com/kedacore/keda/issues/2407
 		It("cache is correctly recreated if SO is deleted and created", func() {
 			// Create the scaling target.