From 81e2d21176770c7b266c444b7d95ca2b80a31e0a Mon Sep 17 00:00:00 2001 From: yongruilin Date: Mon, 15 Sep 2025 20:52:56 +0000 Subject: [PATCH 1/7] feat(validation-gen): Add k8s:customUnique tag for disabling uniqueness validation --- .../cmd/validation-gen/validators/list.go | 74 +++++++++++++++++-- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go index 6fd260655f2..0ab8073749f 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go @@ -27,9 +27,10 @@ import ( ) const ( - listTypeTagName = "k8s:listType" - ListMapKeyTagName = "k8s:listMapKey" - uniqueTagName = "k8s:unique" + listTypeTagName = "k8s:listType" + ListMapKeyTagName = "k8s:listMapKey" + uniqueTagName = "k8s:unique" + customUniqueTagName = "k8s:customUnique" ) // globalListMeta is shared between list-related validators. @@ -40,6 +41,7 @@ func init() { RegisterTagValidator(listTypeTagValidator{byPath: globalListMeta}) RegisterTagValidator(listMapKeyTagValidator{byPath: globalListMeta}) RegisterTagValidator(uniqueTagValidator{byPath: globalListMeta}) + RegisterTagValidator(customUniqueTagValidator{byPath: globalListMeta}) // Finish work on the accumulated list metadata. RegisterFieldValidator(listValidator{byPath: globalListMeta}) @@ -70,6 +72,10 @@ type listMetadata struct { semantic listSemantic keyFields []string // For semantic == map. keyNames []string // For semantic == map. + + // customUnique indicates that k8s:customUnique is set on this list. + // It disables generation of uniqueness validation for this list. + customUnique bool } // makeListMapMatchFunc generates a function that compares two list-map @@ -137,16 +143,18 @@ func (lttv listTypeTagValidator) GetValidations(context Context, tag codetags.Ta } case "set": lm.ownership = ownershipShared - // If uniqueTagValidator has run, lm.semantic will be non-empty and non-atomic. + // If uniqueTagValidator has run for `unique=set` or `unique=map`, + // lm.semantic will be non-empty and non-atomic. if lm.semantic != "" && lm.semantic != semanticAtomic { - return Validations{}, fmt.Errorf("unique tag can only be used with listType=atomic") + return Validations{}, fmt.Errorf("unique tag is redundant for listType=%q", tag.Value) } lm.semantic = semanticSet case "map": lm.ownership = ownershipShared - // If uniqueTagValidator has run, lm.semantic will be non-empty and non-atomic. + // If uniqueTagValidator has run for `unique=set` or `unique=map`, + // lm.semantic will be non-empty and non-atomic. if lm.semantic != "" && lm.semantic != semanticAtomic { - return Validations{}, fmt.Errorf("unique tag can only be used with listType=atomic") + return Validations{}, fmt.Errorf("unique tag is redundant for listType=%q", tag.Value) } if util.NativeType(t.Elem).Kind != types.Struct { return Validations{}, fmt.Errorf("only lists of structs can be list-maps") @@ -267,7 +275,7 @@ func (utv uniqueTagValidator) GetValidations(context Context, tag codetags.Tag) // If listType has already run and set a non-atomic ownership, this is an error. if lm.ownership != "" && lm.ownership != ownershipSingle { - return Validations{}, fmt.Errorf("unique tag can only be used with listType=atomic") + return Validations{}, fmt.Errorf("unique tag may not be used with listType=set or listType=map") } if lm.semantic != "" && lm.semantic != semanticAtomic { @@ -306,6 +314,50 @@ func (utv uniqueTagValidator) Docs() TagDoc { return doc } +type customUniqueTagValidator struct { + byPath map[string]*listMetadata +} + +func (customUniqueTagValidator) Init(Config) {} + +func (customUniqueTagValidator) TagName() string { + return customUniqueTagName +} + +func (customUniqueTagValidator) ValidScopes() sets.Set[Scope] { + return listTagsValidScopes +} + +func (cutv customUniqueTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + // NOTE: pointers to lists are not supported, so we should never see a pointer here. + t := util.NativeType(context.Type) + if t.Kind != types.Slice && t.Kind != types.Array { + return Validations{}, fmt.Errorf("can only be used on list types (%s)", t.Kind) + } + + lm := cutv.byPath[context.Path.String()] + if lm == nil { + lm = &listMetadata{} + cutv.byPath[context.Path.String()] = lm + } + + lm.customUnique = true + + // This tag doesn't generate any validations. It just accumulates + // information for other tags to use. + return Validations{}, nil +} + +func (cutv customUniqueTagValidator) Docs() TagDoc { + doc := TagDoc{ + Tag: cutv.TagName(), + Scopes: cutv.ValidScopes().UnsortedList(), + Description: "Indicates that uniqueness validation for this list is implemented via custom, handwritten validation. This disables generation of uniqueness validation for this list.", + Payloads: nil, + } + return doc +} + type listValidator struct { byPath map[string]*listMetadata } @@ -361,6 +413,12 @@ func (lv listValidator) GetValidations(context Context) (Validations, error) { return Validations{}, err } + if lm.customUnique { + // Uniqueness validation is disabled in generated validation for this list. + // It would defer to handwritten validation to check the uniqueness. + return Validations{}, nil + } + result := Validations{} // Generate uniqueness checks for lists with higher-order semantics. From 09e96ae3f1d6db3d7a3d866f464e79cbe32501c7 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Mon, 15 Sep 2025 22:26:24 +0000 Subject: [PATCH 2/7] chore(validation-gen): Update output_tests for k8s:customUnique --- .../validation-gen/output_tests/tags/unique/doc.go | 11 +++++++++++ .../output_tests/tags/unique/doc_test.go | 14 ++++++++++++++ .../tags/unique/zz_generated.validations.go | 2 ++ 3 files changed, 27 insertions(+) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go index ee5ba38a0e9..c6b95fcd9b9 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go @@ -49,6 +49,17 @@ type Struct struct { // +k8s:unique=map // +k8s:listMapKey=key AtomicListUniqueMap []Item `json:"atomicListUniqueMap"` + + // customUnique with listType=set + // +k8s:listType=set + // +k8s:customUnique + CustomUniqueListWithTypeSet []string `json:"customUniqueListWithTypeSet"` + + // customUnique with listType=map + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:customUnique + CustomUniqueListWithTypeMap []Item `json:"customUniqueListWithTypeMap"` } type Item struct { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go index adfd6f33425..3d8c32e9921 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go @@ -43,6 +43,8 @@ func TestUnique(t *testing.T) { {Key: "key1", Data: "one"}, {Key: "key2", Data: "two"}, }, + CustomUniqueListWithTypeSet: []string{"a", "b", "a"}, + CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}}, }).ExpectValid() // Test empty lists @@ -51,6 +53,8 @@ func TestUnique(t *testing.T) { SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{}, AtomicListUniqueSet: []Item{}, AtomicListUniqueMap: []Item{}, + CustomUniqueListWithTypeSet: []string{}, + CustomUniqueListWithTypeMap: []Item{}, }).ExpectValid() // Test single element lists @@ -59,6 +63,8 @@ func TestUnique(t *testing.T) { SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{{Key1: "a", Key2: "b", Data: "one"}}, AtomicListUniqueSet: []Item{{Key: "single", Data: "one"}}, AtomicListUniqueMap: []Item{{Key: "single", Data: "one"}}, + CustomUniqueListWithTypeSet: []string{"single"}, + CustomUniqueListWithTypeMap: []Item{{Key: "single"}}, }).ExpectValid() // Test duplicate values (should fail validation) @@ -79,6 +85,8 @@ func TestUnique(t *testing.T) { {Key: "key2", Data: "two"}, {Key: "key1", Data: "three"}, }, + CustomUniqueListWithTypeSet: []string{"a", "b", "a"}, + CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}}, }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(3), nil), field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(4), nil), @@ -96,6 +104,8 @@ func TestUnique(t *testing.T) { {Key: "a", Data: "two"}, {Key: "", Data: "three"}, }, + CustomUniqueListWithTypeSet: []string{"", "a", ""}, + CustomUniqueListWithTypeMap: []Item{{Key: ""}, {Key: "a"}, {Key: ""}}, }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(2), nil), field.Duplicate(field.NewPath("atomicListUniqueMap").Index(2), nil), @@ -119,6 +129,8 @@ func TestRatcheting(t *testing.T) { {Key: "key1", Data: "one"}, {Key: "key2", Data: "two"}, }, + CustomUniqueListWithTypeSet: []string{"a", "b", "a"}, + CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}}, } // Same data, different order. @@ -136,6 +148,8 @@ func TestRatcheting(t *testing.T) { {Key: "key2", Data: "two"}, {Key: "key1", Data: "one"}, }, + CustomUniqueListWithTypeSet: []string{"a", "a", "b"}, + CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "a"}, {Key: "b"}}, } // Test that reordering doesn't trigger validation errors diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go index 0adaccffe45..a8b7754b49b 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go @@ -106,5 +106,7 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("atomicListUniqueMap"), obj.AtomicListUniqueMap, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.AtomicListUniqueMap }))...) + // field Struct.CustomUniqueListWithTypeSet has no validation + // field Struct.CustomUniqueListWithTypeMap has no validation return errs } From 8138390e51da76bc3178ac4233c5a5eadb5c8097 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Mon, 15 Sep 2025 23:22:32 +0000 Subject: [PATCH 3/7] feat(certificates): Add k8s:customUnique tag to CertificateSigningRequestStatus --- staging/src/k8s.io/api/certificates/v1/types.go | 1 + staging/src/k8s.io/api/certificates/v1beta1/types.go | 1 + 2 files changed, 2 insertions(+) diff --git a/staging/src/k8s.io/api/certificates/v1/types.go b/staging/src/k8s.io/api/certificates/v1/types.go index aa87fad89b8..8cd56e6db7e 100644 --- a/staging/src/k8s.io/api/certificates/v1/types.go +++ b/staging/src/k8s.io/api/certificates/v1/types.go @@ -181,6 +181,7 @@ type CertificateSigningRequestStatus struct { // +optional // +k8s:listType=map // +k8s:listMapKey=type + // +k8s:customUnique // +k8s:optional // +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember // +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember diff --git a/staging/src/k8s.io/api/certificates/v1beta1/types.go b/staging/src/k8s.io/api/certificates/v1beta1/types.go index 2a9648ef169..c8976d206f2 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/types.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/types.go @@ -178,6 +178,7 @@ type CertificateSigningRequestStatus struct { // +optional // +k8s:listType=map // +k8s:listMapKey=type + // +k8s:customUnique // +k8s:optional // +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember // +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember From 3da0a255f7fe7a0a12f9af0dd668983c0b2253ca Mon Sep 17 00:00:00 2001 From: yongruilin Date: Mon, 15 Sep 2025 23:24:26 +0000 Subject: [PATCH 4/7] Enable listmap uniqueness & run codegen --- .../api/certificates/v1/generated.proto | 1 + .../api/certificates/v1beta1/generated.proto | 1 + .../cross_pkg/zz_generated.validations.go | 2 ++ .../list/zz_generated.validations.go | 4 +++ .../zz_generated.validations.go | 2 ++ .../multiple_keys/zz_generated.validations.go | 8 +++++ .../single_key/zz_generated.validations.go | 10 +++++++ .../item/subfield/zz_generated.validations.go | 4 +++ .../item/typedef/zz_generated.validations.go | 8 +++++ .../union/simple/zz_generated.validations.go | 2 ++ .../union/typedef/zz_generated.validations.go | 2 ++ .../simple/zz_generated.validations.go | 2 ++ .../typedef/zz_generated.validations.go | 2 ++ .../multiple_keys/zz_generated.validations.go | 29 +++++++++++++++++++ .../single_key/zz_generated.validations.go | 19 ++++++++++++ .../tags/opaque/zz_generated.validations.go | 4 +++ .../cmd/validation-gen/validators/list.go | 6 +--- 17 files changed, 101 insertions(+), 5 deletions(-) diff --git a/staging/src/k8s.io/api/certificates/v1/generated.proto b/staging/src/k8s.io/api/certificates/v1/generated.proto index dfe74da4aa0..a689f3e8920 100644 --- a/staging/src/k8s.io/api/certificates/v1/generated.proto +++ b/staging/src/k8s.io/api/certificates/v1/generated.proto @@ -206,6 +206,7 @@ message CertificateSigningRequestStatus { // +optional // +k8s:listType=map // +k8s:listMapKey=type + // +k8s:customUnique // +k8s:optional // +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember // +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember diff --git a/staging/src/k8s.io/api/certificates/v1beta1/generated.proto b/staging/src/k8s.io/api/certificates/v1beta1/generated.proto index 223dc485de9..f96be36d085 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/certificates/v1beta1/generated.proto @@ -185,6 +185,7 @@ message CertificateSigningRequestStatus { // +optional // +k8s:listType=map // +k8s:listMapKey=type + // +k8s:customUnique // +k8s:optional // +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember // +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cross_pkg/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cross_pkg/zz_generated.validations.go index 05f28471525..d0965c5201a 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cross_pkg/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/cross_pkg/zz_generated.validations.go @@ -350,6 +350,8 @@ func Validate_T1(ctx context.Context, op operation.Operation, fldPath *field.Pat errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a other.StructType, b other.StructType) bool { return a.StringField == b.StringField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *other.StructType) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, true, "field T1.SliceOfOtherStruct values") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a other.StructType, b other.StructType) bool { return a.StringField == b.StringField })...) return }(fldPath.Child("listMapOfOtherStruct"), obj.ListMapOfOtherStruct, safe.Field(oldObj, func(oldObj *T1) []other.StructType { return oldObj.ListMapOfOtherStruct }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go index 280a1ecf46e..f211c86d459 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go @@ -176,6 +176,8 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath * errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a ComparableStructWithKey, b ComparableStructWithKey) bool { return a.Key == b.Key }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ComparableStructWithKey) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field MapSliceComparableField[*]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a ComparableStructWithKey, b ComparableStructWithKey) bool { return a.Key == b.Key })...) return }(fldPath.Child("mapSliceComparableField"), obj.MapSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStructWithKey { return oldObj.MapSliceComparableField }))...) @@ -190,6 +192,8 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath * errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a NonComparableStructWithKey, b NonComparableStructWithKey) bool { return a.Key == b.Key }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NonComparableStructWithKey) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field MapSliceNonComparableField[*]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NonComparableStructWithKey, b NonComparableStructWithKey) bool { return a.Key == b.Key })...) // iterate the list and call the type's validation function errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a NonComparableStructWithKey, b NonComparableStructWithKey) bool { return a.Key == b.Key }, validate.SemanticDeepEqual, Validate_NonComparableStructWithKey)...) return diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/immutable_transitions/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/immutable_transitions/zz_generated.validations.go index e72db972b44..1e732d795c4 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/immutable_transitions/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/immutable_transitions/zz_generated.validations.go @@ -66,6 +66,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key1 == "b" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList { return validate.Subfield(ctx, op, fldPath, obj, oldObj, "stringField", func(o *Item) *string { return &o.StringField }, validate.DirectEqualPtr, validate.ImmutableByCompare) })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key1 == b.Key1 })...) return }(fldPath.Child("listField"), obj.ListField, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.ListField }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go index 64479184fb0..528719d8245 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go @@ -65,6 +65,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.StringKey == "target" && item.IntKey == 42 && item.BoolKey == true }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item Items[stringKey=target,intKey=42,boolKey=true]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { + return a.StringKey == b.StringKey && a.IntKey == b.IntKey && a.BoolKey == b.BoolKey + })...) return }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.Items }))...) @@ -79,6 +83,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.StringKey == "target" && item.IntKey == 42 && item.BoolKey == true }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item OutOfOrder[boolKey=42,stringKey=target,intKey=42]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { + return a.StringKey == b.StringKey && a.IntKey == b.IntKey && a.BoolKey == b.BoolKey + })...) return }(fldPath.Child("outOfOrder"), obj.OutOfOrder, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.OutOfOrder }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go index 09fc16b52da..57b0d1b73fa 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go @@ -73,6 +73,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key == "target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item Items[key=target]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key == b.Key })...) return }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.Items }))...) @@ -87,6 +89,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *IntKeyItem) bool { return item.IntField == 42 }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *IntKeyItem) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item IntKeyItems[intField=42]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a IntKeyItem, b IntKeyItem) bool { return a.IntField == b.IntField })...) return }(fldPath.Child("intKeyItems"), obj.IntKeyItems, safe.Field(oldObj, func(oldObj *Struct) []IntKeyItem { return oldObj.IntKeyItems }))...) @@ -101,6 +105,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *BoolKeyItem) bool { return item.BoolField == true }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *BoolKeyItem) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item BoolKeyItems[boolField=true]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a BoolKeyItem, b BoolKeyItem) bool { return a.BoolField == b.BoolField })...) return }(fldPath.Child("boolKeyItems"), obj.BoolKeyItems, safe.Field(oldObj, func(oldObj *Struct) []BoolKeyItem { return oldObj.BoolKeyItems }))...) @@ -115,6 +121,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *TypedefItem) bool { return item.ID == "typedef-target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *TypedefItem) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item TypedefItems[id=typedef-target]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a TypedefItem, b TypedefItem) bool { return a.ID == b.ID })...) return }(fldPath.Child("typedefItems"), obj.TypedefItems, safe.Field(oldObj, func(oldObj *Struct) TypedefItemList { return oldObj.TypedefItems }))...) @@ -153,6 +161,8 @@ func Validate_StructWithNestedTypedef(ctx context.Context, op operation.Operatio errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *NestedTypedefItem) bool { return item.Key == "nested-target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NestedTypedefItem) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item NestedItems[key=nested-target]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NestedTypedefItem, b NestedTypedefItem) bool { return a.Key == b.Key })...) return }(fldPath.Child("nestedItems"), obj.NestedItems, safe.Field(oldObj, func(oldObj *StructWithNestedTypedef) []NestedTypedefItem { return oldObj.NestedItems }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/subfield/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/subfield/zz_generated.validations.go index 2327682dcd4..32857604cc2 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/subfield/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/subfield/zz_generated.validations.go @@ -67,6 +67,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item Items[key=target].stringField") }) })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key == b.Key })...) return }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.Items }))...) @@ -83,6 +85,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return validate.NEQ(ctx, op, fldPath, obj, oldObj, "forbidden") }) })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a RatchetItem, b RatchetItem) bool { return a.Key == b.Key })...) return }(fldPath.Child("ratchetItems"), obj.RatchetItems, safe.Field(oldObj, func(oldObj *Struct) []RatchetItem { return oldObj.RatchetItems }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go index 670a53cc76a..063ac741224 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go @@ -55,6 +55,8 @@ func Validate_ConflictingItemList(ctx context.Context, op operation.Operation, f errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *DualItem) bool { return item.ID == "target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *DualItem) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item ConflictingItems[id=target] from typedef") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a DualItem, b DualItem) bool { return a.ID == b.ID })...) return errs } @@ -65,6 +67,8 @@ func Validate_DualItemList(ctx context.Context, op operation.Operation, fldPath errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *DualItem) bool { return item.ID == "typedef-target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *DualItem) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item DualItems[id=typedef-target] from typedef") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a DualItem, b DualItem) bool { return a.ID == b.ID })...) return errs } @@ -76,6 +80,8 @@ func Validate_ItemList(ctx context.Context, op operation.Operation, fldPath *fie errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key == "validated" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item ItemList[key=validated]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key == b.Key })...) return errs } @@ -86,6 +92,8 @@ func Validate_ItemListAlias(ctx context.Context, op operation.Operation, fldPath errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key == "aliased" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item ItemListAlias[key=aliased]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key == b.Key })...) return errs } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/simple/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/simple/zz_generated.validations.go index bc45a0b0123..31d24f91d7f 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/simple/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/simple/zz_generated.validations.go @@ -64,6 +64,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return nil } // call field-attached validations + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Task, b Task) bool { return a.Name == b.Name })...) errs = append(errs, validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_union_simple_Struct_tasks_, func(list []Task) bool { for i := range list { if list[i].Name == "failed" { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/typedef/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/typedef/zz_generated.validations.go index a3d349ffa72..5f240b9b750 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/typedef/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/union/typedef/zz_generated.validations.go @@ -74,6 +74,8 @@ var unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tag // Validate_TaskList validates an instance of TaskList according // to declarative validation rules in the API schema. func Validate_TaskList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj TaskList) (errs field.ErrorList) { + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Task, b Task) bool { return a.Name == b.Name })...) errs = append(errs, validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_union_typedef_TaskList_, func(list TaskList) bool { for i := range list { if list[i].Name == "failed" { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/simple/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/simple/zz_generated.validations.go index 12aafd2ab81..f4ac450fdaa 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/simple/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/simple/zz_generated.validations.go @@ -64,6 +64,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return nil } // call field-attached validations + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Task, b Task) bool { return a.Name == b.Name })...) errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_zerorooneof_simple_Struct_tasks_, func(list []Task) bool { for i := range list { if list[i].Name == "failed" { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/typedef/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/typedef/zz_generated.validations.go index 52f40df6ded..be89dc4a068 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/typedef/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/zerorooneof/typedef/zz_generated.validations.go @@ -74,6 +74,8 @@ var zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tes // Validate_TaskList validates an instance of TaskList according // to declarative validation rules in the API schema. func Validate_TaskList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj TaskList) (errs field.ErrorList) { + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Task, b Task) bool { return a.Name == b.Name })...) errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_zerorooneof_typedef_TaskList_, func(list TaskList) bool { for i := range list { if list[i].Name == "failed" { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go index 5f37388f71b..f930f28dc92 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go @@ -49,6 +49,17 @@ func RegisterValidations(scheme *testscheme.Scheme) error { return nil } +// Validate_ListType validates an instance of ListType according +// to declarative validation rules in the API schema. +func Validate_ListType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj ListType) (errs field.ErrorList) { + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { + return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field + })...) + + 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) { @@ -65,6 +76,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field }, validate.DirectEqual, validate.ImmutableByCompare)...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { + return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field + })...) return }(fldPath.Child("listField"), obj.ListField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListField }))...) @@ -79,6 +94,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool { return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field }, validate.DirectEqual, validate.ImmutableByCompare)...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool { + return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field + })...) return }(fldPath.Child("listTypedefField"), obj.ListTypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.ListTypedefField }))...) @@ -93,6 +112,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field }, validate.DirectEqual, validate.ImmutableByCompare)...) + // call the type's validation function + errs = append(errs, Validate_ListType(ctx, op, fldPath, obj, oldObj)...) return }(fldPath.Child("typedefField"), obj.TypedefField, safe.Field(oldObj, func(oldObj *Struct) ListType { return oldObj.TypedefField }))...) @@ -109,6 +130,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListComparableField[*]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { + return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field + })...) return }(fldPath.Child("listComparableField"), obj.ListComparableField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListComparableField }))...) @@ -125,6 +150,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NonComparableStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListNonComparableField[*]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NonComparableStruct, b NonComparableStruct) bool { + return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field + })...) return }(fldPath.Child("listNonComparableField"), obj.ListNonComparableField, safe.Field(oldObj, func(oldObj *Struct) []NonComparableStruct { return oldObj.ListNonComparableField }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go index bd1a83c0e91..2a6d52021ad 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go @@ -49,6 +49,15 @@ func RegisterValidations(scheme *testscheme.Scheme) error { return nil } +// Validate_ListType validates an instance of ListType according +// to declarative validation rules in the API schema. +func Validate_ListType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj ListType) (errs field.ErrorList) { + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField })...) + + 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) { @@ -63,6 +72,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field } // call field-attached validations errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, validate.ImmutableByCompare)...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField })...) return }(fldPath.Child("listField"), obj.ListField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListField }))...) @@ -75,6 +86,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field } // call field-attached validations errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, validate.ImmutableByCompare)...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool { return a.KeyField == b.KeyField })...) return }(fldPath.Child("listTypedefField"), obj.ListTypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.ListTypedefField }))...) @@ -87,6 +100,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field } // call field-attached validations errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, validate.ImmutableByCompare)...) + // call the type's validation function + errs = append(errs, Validate_ListType(ctx, op, fldPath, obj, oldObj)...) return }(fldPath.Child("typedefField"), obj.TypedefField, safe.Field(oldObj, func(oldObj *Struct) ListType { return oldObj.TypedefField }))...) @@ -101,6 +116,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListComparableField[*]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField })...) return }(fldPath.Child("listComparableField"), obj.ListComparableField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListComparableField }))...) @@ -115,6 +132,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a NonComparableStruct, b NonComparableStruct) bool { return a.KeyField == b.KeyField }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NonComparableStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListNonComparableField[*]") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NonComparableStruct, b NonComparableStruct) bool { return a.KeyField == b.KeyField })...) return }(fldPath.Child("listNonComparableField"), obj.ListNonComparableField, safe.Field(oldObj, func(oldObj *Struct) []NonComparableStruct { return oldObj.ListNonComparableField }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/opaque/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/opaque/zz_generated.validations.go index 290da8806b6..102652ad48a 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/opaque/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/opaque/zz_generated.validations.go @@ -218,6 +218,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListMapOfStructField vals") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField })...) // iterate the list and call the type's validation function errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField }, validate.DirectEqual, Validate_OtherStruct)...) return @@ -235,6 +237,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListMapOfOpaqueStructField vals") })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField })...) return }(fldPath.Child("listMapOfOpaqueStructField"), obj.ListMapOfOpaqueStructField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListMapOfOpaqueStructField }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go index 0ab8073749f..1bc9ce5e6c8 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go @@ -437,11 +437,7 @@ func (lv listValidator) GetValidations(context Context) (Validations, error) { WithComment(comment) result.AddFunction(f) } - // TODO: Replace with the following once we have a way to either opt-out from - // uniqueness validation of list-maps or settle the decision on how to handle - // the ratcheting cases of it. - // if lm.semantic == semanticMap { - if lm.semantic == semanticMap && lm.ownership == ownershipSingle { + if lm.semantic == semanticMap { // TODO: There are some fields which are declared as maps which do not // enforce uniqueness in manual validation. Those either need to not be // maps or we need to allow types to opt-out from this validation. SSA From 71797498f9cd4d5dcabebdc7bc4fddfb7f0462dd Mon Sep 17 00:00:00 2001 From: yongruilin Date: Mon, 15 Sep 2025 23:32:48 +0000 Subject: [PATCH 5/7] test(certificates): Add ratcheting test for CSR conditions --- .../certificates/certificates/declarative_validation_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/registry/certificates/certificates/declarative_validation_test.go b/pkg/registry/certificates/certificates/declarative_validation_test.go index d82e559eca6..ca0e3ce4df6 100644 --- a/pkg/registry/certificates/certificates/declarative_validation_test.go +++ b/pkg/registry/certificates/certificates/declarative_validation_test.go @@ -177,6 +177,11 @@ func testValidateUpdateForDeclarative(t *testing.T, apiVersion string) { ), subresources: []string{"/approval"}, // Can only modify Approved and Denied conditions on /approval subresource }, + "ratcheting: allow existing duplicate types - valid": { + old: makeValidCSR(withApprovedCondition(), withApprovedCondition(), withDeniedCondition(), withDeniedCondition()), + update: makeValidCSR(withDeniedCondition(), withDeniedCondition(), withApprovedCondition(), withApprovedCondition()), + subresources: []string{"/status"}, + }, } for k, tc := range testCases { From 059d1794e61ccb175855b51d3b181a7f34debc7c Mon Sep 17 00:00:00 2001 From: yongruilin Date: Mon, 15 Sep 2025 23:35:30 +0000 Subject: [PATCH 6/7] test(validation-gen): Enable uniqueness validation tests for listmap --- .../tags/listmap/multiple_keys/doc_test.go | 48 +++++++++---------- .../tags/listmap/single_key/doc_test.go | 48 +++++++++---------- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go index 38c7536eaea..f96ad2b0bae 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go @@ -23,33 +23,29 @@ import ( ) func TestUniqueness(t *testing.T) { - // TODO: enable this once we have a way to either opt-out from this validation - // or settle the decision on how to handle the ratcheting cases. - /* - st := localSchemeBuilder.Test(t) + st := localSchemeBuilder.Test(t) - st.Value(&Struct{ - ListField: []OtherStruct{ - {"key1", 1, "one"}, // unique - {"key2", 2, "two"}, // dup - {"key2", 2, "two"}, - }, - ListTypedefField: []OtherTypedefStruct{ - {"key1", 1, "one"}, // unique - {"key2", 2, "two"}, // dup - {"key2", 2, "two"}, - }, - TypedefField: ListType{ - {"key1", 1, "one"}, // unique - {"key2", 2, "two"}, // dup - {"key2", 2, "two"}, - }, - }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ - field.Duplicate(field.NewPath("listField").Index(2), nil), - field.Duplicate(field.NewPath("listTypedefField").Index(2), nil), - field.Duplicate(field.NewPath("typedefField").Index(2), nil), - }) - */ + st.Value(&Struct{ + ListField: []OtherStruct{ + {"key1", 1, "one"}, // unique + {"key2", 2, "two"}, // dup + {"key2", 2, "two"}, + }, + ListTypedefField: []OtherTypedefStruct{ + {"key1", 1, "one"}, // unique + {"key2", 2, "two"}, // dup + {"key2", 2, "two"}, + }, + TypedefField: ListType{ + {"key1", 1, "one"}, // unique + {"key2", 2, "two"}, // dup + {"key2", 2, "two"}, + }, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.Duplicate(field.NewPath("listField").Index(2), nil), + field.Duplicate(field.NewPath("listTypedefField").Index(2), nil), + field.Duplicate(field.NewPath("typedefField").Index(2), nil), + }) } func TestUpdateCorrelation(t *testing.T) { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go index 9abe74f024d..e331f5d5274 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go @@ -24,33 +24,29 @@ import ( ) func TestUniqueness(t *testing.T) { - // TODO: enable this once we have a way to either opt-out from this validation - // or settle the decision on how to handle the ratcheting cases. - /* - st := localSchemeBuilder.Test(t) + st := localSchemeBuilder.Test(t) - st.Value(&Struct{ - ListField: []OtherStruct{ - {"key1", "one"}, - {"key2", "two"}, - {"key2", "two"}, - }, - ListTypedefField: []OtherTypedefStruct{ - {"key1", "one"}, - {"key2", "two"}, - {"key2", "two"}, - }, - TypedefField: ListType{ - {"key1", "one"}, - {"key2", "two"}, - {"key2", "two"}, - }, - }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ - field.Duplicate(field.NewPath("listField").Index(2), nil), - field.Duplicate(field.NewPath("listTypedefField").Index(2), nil), - field.Duplicate(field.NewPath("typedefField").Index(2), nil), - }) - */ + st.Value(&Struct{ + ListField: []OtherStruct{ + {"key1", "one"}, + {"key2", "two"}, + {"key2", "two"}, + }, + ListTypedefField: []OtherTypedefStruct{ + {"key1", "one"}, + {"key2", "two"}, + {"key2", "two"}, + }, + TypedefField: ListType{ + {"key1", "one"}, + {"key2", "two"}, + {"key2", "two"}, + }, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.Duplicate(field.NewPath("listField").Index(2), nil), + field.Duplicate(field.NewPath("listTypedefField").Index(2), nil), + field.Duplicate(field.NewPath("typedefField").Index(2), nil), + }) } func TestUpdateCorrelation(t *testing.T) { From 7bab54a7c8b7bc315bd686dffc63b18398949d3c Mon Sep 17 00:00:00 2001 From: yongruilin Date: Tue, 16 Sep 2025 22:11:31 +0000 Subject: [PATCH 7/7] emit comment for uniqueness is disabled by k8s:customUnique --- .../v1/zz_generated.validations.go | 1 + .../v1beta1/zz_generated.validations.go | 1 + .../zero_defaults/zz_generated.validations.go | 15 ------- .../tags/unique/zz_generated.validations.go | 16 +++++++- .../cmd/validation-gen/validation.go | 41 +++++++++++-------- .../cmd/validation-gen/validators/list.go | 7 ++-- 6 files changed, 44 insertions(+), 37 deletions(-) diff --git a/pkg/apis/certificates/v1/zz_generated.validations.go b/pkg/apis/certificates/v1/zz_generated.validations.go index f24655e1aa5..8384e157fff 100644 --- a/pkg/apis/certificates/v1/zz_generated.validations.go +++ b/pkg/apis/certificates/v1/zz_generated.validations.go @@ -113,6 +113,7 @@ func Validate_CertificateSigningRequestStatus(ctx context.Context, op operation. // field certificatesv1.CertificateSigningRequestStatus.Conditions errs = append(errs, func(fldPath *field.Path, obj, oldObj []certificatesv1.CertificateSigningRequestCondition) (errs field.ErrorList) { + // Uniqueness validation is implemented via custom, handwritten validation // don't revalidate unchanged data if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { return nil diff --git a/pkg/apis/certificates/v1beta1/zz_generated.validations.go b/pkg/apis/certificates/v1beta1/zz_generated.validations.go index c7be9509d71..8ee94e7cf9d 100644 --- a/pkg/apis/certificates/v1beta1/zz_generated.validations.go +++ b/pkg/apis/certificates/v1beta1/zz_generated.validations.go @@ -113,6 +113,7 @@ func Validate_CertificateSigningRequestStatus(ctx context.Context, op operation. // field certificatesv1beta1.CertificateSigningRequestStatus.Conditions errs = append(errs, func(fldPath *field.Path, obj, oldObj []certificatesv1beta1.CertificateSigningRequestCondition) (errs field.ErrorList) { + // Uniqueness validation is implemented via custom, handwritten validation // don't revalidate unchanged data if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { return nil diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/zz_generated.validations.go index ae37bd15da6..573e76369aa 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/zz_generated.validations.go @@ -57,11 +57,6 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { // optional value-type fields with zero-value defaults are purely documentation - // don't revalidate unchanged data - if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { - return nil - } - // call field-attached validations return }(fldPath.Child("stringField"), &obj.StringField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.StringField }))...) @@ -85,11 +80,6 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) { // optional value-type fields with zero-value defaults are purely documentation - // don't revalidate unchanged data - if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { - return nil - } - // call field-attached validations return }(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }))...) @@ -113,11 +103,6 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field errs = append(errs, func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) { // optional value-type fields with zero-value defaults are purely documentation - // don't revalidate unchanged data - if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { - return nil - } - // call field-attached validations return }(fldPath.Child("boolField"), &obj.BoolField, safe.Field(oldObj, func(oldObj *Struct) *bool { return &oldObj.BoolField }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go index a8b7754b49b..7e123da08ea 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go @@ -106,7 +106,19 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("atomicListUniqueMap"), obj.AtomicListUniqueMap, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.AtomicListUniqueMap }))...) - // field Struct.CustomUniqueListWithTypeSet has no validation - // field Struct.CustomUniqueListWithTypeMap has no validation + // field Struct.CustomUniqueListWithTypeSet + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string) (errs field.ErrorList) { + // Uniqueness validation is implemented via custom, handwritten validation + return + }(fldPath.Child("customUniqueListWithTypeSet"), obj.CustomUniqueListWithTypeSet, safe.Field(oldObj, func(oldObj *Struct) []string { return oldObj.CustomUniqueListWithTypeSet }))...) + + // field Struct.CustomUniqueListWithTypeMap + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []Item) (errs field.ErrorList) { + // Uniqueness validation is implemented via custom, handwritten validation + return + }(fldPath.Child("customUniqueListWithTypeMap"), obj.CustomUniqueListWithTypeMap, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.CustomUniqueListWithTypeMap }))...) + return errs } 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 f9f3fbbf1b8..e659982c0f9 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 @@ -1105,10 +1105,12 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild fldRatchetingChecked := false if !validations.Empty() { emitComments(validations.Comments, bufsw) - emitRatchetingCheck(c, fld.childType, bufsw) - fldRatchetingChecked = true - bufsw.Do("// call field-attached validations\n", nil) - emitCallsToValidators(c, validations.Functions, bufsw) + if len(validations.Functions) > 0 { + emitRatchetingCheck(c, fld.childType, bufsw) + fldRatchetingChecked = true + bufsw.Do("// call field-attached validations\n", nil) + emitCallsToValidators(c, validations.Functions, bufsw) + } } // If the node is nil, this must be a type in a package we are not @@ -1132,11 +1134,14 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild // validations, call its validation function. if validations := fld.fieldValIterations; g.hasValidations(fld.node.elem.node) && !validations.Empty() { emitComments(validations.Comments, bufsw) - if !fldRatchetingChecked { - emitRatchetingCheck(c, fld.childType, bufsw) - fldRatchetingChecked = true + if len(validations.Functions) > 0 { + if !fldRatchetingChecked { + emitRatchetingCheck(c, fld.childType, bufsw) + fldRatchetingChecked = true + } + emitCallsToValidators(c, validations.Functions, bufsw) } - emitCallsToValidators(c, validations.Functions, bufsw) + } // Descend into this field. g.emitValidationForChild(c, fld, bufsw) @@ -1145,21 +1150,25 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild // validations, call its validation function. if validations := fld.fieldKeyIterations; g.hasValidations(fld.node.key.node) && !validations.Empty() { emitComments(validations.Comments, bufsw) - if !fldRatchetingChecked { - emitRatchetingCheck(c, fld.childType, bufsw) - fldRatchetingChecked = true + if len(validations.Functions) > 0 { + if !fldRatchetingChecked { + emitRatchetingCheck(c, fld.childType, bufsw) + fldRatchetingChecked = true + } + emitCallsToValidators(c, validations.Functions, bufsw) } - emitCallsToValidators(c, validations.Functions, bufsw) } // If this field is a map and the value-type has // validations, call its validation function. if validations := fld.fieldValIterations; g.hasValidations(fld.node.elem.node) && !validations.Empty() { emitComments(validations.Comments, bufsw) - if !fldRatchetingChecked { - emitRatchetingCheck(c, fld.childType, bufsw) - fldRatchetingChecked = true + if len(validations.Functions) > 0 { + if !fldRatchetingChecked { + emitRatchetingCheck(c, fld.childType, bufsw) + fldRatchetingChecked = true + } + emitCallsToValidators(c, validations.Functions, bufsw) } - emitCallsToValidators(c, validations.Functions, bufsw) } // Descend into this field. g.emitValidationForChild(c, fld, bufsw) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go index 1bc9ce5e6c8..4af73a195d4 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go @@ -412,15 +412,14 @@ func (lv listValidator) GetValidations(context Context) (Validations, error) { if err := lv.check(lm); err != nil { return Validations{}, err } - + result := Validations{} if lm.customUnique { // Uniqueness validation is disabled in generated validation for this list. // It would defer to handwritten validation to check the uniqueness. - return Validations{}, nil + result.AddComment("Uniqueness validation is implemented via custom, handwritten validation") + return result, nil } - result := Validations{} - // Generate uniqueness checks for lists with higher-order semantics. if lm.semantic == semanticSet { // Only compare primitive values when possible. Slices and maps are not