mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 13:57:38 +00:00
Add +k8s:isSubresource and +k8s:supportsSubresource tags
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"})),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package root
|
||||
@@ -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
|
||||
}
|
||||
@@ -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] {
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
Reference in New Issue
Block a user