mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 15:58:37 +00:00
Add declarative validation utility for use from strategies
This commit is contained in:
parent
5ff334a158
commit
ffc1b32c66
108
staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go
Normal file
108
staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2025 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 rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
// ValidateDeclaratively validates obj against declarative validation tags
|
||||
// defined in its Go type. It uses the API version extracted from ctx and the
|
||||
// provided scheme for validation.
|
||||
//
|
||||
// The ctx MUST contain requestInfo, which determines the target API for
|
||||
// validation. The obj is converted to the API version using the provided scheme
|
||||
// before validation occurs. The scheme MUST have the declarative validation
|
||||
// registered for the requested resource/subresource.
|
||||
//
|
||||
// option should contain any validation options that the declarative validation
|
||||
// tags expect.
|
||||
//
|
||||
// Returns a field.ErrorList containing any validation errors. An internal error
|
||||
// is included if requestInfo is missing from the context or if version
|
||||
// conversion fails.
|
||||
func ValidateDeclaratively(ctx context.Context, options sets.Set[string], scheme *runtime.Scheme, obj runtime.Object) field.ErrorList {
|
||||
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
|
||||
groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||
versionedObj, err := scheme.ConvertToVersion(obj, groupVersion)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error converting to versioned type: %w", err))}
|
||||
}
|
||||
subresources, err := parseSubresourcePath(requestInfo.Subresource)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error parsing subresource path: %w", err))}
|
||||
}
|
||||
return scheme.Validate(ctx, options, versionedObj, subresources...)
|
||||
} else {
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("could not find requestInfo in context"))}
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateUpdateDeclaratively validates obj and oldObj against declarative
|
||||
// validation tags defined in its Go type. It uses the API version extracted from
|
||||
// ctx and the provided scheme for validation.
|
||||
//
|
||||
// The ctx MUST contain requestInfo, which determines the target API for
|
||||
// validation. The obj is converted to the API version using the provided scheme
|
||||
// before validation occurs. The scheme MUST have the declarative validation
|
||||
// registered for the requested resource/subresource.
|
||||
//
|
||||
// option should contain any validation options that the declarative validation
|
||||
// tags expect.
|
||||
//
|
||||
// Returns a field.ErrorList containing any validation errors. An internal error
|
||||
// is included if requestInfo is missing from the context or if version
|
||||
// conversion fails.
|
||||
func ValidateUpdateDeclaratively(ctx context.Context, options sets.Set[string], scheme *runtime.Scheme, obj, oldObj runtime.Object) field.ErrorList {
|
||||
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
|
||||
groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||
versionedObj, err := scheme.ConvertToVersion(obj, groupVersion)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error converting to versioned type: %w", err))}
|
||||
}
|
||||
versionedOldObj, err := scheme.ConvertToVersion(oldObj, groupVersion)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error converting to versioned type: %w", err))}
|
||||
}
|
||||
subresources, err := parseSubresourcePath(requestInfo.Subresource)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error parsing subresource path: %w", err))}
|
||||
}
|
||||
return scheme.ValidateUpdate(ctx, options, versionedObj, versionedOldObj, subresources...)
|
||||
} else {
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("could not find requestInfo in context"))}
|
||||
}
|
||||
}
|
||||
|
||||
func parseSubresourcePath(subresourcePath string) ([]string, error) {
|
||||
if len(subresourcePath) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if subresourcePath[0] != '/' {
|
||||
return nil, fmt.Errorf("invalid subresource path: %s", subresourcePath)
|
||||
}
|
||||
parts := strings.Split(subresourcePath[1:], "/")
|
||||
return parts, nil
|
||||
}
|
183
staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go
Normal file
183
staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go
Normal file
@ -0,0 +1,183 @@
|
||||
/*
|
||||
Copyright 2025 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 rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
fieldtesting "k8s.io/apimachinery/pkg/util/validation/field/testing"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
func TestValidateDeclaratively(t *testing.T) {
|
||||
valid := &Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
}
|
||||
|
||||
invalidRestartPolicy := &Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
RestartPolicy: "INVALID",
|
||||
}
|
||||
|
||||
invalidRestartPolicyErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Invalid value").WithOrigin("invalid-test")
|
||||
mutatedRestartPolicyErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Immutable field").WithOrigin("immutable-test")
|
||||
invalidStatusErr := field.Invalid(field.NewPath("status", "conditions"), "", "Invalid condition").WithOrigin("invalid-condition")
|
||||
invalidIfOptionErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set")
|
||||
invalidSubresourceErr := field.InternalError(nil, fmt.Errorf("unexpected error parsing subresource path: %w", fmt.Errorf("invalid subresource path: %s", "invalid/status")))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
oldObject runtime.Object
|
||||
subresource string
|
||||
options sets.Set[string]
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
object: invalidRestartPolicy,
|
||||
expected: field.ErrorList{invalidRestartPolicyErr},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
object: invalidRestartPolicy,
|
||||
oldObject: valid,
|
||||
expected: field.ErrorList{invalidRestartPolicyErr, mutatedRestartPolicyErr},
|
||||
},
|
||||
{
|
||||
name: "update subresource",
|
||||
subresource: "/status",
|
||||
object: valid,
|
||||
oldObject: valid,
|
||||
expected: field.ErrorList{invalidStatusErr},
|
||||
},
|
||||
{
|
||||
name: "invalid subresource",
|
||||
subresource: "invalid/status",
|
||||
object: valid,
|
||||
oldObject: valid,
|
||||
expected: field.ErrorList{invalidSubresourceErr},
|
||||
},
|
||||
{
|
||||
name: "update with option",
|
||||
options: sets.New("option1"),
|
||||
object: valid,
|
||||
expected: field.ErrorList{invalidIfOptionErr},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
internalGV := schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}
|
||||
v1GV := schema.GroupVersion{Group: "", Version: "v1"}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
scheme.AddKnownTypes(internalGV, &Pod{})
|
||||
scheme.AddKnownTypes(v1GV, &v1.Pod{})
|
||||
|
||||
scheme.AddValidationFunc(&v1.Pod{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList {
|
||||
results := field.ErrorList{}
|
||||
if op.Options.Has("option1") {
|
||||
results = append(results, invalidIfOptionErr)
|
||||
}
|
||||
if len(subresources) == 1 && subresources[0] == "status" {
|
||||
results = append(results, invalidStatusErr)
|
||||
}
|
||||
if op.Type == operation.Update && object.(*v1.Pod).Spec.RestartPolicy != oldObject.(*v1.Pod).Spec.RestartPolicy {
|
||||
results = append(results, mutatedRestartPolicyErr)
|
||||
}
|
||||
if object.(*v1.Pod).Spec.RestartPolicy == "INVALID" {
|
||||
results = append(results, invalidRestartPolicyErr)
|
||||
}
|
||||
return results
|
||||
})
|
||||
err := scheme.AddConversionFunc(&Pod{}, &v1.Pod{}, func(a, b interface{}, scope conversion.Scope) error {
|
||||
if in, ok := a.(*Pod); ok {
|
||||
if out, ok := b.(*v1.Pod); ok {
|
||||
out.APIVersion = in.APIVersion
|
||||
out.Kind = in.Kind
|
||||
out.Spec.RestartPolicy = v1.RestartPolicy(in.RestartPolicy)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
ctx = genericapirequest.WithRequestInfo(ctx, &genericapirequest.RequestInfo{
|
||||
APIGroup: "",
|
||||
APIVersion: "v1",
|
||||
Subresource: tc.subresource,
|
||||
})
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var results field.ErrorList
|
||||
if tc.oldObject == nil {
|
||||
results = ValidateDeclaratively(ctx, tc.options, scheme, tc.object)
|
||||
} else {
|
||||
results = ValidateUpdateDeclaratively(ctx, tc.options, scheme, tc.object, tc.oldObject)
|
||||
}
|
||||
fieldtesting.MatchErrors(t, tc.expected, results, fieldtesting.Match().ByType().ByField().ByOrigin())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fake internal pod type, since core.Pod cannot be imported by this package
|
||||
type Pod struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
RestartPolicy string `json:"restartPolicy"`
|
||||
}
|
||||
|
||||
func (Pod) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
|
||||
|
||||
func (p Pod) DeepCopyObject() runtime.Object {
|
||||
return &Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: p.APIVersion,
|
||||
Kind: p.Kind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: p.Name,
|
||||
Namespace: p.Namespace,
|
||||
},
|
||||
RestartPolicy: p.RestartPolicy,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user