diff --git a/staging/src/k8s.io/apimachinery/pkg/api/operation/operation.go b/staging/src/k8s.io/apimachinery/pkg/api/operation/operation.go index de594e8b3d9..071a00928b4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/operation/operation.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/operation/operation.go @@ -16,7 +16,12 @@ limitations under the License. package operation -import "k8s.io/apimachinery/pkg/util/sets" +import ( + "slices" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" +) // Operation provides contextual information about a validation request and the API // operation being validated. @@ -73,6 +78,29 @@ type Request struct { Subresources []string } +// MatchesSubresource returns true if the request is for the given subresource path. +// The subresource path is a slash-separated list of subresource names. For +// example, `/status`, `/resize`, or `/x/y/z`. +// +// `/` identifies the root resource. That is, MatchesSubresource returns true +// subresourcePath is `/` and Subresources is an empty list. +func (r Request) MatchesSubresource(subresourcePath string) bool { + if len(r.Subresources) == 0 && subresourcePath == "/" { + return true + } + subresource := "/" + strings.Join(r.Subresources, "/") + return subresource == subresourcePath +} + +// SubresourceIn returns true if the request is for a subresource in the given list. +// The subresource path is a slash-separated list of subresource names. For example, +// `/status`, `/resize`, or `/x/y/z`. +// `/` identifies the root resource. That is, SubresourceIn returns true +// subresourcePaths contains `/` and Subresources is an empty list. +func (r Request) SubresourceIn(subresourcePaths []string) bool { + return slices.ContainsFunc(subresourcePaths, r.MatchesSubresource) +} + // Code is the request operation to be validated. type Type uint32 diff --git a/staging/src/k8s.io/apimachinery/pkg/api/operation/operation_test.go b/staging/src/k8s.io/apimachinery/pkg/api/operation/operation_test.go new file mode 100644 index 00000000000..77565ce78b7 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/operation/operation_test.go @@ -0,0 +1,92 @@ +/* +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 operation + +import "testing" + +func TestRequest_SubresourceIn(t *testing.T) { + tests := []struct { + name string + requestSubresources []string + matchSubresources []string + want bool + }{ + { + name: "subresource match", + requestSubresources: []string{"x"}, + matchSubresources: []string{"/x"}, + want: true, + }, + { + name: "subresource no match", + requestSubresources: []string{"x"}, + matchSubresources: []string{"/y"}, + want: false, + }, + { + name: "root match", + requestSubresources: []string{}, + matchSubresources: []string{"/"}, + want: true, + }, + { + name: "subresource does not match root", + requestSubresources: []string{"x"}, + matchSubresources: []string{"/"}, + want: false, + }, + { + name: "root does not match subresource", + requestSubresources: []string{}, + matchSubresources: []string{"/x"}, + want: false, + }, + { + name: "root matches root and subresource", + requestSubresources: []string{}, + matchSubresources: []string{"/", "/x"}, + want: true, + }, + { + name: "subresource matches root and subresource", + requestSubresources: []string{"x"}, + matchSubresources: []string{"/", "/x"}, + want: true, + }, + { + name: "subresource matches multiple", + requestSubresources: []string{"y"}, + matchSubresources: []string{"/x", "/y"}, + want: true, + }, + { + name: "nested subresource match", + requestSubresources: []string{"x", "y", "z"}, + matchSubresources: []string{"/x/y/z"}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Request{Subresources: tt.requestSubresources} + if got := r.SubresourceIn(tt.matchSubresources); got != tt.want { + t.Errorf("Request.SubresourceIn() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go index b8ddd2fac37..e7e82aea1ae 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go @@ -19,9 +19,10 @@ package rest import ( "context" "fmt" - "k8s.io/apimachinery/pkg/api/operation" "strings" + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" @@ -150,7 +151,7 @@ func requestInfo(ctx context.Context, subresourceMapper GroupVersionKindProvider if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found { groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} if subresourceMapper != nil { - return subresourceMapper.GroupVersionKind(groupVersion).GroupVersion(), nil, nil + groupVersion = subresourceMapper.GroupVersionKind(groupVersion).GroupVersion() } subresources, err := parseSubresourcePath(requestInfo.Subresource) if err != nil { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/is_subresource/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/is_subresource/doc.go new file mode 100644 index 00000000000..c164f71809b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/is_subresource/doc.go @@ -0,0 +1,38 @@ +/* +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. +*/ + +// Note: this selects all types in the package. +// +k8s:validation-gen=* +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package subresource + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +// Root resource is supported by default + +// +k8s:supportsSubresource=/status +// +k8s:supportsSubresource=/scale +// +k8s:supportsSubresource=/x/y + +// T1 is a test type +type T1 struct { + // +k8s:validateTrue="field T1.S" + S string `json:"s"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/is_subresource/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/is_subresource/doc_test.go new file mode 100644 index 00000000000..f58e55f8294 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/is_subresource/doc_test.go @@ -0,0 +1,43 @@ +/* +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 subresource + +import ( + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestRegisterValidations(t *testing.T) { + st := localSchemeBuilder.Test(t) + + t1 := &T1{} + + st.Value(t1).ExpectValid() + + st.Value(t1).Subresources([]string{"status"}).ExpectValid() + st.Value(t1).Subresources([]string{"scale"}).ExpectValid() + st.Value(t1).Subresources([]string{"x", "y"}).ExpectValid() + + st.Value(t1).Subresources([]string{"unknown"}).ExpectInvalid( + field.InternalError(nil, fmt.Errorf("no validation found for %T, subresources: %v", t1, []string{"unknown"})), + ) + st.Value(t1).Subresources([]string{"x", "unknown"}).ExpectInvalid( + field.InternalError(nil, fmt.Errorf("no validation found for %T, subresources: %v", t1, []string{"x", "unknown"})), + ) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/root/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/root/doc.go new file mode 100644 index 00000000000..561f1e31e40 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/root/doc.go @@ -0,0 +1,35 @@ +/* +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. +*/ + +// Note: this selects all types in the package. +// +k8s:validation-gen=* +// +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 trivial + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type T1 struct{} + +type T2 struct{} + +type E1 string + +type E2 string diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/root/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/root/doc_test.go new file mode 100644 index 00000000000..754d3d260a7 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/root/doc_test.go @@ -0,0 +1,17 @@ +/* +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 subresource diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/doc.go new file mode 100644 index 00000000000..4ae53556c99 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/doc.go @@ -0,0 +1,33 @@ +/* +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. +*/ + +// Note: this selects all types in the package. +// +k8s:validation-gen=* +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package root + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +// Root resource is supported by default + +type T1 struct { + // +k8s:validateFalse="field T1.S" + S string `json:"s"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/doc_test.go new file mode 100644 index 00000000000..c8040311d47 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/doc_test.go @@ -0,0 +1 @@ +package root diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/zz_generated.validations.go new file mode 100644 index 00000000000..cf3da29f80a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/supported_resources/subresource/zz_generated.validations.go @@ -0,0 +1,58 @@ +//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 root + +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 { + scheme.AddValidationFunc((*T1)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + if op.Request.ResourceIn([]string{"/"}) { + return Validate_T1(ctx, op, nil /* fldPath */, obj.(*T1), safe.Cast[*T1](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresources: %v", obj, op.Request.Subresources))} + }) + return nil +} + +func Validate_T1(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *T1) (errs field.ErrorList) { + // field T1.S + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field T1.S")...) + return + }(fldPath.Child("s"), &obj.S, safe.Field(oldObj, func(oldObj *T1) *string { return &oldObj.S }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go index a6bd3d9075c..38a8bca72bb 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go @@ -38,6 +38,13 @@ const ( inputTagName = "k8s:validation-gen-input" schemeRegistryTagName = "k8s:validation-gen-scheme-registry" // defaults to k8s.io/apimachinery/pkg.runtime.Scheme testFixtureTagName = "k8s:validation-gen-test-fixture" // if set, generate go test files for test fixtures. Supported values: "validateFalse". + + // name of the subresource that this type represents and can validate declaratively. + isSubresourceTagName = "k8s:isSubresource" + + // name of a subresource that this type can validate declaratively, tag may be + // repeated to support multiple subresources. + supportsSubresourceTagName = "k8s:supportsSubresource" ) var ( @@ -116,6 +123,39 @@ func schemeRegistryTag(pkg *types.Package) types.Name { return types.ParseFullyQualifiedName(values[0].Value) } +func isSubresourceTag(t *types.Type) (string, bool) { + comments := append(t.SecondClosestCommentLines, t.CommentLines...) + tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{isSubresourceTagName}, comments) + if err != nil { + klog.Fatalf("Failed to extract isSubresource tags: %v", err) + } + values, found := tags[isSubresourceTagName] + if !found || len(values) == 0 { + return "", false + } + if len(values) > 1 { + panic(fmt.Sprintf("Type %q contains more than one usage of %q", t.Name.String(), isSubresourceTagName)) + } + return values[0].Value, true +} + +func supportedSubresourceTags(t *types.Type) sets.Set[string] { + comments := append(t.SecondClosestCommentLines, t.CommentLines...) + tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{supportsSubresourceTagName}, comments) + if err != nil { + klog.Fatalf("Failed to extract supportedSubresource tags: %v", err) + } + values, found := tags[supportsSubresourceTagName] + if !found || len(values) == 0 { + return sets.New[string]() + } + subresources := sets.New[string]() + for _, tag := range values { + subresources.Insert(tag.Value) + } + return subresources +} + var testFixtureTagValues = sets.New("validateFalse") func testFixtureTag(pkg *types.Package) sets.Set[string] { 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 69cbc558415..4742458c121 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 @@ -52,7 +52,7 @@ var ( safePkg = "k8s.io/apimachinery/pkg/api/safe" safePkgSymbols = mkPkgNames(safePkg, "Field", "Cast") operationPkg = "k8s.io/apimachinery/pkg/api/operation" - operationPkgSymbols = mkPkgNames(operationPkg, "Operation") + operationPkgSymbols = mkPkgNames(operationPkg, "Operation", "MatchesSubresource") contextPkg = "context" contextPkgSymbols = mkPkgNames(contextPkg, "Context") ) @@ -816,14 +816,17 @@ func (g *genValidations) emitRegisterFunction(c *generator.Context, schemeRegist panic(fmt.Sprintf("found nil node for root-type %v", rootType)) } + supportedResources := g.toResourceList(rootType) + targs := generator.Args{ - "rootType": rootType, - "typePfx": "", - "field": mkSymbolArgs(c, fieldPkgSymbols), - "fmt": mkSymbolArgs(c, fmtPkgSymbols), - "operation": mkSymbolArgs(c, operationPkgSymbols), - "safe": mkSymbolArgs(c, safePkgSymbols), - "context": mkSymbolArgs(c, contextPkgSymbols), + "rootType": rootType, + "typePfx": "", + "field": mkSymbolArgs(c, fieldPkgSymbols), + "fmt": mkSymbolArgs(c, fmtPkgSymbols), + "operation": mkSymbolArgs(c, operationPkgSymbols), + "safe": mkSymbolArgs(c, safePkgSymbols), + "context": mkSymbolArgs(c, contextPkgSymbols), + "supportsResources": strings.Join(supportedResources, ", "), } if !isNilableType(rootType) { targs["typePfx"] = "*" @@ -835,7 +838,7 @@ func (g *genValidations) emitRegisterFunction(c *generator.Context, schemeRegist sw.Do("scheme.AddValidationFunc(", targs) sw.Do(" ($.typePfx$$.rootType|raw$)(nil), ", targs) sw.Do(" func(ctx $.context.Context$, op $.operation.Operation|raw$, obj, oldObj interface{}) $.field.ErrorList|raw$ {\n", targs) - sw.Do(" if len(op.Request.Subresources) == 0 || (len(op.Request.Subresources) == 1 && op.Request.Subresources[0] == \"scale\") {\n", targs) + sw.Do(" if op.Request.SubresourceIn([]string{$.supportsResources$}) {\n", targs) sw.Do(" return $.rootType|objectvalidationfn$(", targs) sw.Do(" ctx, ", targs) sw.Do(" op, ", targs) @@ -854,6 +857,23 @@ func (g *genValidations) emitRegisterFunction(c *generator.Context, schemeRegist sw.Do("}\n\n", nil) } +// toResourceList returns a list of resources that are supported by a kind. +func (g *genValidations) toResourceList(rootType *types.Type) []string { + supportedSubresources := supportedSubresourceTags(rootType) + + if subresource, isSubresource := isSubresourceTag(rootType); isSubresource { + supportedSubresources.Insert(subresource) + } else { + supportedSubresources.Insert("/") + } + supported := supportedSubresources.UnsortedList() + slices.Sort(supported) + for i, subresource := range supported { + supported[i] = strconv.Quote(subresource) + } + return supported +} + // emitValidationFunction emits a validation function for the specified type. func (g *genValidations) emitValidationFunction(c *generator.Context, t *types.Type, sw *generator.SnippetWriter) { if !g.hasValidations(g.discovered.typeNodes[t]) {