mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-14 13:45:06 +00:00
Merge pull request #130549 from jpbetz/validation-gen-pr2
KEP-5073: Add declarative validation to scheme
This commit is contained in:
@@ -17,15 +17,18 @@ limitations under the License.
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/naming"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Scheme defines methods for serializing and deserializing API objects, a type
|
||||
@@ -68,6 +71,12 @@ type Scheme struct {
|
||||
// the provided object must be a pointer.
|
||||
defaulterFuncs map[reflect.Type]func(interface{})
|
||||
|
||||
// validationFuncs is a map to funcs to be called with an object to perform validation.
|
||||
// The provided object must be a pointer.
|
||||
// If oldObject is non-nil, update validation is performed and may perform additional
|
||||
// validation such as transition rules and immutability checks.
|
||||
validationFuncs map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList
|
||||
|
||||
// converter stores all registered conversion functions. It also has
|
||||
// default converting behavior.
|
||||
converter *conversion.Converter
|
||||
@@ -96,6 +105,7 @@ func NewScheme() *Scheme {
|
||||
unversionedKinds: map[string]reflect.Type{},
|
||||
fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
|
||||
defaulterFuncs: map[reflect.Type]func(interface{}){},
|
||||
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresource ...string) field.ErrorList{},
|
||||
versionPriority: map[string][]string{},
|
||||
schemeName: naming.GetNameFromCallsite(internalPackages...),
|
||||
}
|
||||
@@ -347,6 +357,35 @@ func (s *Scheme) Default(src Object) {
|
||||
}
|
||||
}
|
||||
|
||||
// AddValidationFunc registered a function that can validate the object, and
|
||||
// oldObject. These functions will be invoked when Validate() or ValidateUpdate()
|
||||
// is called. The function will never be called unless the validated object
|
||||
// matches srcType. If this function is invoked twice with the same srcType, the
|
||||
// fn passed to the later call will be used instead.
|
||||
func (s *Scheme) AddValidationFunc(srcType Object, fn func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList) {
|
||||
s.validationFuncs[reflect.TypeOf(srcType)] = fn
|
||||
}
|
||||
|
||||
// Validate validates the provided Object according to the generated declarative validation code.
|
||||
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
|
||||
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
|
||||
func (s *Scheme) Validate(ctx context.Context, options sets.Set[string], object Object, subresources ...string) field.ErrorList {
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Create, Options: options}, object, nil, subresources...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate validates the provided object and oldObject according to the generated declarative validation code.
|
||||
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
|
||||
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
|
||||
func (s *Scheme) ValidateUpdate(ctx context.Context, options sets.Set[string], object, oldObject Object, subresources ...string) field.ErrorList {
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Update, Options: options}, object, oldObject, subresources...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert will attempt to convert in into out. Both must be pointers. For easy
|
||||
// testing of conversion functions. Returns an error if the conversion isn't
|
||||
// possible. You can call this with types that haven't been registered (for example,
|
||||
|
@@ -17,12 +17,15 @@ limitations under the License.
|
||||
package runtime_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -30,6 +33,9 @@ import (
|
||||
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
fieldtesting "k8s.io/apimachinery/pkg/util/validation/field/testing"
|
||||
)
|
||||
|
||||
type testConversions struct {
|
||||
@@ -1009,3 +1015,101 @@ func TestMetaValuesUnregisteredConvert(t *testing.T) {
|
||||
t.Errorf("Expected %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterValidate(t *testing.T) {
|
||||
invalidValue := field.Invalid(field.NewPath("testString"), "", "Invalid value").WithOrigin("invalid-value")
|
||||
invalidLength := field.Invalid(field.NewPath("testString"), "", "Invalid length").WithOrigin("invalid-length")
|
||||
invalidStatusErr := field.Invalid(field.NewPath("testString"), "", "Invalid condition").WithOrigin("invalid-condition")
|
||||
invalidIfOptionErr := field.Invalid(field.NewPath("testString"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
oldObject runtime.Object
|
||||
subresource []string
|
||||
options sets.Set[string]
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "single error",
|
||||
object: &TestType1{},
|
||||
expected: field.ErrorList{invalidValue},
|
||||
},
|
||||
{
|
||||
name: "multiple errors",
|
||||
object: &TestType2{},
|
||||
expected: field.ErrorList{invalidValue, invalidLength},
|
||||
},
|
||||
{
|
||||
name: "update error",
|
||||
object: &TestType2{},
|
||||
oldObject: &TestType2{},
|
||||
expected: field.ErrorList{invalidLength},
|
||||
},
|
||||
{
|
||||
name: "options error",
|
||||
object: &TestType1{},
|
||||
options: sets.New("option1"),
|
||||
expected: field.ErrorList{invalidIfOptionErr},
|
||||
},
|
||||
{
|
||||
name: "subresource error",
|
||||
object: &TestType1{},
|
||||
subresource: []string{"status"},
|
||||
expected: field.ErrorList{invalidStatusErr},
|
||||
},
|
||||
}
|
||||
|
||||
s := runtime.NewScheme()
|
||||
ctx := context.Background()
|
||||
|
||||
// register multiple types for testing to ensure registration is working as expected
|
||||
s.AddValidationFunc(&TestType1{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList {
|
||||
if op.Options.Has("option1") {
|
||||
return field.ErrorList{invalidIfOptionErr}
|
||||
}
|
||||
if len(subresources) == 1 && subresources[0] == "status" {
|
||||
return field.ErrorList{invalidStatusErr}
|
||||
}
|
||||
return field.ErrorList{invalidValue}
|
||||
})
|
||||
|
||||
s.AddValidationFunc(&TestType2{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList {
|
||||
if oldObject != nil {
|
||||
return field.ErrorList{invalidLength}
|
||||
}
|
||||
return field.ErrorList{invalidValue, invalidLength}
|
||||
})
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var results field.ErrorList
|
||||
if tc.oldObject == nil {
|
||||
results = s.Validate(ctx, tc.options, tc.object, tc.subresource...)
|
||||
} else {
|
||||
results = s.ValidateUpdate(ctx, tc.options, tc.object, tc.oldObject, tc.subresource...)
|
||||
}
|
||||
fieldtesting.MatchErrors(t, tc.expected, results, fieldtesting.Match().ByType().ByField().ByOrigin())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type TestType1 struct {
|
||||
Version string `json:"apiVersion,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
TestString string `json:"testString"`
|
||||
}
|
||||
|
||||
func (TestType1) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
|
||||
|
||||
func (TestType1) DeepCopyObject() runtime.Object { return nil }
|
||||
|
||||
type TestType2 struct {
|
||||
Version string `json:"apiVersion,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
TestString string `json:"testString"`
|
||||
}
|
||||
|
||||
func (TestType2) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
|
||||
|
||||
func (TestType2) DeepCopyObject() runtime.Object { return nil }
|
||||
|
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,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user