mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-09 00:34:10 -04:00
Merge pull request #134094 from lalitc375/cherry-pick-pr-171
Add support for k8s-long-name-caseless format.
This commit is contained in:
commit
203793fe65
7 changed files with 292 additions and 8 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue