Add new format k8s-extended-resource-name

This commit is contained in:
Lalit Chauhan
2025-09-30 22:14:35 +00:00
parent a4f14e57f4
commit adbea00238
6 changed files with 312 additions and 2 deletions

View File

@@ -27,7 +27,10 @@ import (
)
const (
uuidErrorMessage = "must be a lowercase UUID in 8-4-4-4-12 format"
uuidErrorMessage = "must be a lowercase UUID in 8-4-4-4-12 format"
defaultResourceRequestsPrefix = "requests."
// Default namespace prefix.
resourceDefaultNamespacePrefix = "kubernetes.io/"
)
// ShortName verifies that the specified value is a valid "short name"
@@ -180,3 +183,38 @@ func ResourcePoolName[T ~string](ctx context.Context, op operation.Operation, fl
}
return allErrs.WithOrigin("format=k8s-resource-pool-name")
}
// ExtendedResourceName verifies that the specified value is a valid extended resource name.
// An extended resource name is a domain-prefixed name that does not use the "kubernetes.io"
// or "requests." prefixes. It must be a valid label key when prefixed with "requests.".
//
// - must have slash domain and name.
// - must not have the "kubernetes.io" domain
// - must not have the "requests." prefix
// - name must be 63 characters or less
// - must be a valid label key when prefixed with "requests."
// -- must contain only alphanumeric characters, dashes, underscores, or dots
// -- must end with an alphanumeric character
func ExtendedResourceName[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
if value == nil {
return nil
}
val := (string)(*value)
allErrs := field.ErrorList{}
if !strings.Contains(val, "/") {
allErrs = append(allErrs, field.Invalid(fldPath, val, "a qualified name must be a domain-prefixed path, such as 'example.com/my-prop'"))
} else if strings.Contains(val, resourceDefaultNamespacePrefix) {
allErrs = append(allErrs, field.Invalid(fldPath, val, fmt.Sprintf("must not have %q domain", resourceDefaultNamespacePrefix)))
}
if strings.HasPrefix(val, defaultResourceRequestsPrefix) {
allErrs = append(allErrs, field.Invalid(fldPath, val, fmt.Sprintf("must not have %q prefix", defaultResourceRequestsPrefix)))
}
// Ensure it satisfies the rules in IsLabelKey() after converted into quota resource name
nameForQuota := fmt.Sprintf("%s%s", defaultResourceRequestsPrefix, val)
for _, msg := range content.IsLabelKey(nameForQuota) {
allErrs = append(allErrs, field.Invalid(fldPath, val, msg))
}
return allErrs.WithOrigin("format=k8s-extended-resource-name")
}

View File

@@ -572,3 +572,58 @@ func TestResourcePoolName(t *testing.T) {
})
}
}
func TestExtendedResourceName(t *testing.T) {
ctx := context.Background()
fldPath := field.NewPath("test")
testCases := []struct {
name string
input string
wantErrs field.ErrorList
}{
{
name: "valid",
input: "example-kub.io/foo",
wantErrs: nil,
},
{
name: "invalid: no domain",
input: "foo",
wantErrs: field.ErrorList{
field.Invalid(fldPath, "foo", "a qualified name must be a domain-prefixed path, such as 'example.com/my-prop'").WithOrigin("format=k8s-extended-resource-name"),
},
},
{
name: "invalid: kubernetes.io domain",
input: "kubernetes.io/foo",
wantErrs: field.ErrorList{
field.Invalid(fldPath, "kubernetes.io/foo", "must not have 'kubernetes.io' domain").WithOrigin("format=k8s-extended-resource-name"),
},
},
{
name: "invalid: requests prefix",
input: "requests.example.com/foo",
wantErrs: field.ErrorList{
field.Invalid(fldPath, "requests.example.com/foo", "must not have 'requests.' prefix").WithOrigin("format=k8s-extended-resource-name"),
},
},
{
name: "invalid: name too long",
input: "example.com/" + strings.Repeat("a", 64),
wantErrs: field.ErrorList{
field.Invalid(fldPath, "example.com/"+strings.Repeat("a", 64), "name part must be no more than 63 bytes").WithOrigin("format=k8s-extended-resource-name"),
},
},
}
exactMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByDetailSubstring()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
value := &tc.input
gotErrs := ExtendedResourceName(ctx, operation.Operation{}, fldPath, value, nil)
exactMatcher.Test(t, tc.wantErrs, gotErrs)
})
}
}

View File

@@ -0,0 +1,42 @@
/*
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 k8sextendedresourcename
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
// MyType is a struct that contains a field with the extended-resource-name format.
// +k8s:validation:Required
type MyType struct {
TypeMeta int
// +k8s:Optional
// +k8s:format=k8s-extended-resource-name
NameField string `json:"nameField"`
// +k8s:Optional
// +k8s:format=k8s-extended-resource-name
NamePtrField *string `json:"namePtrField"`
// Note: no validation here
NameTypedefField NameStringType `json:"nameTypedefField"`
}
// +k8s:format=k8s-extended-resource-name
type NameStringType string

View File

@@ -0,0 +1,68 @@
/*
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 k8sextendedresourcename
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func TestK8sExtendedResourceName(t *testing.T) {
st := localSchemeBuilder.Test(t)
st.Value(&MyType{
NameField: "example.com/my-resource",
NamePtrField: ptr.To("my-domain.org/foo"),
NameTypedefField: "example.com/another-resource",
}).ExpectValid()
st.Value(&MyType{
NameField: "example.com/my_resource",
NamePtrField: ptr.To("example.com/My-Resource"),
NameTypedefField: "example.com/my.resource",
}).ExpectValid()
invalidStruct := &MyType{
NameField: "kubernetes.io/my-resource",
NamePtrField: ptr.To("requests.example.com/my-resource"),
NameTypedefField: "my-resource",
}
st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("nameField"), invalidStruct.NameField, "a qualified name must not be a reserved name").WithOrigin("format=k8s-extended-resource-name"),
field.Invalid(field.NewPath("namePtrField"), *invalidStruct.NamePtrField, "a qualified name must not have a reserved prefix").WithOrigin("format=k8s-extended-resource-name"),
field.Invalid(field.NewPath("nameTypedefField"), invalidStruct.NameTypedefField, "a qualified name must be a valid domain prefix and a name separated by a slash").WithOrigin("format=k8s-extended-resource-name"),
})
// Test validation ratcheting
st.Value(invalidStruct).OldValue(invalidStruct).ExpectValid()
const commonDetail = "a valid extended resource name must consist of a domain name prefix and a path segment separated by a slash, where the path segment consists of alphanumeric characters, '-', and must start and end with an alphanumeric character, and the domain name prefix is a valid DNS subdomain name, with the exception that 'requests' is a valid domain name prefix"
invalidStruct = &MyType{
NameField: "example.com/my-resource-",
NamePtrField: ptr.To("example.com/-my-resource"),
NameTypedefField: "example.com/",
}
st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("nameField"), invalidStruct.NameField, commonDetail).WithOrigin("format=k8s-extended-resource-name"),
field.Invalid(field.NewPath("namePtrField"), *invalidStruct.NamePtrField, commonDetail).WithOrigin("format=k8s-extended-resource-name"),
field.Invalid(field.NewPath("nameTypedefField"), invalidStruct.NameTypedefField, commonDetail).WithOrigin("format=k8s-extended-resource-name"),
})
// 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 k8sextendedresourcename
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 MyType
scheme.AddValidationFunc((*MyType)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_MyType(ctx, op, nil /* fldPath */, obj.(*MyType), safe.Cast[*MyType](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
// Validate_MyType validates an instance of MyType according
// to declarative validation rules in the API schema.
func Validate_MyType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MyType) (errs field.ErrorList) {
// field MyType.TypeMeta has no validation
// field MyType.NameField
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.ExtendedResourceName(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("nameField"), &obj.NameField, safe.Field(oldObj, func(oldObj *MyType) *string { return &oldObj.NameField }))...)
// field MyType.NamePtrField
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.ExtendedResourceName(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("namePtrField"), obj.NamePtrField, safe.Field(oldObj, func(oldObj *MyType) *string { return oldObj.NamePtrField }))...)
// field MyType.NameTypedefField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NameStringType) (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_NameStringType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("nameTypedefField"), &obj.NameTypedefField, safe.Field(oldObj, func(oldObj *MyType) *NameStringType { return &oldObj.NameTypedefField }))...)
return errs
}
// Validate_NameStringType validates an instance of NameStringType according
// to declarative validation rules in the API schema.
func Validate_NameStringType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NameStringType) (errs field.ErrorList) {
errs = append(errs, validate.ExtendedResourceName(ctx, op, fldPath, obj, oldObj)...)
return errs
}

View File

@@ -5,7 +5,7 @@ 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
http://www.apache.org/licenses/LICENSE-20.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@@ -52,6 +52,7 @@ var (
// TODO: uncomment the following when we've done the homework
// to be sure it works the current state of IP manual-ratcheting
// ipSloppyValidator = types.Name{Package: libValidationPkg, Name: "IPSloppy"}
extendedResourceNameValidator = types.Name{Package: libValidationPkg, Name: "ExtendedResourceName"}
labelKeyValidator = types.Name{Package: libValidationPkg, Name: "LabelKey"}
labelValueValidator = types.Name{Package: libValidationPkg, Name: "LabelValue"}
longNameCaselessValidator = types.Name{Package: libValidationPkg, Name: "LongNameCaseless"}
@@ -85,6 +86,8 @@ func getFormatValidationFunction(format string) (FunctionGen, error) {
switch format {
// Keep this sequence alphabetized.
case "k8s-extended-resource-name":
return Function(formatTagName, DefaultFlags, extendedResourceNameValidator), nil
// TODO: uncomment the following when we've done the homework
// to be sure it works the current state of IP manual-ratcheting
/*
@@ -118,6 +121,9 @@ func (ftv formatTagValidator) Docs() TagDoc {
Scopes: ftv.ValidScopes().UnsortedList(),
Description: "Indicates that a string field has a particular format.",
Payloads: []TagPayloadDoc{{ // Keep this list alphabetized.
Description: "k8s-extended-resource-name",
Docs: "This field holds a Kubernetes extended resource name, which is a domain-prefixed name that does not use the 'kubernetes.io' or 'requests.' prefixes.",
}, {
Description: "k8s-ip",
Docs: "This field holds an IPv4 or IPv6 address value. IPv4 octets may have leading zeros.",
}, {