Add +k8s:isSubresource and +k8s:supportsSubresource tags

This commit is contained in:
Joe Betz
2025-05-19 11:18:06 -04:00
parent 105391403f
commit 6ca6b7bb6a
12 changed files with 418 additions and 12 deletions

View File

@@ -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

View File

@@ -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)
}
})
}
}

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -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"})),
)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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] {

View File

@@ -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]) {