Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
- **General**: Fix ScaledObject admission webhook to return validation error from `verifyReplicaCount`, preventing invalid ScaledObjects from being created ([#5954](https://github.com/kedacore/keda/issues/5954))
- **General**: Fix ScaledObject Ready condition not reflecting HPA status ([#7649](https://github.com/kedacore/keda/issues/7649))
- **General**: Handle paused scaling directly in reconciler ([#7663](https://github.com/kedacore/keda/issues/7663))
- **General**: Reject ScaledObject creation and update when the name exceeds 63 characters ([#6998](https://github.com/kedacore/keda/issues/6998))
- **AWS Scalers**: Fix TCP connection leak by closing HTTP idle connections on scaler `Close()` for SQS, Kinesis, DynamoDB, DynamoDB Streams, and CloudWatch scalers ([#7756](https://github.com/kedacore/keda/issues/7756))
- **Azure Data Explorer Scaler**: Remove clientSecretFromEnv support ([#7554](https://github.com/kedacore/keda/pull/7554))
- **Azure Event Hub Scaler**: Reject non-positive `unprocessedEventThreshold` to prevent integer division by zero when computing lag ([#7732](https://github.com/kedacore/keda/issues/7732))
Expand Down
21 changes: 21 additions & 0 deletions apis/keda/v1alpha1/scaledobject_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ var restMapper meta.RESTMapper
var memoryString = "memory"
var cpuString = "cpu"

// maxK8sLabelValueLength is the Kubernetes label value limit. The ScaledObject name is used as a label value (scaledobject.keda.sh/name=<so.Name>) on the SO and HPA, and the generated HPA name (keda-hpa-<so.Name> when no custom name is set) is itself a DNS-1123 label.
const maxK8sLabelValueLength = 63

func (so *ScaledObject) SetupWebhookWithManager(mgr ctrl.Manager, cacheMissFallback bool) error {
err := setupKubernetesClients(mgr, cacheMissFallback)
if err != nil {
Expand Down Expand Up @@ -164,6 +167,7 @@ func validateWorkload(so *ScaledObject, action string, dryRun bool) (admission.W
"verifyHpas": verifyHpas,
"verifyReplicaCount": verifyReplicaCount,
"verifyFallback": verifyFallback,
"verifyName": verifyName,
}

for functionName, function := range verifyFunctions {
Expand Down Expand Up @@ -199,6 +203,23 @@ func verifyReplicaCount(incomingSo *ScaledObject, action string, _ bool) error {
return err
}

func verifyName(incomingSo *ScaledObject, action string, _ bool) error {
if len(incomingSo.Name) > maxK8sLabelValueLength {
err := fmt.Errorf("scaledobject name %q is %d characters long; must be no more than %d characters because it is used as the %q label value", incomingSo.Name, len(incomingSo.Name), maxK8sLabelValueLength, ScaledObjectOwnerAnnotation)
scaledobjectlog.WithValues("name", incomingSo.Name).Error(err, "validation error")
metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "name-too-long")
return err
}
hpaName := getHpaName(*incomingSo)
if len(hpaName) > maxK8sLabelValueLength {
err := fmt.Errorf("HPA name %q derived from scaledobject is %d characters long; must be no more than %d characters; set spec.advanced.horizontalPodAutoscalerConfig.name to a shorter name or shorten the scaledobject name", hpaName, len(hpaName), maxK8sLabelValueLength)
scaledobjectlog.WithValues("name", incomingSo.Name).Error(err, "validation error")
metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "hpa-name-too-long")
return err
}
return nil
}

func verifyFallback(incomingSo *ScaledObject, action string, _ bool) error {
err := CheckFallbackValid(incomingSo)
if err != nil {
Expand Down
83 changes: 83 additions & 0 deletions apis/keda/v1alpha1/scaledobject_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package v1alpha1

import (
"context"
"strings"
"time"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -732,6 +733,88 @@ var _ = It("should validate the so update if it's removing the finalizer even if
}).ShouldNot(HaveOccurred())
})

var _ = It("shouldn't validate the so creation when the name exceeds the label value limit", func() {

namespaceName := "so-name-too-long"
namespace := createNamespace(namespaceName)
longName := strings.Repeat("a", maxK8sLabelValueLength+1)
so := createScaledObject(longName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "short-hpa")

err := k8sClient.Create(context.Background(), namespace)
Expect(err).ToNot(HaveOccurred())

Eventually(func() error {
return k8sClient.Create(context.Background(), so)
}).Should(HaveOccurred())
})

var _ = It("should validate the so creation when the name is at the label value limit and a custom HPA name is set", func() {

namespaceName := "so-name-max-with-custom-hpa"
namespace := createNamespace(namespaceName)
maxName := strings.Repeat("a", maxK8sLabelValueLength)
so := createScaledObject(maxName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "short-hpa")

err := k8sClient.Create(context.Background(), namespace)
Expect(err).ToNot(HaveOccurred())

Eventually(func() error {
return k8sClient.Create(context.Background(), so)
}).ShouldNot(HaveOccurred())
})

var _ = It("shouldn't validate the so creation when no custom HPA name is set and the generated HPA name would exceed the label value limit", func() {

namespaceName := "so-generated-hpa-too-long"
namespace := createNamespace(namespaceName)
// 55 chars: keda-hpa- prefix (9) + 55 = 64, overflows
longName := strings.Repeat("a", maxK8sLabelValueLength-len("keda-hpa-")+1)
so := createScaledObject(longName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "")

err := k8sClient.Create(context.Background(), namespace)
Expect(err).ToNot(HaveOccurred())

Eventually(func() error {
return k8sClient.Create(context.Background(), so)
}).Should(HaveOccurred())
})

var _ = It("should validate the so creation when no custom HPA name is set and the generated HPA name fits within the label value limit", func() {

namespaceName := "so-generated-hpa-max"
namespace := createNamespace(namespaceName)
// 54 chars: keda-hpa- prefix (9) + 54 = 63, fits
maxName := strings.Repeat("a", maxK8sLabelValueLength-len("keda-hpa-"))
so := createScaledObject(maxName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "")

err := k8sClient.Create(context.Background(), namespace)
Expect(err).ToNot(HaveOccurred())

Eventually(func() error {
return k8sClient.Create(context.Background(), so)
}).ShouldNot(HaveOccurred())
})

var _ = It("shouldn't validate the so update when removing the custom HPA name would make the generated HPA name exceed the label value limit", func() {

namespaceName := "so-update-remove-custom-hpa"
namespace := createNamespace(namespaceName)
// 60 chars: passes create with custom HPA name, but keda-hpa-<60> = 69 would overflow
longName := strings.Repeat("a", 60)
so := createScaledObject(longName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "short-hpa")

err := k8sClient.Create(context.Background(), namespace)
Expect(err).ToNot(HaveOccurred())

err = k8sClient.Create(context.Background(), so)
Expect(err).ToNot(HaveOccurred())

so.Spec.Advanced.HorizontalPodAutoscalerConfig = nil
Eventually(func() error {
return k8sClient.Update(context.Background(), so)
}).Should(HaveOccurred())
})

var _ = It("shouldn't create so when stabilizationWindowSeconds exceeds 3600", func() {

namespaceName := "fail-so-creation"
Expand Down
Loading