diff --git a/controller/names/dns1035.go b/controller/names/dns1035.go index 56939fae..a6632a41 100644 --- a/controller/names/dns1035.go +++ b/controller/names/dns1035.go @@ -36,37 +36,59 @@ const ( // - base is the name of workload, such as "deployment", "statefulset", "daemonset". // - uniqueName is a random string, such as "12345" or ordinal index. func GenerateDNS1035Label(base, uniqueName string) string { - return GenerateDNS1035LabelByMaxLength(base, uniqueName, validation.DNS1035LabelMaxLength) + return generateDNS1035LabelByMaxLength(base, uniqueName, validation.DNS1035LabelMaxLength) } // GenerateDNS1035LabelByMaxLength generates a valid DNS label (compliant with RFC 1035) // limited by the specified maximum length. func GenerateDNS1035LabelByMaxLength(base, uniqueName string, maxLength int) string { - result := generateDNS1035LabelByMaxLength(base, uniqueName, maxLength) - // remove all suffix "-" - return strings.TrimRight(result, "-") + return generateDNS1035LabelByMaxLength(base, uniqueName, maxLength) } -// GenerateDNS1035LabelGenerateName generates a valid DNS label prefix (compliant with RFC 1035) -// -// Usually you can set the result in metadata.generateName. kube-apiserver will combine the prefix -// with a unique suffix. Currently, the suffix is a random string with length 5. -func GenerateDNS1035LabelGenerateName(base string) string { - return GenerateDNS1035LabelPrefixByMaxLength(base, MaxGeneratedNameLength) +func generateDNS1035LabelByMaxLength(base, unique string, maxLength int) string { + return genericNameGenerator(base, unique, maxLength, validation.DNS1035LabelMaxLength, fixDNS1035Label) } -// GenerateDNS1035LabelPrefixByMaxLength generates a valid DNS label prefix (compliant with RFC 1035) -// limited by the specified maximum length. -func GenerateDNS1035LabelPrefixByMaxLength(base string, maxLength int) string { - return generateDNS1035LabelByMaxLength(base, "", maxLength) +func fixDNS1035Label(label string) string { + // Convert to lowercase + label = strings.ToLower(label) + + var builder strings.Builder + firstChar := true + + // Process each character in the label + for i := 0; i < len(label); i++ { + c := label[i] + + if firstChar { + // First character must be letter + if c >= 'a' && c <= 'z' { + builder.WriteByte(c) + firstChar = false + } + // Skip non-alphanumeric characters at the beginning + continue + } + + // Subsequent characters: allow alphanumeric and dash + if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' { + builder.WriteByte(c) + } else { + // Replace invalid characters with dash + builder.WriteByte('-') + } + } + + result := builder.String() + return strings.TrimRight(result, "-") } -func generateDNS1035LabelByMaxLength(base, unique string, maxLength int) string { +func genericNameGenerator(base, unique string, maxLength, maxLimit int, fixFn func(string) string) string { if maxLength <= 0 { return "" } - if maxLength > validation.DNS1035LabelMaxLength { - maxLength = validation.DNS1035LabelMaxLength + if maxLength > maxLimit { + maxLength = maxLimit } result := unique @@ -80,35 +102,9 @@ func generateDNS1035LabelByMaxLength(base, unique string, maxLength int) string if len(base) > maxPrefixLength-1 { base = base[:maxPrefixLength-1] } + result = base + "-" + result } - // to lower - result = strings.ToLower(result) - - b := strings.Builder{} - - firstLetter := false - // fix result to match DNS1035Label prefix - // 1. first letter must be [a-z] - // 2. only contain characters in [-a-z0-9] - for i := range len(result) { - if !firstLetter { - if result[i] < 'a' || result[i] > 'z' { - // DNS1035Label must start with a lowercase letter - continue - } - firstLetter = true - b.WriteByte(result[i]) - continue - } - if (result[i] >= 'a' && result[i] <= 'z') || (result[i] >= '0' && result[i] <= '9') { - // write a-z, 0-9 - b.WriteByte(result[i]) - } else { - // change other characters to "-" - b.WriteByte('-') - } - } - return b.String() + return fixFn(result) } diff --git a/controller/names/dns1035_test.go b/controller/names/dns1035_test.go index 80560b03..7e1a6b74 100644 --- a/controller/names/dns1035_test.go +++ b/controller/names/dns1035_test.go @@ -19,84 +19,103 @@ package names import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "k8s.io/apimachinery/pkg/util/validation" ) -func Test_generateDNS1035LabelPrefix(t *testing.T) { +type dns1035TestSuite struct { + suite.Suite +} + +func TestDNS1035TestSuite(t *testing.T) { + suite.Run(t, new(dns1035TestSuite)) +} + +func (s *dns1035TestSuite) Test_fixDNS1035Label() { tests := []struct { - name string - base string - maxLength int - want string + name string + label string + want string }{ { - name: "maxLength > DNS1035LabelMaxLength", - base: "ab-1234567890123456789012345678901234567890123456789012345678901234567890", - maxLength: 70, - want: "ab-12345678901234567890123456789012345678901234567890123456789-", + name: "empty string", + label: "", + want: "", }, { - name: "maxLength == DNS1035LabelMaxLength", - base: "test-123456789012345678901234567890123456789012345678901234567890", - maxLength: MaxGeneratedNameLength, - want: "test-1234567890123456789012345678901234567890123456789012-", + name: "valid lowercase", + label: "valid-label", + want: "valid-label", }, { - name: "normal case", - base: "test-123456789012345678901234567890123456789012345678901234567890", - maxLength: 10, - want: "test-1234-", + name: "uppercase to lowercase", + label: "VALID-LABEL", + want: "valid-label", }, { - name: "normal case", - base: "test-123456789012345678901234567890123456789012345678901234567890", - maxLength: 5, - want: "test-", + name: "invalid characters replaced with dash", + label: "invalid.label@123", + want: "invalid-label-123", }, { - name: "normal case", - base: "test-123456789012345678901234567890123456789012345678901234567890", - maxLength: 6, - want: "test--", + name: "starting with number - skip until letter", + label: "123abc", + want: "abc", }, { - name: "normal case", - base: "test-1", - maxLength: MaxGeneratedNameLength, - want: "test-1-", + name: "starting with special characters - skip until letter", + label: "@#$abc", + want: "abc", }, { - name: "replace invalid characters to '-'", - base: "test.#@1", - maxLength: MaxGeneratedNameLength, - want: "test---1-", + name: "starting with dash - skip until letter", + label: "-abc", + want: "abc", }, { - name: "maxLength == 0", - base: "test.1", - maxLength: 0, - want: "", + name: "only numbers and special characters", + label: "123@#$", + want: "", }, { - name: "trimleft base", - base: "0-abcd", - maxLength: validation.DNS1035LabelMaxLength, - want: "abcd-", + name: "trailing dashes trimmed", + label: "label---", + want: "label", + }, + { + name: "mixed case with special characters", + label: "My.Test.Label@123", + want: "my-test-label-123", + }, + { + name: "single letter", + label: "A", + want: "a", + }, + { + name: "starting with underscore and number", + label: "_123abc", + want: "abc", + }, + { + name: "consecutive special characters become single dash", + label: "test..label@@123", + want: "test--label--123", }, } + for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := GenerateDNS1035LabelPrefixByMaxLength(tt.base, tt.maxLength) - assert.Equal(t, tt.want, got) + s.Run(tt.name, func() { + got := fixDNS1035Label(tt.label) + s.Equal(tt.want, got) if len(got) > 0 { - assert.Equal(t, byte('-'), got[len(got)-1], "prefix should end with '-'") + s.Empty(validation.IsDNS1035Label(got)) } }) } } -func Test_GenerateDNS1035Label(t *testing.T) { +func (s *dns1035TestSuite) Test_GenerateDNS1035Label() { tests := []struct { name string base string @@ -184,10 +203,12 @@ func Test_GenerateDNS1035Label(t *testing.T) { } for _, tt := range tests { - got := GenerateDNS1035LabelByMaxLength(tt.base, tt.unique, tt.maxLength) - assert.Equal(t, tt.want, got) - if len(got) > 0 { - assert.Empty(t, validation.IsDNS1035Label(got), "should be a valid DNS1035 label") - } + s.Run(tt.name, func() { + got := GenerateDNS1035LabelByMaxLength(tt.base, tt.unique, tt.maxLength) + s.Equal(tt.want, got) + if len(got) > 0 { + s.Empty(validation.IsDNS1035Label(got), "should be a valid DNS1035 label") + } + }) } } diff --git a/controller/names/dns1123.go b/controller/names/dns1123.go new file mode 100644 index 00000000..172ce85e --- /dev/null +++ b/controller/names/dns1123.go @@ -0,0 +1,116 @@ +/* +Copyright 2025 The KusionStack Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package names + +import ( + "strings" + + "k8s.io/apimachinery/pkg/util/validation" +) + +// GenerateDNS1123Label generates a valid DNS label (compliant with RFC 1123). +// The result is usually combined by the base and uniqueName, such as "base-uniqueName". +// If the generated name is too long, the suffix of base will be truncated to ensure the +// final name is shorter than 63 characters. +// +// Usually: +// - base is the name of workload, such as "deployment", "statefulset", "daemonset". +// - uniqueName is a random string, such as "12345" or ordinal index. +func GenerateDNS1123Label(base, unique string) string { + return genereateDNS1123LabelByMaxLength(base, unique, validation.DNS1123LabelMaxLength) +} + +// GenerateDNS1123LabelByMaxLength generates a valid DNS label (compliant with RFC 1123) +// limited by the specified maximum length. +func GenereateDNS1123LabelByMaxLength(base, unique string, maxLength int) string { + return genereateDNS1123LabelByMaxLength(base, unique, maxLength) +} + +func genereateDNS1123LabelByMaxLength(base, unique string, maxLength int) string { + return genericNameGenerator(base, unique, maxLength, validation.DNS1123LabelMaxLength, fixDNS1123Label) +} + +// GenerateDNS1123Subdomain generates a valid DNS subdomain (compliant with RFC 1123). +// The result is usually combined by the base and uniqueName, such as "base-uniqueName". +// If the generated name is too long, the suffix of base will be truncated to ensure the +// final name is shorter than 253 characters. +// +// Usually: +// - base is the name of workload, such as "deployment", "statefulset", "daemonset". +// - uniqueName is a random string, such as "12345" or ordinal index. +func GenerateDNS1123Subdomain(base, unique string) string { + return generateDNS1123SubdomainByMaxLength(base, unique, validation.DNS1123SubdomainMaxLength) +} + +// GenerateDNS1123SubdomainByMaxLength generates a valid DNS subdomain (compliant with RFC 1123) +// limited by the specified maximum length. +func GenerateDNS1123SubdomainByMaxLength(base, unique string, maxLength int) string { + return generateDNS1123SubdomainByMaxLength(base, unique, maxLength) +} + +func generateDNS1123SubdomainByMaxLength(base, unique string, maxLength int) string { + return genericNameGenerator(base, unique, maxLength, validation.DNS1123SubdomainMaxLength, fixDNS1123Subdomain) +} + +func fixDNS1123Label(label string) string { + // Convert to lowercase + label = strings.ToLower(label) + + var builder strings.Builder + firstChar := true + + // Process each character in the label + for i := 0; i < len(label); i++ { + c := label[i] + + if firstChar { + // First character must be alphanumeric + if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') { + builder.WriteByte(c) + firstChar = false + } + // Skip non-alphanumeric characters at the beginning + continue + } + + // Subsequent characters: allow alphanumeric and dash + if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' { + builder.WriteByte(c) + } else { + // Replace invalid characters with dash + builder.WriteByte('-') + } + } + + result := builder.String() + + return strings.TrimRight(result, "-") +} + +func fixDNS1123Subdomain(input string) string { + // Convert to lowercase + input = strings.ToLower(input) + labels := strings.Split(input, ".") + result := []string{} + for _, label := range labels { + fixedLabel := fixDNS1123Label(label) + if len(fixedLabel) > 0 { + result = append(result, fixedLabel) + } + } + return strings.Join(result, ".") +} diff --git a/controller/names/dns1123_test.go b/controller/names/dns1123_test.go new file mode 100644 index 00000000..8eaefc1c --- /dev/null +++ b/controller/names/dns1123_test.go @@ -0,0 +1,301 @@ +/* +Copyright 2025 The KusionStack Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package names + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "k8s.io/apimachinery/pkg/util/validation" +) + +type dns1123TestSuite struct { + suite.Suite +} + +func TestDNS1123TestSuite(t *testing.T) { + suite.Run(t, new(dns1123TestSuite)) +} + +func (s *dns1123TestSuite) TestFixDNS1123Label() { + testCases := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "valid label", + input: "valid-label", + expected: "valid-label", + }, + { + name: "uppercase conversion", + input: "Valid-Label", + expected: "valid-label", + }, + { + name: "invalid characters", + input: "invalid@label#123", + expected: "invalid-label-123", + }, + { + name: "starting with number", + input: "123label", + expected: "123label", + }, + { + name: "starting with dash", + input: "-label", + expected: "label", + }, + { + name: "starting with special character", + input: "@label", + expected: "label", + }, + { + name: "ending with dash", + input: "label-", + expected: "label", + }, + { + name: "multiple dashes", + input: "label---test", + expected: "label---test", + }, + { + name: "only special characters", + input: "@#$%", + expected: "", + }, + { + name: "dots in label", + input: "label.with.dots", + expected: "label-with-dots", + }, + { + name: "spaces and tabs", + input: "label with spaces", + expected: "label-with-spaces", + }, + { + name: "underscores", + input: "label_with_underscores", + expected: "label-with-underscores", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + result := fixDNS1123Label(tc.input) + s.Equal(tc.expected, result) + if len(result) > 0 { + s.Empty(validation.IsDNS1123Subdomain(result)) + } + }) + } +} + +func (s *dns1123TestSuite) TestFixDNS1123Subdomain() { + testCases := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "valid subdomain", + input: "valid-subdomain.example.com", + expected: "valid-subdomain.example.com", + }, + { + name: "uppercase conversion", + input: "Valid-Subdomain.Example.COM", + expected: "valid-subdomain.example.com", + }, + { + name: "invalid characters in labels", + input: "invalid@subdomain#123.example.com", + expected: "invalid-subdomain-123.example.com", + }, + { + name: "starting with special characters", + input: "@subdomain.example.com", + expected: "subdomain.example.com", + }, + { + name: "ending with special characters", + input: "subdomain@.example.com", + expected: "subdomain.example.com", + }, + { + name: "multiple dots", + input: "sub..domain.example.com", + expected: "sub.domain.example.com", + }, + { + name: "leading dots", + input: ".subdomain.example.com", + expected: "subdomain.example.com", + }, + { + name: "trailing dots", + input: "subdomain.example.com.", + expected: "subdomain.example.com", + }, + { + name: "only special characters", + input: "@#$%.@#$%", + expected: "", + }, + { + name: "spaces in labels", + input: "sub domain.example com", + expected: "sub-domain.example-com", + }, + { + name: "underscores in labels", + input: "sub_domain.example_com", + expected: "sub-domain.example-com", + }, + { + name: "mixed case with dots and dashes", + input: "My-Sub.Domain.Example-Test.COM", + expected: "my-sub.domain.example-test.com", + }, + { + name: "single label", + input: "singlelabel", + expected: "singlelabel", + }, + { + name: "label with numbers", + input: "label123.test456.com", + expected: "label123.test456.com", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + result := fixDNS1123Subdomain(tc.input) + s.Equal(tc.expected, result) + if len(result) > 0 { + s.Empty(validation.IsDNS1123Subdomain(result)) + } + }) + } +} + +func (s *dns1123TestSuite) TestGenerateDNS1123SubdomainByMaxLength() { + testCases := []struct { + name string + base string + unique string + maxLength int + expected string + }{ + { + name: "normal case", + base: "test", + unique: "unique", + maxLength: 20, + expected: "test-unique", + }, + { + name: "base too long", + base: "extremelylongbasename", + unique: "short", + maxLength: 10, + expected: "extr-short", + }, + { + name: "unique too long", + base: "short", + unique: "extremelylonguniquename", + maxLength: 10, + expected: "extremelyl", + }, + { + name: "zero max length", + base: "test", + unique: "unique", + maxLength: 0, + expected: "", + }, + { + name: "negative max length", + base: "test", + unique: "unique", + maxLength: -5, + expected: "", + }, + { + name: "max length exceeds DNS limit", + base: "test", + unique: "unique", + maxLength: 300, + expected: "test-unique", + }, + { + name: "special characters in base and unique", + base: "Test@Base", + unique: "Unique#123", + maxLength: 20, + expected: "test-base-unique-123", + }, + { + name: "empty base", + base: "", + unique: "unique", + maxLength: 10, + expected: "unique", + }, + { + name: "empty unique", + base: "base", + unique: "", + maxLength: 10, + expected: "base", + }, + { + name: "both empty", + base: "", + unique: "", + maxLength: 10, + expected: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + result := generateDNS1123SubdomainByMaxLength(tc.base, tc.unique, tc.maxLength) + s.Equal(tc.expected, result) + if len(result) > 0 { + s.Empty(validation.IsDNS1123Subdomain(result)) + } + }) + } +}