From ff4697acfdafc8889e31b8724ab272b7628c36b7 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Wed, 13 May 2026 12:50:43 +0200 Subject: [PATCH 1/5] fix: label values when dot are present Label values can't start nor finish with a `.` (dot). This contribution ensures that any dot at the beginning or end of a label value is removed. Signed-off-by: Francesco Ilario rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- pkg/formatting/k8labels.go | 4 ++-- pkg/formatting/k8labels_test.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pkg/formatting/k8labels.go b/pkg/formatting/k8labels.go index 4274c9c2fa..d9cb822a83 100644 --- a/pkg/formatting/k8labels.go +++ b/pkg/formatting/k8labels.go @@ -18,8 +18,8 @@ func CleanValueKubernetes(s string) string { } replasoeur := strings.NewReplacer(":", "-", "/", "-", " ", "_", "[", "__", "]", "__") - s = strings.TrimRight(s, " -_[]") - s = strings.TrimLeft(s, " -_[]") + s = strings.TrimRight(s, " .-_[]") + s = strings.TrimLeft(s, " .-_[]") replaced := replasoeur.Replace(s) return strings.TrimSpace(replaced) } diff --git a/pkg/formatting/k8labels_test.go b/pkg/formatting/k8labels_test.go index d5fc80bf2c..42b24cadba 100644 --- a/pkg/formatting/k8labels_test.go +++ b/pkg/formatting/k8labels_test.go @@ -57,6 +57,21 @@ func TestK8LabelsCleanup(t *testing.T) { str: "secret:name/with_underscores", want: "secret-name-with_underscores", }, + { + name: "starts with a .", + str: ".i-start-with-a-dot", + want: "i-start-with-a-dot", + }, + { + name: "ends with a .", + str: "i-end-with-a-dot.", + want: "i-end-with-a-dot", + }, + { + name: "long value once cut starts with a .", + str: "everythingbeforethedotisdropped.thesehereare61chars-62withdot-previouscharacterswillbedropped", + want: "thesehereare61chars-62withdot-previouscharacterswillbedropped", + }, } for _, tt := range tests { From 447b6c70a53c39b0fc443cd64190c5a872708e83 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Wed, 13 May 2026 16:25:54 +0200 Subject: [PATCH 2/5] add sanification of the input and improve logic Signed-off-by: Francesco Ilario rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- pkg/formatting/k8labels.go | 103 ++++++++++++++++++++++++++++--- pkg/formatting/k8labels_test.go | 105 ++++++++++++++++++++++++++++---- 2 files changed, 185 insertions(+), 23 deletions(-) diff --git a/pkg/formatting/k8labels.go b/pkg/formatting/k8labels.go index d9cb822a83..7836885ab9 100644 --- a/pkg/formatting/k8labels.go +++ b/pkg/formatting/k8labels.go @@ -1,25 +1,108 @@ package formatting import ( + "slices" "strings" + "unicode" + + "k8s.io/apimachinery/pkg/util/validation" ) -// CleanValueKubernetes conform a string to kubernetes naming convention +var ( + managedSpecialCharsMap = map[rune]string{ + ':': "-", + '/': "-", + ' ': "_", + '[': "__", + ']': "__", + } + allowedSpecialCharsRFC1123 = ".-_" + allowedSpecialCharsRFC1123Runes = []rune(allowedSpecialCharsRFC1123) + + // Build the replacer starting from the managedSpecialCharsMap + managedSpecialCharsInLabelValueReplacer = strings.NewReplacer(pairs(managedSpecialCharsMap)...) +) + +// CleanValueKubernetes conforms a string to kubernetes naming convention // see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names // rules are: // • contain at most 63 characters -// • contain only lowercase alphanumeric characters or '-' +// • contain only alphanumeric characters or '-', '.', and '_' // • start with an alphanumeric character // • end with an alphanumeric character. func CleanValueKubernetes(s string) string { - if len(s) >= 63 { - // keep the last 62 characters - s = s[len(s)-62:] + // cut short if the string is already a valid label + if len(validation.IsValidLabelValue(s)) == 0 { + return s + } + + // sanitize the input + sanitized := sanitizeLabelValue(s) + + // replace the managed special symbols + replaced := managedSpecialCharsInLabelValueReplacer.Replace(sanitized) + + // trim unwanted chars + trimmed := strings.Trim(replaced, allowedSpecialCharsRFC1123) + + // cut to max length + cut := cutToLabelValueMaxLength(trimmed) + + // trim left again to ensure no invalid values + // are at the edge after the cut + return strings.TrimLeft(cut, allowedSpecialCharsRFC1123) +} + +// cutToLabelValueMaxLength cuts the provided string to the maximum length allowed +// for a kubernetes label (63 chars) +func cutToLabelValueMaxLength(s string) string { + if len(s) > validation.LabelValueMaxLength { + return s[len(s)-(validation.LabelValueMaxLength):] } + return s +} + +// sanitizeLabelValue removes all unmanaged characters from the input string +func sanitizeLabelValue(s string) string { + b := strings.Builder{} + for _, r := range s { + if isSafe(r) { + b.WriteRune(r) + } + } + return b.String() +} + +// pairs extracts all the key-value pairs from a rune-string map +func pairs(m map[rune]string) []string { + ss := make([]string, 0, len(m)*2) + for k, v := range m { + ss = append(ss, string(k), v) + } + return ss +} + +// isSafe is a helper to identify if the processed rune is safe to process +// or should be dropped +func isSafe(r rune) bool { + return !(unicode.IsSpace(r) && r != ' ') && + (isAllowedSpecialCharRFC1123(r) || isAlphanumeric(r) || isManagedSpecialChar(r)) +} + +// isAlphanumeric returns true if the rune is an alphanumeric value +func isAlphanumeric(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') +} + +// isAllowedSpecialCharRFC1123 returns true if the rune is an allowed char as per +// the RFC1123 +func isAllowedSpecialCharRFC1123(r rune) bool { + return slices.Contains(allowedSpecialCharsRFC1123Runes, r) +} - replasoeur := strings.NewReplacer(":", "-", "/", "-", " ", "_", "[", "__", "]", "__") - s = strings.TrimRight(s, " .-_[]") - s = strings.TrimLeft(s, " .-_[]") - replaced := replasoeur.Replace(s) - return strings.TrimSpace(replaced) +// isManagedSpecialChar returns true if the rune is a managed special char. +// The normalization of the value will replace it with its counterpart. +func isManagedSpecialChar(r rune) bool { + _, ok := managedSpecialCharsMap[r] + return ok } diff --git a/pkg/formatting/k8labels_test.go b/pkg/formatting/k8labels_test.go index 42b24cadba..eeddfbf37b 100644 --- a/pkg/formatting/k8labels_test.go +++ b/pkg/formatting/k8labels_test.go @@ -3,6 +3,8 @@ package formatting import ( "strings" "testing" + + "k8s.io/apimachinery/pkg/util/validation" ) func TestK8LabelsCleanup(t *testing.T) { @@ -13,7 +15,7 @@ func TestK8LabelsCleanup(t *testing.T) { }{ { name: "clean characters for k8 labels", - str: "foo/bar hello", + str: "foo/bar h#@?!ello", want: "foo-bar_hello", }, { @@ -36,11 +38,15 @@ func TestK8LabelsCleanup(t *testing.T) { str: "foo\n", want: "foo", }, - + { + name: "remove new line from the middle", + str: "foo\nfoo", + want: "foofoo", + }, { name: "secret name longer than 63 characters", str: strings.Repeat("a", 64), - want: strings.Repeat("a", 62), + want: strings.Repeat("a", 63), }, { name: "secret name ends with non-alphanumeric character", @@ -58,26 +64,99 @@ func TestK8LabelsCleanup(t *testing.T) { want: "secret-name-with_underscores", }, { - name: "starts with a .", - str: ".i-start-with-a-dot", - want: "i-start-with-a-dot", + name: "has an invalid start (.)", + str: ".i-start-with-an-invalid-char", + want: "i-start-with-an-invalid-char", + }, + { + name: "has an invalid end (.)", + str: "i-end-with-an-invalid-char.", + want: "i-end-with-an-invalid-char", + }, + { + name: "has an invalid start (:)", + str: ":i-start-with-an-invalid-char", + want: "i-start-with-an-invalid-char", + }, + { + name: "has an invalid end (:)", + str: "i-end-with-an-invalid-char:", + want: "i-end-with-an-invalid-char", + }, + { + name: "has an invalid start (/)", + str: "/i-start-with-an-invalid-char", + want: "i-start-with-an-invalid-char", + }, + { + name: "has an invalid end (/)", + str: "i-end-with-an-invalid-char/", + want: "i-end-with-an-invalid-char", + }, + { + name: "has an invalid start (-)", + str: "-i-start-with-an-invalid-char", + want: "i-start-with-an-invalid-char", }, { - name: "ends with a .", - str: "i-end-with-a-dot.", - want: "i-end-with-a-dot", + name: "has an invalid end (-)", + str: "i-end-with-an-invalid-char-", + want: "i-end-with-an-invalid-char", + }, + { + name: "has an invalid start ( )", + str: " i-start-with-an-invalid-char", + want: "i-start-with-an-invalid-char", + }, + { + name: "has an invalid end ( )", + str: "i-end-with-an-invalid-char ", + want: "i-end-with-an-invalid-char", + }, + { + name: "has an invalid start ([)", + str: "[i-start-with-an-invalid-char", + want: "i-start-with-an-invalid-char", + }, + { + name: "has an invalid end ([)", + str: "i-end-with-an-invalid-char[", + want: "i-end-with-an-invalid-char", + }, + { + name: "has an invalid start (])", + str: "]i-start-with-an-invalid-char", + want: "i-start-with-an-invalid-char", + }, + { + name: "has an invalid end (])", + str: "i-end-with-an-invalid-char]", + want: "i-end-with-an-invalid-char", }, { name: "long value once cut starts with a .", - str: "everythingbeforethedotisdropped.thesehereare61chars-62withdot-previouscharacterswillbedropped", - want: "thesehereare61chars-62withdot-previouscharacterswillbedropped", + str: "dropped." + strings.Repeat("a", 62), + want: strings.Repeat("a", 62), + }, + { + name: "ones with special chars won't get longer", + str: strings.Repeat("[exp-63]", 10), + want: "63____exp-63____exp-63____exp-63____exp-63____exp-63____exp-63", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := CleanValueKubernetes(tt.str); got != tt.want { - t.Errorf("K8LabelsCleanup() = %v, want %v", got, tt.want) + got := CleanValueKubernetes(tt.str) + validationErrs := validation.IsValidLabelValue(got) + + switch { + case len(got) > validation.LabelValueMaxLength: + t.Errorf("K8LabelsCleanup() = %v, produced string is too long (%v/%v)", got, len(got), validation.LabelValueMaxLength) + case len(validationErrs) > 0: + t.Errorf("K8LabelsCleanup() = %v, is not a valid label: %v", got, validationErrs) + case got != tt.want: + t.Errorf("K8LabelsCleanup() = given %v, got %v, want %v", tt.str, got, tt.want) } }) } From c95a1d7b0903deafcc792fec3220bf61d2aa9529 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Thu, 14 May 2026 09:26:21 +0200 Subject: [PATCH 3/5] fix: move from RFC1123 to LabelValue Signed-off-by: Francesco Ilario rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- pkg/formatting/k8labels.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/formatting/k8labels.go b/pkg/formatting/k8labels.go index 7836885ab9..dedf189c43 100644 --- a/pkg/formatting/k8labels.go +++ b/pkg/formatting/k8labels.go @@ -16,8 +16,8 @@ var ( '[': "__", ']': "__", } - allowedSpecialCharsRFC1123 = ".-_" - allowedSpecialCharsRFC1123Runes = []rune(allowedSpecialCharsRFC1123) + allowedSpecialCharsLabelValue = ".-_" + allowedSpecialCharsLabelValueRunes = []rune(allowedSpecialCharsLabelValue) // Build the replacer starting from the managedSpecialCharsMap managedSpecialCharsInLabelValueReplacer = strings.NewReplacer(pairs(managedSpecialCharsMap)...) @@ -43,14 +43,14 @@ func CleanValueKubernetes(s string) string { replaced := managedSpecialCharsInLabelValueReplacer.Replace(sanitized) // trim unwanted chars - trimmed := strings.Trim(replaced, allowedSpecialCharsRFC1123) + trimmed := strings.Trim(replaced, allowedSpecialCharsLabelValue) // cut to max length cut := cutToLabelValueMaxLength(trimmed) // trim left again to ensure no invalid values // are at the edge after the cut - return strings.TrimLeft(cut, allowedSpecialCharsRFC1123) + return strings.TrimLeft(cut, allowedSpecialCharsLabelValue) } // cutToLabelValueMaxLength cuts the provided string to the maximum length allowed @@ -86,7 +86,7 @@ func pairs(m map[rune]string) []string { // or should be dropped func isSafe(r rune) bool { return !(unicode.IsSpace(r) && r != ' ') && - (isAllowedSpecialCharRFC1123(r) || isAlphanumeric(r) || isManagedSpecialChar(r)) + (isAllowedSpecialCharLabelValue(r) || isAlphanumeric(r) || isManagedSpecialChar(r)) } // isAlphanumeric returns true if the rune is an alphanumeric value @@ -94,14 +94,14 @@ func isAlphanumeric(r rune) bool { return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') } -// isAllowedSpecialCharRFC1123 returns true if the rune is an allowed char as per -// the RFC1123 -func isAllowedSpecialCharRFC1123(r rune) bool { - return slices.Contains(allowedSpecialCharsRFC1123Runes, r) +// isAllowedSpecialCharLabelValue returns true if the rune is an allowed char as per +// Kubernetes Label Values +func isAllowedSpecialCharLabelValue(r rune) bool { + return slices.Contains(allowedSpecialCharsLabelValueRunes, r) } // isManagedSpecialChar returns true if the rune is a managed special char. -// The normalization of the value will replace it with its counterpart. +// The normalization of the value will replace it with its counterpart func isManagedSpecialChar(r rune) bool { _, ok := managedSpecialCharsMap[r] return ok From a1da96254890a1b08674c36c67b374696f293bf7 Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Thu, 14 May 2026 09:36:55 +0200 Subject: [PATCH 4/5] fix: linter complaints Signed-off-by: Francesco Ilario rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- pkg/formatting/k8labels.go | 17 ++++++++--------- pkg/formatting/k8labels_test.go | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pkg/formatting/k8labels.go b/pkg/formatting/k8labels.go index dedf189c43..2511a34075 100644 --- a/pkg/formatting/k8labels.go +++ b/pkg/formatting/k8labels.go @@ -19,7 +19,7 @@ var ( allowedSpecialCharsLabelValue = ".-_" allowedSpecialCharsLabelValueRunes = []rune(allowedSpecialCharsLabelValue) - // Build the replacer starting from the managedSpecialCharsMap + // Build the replacer starting from the managedSpecialCharsMap. managedSpecialCharsInLabelValueReplacer = strings.NewReplacer(pairs(managedSpecialCharsMap)...) ) @@ -54,7 +54,7 @@ func CleanValueKubernetes(s string) string { } // cutToLabelValueMaxLength cuts the provided string to the maximum length allowed -// for a kubernetes label (63 chars) +// for a kubernetes Label (63 chars). func cutToLabelValueMaxLength(s string) string { if len(s) > validation.LabelValueMaxLength { return s[len(s)-(validation.LabelValueMaxLength):] @@ -62,7 +62,7 @@ func cutToLabelValueMaxLength(s string) string { return s } -// sanitizeLabelValue removes all unmanaged characters from the input string +// sanitizeLabelValue removes all unmanaged characters from the input string. func sanitizeLabelValue(s string) string { b := strings.Builder{} for _, r := range s { @@ -73,7 +73,7 @@ func sanitizeLabelValue(s string) string { return b.String() } -// pairs extracts all the key-value pairs from a rune-string map +// pairs extracts all the key-value pairs from a rune-string map. func pairs(m map[rune]string) []string { ss := make([]string, 0, len(m)*2) for k, v := range m { @@ -82,26 +82,25 @@ func pairs(m map[rune]string) []string { return ss } -// isSafe is a helper to identify if the processed rune is safe to process -// or should be dropped +// isSafe a helper to identify if a rune is safe to process or should be dropped. func isSafe(r rune) bool { return !(unicode.IsSpace(r) && r != ' ') && (isAllowedSpecialCharLabelValue(r) || isAlphanumeric(r) || isManagedSpecialChar(r)) } -// isAlphanumeric returns true if the rune is an alphanumeric value +// isAlphanumeric returns true if the rune is an alphanumeric value. func isAlphanumeric(r rune) bool { return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') } // isAllowedSpecialCharLabelValue returns true if the rune is an allowed char as per -// Kubernetes Label Values +// Kubernetes Label Values. func isAllowedSpecialCharLabelValue(r rune) bool { return slices.Contains(allowedSpecialCharsLabelValueRunes, r) } // isManagedSpecialChar returns true if the rune is a managed special char. -// The normalization of the value will replace it with its counterpart +// The normalization of the value will replace it with its counterpart. func isManagedSpecialChar(r rune) bool { _, ok := managedSpecialCharsMap[r] return ok diff --git a/pkg/formatting/k8labels_test.go b/pkg/formatting/k8labels_test.go index eeddfbf37b..43e4f41dff 100644 --- a/pkg/formatting/k8labels_test.go +++ b/pkg/formatting/k8labels_test.go @@ -40,8 +40,8 @@ func TestK8LabelsCleanup(t *testing.T) { }, { name: "remove new line from the middle", - str: "foo\nfoo", - want: "foofoo", + str: "foo\nbar", + want: "foobar", }, { name: "secret name longer than 63 characters", From 3c0e4299ebcac4ed2005b523ef4c165c274cdb3a Mon Sep 17 00:00:00 2001 From: Francesco Ilario Date: Thu, 14 May 2026 09:37:08 +0200 Subject: [PATCH 5/5] fix: remove unneeded check Signed-off-by: Francesco Ilario rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- pkg/formatting/k8labels.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/formatting/k8labels.go b/pkg/formatting/k8labels.go index 2511a34075..b4afaf3279 100644 --- a/pkg/formatting/k8labels.go +++ b/pkg/formatting/k8labels.go @@ -3,7 +3,6 @@ package formatting import ( "slices" "strings" - "unicode" "k8s.io/apimachinery/pkg/util/validation" ) @@ -84,8 +83,7 @@ func pairs(m map[rune]string) []string { // isSafe a helper to identify if a rune is safe to process or should be dropped. func isSafe(r rune) bool { - return !(unicode.IsSpace(r) && r != ' ') && - (isAllowedSpecialCharLabelValue(r) || isAlphanumeric(r) || isManagedSpecialChar(r)) + return isAllowedSpecialCharLabelValue(r) || isAlphanumeric(r) || isManagedSpecialChar(r) } // isAlphanumeric returns true if the rune is an alphanumeric value.