Skip to content

Commit 92bda55

Browse files
authored
feat: reject ScaledObject creation and update when the name exceeds 63 characters (#7762)
* feat: reject ScaledObject when name exceeds 54 characters Signed-off-by: Rick Brouwer <rickbrouwer@gmail.com> * feat: reject ScaledObject creation and update when the name exceeds 63 characters Signed-off-by: Rick Brouwer <rickbrouwer@gmail.com> --------- Signed-off-by: Rick Brouwer <rickbrouwer@gmail.com>
1 parent 8dae444 commit 92bda55

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
9898
- **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))
9999
- **General**: Fix ScaledObject Ready condition not reflecting HPA status ([#7649](https://github.com/kedacore/keda/issues/7649))
100100
- **General**: Handle paused scaling directly in reconciler ([#7663](https://github.com/kedacore/keda/issues/7663))
101+
- **General**: Reject ScaledObject creation and update when the name exceeds 63 characters ([#6998](https://github.com/kedacore/keda/issues/6998))
101102
- **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))
102103
- **Azure Data Explorer Scaler**: Remove clientSecretFromEnv support ([#7554](https://github.com/kedacore/keda/pull/7554))
103104
- **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))

apis/keda/v1alpha1/scaledobject_webhook.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ var restMapper meta.RESTMapper
5252
var memoryString = "memory"
5353
var cpuString = "cpu"
5454

55+
// 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.
56+
const maxK8sLabelValueLength = 63
57+
5558
func (so *ScaledObject) SetupWebhookWithManager(mgr ctrl.Manager, cacheMissFallback bool) error {
5659
err := setupKubernetesClients(mgr, cacheMissFallback)
5760
if err != nil {
@@ -164,6 +167,7 @@ func validateWorkload(so *ScaledObject, action string, dryRun bool) (admission.W
164167
"verifyHpas": verifyHpas,
165168
"verifyReplicaCount": verifyReplicaCount,
166169
"verifyFallback": verifyFallback,
170+
"verifyName": verifyName,
167171
}
168172

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

206+
func verifyName(incomingSo *ScaledObject, action string, _ bool) error {
207+
if len(incomingSo.Name) > maxK8sLabelValueLength {
208+
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)
209+
scaledobjectlog.WithValues("name", incomingSo.Name).Error(err, "validation error")
210+
metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "name-too-long")
211+
return err
212+
}
213+
hpaName := getHpaName(*incomingSo)
214+
if len(hpaName) > maxK8sLabelValueLength {
215+
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)
216+
scaledobjectlog.WithValues("name", incomingSo.Name).Error(err, "validation error")
217+
metricscollector.RecordScaledObjectValidatingErrors(incomingSo.Namespace, action, "hpa-name-too-long")
218+
return err
219+
}
220+
return nil
221+
}
222+
202223
func verifyFallback(incomingSo *ScaledObject, action string, _ bool) error {
203224
err := CheckFallbackValid(incomingSo)
204225
if err != nil {

apis/keda/v1alpha1/scaledobject_webhook_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package v1alpha1
1818

1919
import (
2020
"context"
21+
"strings"
2122
"time"
2223

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

736+
var _ = It("shouldn't validate the so creation when the name exceeds the label value limit", func() {
737+
738+
namespaceName := "so-name-too-long"
739+
namespace := createNamespace(namespaceName)
740+
longName := strings.Repeat("a", maxK8sLabelValueLength+1)
741+
so := createScaledObject(longName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "short-hpa")
742+
743+
err := k8sClient.Create(context.Background(), namespace)
744+
Expect(err).ToNot(HaveOccurred())
745+
746+
Eventually(func() error {
747+
return k8sClient.Create(context.Background(), so)
748+
}).Should(HaveOccurred())
749+
})
750+
751+
var _ = It("should validate the so creation when the name is at the label value limit and a custom HPA name is set", func() {
752+
753+
namespaceName := "so-name-max-with-custom-hpa"
754+
namespace := createNamespace(namespaceName)
755+
maxName := strings.Repeat("a", maxK8sLabelValueLength)
756+
so := createScaledObject(maxName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "short-hpa")
757+
758+
err := k8sClient.Create(context.Background(), namespace)
759+
Expect(err).ToNot(HaveOccurred())
760+
761+
Eventually(func() error {
762+
return k8sClient.Create(context.Background(), so)
763+
}).ShouldNot(HaveOccurred())
764+
})
765+
766+
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() {
767+
768+
namespaceName := "so-generated-hpa-too-long"
769+
namespace := createNamespace(namespaceName)
770+
// 55 chars: keda-hpa- prefix (9) + 55 = 64, overflows
771+
longName := strings.Repeat("a", maxK8sLabelValueLength-len("keda-hpa-")+1)
772+
so := createScaledObject(longName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "")
773+
774+
err := k8sClient.Create(context.Background(), namespace)
775+
Expect(err).ToNot(HaveOccurred())
776+
777+
Eventually(func() error {
778+
return k8sClient.Create(context.Background(), so)
779+
}).Should(HaveOccurred())
780+
})
781+
782+
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() {
783+
784+
namespaceName := "so-generated-hpa-max"
785+
namespace := createNamespace(namespaceName)
786+
// 54 chars: keda-hpa- prefix (9) + 54 = 63, fits
787+
maxName := strings.Repeat("a", maxK8sLabelValueLength-len("keda-hpa-"))
788+
so := createScaledObject(maxName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "")
789+
790+
err := k8sClient.Create(context.Background(), namespace)
791+
Expect(err).ToNot(HaveOccurred())
792+
793+
Eventually(func() error {
794+
return k8sClient.Create(context.Background(), so)
795+
}).ShouldNot(HaveOccurred())
796+
})
797+
798+
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() {
799+
800+
namespaceName := "so-update-remove-custom-hpa"
801+
namespace := createNamespace(namespaceName)
802+
// 60 chars: passes create with custom HPA name, but keda-hpa-<60> = 69 would overflow
803+
longName := strings.Repeat("a", 60)
804+
so := createScaledObject(longName, namespaceName, workloadName, "apps/v1", "Deployment", false, map[string]string{}, "short-hpa")
805+
806+
err := k8sClient.Create(context.Background(), namespace)
807+
Expect(err).ToNot(HaveOccurred())
808+
809+
err = k8sClient.Create(context.Background(), so)
810+
Expect(err).ToNot(HaveOccurred())
811+
812+
so.Spec.Advanced.HorizontalPodAutoscalerConfig = nil
813+
Eventually(func() error {
814+
return k8sClient.Update(context.Background(), so)
815+
}).Should(HaveOccurred())
816+
})
817+
735818
var _ = It("shouldn't create so when stabilizationWindowSeconds exceeds 3600", func() {
736819

737820
namespaceName := "fail-so-creation"

0 commit comments

Comments
 (0)