diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index 06cf15e8c0d..714d54d6718 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -11,6 +11,7 @@ go_test( srcs = [ "create_test.go", "namer_test.go", + "response_test.go", "rest_test.go", ], embed = [":go_default_library"], @@ -21,6 +22,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/testapigroup/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", @@ -33,6 +35,7 @@ go_test( "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go index c84d8634b90..d2782178fd7 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go @@ -38,6 +38,24 @@ import ( // the client's desired form, as well as ensuring any API level fields like self-link // are properly set. func transformObject(ctx context.Context, obj runtime.Object, opts interface{}, mediaType negotiation.MediaTypeOptions, scope *RequestScope, req *http.Request) (runtime.Object, error) { + if co, ok := obj.(runtime.CacheableObject); ok { + if mediaType.Convert != nil { + // Non-nil mediaType.Convert means that some conversion of the object + // has to happen. Currently conversion may potentially modify the + // object or assume something about it (e.g. asTable operates on + // reflection, which won't work for any wrapper). + // To ensure it will work correctly, let's operate on base objects + // and not cache it for now. + // + // TODO: Long-term, transformObject should be changed so that it + // implements runtime.Encoder interface. + return doTransformObject(ctx, co.GetObject(), opts, mediaType, scope, req) + } + } + return doTransformObject(ctx, obj, opts, mediaType, scope, req) +} + +func doTransformObject(ctx context.Context, obj runtime.Object, opts interface{}, mediaType negotiation.MediaTypeOptions, scope *RequestScope, req *http.Request) (runtime.Object, error) { if _, ok := obj.(*metav1.Status); ok { return obj, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response_test.go new file mode 100644 index 00000000000..d47a81e0c91 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2019 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 handlers + +import ( + "context" + "fmt" + "io" + "net/http" + "reflect" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + examplev1 "k8s.io/apiserver/pkg/apis/example/v1" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" +) + +var _ runtime.CacheableObject = &mockCacheableObject{} + +type mockCacheableObject struct { + gvk schema.GroupVersionKind + obj runtime.Object +} + +// DeepCopyObject implements runtime.Object interface. +func (m *mockCacheableObject) DeepCopyObject() runtime.Object { + panic("DeepCopy unimplemented for mockCacheableObject") +} + +// GetObjectKind implements runtime.Object interface. +func (m *mockCacheableObject) GetObjectKind() schema.ObjectKind { + return m +} + +// GroupVersionKind implements schema.ObjectKind interface. +func (m *mockCacheableObject) GroupVersionKind() schema.GroupVersionKind { + return m.gvk +} + +// SetGroupVersionKind implements schema.ObjectKind interface. +func (m *mockCacheableObject) SetGroupVersionKind(gvk schema.GroupVersionKind) { + m.gvk = gvk +} + +// CacheEncode implements runtime.CacheableObject interface. +func (m *mockCacheableObject) CacheEncode(id runtime.Identifier, encode func(runtime.Object, io.Writer) error, w io.Writer) error { + return fmt.Errorf("unimplemented") +} + +// GetObject implements runtime.CacheableObject interface. +func (m *mockCacheableObject) GetObject() runtime.Object { + return m.obj +} + +type mockNamer struct{} + +func (*mockNamer) Namespace(_ *http.Request) (string, error) { return "", nil } +func (*mockNamer) Name(_ *http.Request) (string, string, error) { return "", "", nil } +func (*mockNamer) ObjectName(_ runtime.Object) (string, string, error) { return "", "", nil } +func (*mockNamer) SetSelfLink(_ runtime.Object, _ string) error { return nil } +func (*mockNamer) GenerateLink(_ *request.RequestInfo, _ runtime.Object) (string, error) { + return "", nil +} +func (*mockNamer) GenerateListLink(_ *http.Request) (string, error) { return "", nil } + +func TestCacheableObject(t *testing.T) { + pomGVK := metav1.SchemeGroupVersion.WithKind("PartialObjectMetadata") + tableGVK := metav1.SchemeGroupVersion.WithKind("Table") + + status := &metav1.Status{Status: "status"} + pod := &examplev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + } + podMeta := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + } + podMeta.GetObjectKind().SetGroupVersionKind(pomGVK) + podTable := &metav1.Table{ + Rows: []metav1.TableRow{ + { + Cells: []interface{}{pod.Name, pod.CreationTimestamp.Time.UTC().Format(time.RFC3339)}, + }, + }, + } + + tableConvertor := rest.NewDefaultTableConvertor(examplev1.Resource("Pod")) + + testCases := []struct { + desc string + object runtime.Object + opts *metav1beta1.TableOptions + mediaType negotiation.MediaTypeOptions + + expectedUnwrap bool + expectedObj runtime.Object + expectedErr error + }{ + { + desc: "metav1.Status", + object: status, + expectedObj: status, + expectedErr: nil, + }, + { + desc: "cacheableObject nil convert", + object: &mockCacheableObject{obj: pod}, + mediaType: negotiation.MediaTypeOptions{}, + expectedObj: &mockCacheableObject{obj: pod}, + expectedErr: nil, + }, + { + desc: "cacheableObject as PartialObjectMeta", + object: &mockCacheableObject{obj: pod}, + mediaType: negotiation.MediaTypeOptions{Convert: &pomGVK}, + expectedObj: podMeta, + expectedErr: nil, + }, + { + desc: "cacheableObject as Table", + object: &mockCacheableObject{obj: pod}, + opts: &metav1beta1.TableOptions{NoHeaders: true, IncludeObject: metav1.IncludeNone}, + mediaType: negotiation.MediaTypeOptions{Convert: &tableGVK}, + expectedObj: podTable, + expectedErr: nil, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + result, err := transformObject( + request.WithRequestInfo(context.TODO(), &request.RequestInfo{}), + test.object, test.opts, test.mediaType, + &RequestScope{ + Namer: &mockNamer{}, + TableConvertor: tableConvertor, + }, + nil) + + if err != test.expectedErr { + t.Errorf("unexpected error: %v, expected: %v", err, test.expectedErr) + } + if a, e := result, test.expectedObj; !reflect.DeepEqual(a, e) { + t.Errorf("unexpected result: %v, expected: %v", a, e) + } + }) + } +}