Merge pull request #134094 from lalitc375/cherry-pick-pr-171

Add support for k8s-long-name-caseless format.
This commit is contained in:
Kubernetes Prow Robot 2025-09-18 11:22:10 -07:00 committed by GitHub
commit 203793fe65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 292 additions and 8 deletions

View file

@ -67,6 +67,9 @@ func IsDNS1123Subdomain(value string) []string {
// IsDNS1123SubdomainCaseless tests for a string that conforms to the definition of a
// subdomain in DNS (RFC 1123).
// Deprecated: Use IsDNS1123Subdomain for strict, lowercase validation.
// Case-insensitive names are not recommended as they can lead to ambiguity
// (e.g., 'Foo', 'FOO', and 'foo' would be allowed names for foo).
func IsDNS1123SubdomainCaseless(value string) []string {
return isDNS1123Subdomain(value, true)
}

View file

@ -97,6 +97,9 @@ func LabelKey[T ~string](_ context.Context, op operation.Operation, fldPath *fie
// - must be less than 254 characters long
// - each element must start and end with alphanumeric characters
// - each element must contain only alphanumeric characters or dashes
//
// Deprecated: Case-insensitive names are not recommended as they can lead to ambiguity
// (e.g., 'Foo', 'FOO', and 'foo' would be allowed names for foo). Use LongName for strict, lowercase validation.
func LongNameCaseless[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value == nil {
return nil

View file

@ -105,9 +105,9 @@ func TestLongName(t *testing.T) {
},
}, {
name: "invalid: too long",
input: "0123456789012345678901234567890123456789012345678901234567890123.0123456789012345678901234567890123456789012345678901234567890123.0123456789012345678901234567890123456789012345678901234567890123.01234567890123456789012345678901234567890123456789012345678901234",
input: strings.Repeat("a", 254),
wantErrs: field.ErrorList{
field.Invalid(fldPath, "0123456789012345678901234567890123456789012345678901234567890123.0123456789012345678901234567890123456789012345678901234567890123.0123456789012345678901234567890123456789012345678901234567890123.01234567890123456789012345678901234567890123456789012345678901234", "must be no more than 253 bytes").WithOrigin("format=k8s-long-name"),
field.Invalid(fldPath, strings.Repeat("a", 254), "must be no more than 253 bytes").WithOrigin("format=k8s-long-name"),
},
}, {
name: "invalid: starts with dash",
@ -411,3 +411,61 @@ func TestLabelValue(t *testing.T) {
})
}
}
func TestLongNameCaseless(t *testing.T) {
ctx := context.Background()
fldPath := field.NewPath("test")
testCases := []struct {
name string
input string
wantErrs field.ErrorList
}{{
name: "valid",
input: "A.b.C",
wantErrs: nil,
}, {
name: "invalid: empty",
input: "",
wantErrs: field.ErrorList{
field.Invalid(fldPath, "", "an RFC 1123 subdomain must consist of alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character").WithOrigin("format=k8s-long-name-caseless"),
},
}, {
name: "invalid: too long",
input: strings.Repeat("a", 254),
wantErrs: field.ErrorList{
field.Invalid(fldPath, strings.Repeat("a", 254), "must be no more than 253 bytes").WithOrigin("format=k8s-long-name-caseless"),
},
}, {
name: "invalid: starts with dash",
input: "-A.b.C",
wantErrs: field.ErrorList{
field.Invalid(fldPath, "-A.b.C", "an RFC 1123 subdomain must consist of alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character").WithOrigin("format=k8s-long-name-caseless"),
},
},
{
name: "invalid: ends with dash",
input: "A.b.C-",
wantErrs: field.ErrorList{
field.Invalid(fldPath, "A.b.C-", "an RFC 1123 subdomain must consist of alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character").WithOrigin("format=k8s-long-name-caseless"),
},
},
{
name: "invalid: other chars",
input: "A_b.C",
wantErrs: field.ErrorList{
field.Invalid(fldPath, "A_b.C", "an RFC 1123 subdomain must consist of alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character").WithOrigin("format=k8s-long-name-caseless"),
},
},
}
matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
value := tc.input
gotErrs := LongNameCaseless(ctx, operation.Operation{}, fldPath, &value, nil)
matcher.Test(t, tc.wantErrs, gotErrs)
})
}
}

View file

@ -0,0 +1,41 @@
/*
Copyright 2025 The Kubernetes 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.
*/
// +k8s:validation-gen=TypeMeta
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
// This is a test package.
package format
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
type Struct struct {
TypeMeta int
// +k8s:format=k8s-long-name-caseless
LongNameField string `json:"longNameField"`
// +k8s:format=k8s-long-name-caseless
LongNamePtrField *string `json:"longNamePtrField"`
// Note: no validation here
LongNameTypedefField LongNameStringType `json:"longNameTypedefField"`
}
// +k8s:format=k8s-long-name-caseless
type LongNameStringType string

View file

@ -0,0 +1,72 @@
/*
Copyright 2025 The Kubernetes 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 format
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func TestCaseless(t *testing.T) {
st := localSchemeBuilder.Test(t)
st.Value(&Struct{
LongNameField: "foo.bar",
LongNamePtrField: ptr.To("foo.bar"),
LongNameTypedefField: "foo.bar",
}).ExpectValid()
st.Value(&Struct{
LongNameField: "1.2.3.4",
LongNamePtrField: ptr.To("1.2.3.4"),
LongNameTypedefField: "1.2.3.4",
}).ExpectValid()
st.Value(&Struct{
LongNameField: "Foo.Bar",
LongNamePtrField: ptr.To("Foo.Bar"),
LongNameTypedefField: "Foo.Bar",
}).ExpectValid()
invalidStruct := &Struct{
LongNameField: "",
LongNamePtrField: ptr.To(""),
LongNameTypedefField: "",
}
st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("longNameField"), nil, "").WithOrigin("format=k8s-long-name-caseless"),
field.Invalid(field.NewPath("longNamePtrField"), nil, "").WithOrigin("format=k8s-long-name-caseless"),
field.Invalid(field.NewPath("longNameTypedefField"), nil, "").WithOrigin("format=k8s-long-name-caseless"),
})
// Test validation ratcheting
st.Value(invalidStruct).OldValue(invalidStruct).ExpectValid()
invalidStruct = &Struct{
LongNameField: "Not a LongName",
LongNamePtrField: ptr.To("Not a LongName"),
LongNameTypedefField: "Not a LongName",
}
st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("longNameField"), nil, "").WithOrigin("format=k8s-long-name-caseless"),
field.Invalid(field.NewPath("longNamePtrField"), nil, "").WithOrigin("format=k8s-long-name-caseless"),
field.Invalid(field.NewPath("longNameTypedefField"), nil, "").WithOrigin("format=k8s-long-name-caseless"),
})
// Test validation ratcheting
st.Value(invalidStruct).OldValue(invalidStruct).ExpectValid()
}

View file

@ -0,0 +1,101 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes 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.
*/
// Code generated by validation-gen. DO NOT EDIT.
package format
import (
context "context"
fmt "fmt"
operation "k8s.io/apimachinery/pkg/api/operation"
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
field "k8s.io/apimachinery/pkg/util/validation/field"
testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme"
)
func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *testscheme.Scheme) error {
// type Struct
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
// Validate_LongNameStringType validates an instance of LongNameStringType according
// to declarative validation rules in the API schema.
func Validate_LongNameStringType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *LongNameStringType) (errs field.ErrorList) {
errs = append(errs, validate.LongNameCaseless(ctx, op, fldPath, obj, oldObj)...)
return errs
}
// Validate_Struct validates an instance of Struct according
// to declarative validation rules in the API schema.
func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) {
// field Struct.TypeMeta has no validation
// field Struct.LongNameField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
errs = append(errs, validate.LongNameCaseless(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("longNameField"), &obj.LongNameField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.LongNameField }))...)
// field Struct.LongNamePtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
errs = append(errs, validate.LongNameCaseless(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("longNamePtrField"), obj.LongNamePtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.LongNamePtrField }))...)
// field Struct.LongNameTypedefField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *LongNameStringType) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call the type's validation function
errs = append(errs, Validate_LongNameStringType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("longNameTypedefField"), &obj.LongNameTypedefField, safe.Field(oldObj, func(oldObj *Struct) *LongNameStringType { return &oldObj.LongNameTypedefField }))...)
return errs
}

View file

@ -49,12 +49,13 @@ func (formatTagValidator) ValidScopes() sets.Set[Scope] {
var (
// Keep this list alphabetized.
ipSloppyValidator = types.Name{Package: libValidationPkg, Name: "IPSloppy"}
labelKeyValidator = types.Name{Package: libValidationPkg, Name: "LabelKey"}
labelValueValidator = types.Name{Package: libValidationPkg, Name: "LabelValue"}
longNameValidator = types.Name{Package: libValidationPkg, Name: "LongName"}
shortNameValidator = types.Name{Package: libValidationPkg, Name: "ShortName"}
uuidValidator = types.Name{Package: libValidationPkg, Name: "UUID"}
ipSloppyValidator = types.Name{Package: libValidationPkg, Name: "IPSloppy"}
labelKeyValidator = types.Name{Package: libValidationPkg, Name: "LabelKey"}
labelValueValidator = types.Name{Package: libValidationPkg, Name: "LabelValue"}
longNameCaselessValidator = types.Name{Package: libValidationPkg, Name: "LongNameCaseless"}
longNameValidator = types.Name{Package: libValidationPkg, Name: "LongName"}
shortNameValidator = types.Name{Package: libValidationPkg, Name: "ShortName"}
uuidValidator = types.Name{Package: libValidationPkg, Name: "UUID"}
)
func (formatTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
@ -89,6 +90,8 @@ func getFormatValidationFunction(format string) (FunctionGen, error) {
return Function(formatTagName, DefaultFlags, labelValueValidator), nil
case "k8s-long-name":
return Function(formatTagName, DefaultFlags, longNameValidator), nil
case "k8s-long-name-caseless":
return Function(formatTagName, DefaultFlags, longNameCaselessValidator), nil
case "k8s-short-name":
return Function(formatTagName, DefaultFlags, shortNameValidator), nil
case "k8s-uuid":
@ -116,6 +119,9 @@ func (ftv formatTagValidator) Docs() TagDoc {
}, {
Description: "k8s-long-name",
Docs: "This field holds a Kubernetes \"long name\", aka a \"DNS subdomain\" value.",
}, {
Description: "k8s-long-name-caseless",
Docs: "Deprecated: This field holds a case-insensitive Kubernetes \"long name\", aka a \"DNS subdomain\" value.",
}, {
Description: "k8s-short-name",
Docs: "This field holds a Kubernetes \"short name\", aka a \"DNS label\" value.",