diff --git a/CHANGELOG.md b/CHANGELOG.md index ae977f01872..a6efbd37009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio ### New +- **General**: Prevent multiple ScaledObjects managing one HPA ([#6130](https://github.com/kedacore/keda/issues/6130)) - **CloudEventSource**: Introduce ClusterCloudEventSource ([#3533](https://github.com/kedacore/keda/issues/3533)) - **CloudEventSource**: Provide ClusterCloudEventSource around the management of ScaledJobs resources ([#3523](https://github.com/kedacore/keda/issues/3523)) - **CloudEventSource**: Provide ClusterCloudEventSource around the management of TriggerAuthentication/ClusterTriggerAuthentication resources ([#3524](https://github.com/kedacore/keda/issues/3524)) diff --git a/apis/keda/v1alpha1/scaledobject_webhook.go b/apis/keda/v1alpha1/scaledobject_webhook.go index b3602d16739..fa5ac7639ed 100644 --- a/apis/keda/v1alpha1/scaledobject_webhook.go +++ b/apis/keda/v1alpha1/scaledobject_webhook.go @@ -277,6 +277,7 @@ func verifyScaledObjects(incomingSo *ScaledObject, action string, _ bool) error return err } + incomingSoHpaName := getHpaName(*incomingSo) for _, so := range soList.Items { if so.Name == incomingSo.Name { continue @@ -297,6 +298,13 @@ func verifyScaledObjects(incomingSo *ScaledObject, action string, _ bool) error metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "other-scaled-object") return err } + + if getHpaName(so) == incomingSoHpaName { + err = fmt.Errorf("the HPA '%s' is already managed by the ScaledObject '%s'", so.Spec.Advanced.HorizontalPodAutoscalerConfig.Name, so.Name) + scaledobjectlog.Error(err, "validation error") + metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "other-scaled-object-hpa") + return err + } } // verify ScalingModifiers structure if defined in ScaledObject @@ -544,3 +552,11 @@ func isContainerResourceLimitSet(ctx context.Context, namespace string, triggerT return false } + +func getHpaName(so ScaledObject) string { + if so.Spec.Advanced == nil || so.Spec.Advanced.HorizontalPodAutoscalerConfig.Name == "" { + return fmt.Sprintf("keda-hpa-%s", so.Name) + } + + return so.Spec.Advanced.HorizontalPodAutoscalerConfig.Name +} diff --git a/tests/internals/scaled_object_validation/scaled_object_validation_test.go b/tests/internals/scaled_object_validation/scaled_object_validation_test.go index 9cdaff34515..65793ff98bf 100644 --- a/tests/internals/scaled_object_validation/scaled_object_validation_test.go +++ b/tests/internals/scaled_object_validation/scaled_object_validation_test.go @@ -131,6 +131,27 @@ spec: desiredReplicas: '1' ` + customHpaScaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + advanced: + horizontalPodAutoscalerConfig: + name: {{.HpaName}} + triggers: + - type: cron + metadata: + timezone: Etc/UTC + start: 0 * * * * + end: 1 * * * * + desiredReplicas: '1' + ` + hpaTemplate = ` apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler @@ -179,6 +200,8 @@ func TestScaledObjectValidations(t *testing.T) { testScaledWorkloadByOtherScaledObject(t, data) + testManagedHpaByOtherScaledObject(t, data) + testScaledWorkloadByOtherHpa(t, data) testScaledWorkloadByOtherHpaWithOwnershipTransfer(t, data) @@ -220,6 +243,24 @@ func testScaledWorkloadByOtherScaledObject(t *testing.T, data templateData) { KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) } +func testManagedHpaByOtherScaledObject(t *testing.T, data templateData) { + t.Log("--- already managed hpa by other scaledobject---") + + data.HpaName = hpaName + + data.ScaledObjectName = scaledObject1Name + err := KubectlApplyWithErrors(t, data, "scaledObjectTemplate", customHpaScaledObjectTemplate) + assert.NoErrorf(t, err, "cannot deploy the scaledObject - %s", err) + + data.ScaledObjectName = scaledObject2Name + err = KubectlApplyWithErrors(t, data, "scaledObjectTemplate", customHpaScaledObjectTemplate) + assert.Errorf(t, err, "can deploy the scaledObject - %s", err) + assert.Contains(t, err.Error(), fmt.Sprintf("the HPA '%s' is already managed by the ScaledObject '%s", hpaName, scaledObject1Name)) + + data.ScaledObjectName = scaledObject1Name + KubectlDeleteWithTemplate(t, data, "scaledObjectTemplate", scaledObjectTemplate) +} + func testScaledWorkloadByOtherHpa(t *testing.T, data templateData) { t.Log("--- already scaled workload by other hpa---")