mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-17 20:00:07 +00:00
Add new format k8s-extended-resource-name
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.",
|
||||
}, {
|
||||
|
||||
Reference in New Issue
Block a user