mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-27 17:09:06 +00:00
Add +k8s:ifEnabled and +k8s:ifDisabled tags
Rename if(OptionsEnabled|Disabled) -> if(Enabled|Disabled) Drop stability Use positional arg for option name
This commit is contained in:
35
staging/src/k8s.io/apimachinery/pkg/api/validate/options.go
Normal file
35
staging/src/k8s.io/apimachinery/pkg/api/validate/options.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// IfOption conditionally evaluates a validation function. If the option and enabled are both true the validator
|
||||
// is called. If the option and enabled are both false the validator is called. Otherwise, the validator is not called.
|
||||
func IfOption[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T,
|
||||
optionName string, enabled bool, validator func(context.Context, operation.Operation, *field.Path, *T, *T) field.ErrorList,
|
||||
) field.ErrorList {
|
||||
if op.HasOption(optionName) == enabled {
|
||||
return validator(ctx, op, fldPath, value, oldValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2024 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 options
|
||||
|
||||
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
|
||||
var localSchemeBuilder = testscheme.New()
|
||||
|
||||
type Struct struct {
|
||||
TypeMeta int
|
||||
|
||||
// +k8s:ifEnabled(FeatureX)=+k8s:subfield(xEnabledField)=+k8s:validateFalse="field Struct.ObjectMeta.XEnabledField"
|
||||
ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// +k8s:ifEnabled(FeatureX)=+k8s:validateFalse="field Struct.XEnabledField"
|
||||
XEnabledField string `json:"xEnabledField"`
|
||||
|
||||
// +k8s:ifDisabled(FeatureX)=+k8s:validateFalse="field Struct.XDisabledField"
|
||||
XDisabledField string `json:"xDisabledField"`
|
||||
|
||||
// +k8s:ifEnabled(FeatureY)=+k8s:validateFalse="field Struct.YEnabledField"
|
||||
YEnabledField string `json:"yEnabledField"`
|
||||
|
||||
// +k8s:ifDisabled(FeatureY)=+k8s:validateFalse="field Struct.YDisabledField"
|
||||
YDisabledField string `json:"yDisabledField"`
|
||||
|
||||
// +k8s:ifEnabled(FeatureX)=+k8s:validateFalse="field Struct.XYMixedField/X"
|
||||
// +k8s:ifDisabled(FeatureY)=+k8s:validateFalse="field Struct.XYMixedField/Y"
|
||||
XYMixedField string `json:"xyMixedField"`
|
||||
}
|
||||
|
||||
type ObjectMeta struct {
|
||||
XEnabledField string `json:"xEnabledField"`
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2024 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 options
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
st := localSchemeBuilder.Test(t)
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifDisabled validations should trigger
|
||||
"xDisabledField": {"field Struct.XDisabledField"},
|
||||
"yDisabledField": {"field Struct.YDisabledField"},
|
||||
"xyMixedField": {"field Struct.XYMixedField/Y"},
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).Opts([]string{"FeatureX", "FeatureY"}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifEnabled validations should trigger
|
||||
"metadata.xEnabledField": {"field Struct.ObjectMeta.XEnabledField"},
|
||||
"xEnabledField": {"field Struct.XEnabledField"},
|
||||
"yEnabledField": {"field Struct.YEnabledField"},
|
||||
"xyMixedField": {"field Struct.XYMixedField/X"},
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).Opts([]string{"FeatureX"}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifEnabled validations should trigger
|
||||
"metadata.xEnabledField": {"field Struct.ObjectMeta.XEnabledField"},
|
||||
"xEnabledField": {"field Struct.XEnabledField"},
|
||||
"yDisabledField": {"field Struct.YDisabledField"},
|
||||
"xyMixedField": {
|
||||
"field Struct.XYMixedField/X",
|
||||
"field Struct.XYMixedField/Y"},
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).Opts([]string{"FeatureY"}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifEnabled validations should trigger
|
||||
"xDisabledField": {"field Struct.XDisabledField"},
|
||||
"yEnabledField": {"field Struct.YEnabledField"},
|
||||
})
|
||||
}
|
||||
@@ -37,6 +37,7 @@ 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 "/":
|
||||
@@ -47,17 +48,21 @@ func RegisterValidations(scheme *testscheme.Scheme) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.ObjectMeta
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *ObjectMeta) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ObjectMeta) field.ErrorList {
|
||||
return validate.Subfield(ctx, op, fldPath, obj, oldObj, "xEnabledField", func(o *ObjectMeta) *string { return &o.XEnabledField }, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.Subfield(ctx, op, fldPath, obj, oldObj, "xEnabledField", func(o *ObjectMeta) *string { return &o.XEnabledField }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ObjectMeta.XEnabledField")
|
||||
})
|
||||
})...)
|
||||
@@ -67,9 +72,11 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
|
||||
// field Struct.XEnabledField
|
||||
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 // no changes
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XEnabledField")
|
||||
})...)
|
||||
@@ -79,9 +86,11 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
|
||||
// field Struct.XDisabledField
|
||||
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 // no changes
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XDisabledField")
|
||||
})...)
|
||||
@@ -91,9 +100,11 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
|
||||
// field Struct.YEnabledField
|
||||
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 // no changes
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.YEnabledField")
|
||||
})...)
|
||||
@@ -103,9 +114,11 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
|
||||
// field Struct.YDisabledField
|
||||
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 // no changes
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.YDisabledField")
|
||||
})...)
|
||||
@@ -115,9 +128,11 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
|
||||
// field Struct.XYMixedField
|
||||
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 // no changes
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XYMixedField/Y")
|
||||
})...)
|
||||
|
||||
@@ -80,8 +80,7 @@ func (itv ifTagValidator) GetValidations(context Context, tag codetags.Tag) (Val
|
||||
|
||||
func (itv ifTagValidator) Docs() TagDoc {
|
||||
doc := TagDoc{
|
||||
Tag: itv.TagName(),
|
||||
StabilityLevel: Alpha,
|
||||
Tag: itv.TagName(),
|
||||
Args: []TagArgDoc{{
|
||||
Description: "<option>",
|
||||
Type: codetags.ArgTypeString,
|
||||
|
||||
@@ -196,28 +196,24 @@ func (reg *registry) ExtractValidations(context Context, tags ...codetags.Tag) (
|
||||
|
||||
func (reg *registry) sortTagsIntoPhases(tags []codetags.Tag) [][]codetags.Tag {
|
||||
// First sort all tags by their name, so the final output is deterministic.
|
||||
// It is important to do this before validations are generated.
|
||||
//
|
||||
// It makes more sense to sort here, rather than when emitting because:
|
||||
// Some tags are "meta" tags which wrap other tags. For example:
|
||||
//
|
||||
// Consider a type or field with the following comments:
|
||||
// // +k8s:validateFalse="111"
|
||||
// // +k8s:validateFalse="222"
|
||||
// // +k8s:ifEnabled(Foo)=+k8s:validateFalse="333"
|
||||
//
|
||||
// // +k8s:validateFalse="111"
|
||||
// // +k8s:validateFalse="222"
|
||||
// // +k8s:ifOptionEnabled(Foo)=+k8s:validateFalse="333"
|
||||
// Tag extraction will group these by tag name. The first two are
|
||||
// instances of "k8s:validateFalse", while the third is an instance of
|
||||
// "k8s:ifEnabled".
|
||||
//
|
||||
// Tag extraction will retain the relative order between 111 and 222, but
|
||||
// 333 is extracted as tag "k8s:ifOptionEnabled". Those are all in a map,
|
||||
// which we iterate (in a random order). When it reaches the emit stage,
|
||||
// the "ifOptionEnabled" part is gone, and we will have 3 FunctionGen
|
||||
// objects, all with tag "k8s:validateFalse", in a non-deterministic order
|
||||
// because of the map iteration. If we sort them at that point, we won't
|
||||
// have enough information to do something smart, unless we look at the
|
||||
// args, which are opaque to us.
|
||||
//
|
||||
// Sorting it earlier means we can sort "k8s:ifOptionEnabled" against
|
||||
// "k8s:validateFalse". All of the records within each of those is
|
||||
// relatively ordered, so the result here would be to put "ifOptionEnabled"
|
||||
// before "validateFalse" (lexicographical is better than random).
|
||||
// Without sorting, the order in which tag validators are called is not defined
|
||||
// (map iteration). This can lead to non-deterministic order of the generated
|
||||
// validations. By sorting the tags by name first, we ensure that "k8s:ifEnabled"
|
||||
// is processed before or after "k8s:validateFalse" consistently, allowing the
|
||||
// "k8s:validateFalse" tags to remain grouped together. The tags for each name
|
||||
// are processed in order of appearance, so relative ordering is preserved.
|
||||
sortedTags := make([]codetags.Tag, len(tags))
|
||||
copy(sortedTags, tags)
|
||||
slices.SortFunc(sortedTags, func(a, b codetags.Tag) int {
|
||||
|
||||
Reference in New Issue
Block a user