Simplify subresource matching

This commit is contained in:
Joe Betz
2025-05-23 19:30:09 -04:00
parent 9715c90b31
commit 8ae7171041
12 changed files with 194 additions and 128 deletions

View File

@@ -17,7 +17,6 @@ limitations under the License.
package operation
import (
"slices"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
@@ -78,31 +77,12 @@ 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
}
return r.SubresourcePath() == 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)
}
// SubresourcePath returns the path is a slash-separated list of subresource
// names. For example, `/status`, `/resize`, or `/x/y/z`.
func (r Request) SubresourcePath() string {
if len(r.Subresources) == 0 {
return "/"
}
return "/" + strings.Join(r.Subresources, "/")
}

View File

@@ -1,92 +0,0 @@
/*
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

@@ -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 issubresource
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.MatchesSubresource("/scale") {
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, subresource: %v", obj, op.Request.SubresourcePath()))}
})
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, true, "field T1.S")...)
return
}(fldPath.Child("s"), &obj.S, safe.Field(oldObj, func(oldObj *T1) *string { return &oldObj.S }))...)
return errs
}

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.MatchesSubresource("/") {
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, subresource: %v", obj, op.Request.SubresourcePath()))}
})
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, true, "field T1.S")...)
return
}(fldPath.Child("s"), &obj.S, safe.Field(oldObj, func(oldObj *T1) *string { return &oldObj.S }))...)
return errs
}

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 subresource
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.MatchesSubresource("/") || op.Request.MatchesSubresource("/scale") || op.Request.MatchesSubresource("/status") || op.Request.MatchesSubresource("/x/y") {
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, subresource: %v", obj, op.Request.SubresourcePath()))}
})
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, true, "field T1.S")...)
return
}(fldPath.Child("s"), &obj.S, safe.Field(oldObj, func(oldObj *T1) *string { return &oldObj.S }))...)
return errs
}

View File

@@ -816,30 +816,34 @@ 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),
"supportsResources": strings.Join(supportedResources, ", "),
"rootType": rootType,
"typePfx": "",
"field": mkSymbolArgs(c, fieldPkgSymbols),
"fmt": mkSymbolArgs(c, fmtPkgSymbols),
"operation": mkSymbolArgs(c, operationPkgSymbols),
"safe": mkSymbolArgs(c, safePkgSymbols),
"context": mkSymbolArgs(c, contextPkgSymbols),
}
if !isNilableType(rootType) {
targs["typePfx"] = "*"
}
// TODO: Remove special-casing for `/` and `/scale` resources once ratcheting is introduced.
// This uses a typed nil pointer, rather than a real instance because
// we need the type information, but not an instance of the type.
sw.Do("$.rootType|private$SupportedResources := []string{$.supportsResources$}\n", targs)
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 op.Request.SubresourceIn($.rootType|private$SupportedResources) {\n", targs)
sw.Do("switch op.Request.SubresourcePath() {\n", nil)
sw.Do("case ", nil)
for i, s := range g.toResourceList(rootType) {
if i > 0 {
sw.Do(", ", nil)
}
sw.Do("$.$", s)
}
sw.Do(":\n", nil)
sw.Do(" return $.rootType|objectvalidationfn$(", targs)
sw.Do(" ctx, ", targs)
sw.Do(" op, ", targs)