From efe4d17d15e4abf9759cced5265fda6565791a85 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Thu, 31 Jul 2025 13:30:12 -0700 Subject: [PATCH] Add support for validation cohorts On a given field or type, there may be multiple "cohorts" of validation which need to be processed together. For example, a short-circuit validation and a non-short-circuit for the same subfield. The unnamed cohort (aka the default cohort) is first, followed by named cohorts in order they are created. Named cohorts are emitted as inline functions, so early-return can be used. This allows tags like k8s:subfield or k8s:item to generate cohorts (named after the field's JSON name or the selector string, respectively). Subsequent commits will add support to those tags. Co-Authored-by: Yongrui Lin --- .../output_tests/cohorts/doc.go | 55 +++++ .../cohorts/testdata/validate-false.json | 7 + .../cohorts/zz_generated.validations.go | 107 +++++++++ .../cohorts/zz_generated.validations_test.go | 30 +++ .../cmd/validation-gen/validation.go | 225 +++++++++++------- .../cmd/validation-gen/validation_test.go | 110 +++++++++ .../cmd/validation-gen/validators/testing.go | 12 +- .../validation-gen/validators/validators.go | 4 + 8 files changed, 459 insertions(+), 91 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/testdata/validate-false.json create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations_test.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/doc.go new file mode 100644 index 00000000000..82482784f49 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/doc.go @@ -0,0 +1,55 @@ +/* +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 +// +k8s:validation-gen-test-fixture=validateFalse + +// This is a test package. +package cohorts + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +// Note: the following are out of order on purpose. +// They should be emitted as: +// - T {ShortCircuit,Regular} +// - T c2 {ShortCircuit,Regular} +// - T c1 {ShortCircuit,Regular} +// +// +k8s:validateFalse(cohort: "c2")="type T c2 Regular" +// +k8s:validateFalse(cohort: "c1")="type T c1 Regular" +// +k8s:validateFalse(cohort: "c1", flags: "ShortCircuit")="type T c1 ShortCircuit" +// +k8s:validateFalse(cohort: "c2", flags: "ShortCircuit")="type T c2 ShortCircuit" +// +k8s:validateFalse="type T Regular" +// +k8s:validateFalse(flags: "ShortCircuit")="type T ShortCircuit" +type T struct { + TypeMeta int + + // Note: the following are out of order on purpose. + // They should be emitted as: + // - T.S {ShortCircuit,Regular} + // - T.S c2 {ShortCircuit,Regular} + // - T.S c1 {ShortCircuit,Regular} + // +k8s:validateFalse(cohort: "c2")="field T.S c2 Regular" + // +k8s:validateFalse(cohort: "c1")="field T.S c1 Regular" + // +k8s:validateFalse(cohort: "c1", flags: "ShortCircuit")="field T.S c1 ShortCircuit" + // +k8s:validateFalse(cohort: "c2", flags: "ShortCircuit")="field T.S c2 ShortCircuit" + // +k8s:validateFalse="field T.S Regular" + // +k8s:validateFalse(flags: "ShortCircuit")="field T.S ShortCircuit" + S string `json:"s"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/testdata/validate-false.json b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/testdata/validate-false.json new file mode 100644 index 00000000000..ad04e399125 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/testdata/validate-false.json @@ -0,0 +1,7 @@ +{ + "*cohorts.T": { + "": [ + "type T ShortCircuit" + ] + } +} \ No newline at end of file diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations.go new file mode 100644 index 00000000000..04c5b27a016 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations.go @@ -0,0 +1,107 @@ +//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 cohorts + +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 T + scheme.AddValidationFunc((*T)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_T(ctx, op, nil /* fldPath */, obj.(*T), safe.Cast[*T](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_T validates an instance of T according +// to declarative validation rules in the API schema. +func Validate_T(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *T) (errs field.ErrorList) { + if e := validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type T ShortCircuit"); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type T Regular")...) + func() { // cohort c2 + if e := validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type T c2 ShortCircuit"); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type T c2 Regular")...) + }() + func() { // cohort c1 + if e := validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type T c1 ShortCircuit"); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type T c1 Regular")...) + }() + + // field T.TypeMeta has no validation + + // field T.S + 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 + if e := validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field T.S ShortCircuit"); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field T.S Regular")...) + func() { // cohort c2 + if e := validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field T.S c2 ShortCircuit"); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field T.S c2 Regular")...) + }() + func() { // cohort c1 + if e := validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field T.S c1 ShortCircuit"); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field T.S c1 Regular")...) + }() + return + }(fldPath.Child("s"), &obj.S, safe.Field(oldObj, func(oldObj *T) *string { return &oldObj.S }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations_test.go new file mode 100644 index 00000000000..e9214fd3eb3 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cohorts/zz_generated.validations_test.go @@ -0,0 +1,30 @@ +//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 cohorts + +import ( + "testing" +) + +func TestValidation(t *testing.T) { + localSchemeBuilder.Test(t).ValidateFixtures() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go index b327438bb53..2d47bf2c2eb 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go @@ -1280,12 +1280,138 @@ func emitRatchetingCheck(c *generator.Context, t *types.Type, sw *generator.Snip // variables named "obj" and "oldObj", and the field path to this value is // named "fldPath". func emitCallsToValidators(c *generator.Context, validations []validators.FunctionGen, sw *generator.SnippetWriter) { - // Helper func - sort := func(in []validators.FunctionGen) []validators.FunctionGen { - sooner := make([]validators.FunctionGen, 0, len(in)) - later := make([]validators.FunctionGen, 0, len(in)) + // Group and sort the inputs. + cohorts := sortIntoCohorts(validations) - for _, fg := range in { + for _, validations := range cohorts { + cohortName := validations[0].Cohort + if cohortName != "" { + sw.Do("func() { // cohort $.$\n", cohortName) + } + for _, v := range validations { + isShortCircuit := v.Flags.IsSet(validators.ShortCircuit) + isNonError := v.Flags.IsSet(validators.NonError) + + targs := generator.Args{ + "funcName": c.Universe.Type(v.Function), + "field": mkSymbolArgs(c, fieldPkgSymbols), + } + + emitCall := func() { + sw.Do("$.funcName|raw$", targs) + if typeArgs := v.TypeArgs; len(typeArgs) > 0 { + sw.Do("[", nil) + for i, typeArg := range typeArgs { + sw.Do("$.|raw$", c.Universe.Type(typeArg)) + if i < len(typeArgs)-1 { + sw.Do(",", nil) + } + } + sw.Do("]", nil) + } + sw.Do("(ctx, op, fldPath, obj, oldObj", targs) + for _, arg := range v.Args { + sw.Do(", ", nil) + toGolangSourceDataLiteral(sw, c, arg) + } + sw.Do(")", targs) + } + + // If validation is conditional, wrap the validation function with a conditions check. + if !v.Conditions.Empty() { + emitBaseFunction := emitCall + emitCall = func() { + sw.Do("func() $.field.ErrorList|raw$ {\n", targs) + sw.Do(" if ", nil) + firstCondition := true + if len(v.Conditions.OptionEnabled) > 0 { + sw.Do("op.HasOption($.$)", strconv.Quote(v.Conditions.OptionEnabled)) + firstCondition = false + } + if len(v.Conditions.OptionDisabled) > 0 { + if !firstCondition { + sw.Do(" && ", nil) + } + sw.Do("!op.HasOption($.$)", strconv.Quote(v.Conditions.OptionDisabled)) + } + sw.Do(" {\n", nil) + sw.Do(" return ", nil) + emitBaseFunction() + sw.Do("\n", nil) + sw.Do(" } else {\n", nil) + sw.Do(" return nil // skip validation\n", nil) + sw.Do(" }\n", nil) + sw.Do("}()", nil) + } + } + + for _, comment := range v.Comments { + sw.Do("// $.$\n", comment) + } + if isShortCircuit { + sw.Do("if e := ", nil) + emitCall() + sw.Do("; len(e) != 0 {\n", nil) + if !isNonError { + sw.Do("errs = append(errs, e...)\n", nil) + } + sw.Do(" return // do not proceed\n", nil) + sw.Do("}\n", nil) + } else { + if isNonError { + emitCall() + } else { + sw.Do("errs = append(errs, ", nil) + emitCall() + sw.Do("...)\n", nil) + } + } + } + if cohortName != "" { + sw.Do("}()\n", nil) + } + } +} + +// sortIntoCohorts groups the inputs into a list of cohorts. Within each +// cohort, function calls are sorted such that short-circuiting function +// calls are handled before others. The first cohort is always the +// default cohort (named "") if it exists. Other cohorts are returned in +// the order they were defined in the input. +func sortIntoCohorts(in []validators.FunctionGen) [][]validators.FunctionGen { + defaultCohort := make([]validators.FunctionGen, 0, len(in)) + namedCohorts := map[string][]validators.FunctionGen{} + idx := make([]string, 0, len(in)) + for _, fg := range in { + key := fg.Cohort + if key == "" { + defaultCohort = append(defaultCohort, fg) + } else { + if !slices.Contains(idx, key) { + idx = append(idx, key) + } + namedCohorts[key] = append(namedCohorts[key], fg) + } + } + if len(defaultCohort) > 0 { + idx = append([]string{""}, idx...) + } + // NOTE: we do not sort cohorts by name, because we want to preserve + // their definition order. + + result := make([][]validators.FunctionGen, 0, len(in)) + for _, key := range idx { + var cohort []validators.FunctionGen + if key == "" { + cohort = defaultCohort + } else { + cohort = namedCohorts[key] + } + + sooner := make([]validators.FunctionGen, 0, len(cohort)) + later := make([]validators.FunctionGen, 0, len(cohort)) + + for _, fg := range cohort { isShortCircuit := (fg.Flags.IsSet(validators.ShortCircuit)) if isShortCircuit { @@ -1294,92 +1420,11 @@ func emitCallsToValidators(c *generator.Context, validations []validators.Functi later = append(later, fg) } } - result := sooner - result = append(result, later...) - return result - } - - validations = sort(validations) - - for _, v := range validations { - isShortCircuit := v.Flags.IsSet(validators.ShortCircuit) - isNonError := v.Flags.IsSet(validators.NonError) - - targs := generator.Args{ - "funcName": c.Universe.Type(v.Function), - "field": mkSymbolArgs(c, fieldPkgSymbols), - } - - emitCall := func() { - sw.Do("$.funcName|raw$", targs) - if typeArgs := v.TypeArgs; len(typeArgs) > 0 { - sw.Do("[", nil) - for i, typeArg := range typeArgs { - sw.Do("$.|raw$", c.Universe.Type(typeArg)) - if i < len(typeArgs)-1 { - sw.Do(",", nil) - } - } - sw.Do("]", nil) - } - sw.Do("(ctx, op, fldPath, obj, oldObj", targs) - for _, arg := range v.Args { - sw.Do(", ", nil) - toGolangSourceDataLiteral(sw, c, arg) - } - sw.Do(")", targs) - } - - // If validation is conditional, wrap the validation function with a conditions check. - if !v.Conditions.Empty() { - emitBaseFunction := emitCall - emitCall = func() { - sw.Do("func() $.field.ErrorList|raw$ {\n", targs) - sw.Do(" if ", nil) - firstCondition := true - if len(v.Conditions.OptionEnabled) > 0 { - sw.Do("op.HasOption($.$)", strconv.Quote(v.Conditions.OptionEnabled)) - firstCondition = false - } - if len(v.Conditions.OptionDisabled) > 0 { - if !firstCondition { - sw.Do(" && ", nil) - } - sw.Do("!op.HasOption($.$)", strconv.Quote(v.Conditions.OptionDisabled)) - } - sw.Do(" {\n", nil) - sw.Do(" return ", nil) - emitBaseFunction() - sw.Do("\n", nil) - sw.Do(" } else {\n", nil) - sw.Do(" return nil // skip validation\n", nil) - sw.Do(" }\n", nil) - sw.Do("}()", nil) - } - } - - for _, comment := range v.Comments { - sw.Do("// $.$\n", comment) - } - if isShortCircuit { - sw.Do("if e := ", nil) - emitCall() - sw.Do("; len(e) != 0 {\n", nil) - if !isNonError { - sw.Do("errs = append(errs, e...)\n", nil) - } - sw.Do(" return // do not proceed\n", nil) - sw.Do("}\n", nil) - } else { - if isNonError { - emitCall() - } else { - sw.Do("errs = append(errs, ", nil) - emitCall() - sw.Do("...)\n", nil) - } - } + sorted := sooner + sorted = append(sorted, later...) + result = append(result, sorted) } + return result } func emitComments(comments []string, sw *generator.SnippetWriter) { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validation_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validation_test.go index 5debd0856e8..5d53a9309a9 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validation_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validation_test.go @@ -17,8 +17,10 @@ limitations under the License. package main import ( + "reflect" "testing" + "k8s.io/code-generator/cmd/validation-gen/validators" "k8s.io/gengo/v2/types" ) @@ -366,3 +368,111 @@ func TestGetLeafTypeAndPrefixes(t *testing.T) { } } } + +func TestSortIntoCohorts(t *testing.T) { + cases := []struct { + in []validators.FunctionGen + expected [][]validators.FunctionGen + }{{ + // empty + in: []validators.FunctionGen{}, + expected: [][]validators.FunctionGen{}, + }, { + // default cohort + in: []validators.FunctionGen{ + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + }, + expected: [][]validators.FunctionGen{{ + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + }}, + }, { + // default cohort, not already sorted by name + in: []validators.FunctionGen{ + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + }, + expected: [][]validators.FunctionGen{{ + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + }}, + }, { + // default cohort, with a short-circuit + in: []validators.FunctionGen{ + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "", Flags: validators.ShortCircuit}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + }, + expected: [][]validators.FunctionGen{{ + {TagName: "b", Cohort: "", Flags: validators.ShortCircuit}, + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + }}, + }, { + // default cohort, with 2 short-circuits + in: []validators.FunctionGen{ + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "", Flags: validators.ShortCircuit}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "d", Cohort: "", Flags: validators.ShortCircuit}, + }, + expected: [][]validators.FunctionGen{{ + {TagName: "b", Cohort: "", Flags: validators.ShortCircuit}, + {TagName: "d", Cohort: "", Flags: validators.ShortCircuit}, + {TagName: "a", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + }}, + }, { + // default and non-default cohorts + in: []validators.FunctionGen{ + {TagName: "a", Cohort: "foo", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "bar", Flags: validators.DefaultFlags}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "d", Cohort: "foo", Flags: validators.DefaultFlags}, + {TagName: "e", Cohort: "bar", Flags: validators.DefaultFlags}, + {TagName: "f", Cohort: "", Flags: validators.DefaultFlags}, + }, + expected: [][]validators.FunctionGen{{ + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "f", Cohort: "", Flags: validators.DefaultFlags}, + }, { + {TagName: "a", Cohort: "foo", Flags: validators.DefaultFlags}, + {TagName: "d", Cohort: "foo", Flags: validators.DefaultFlags}, + }, { + {TagName: "b", Cohort: "bar", Flags: validators.DefaultFlags}, + {TagName: "e", Cohort: "bar", Flags: validators.DefaultFlags}, + }}, + }, { + // default and non-default cohorts with short-circuit + in: []validators.FunctionGen{ + {TagName: "a", Cohort: "foo", Flags: validators.DefaultFlags}, + {TagName: "b", Cohort: "bar", Flags: validators.DefaultFlags}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + {TagName: "d", Cohort: "foo", Flags: validators.ShortCircuit}, + {TagName: "e", Cohort: "bar", Flags: validators.ShortCircuit}, + {TagName: "f", Cohort: "", Flags: validators.ShortCircuit}, + }, + expected: [][]validators.FunctionGen{{ + {TagName: "f", Cohort: "", Flags: validators.ShortCircuit}, + {TagName: "c", Cohort: "", Flags: validators.DefaultFlags}, + }, { + {TagName: "d", Cohort: "foo", Flags: validators.ShortCircuit}, + {TagName: "a", Cohort: "foo", Flags: validators.DefaultFlags}, + }, { + {TagName: "e", Cohort: "bar", Flags: validators.ShortCircuit}, + {TagName: "b", Cohort: "bar", Flags: validators.DefaultFlags}, + }}, + }} + + for _, tc := range cases { + out := sortIntoCohorts(tc.in) + if !reflect.DeepEqual(out, tc.expected) { + t.Errorf("expected %v, got %v", tc.expected, out) + } + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/testing.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/testing.go index 99679abb7d8..fc9dafd705a 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/testing.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/testing.go @@ -73,7 +73,9 @@ func (frtv fixedResultTagValidator) GetValidations(context Context, tag codetags if err != nil { return result, fmt.Errorf("can't decode tag payload: %w", err) } - result.AddFunction(Function(frtv.TagName(), args.flags, fixedResultValidator, frtv.result, args.msg).WithTypeArgs(args.typeArgs...)) + fn := Function(frtv.TagName(), args.flags, fixedResultValidator, frtv.result, args.msg).WithTypeArgs(args.typeArgs...) + fn.Cohort = args.cohort + result.AddFunction(fn) return result, nil } @@ -86,6 +88,7 @@ type fixedResultArgs struct { flags FunctionFlags msg string typeArgs []types.Name + cohort string } func (fixedResultTagValidator) toFixedResultArgs(in codetags.Tag) (fixedResultArgs, error) { @@ -111,6 +114,8 @@ func (fixedResultTagValidator) toFixedResultArgs(in codetags.Tag) (fixedResultAr } result.typeArgs = []types.Name{{Package: "", Name: tn}} } + case "cohort": + result.cohort = a.Value } } if in.ValueType == codetags.ValueTypeString { @@ -149,6 +154,11 @@ func (frtv fixedResultTagValidator) Docs() TagDoc { Description: "", Docs: "The type arg in generated code (must be the value-type, not pointer).", Type: codetags.ArgTypeString, + }, { + Name: "cohort", + Description: "", + Docs: "An optional cohort name to group multiple validations.", + Type: codetags.ArgTypeString, }} if frtv.result { doc.Description = "Always passes validation (useful for testing)." diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go index 40e3adc3cb4..a9f488259cf 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go @@ -445,6 +445,10 @@ type FunctionGen struct { // TagName is the tag which triggered this function. TagName string + // Cohort indicates a set of related functions which are processed + // together. + Cohort string + // Flags holds the options for this validator function. Flags FunctionFlags