From f203e42cb98ed4bac7ad8ebbed717d3bd42f55b6 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 29 May 2017 15:08:17 -0400 Subject: [PATCH] Expose a default Table and partial output via Accept headers All generic registries expose metadata output, and refactor endpoints to allow negotiation to handle those responses. Add support for PartialObjectMetadata being returned for objects as well. --- .../k8s.io/apimachinery/pkg/api/meta/BUILD | 1 + .../k8s.io/apimachinery/pkg/api/meta/meta.go | 30 +++ .../src/k8s.io/apiserver/pkg/endpoints/BUILD | 3 + .../apiserver/pkg/endpoints/apiserver_test.go | 255 +++++++++++++++++- .../apiserver/pkg/endpoints/handlers/BUILD | 2 + .../endpoints/handlers/negotiation/errors.go | 8 + .../handlers/negotiation/negotiate.go | 118 ++++---- .../pkg/endpoints/handlers/response.go | 195 ++++++++++++++ .../handlers/responsewriters/status.go | 4 +- .../handlers/responsewriters/status_test.go | 2 +- .../handlers/responsewriters/writers.go | 33 ++- .../apiserver/pkg/endpoints/handlers/rest.go | 43 ++- .../apiserver/pkg/endpoints/installer.go | 12 +- .../apiserver/pkg/registry/rest/rest.go | 5 + .../apiserver/pkg/registry/rest/table.go | 106 ++++++++ 15 files changed, 731 insertions(+), 86 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go create mode 100644 staging/src/k8s.io/apiserver/pkg/registry/rest/table.go diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD index f4b3598def0..f6d13979840 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD @@ -43,6 +43,7 @@ go_library( "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go index 9cc37297414..01eb0503137 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go @@ -23,6 +23,7 @@ import ( "github.com/golang/glog" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -89,6 +90,35 @@ func Accessor(obj interface{}) (metav1.Object, error) { } } +// AsPartialObjectMetadata takes the metav1 interface and returns a partial object. +// TODO: consider making this solely a conversion action. +func AsPartialObjectMetadata(m metav1.Object) *metav1alpha1.PartialObjectMetadata { + switch t := m.(type) { + case *metav1.ObjectMeta: + return &metav1alpha1.PartialObjectMetadata{ObjectMeta: *t} + default: + return &metav1alpha1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: m.GetName(), + GenerateName: m.GetGenerateName(), + Namespace: m.GetNamespace(), + SelfLink: m.GetSelfLink(), + UID: m.GetUID(), + ResourceVersion: m.GetResourceVersion(), + Generation: m.GetGeneration(), + CreationTimestamp: m.GetCreationTimestamp(), + DeletionTimestamp: m.GetDeletionTimestamp(), + DeletionGracePeriodSeconds: m.GetDeletionGracePeriodSeconds(), + Labels: m.GetLabels(), + Annotations: m.GetAnnotations(), + OwnerReferences: m.GetOwnerReferences(), + Finalizers: m.GetFinalizers(), + ClusterName: m.GetClusterName(), + }, + } + } +} + // TypeAccessor returns an interface that allows retrieving and modifying the APIVersion // and Kind of an in-memory internal object. // TODO: this interface is used to test code that does not have ObjectMeta or ListMeta diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD index 2bdc93a93ad..53581c3419c 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD @@ -28,12 +28,15 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/api/testing:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer/streaming:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index 7cc02f7b279..24f0698f9ee 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -43,11 +43,14 @@ import ( apitesting "k8s.io/apimachinery/pkg/api/testing" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" @@ -754,6 +757,15 @@ func (storage *SimpleTypedStorage) checkContext(ctx request.Context) { storage.actualNamespace, storage.namespacePresent = request.NamespaceFrom(ctx) } +func bodyOrDie(response *http.Response) string { + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + panic(err) + } + return string(body) +} + func extractBody(response *http.Response, object runtime.Object) (string, error) { return extractBodyDecoder(response, object, codec) } @@ -767,6 +779,16 @@ func extractBodyDecoder(response *http.Response, object runtime.Object, decoder return string(body), runtime.DecodeInto(decoder, body, object) } +func extractBodyObject(response *http.Response, decoder runtime.Decoder) (runtime.Object, string, error) { + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, string(body), err + } + obj, err := runtime.Decode(decoder, body) + return obj, string(body), err +} + func TestNotFound(t *testing.T) { type T struct { Method string @@ -1531,6 +1553,222 @@ func TestGetPretty(t *testing.T) { } } +func TestGetTable(t *testing.T) { + now := metav1.Now() + storage := map[string]rest.Storage{} + obj := genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("abcdef0123")}, + Other: "foo", + } + simpleStorage := SimpleRESTStorage{ + item: obj, + } + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id", + name: "id", + namespace: "default", + } + storage["simple"] = &simpleStorage + handler := handleLinker(storage, selfLinker) + server := httptest.NewServer(handler) + defer server.Close() + + m, err := meta.Accessor(&obj) + if err != nil { + t.Fatal(err) + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + encodedBody, err := runtime.Encode(metainternalversion.Codecs.LegacyCodec(metav1alpha1.SchemeGroupVersion), partial) + if err != nil { + t.Fatal(err) + } + // the codec includes a trailing newline that is not present during decode + encodedBody = bytes.TrimSpace(encodedBody) + + metaDoc := metav1.ObjectMeta{}.SwaggerDoc() + + tests := []struct { + accept string + params url.Values + pretty bool + expected *metav1alpha1.Table + statusCode int + }{ + { + accept: runtime.ContentTypeJSON + ";as=Table;v=v1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + accept: runtime.ContentTypeJSON + ";as=Table;v=v1alpha1;g=meta.k8s.io", + expected: &metav1alpha1.Table{ + TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"}, + ColumnDefinitions: []metav1alpha1.TableColumnDefinition{ + {Name: "Namespace", Type: "string", Description: metaDoc["namespace"]}, + {Name: "Name", Type: "string", Description: metaDoc["name"]}, + {Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]}, + }, + Rows: []metav1alpha1.TableRow{ + {Cells: []interface{}{"ns1", "foo1", now.Time.UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}}, + }, + }, + }, + } + for i, test := range tests { + u, err := url.Parse(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id") + if err != nil { + t.Fatal(err) + } + u.RawQuery = test.params.Encode() + req := &http.Request{Method: "GET", URL: u} + req.Header = http.Header{} + req.Header.Set("Accept", test.accept) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if test.statusCode != 0 { + if resp.StatusCode != test.statusCode { + t.Errorf("%d: unexpected response: %#v", resp) + } + continue + } + if resp.StatusCode != http.StatusOK { + t.Fatal(err) + } + var itemOut metav1alpha1.Table + if _, err = extractBody(resp, &itemOut); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.expected, &itemOut) { + t.Errorf("%d: did not match: %s", i, diff.ObjectReflectDiff(test.expected, &itemOut)) + } + } +} + +func TestGetPartialObjectMetadata(t *testing.T) { + now := metav1.Time{metav1.Now().Rfc3339Copy().Local()} + storage := map[string]rest.Storage{} + simpleStorage := SimpleRESTStorage{ + item: genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("abcdef0123")}, + Other: "foo", + }, + list: []genericapitesting.Simple{ + { + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("newer")}, + Other: "foo", + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "ns2", CreationTimestamp: now, UID: types.UID("older")}, + Other: "bar", + }, + }, + } + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id", + alternativeSet: sets.NewString("/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple"), + name: "id", + namespace: "default", + } + storage["simple"] = &simpleStorage + handler := handleLinker(storage, selfLinker) + server := httptest.NewServer(handler) + defer server.Close() + + tests := []struct { + accept string + params url.Values + pretty bool + list bool + expected runtime.Object + expectKind schema.GroupVersionKind + statusCode int + }{ + { + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadata;v=v1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + list: true, + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadata;v=v1alpha1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadataList;v=v1alpha1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadata;v=v1alpha1;g=meta.k8s.io", + expected: &metav1alpha1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("abcdef0123")}, + }, + expectKind: schema.GroupVersionKind{Kind: "PartialObjectMetadata", Group: "meta.k8s.io", Version: "v1alpha1"}, + }, + { + list: true, + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadataList;v=v1alpha1;g=meta.k8s.io", + expected: &metav1alpha1.PartialObjectMetadataList{ + Items: []*metav1alpha1.PartialObjectMetadata{ + { + TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1alpha1", Kind: "PartialObjectMetadata"}, + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("newer")}, + }, + { + TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1alpha1", Kind: "PartialObjectMetadata"}, + ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "ns2", CreationTimestamp: now, UID: types.UID("older")}, + }, + }, + }, + expectKind: schema.GroupVersionKind{Kind: "PartialObjectMetadataList", Group: "meta.k8s.io", Version: "v1alpha1"}, + }, + } + for i, test := range tests { + suffix := "/namespaces/default/simple/id" + if test.list { + suffix = "/namespaces/default/simple" + } + u, err := url.Parse(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + suffix) + if err != nil { + t.Fatal(err) + } + u.RawQuery = test.params.Encode() + req := &http.Request{Method: "GET", URL: u} + req.Header = http.Header{} + req.Header.Set("Accept", test.accept) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if test.statusCode != 0 { + if resp.StatusCode != test.statusCode { + t.Errorf("%d: unexpected response: %#v", i, resp) + } + continue + } + if resp.StatusCode != http.StatusOK { + t.Errorf("%d: invalid status: %#v\n%s", i, resp, bodyOrDie(resp)) + continue + } + itemOut, body, err := extractBodyObject(resp, metainternalversion.Codecs.LegacyCodec(metav1alpha1.SchemeGroupVersion)) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.expected, itemOut) { + t.Errorf("%d: did not match: %s", i, diff.ObjectReflectDiff(test.expected, itemOut)) + } + obj := &unstructured.Unstructured{} + if err := json.Unmarshal([]byte(body), obj); err != nil { + t.Fatal(err) + } + if obj.GetObjectKind().GroupVersionKind() != test.expectKind { + t.Errorf("%d: unexpected kind: %#v", i, obj.GetObjectKind().GroupVersionKind()) + } + } +} + func TestGetBinary(t *testing.T) { simpleStorage := SimpleRESTStorage{ stream: &SimpleStream{ @@ -2952,12 +3190,13 @@ func TestUpdateChecksDecode(t *testing.T) { } type setTestSelfLinker struct { - t *testing.T - expectedSet string - name string - namespace string - called bool - err error + t *testing.T + expectedSet string + alternativeSet sets.String + name string + namespace string + called bool + err error } func (s *setTestSelfLinker) Namespace(runtime.Object) (string, error) { return s.namespace, s.err } @@ -2965,7 +3204,9 @@ func (s *setTestSelfLinker) Name(runtime.Object) (string, error) { return s func (s *setTestSelfLinker) SelfLink(runtime.Object) (string, error) { return "", s.err } func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error { if e, a := s.expectedSet, selfLink; e != a { - s.t.Errorf("expected '%v', got '%v'", e, a) + if !s.alternativeSet.Has(a) { + s.t.Errorf("expected '%v', got '%v'", e, a) + } } s.called = true return s.err diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index ae2b0a8b537..15e8cd11873 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -38,6 +38,7 @@ go_library( "namer.go", "patch.go", "proxy.go", + "response.go", "rest.go", "watch.go", ], @@ -50,6 +51,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go index cd262706c26..07bc8e280f4 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go @@ -29,6 +29,10 @@ type errNotAcceptable struct { accepted []string } +func NewNotAcceptableError(accepted []string) error { + return errNotAcceptable{accepted} +} + func (e errNotAcceptable) Error() string { return fmt.Sprintf("only the following media types are accepted: %v", strings.Join(e.accepted, ", ")) } @@ -47,6 +51,10 @@ type errUnsupportedMediaType struct { accepted []string } +func NewUnsupportedMediaTypeError(accepted []string) error { + return errUnsupportedMediaType{accepted} +} + func (e errUnsupportedMediaType) Error() string { return fmt.Sprintf("the body of the request was in an unknown format - accepted media types include: %v", strings.Join(e.accepted, ", ")) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go index c3948d4cde3..896961b6ba1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go @@ -40,27 +40,32 @@ func MediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, strea return mediaTypes, streamMediaTypes } -func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions) +func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (MediaTypeOptions, runtime.SerializerInfo, error) { + mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), restrictions) if !ok { supported, _ := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errNotAcceptable{supported} + return mediaType, runtime.SerializerInfo{}, NewNotAcceptableError(supported) } // TODO: move into resthandler - info := mediaType.accepted.Serializer - if (mediaType.pretty || isPrettyPrint(req)) && info.PrettySerializer != nil { + info := mediaType.Accepted.Serializer + if (mediaType.Pretty || isPrettyPrint(req)) && info.PrettySerializer != nil { info.Serializer = info.PrettySerializer } - return info, nil + return mediaType, info, nil +} + +func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { + _, info, err := NegotiateOutputMediaType(req, ns, DefaultEndpointRestrictions) + return info, err } func NegotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions) - if !ok || mediaType.accepted.Serializer.StreamSerializer == nil { + mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), DefaultEndpointRestrictions) + if !ok || mediaType.Accepted.Serializer.StreamSerializer == nil { _, supported := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errNotAcceptable{supported} + return runtime.SerializerInfo{}, NewNotAcceptableError(supported) } - return mediaType.accepted.Serializer, nil + return mediaType.Accepted.Serializer, nil } func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { @@ -72,7 +77,7 @@ func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer mediaType, _, err := mime.ParseMediaType(mediaType) if err != nil { _, supported := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} + return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported) } for _, info := range mediaTypes { @@ -83,7 +88,7 @@ func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer } _, supported := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} + return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported) } // isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent @@ -131,9 +136,9 @@ func negotiate(header string, alternatives []string) (goautoneg.Accept, bool) { return goautoneg.Accept{}, false } -// endpointRestrictions is an interface that allows content-type negotiation +// EndpointRestrictions is an interface that allows content-type negotiation // to verify server support for specific options -type endpointRestrictions interface { +type EndpointRestrictions interface { // AllowsConversion should return true if the specified group version kind // is an allowed target object. AllowsConversion(schema.GroupVersionKind) bool @@ -145,7 +150,7 @@ type endpointRestrictions interface { AllowsStreamSchema(schema string) bool } -var defaultEndpointRestrictions = emptyEndpointRestrictions{} +var DefaultEndpointRestrictions = emptyEndpointRestrictions{} type emptyEndpointRestrictions struct{} @@ -153,9 +158,9 @@ func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false } func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" } -// acceptedMediaType contains information about a valid media type that the +// AcceptedMediaType contains information about a valid media type that the // server can serialize. -type acceptedMediaType struct { +type AcceptedMediaType struct { // Type is the first part of the media type ("application") Type string // SubType is the second part of the media type ("json") @@ -164,40 +169,40 @@ type acceptedMediaType struct { Serializer runtime.SerializerInfo } -// mediaTypeOptions describes information for a given media type that may alter +// MediaTypeOptions describes information for a given media type that may alter // the server response -type mediaTypeOptions struct { +type MediaTypeOptions struct { // pretty is true if the requested representation should be formatted for human // viewing - pretty bool + Pretty bool // stream, if set, indicates that a streaming protocol variant of this encoding // is desired. The only currently supported value is watch which returns versioned // events. In the future, this may refer to other stream protocols. - stream string + Stream string // convert is a request to alter the type of object returned by the server from the // normal response - convert *schema.GroupVersionKind + Convert *schema.GroupVersionKind // useServerVersion is an optional version for the server group - useServerVersion string + UseServerVersion string // export is true if the representation requested should exclude fields the server // has set - export bool + Export bool // unrecognized is a list of all unrecognized keys - unrecognized []string + Unrecognized []string // the accepted media type from the client - accepted *acceptedMediaType + Accepted *AcceptedMediaType } // acceptMediaTypeOptions returns an options object that matches the provided media type params. If // it returns false, the provided options are not allowed and the media type must be skipped. These // parameters are unversioned and may not be changed. -func acceptMediaTypeOptions(params map[string]string, accepts *acceptedMediaType, endpoint endpointRestrictions) (mediaTypeOptions, bool) { - var options mediaTypeOptions +func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) { + var options MediaTypeOptions // extract all known parameters for k, v := range params { @@ -205,66 +210,65 @@ func acceptMediaTypeOptions(params map[string]string, accepts *acceptedMediaType // controls transformation of the object when returned case "as": - if options.convert == nil { - options.convert = &schema.GroupVersionKind{} + if options.Convert == nil { + options.Convert = &schema.GroupVersionKind{} } - options.convert.Kind = v + options.Convert.Kind = v case "g": - if options.convert == nil { - options.convert = &schema.GroupVersionKind{} + if options.Convert == nil { + options.Convert = &schema.GroupVersionKind{} } - options.convert.Group = v + options.Convert.Group = v case "v": - if options.convert == nil { - options.convert = &schema.GroupVersionKind{} + if options.Convert == nil { + options.Convert = &schema.GroupVersionKind{} } - options.convert.Version = v + options.Convert.Version = v // controls the streaming schema case "stream": if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) { - return mediaTypeOptions{}, false + return MediaTypeOptions{}, false } - options.stream = v + options.Stream = v // controls the version of the server API group used // for generic output case "sv": if len(v) > 0 && !endpoint.AllowsServerVersion(v) { - return mediaTypeOptions{}, false + return MediaTypeOptions{}, false } - options.useServerVersion = v + options.UseServerVersion = v // if specified, the server should transform the returned // output and remove fields that are always server specified, // or which fit the default behavior. case "export": - options.export = v == "1" + options.Export = v == "1" // if specified, the pretty serializer will be used case "pretty": - options.pretty = v == "1" + options.Pretty = v == "1" default: - options.unrecognized = append(options.unrecognized, k) + options.Unrecognized = append(options.Unrecognized, k) } } - if options.convert != nil && !endpoint.AllowsConversion(*options.convert) { - return mediaTypeOptions{}, false + if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert) { + return MediaTypeOptions{}, false } - options.accepted = accepts - + options.Accepted = accepts return options, true } -// negotiateMediaTypeOptions returns the most appropriate content type given the accept header and +// NegotiateMediaTypeOptions returns the most appropriate content type given the accept header and // a list of alternatives along with the accepted media type parameters. -func negotiateMediaTypeOptions(header string, accepted []acceptedMediaType, endpoint endpointRestrictions) (mediaTypeOptions, bool) { +func NegotiateMediaTypeOptions(header string, accepted []AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) { if len(header) == 0 && len(accepted) > 0 { - return mediaTypeOptions{ - accepted: &accepted[0], + return MediaTypeOptions{ + Accepted: &accepted[0], }, true } @@ -282,19 +286,19 @@ func negotiateMediaTypeOptions(header string, accepted []acceptedMediaType, endp } } } - return mediaTypeOptions{}, false + return MediaTypeOptions{}, false } -// acceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which +// AcceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which // allowed media types the server exposes. -func acceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []acceptedMediaType { - var acceptedMediaTypes []acceptedMediaType +func AcceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []AcceptedMediaType { + var acceptedMediaTypes []AcceptedMediaType for _, info := range ns.SupportedMediaTypes() { segments := strings.SplitN(info.MediaType, "/", 2) if len(segments) == 1 { segments = append(segments, "*") } - t := acceptedMediaType{ + t := AcceptedMediaType{ Type: segments[0], SubType: segments[1], Serializer: info, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go new file mode 100644 index 00000000000..70aff719880 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go @@ -0,0 +1,195 @@ +/* +Copyright 2017 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 ( + "fmt" + "net/http" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/request" +) + +// transformResponseObject takes an object loaded from storage and performs any necessary transformations. +// Will write the complete response object. +func transformResponseObject(ctx request.Context, scope RequestScope, req *http.Request, w http.ResponseWriter, statusCode int, result runtime.Object) { + // TODO: use returned serializer + mediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope) + if err != nil { + status := responsewriters.ErrorToAPIStatus(err) + responsewriters.WriteRawJSON(int(status.Code), status, w) + return + } + + // If conversion was allowed by the scope, perform it before writing the response + if target := mediaType.Convert; target != nil { + switch { + + case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1alpha1.SchemeGroupVersion: + if meta.IsListType(result) { + // TODO: this should be calculated earlier + err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadata, but the requested object is a list (%T)", result)) + scope.err(err, w, req) + return + } + m, err := meta.Accessor(result) + if err != nil { + scope.err(err, w, req) + return + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + scope.err(err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1alpha1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, partial) + return + + case target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1alpha1.SchemeGroupVersion: + if !meta.IsListType(result) { + // TODO: this should be calculated earlier + err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadataList, but the requested object is not a list (%T)", result)) + scope.err(err, w, req) + return + } + list := &metav1alpha1.PartialObjectMetadataList{} + err := meta.EachListItem(result, func(obj runtime.Object) error { + m, err := meta.Accessor(obj) + if err != nil { + return err + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + list.Items = append(list.Items, partial) + return nil + }) + if err != nil { + scope.err(err, w, req) + return + } + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + scope.err(err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1alpha1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, list) + return + + case target.Kind == "Table" && target.GroupVersion() == metav1alpha1.SchemeGroupVersion: + // TODO: relax the version abstraction + // TODO: skip if this is a status response (delete without body)? + + opts := &metav1alpha1.TableOptions{} + if err := metav1alpha1.ParameterCodec.DecodeParameters(req.URL.Query(), metav1alpha1.SchemeGroupVersion, opts); err != nil { + scope.err(err, w, req) + return + } + + table, err := scope.TableConvertor.ConvertToTable(ctx, result, opts) + if err != nil { + scope.err(err, w, req) + return + } + + for i := range table.Rows { + item := &table.Rows[i] + switch opts.IncludeObject { + case metav1alpha1.IncludeObject: + item.Object.Object, err = scope.Convertor.ConvertToVersion(item.Object.Object, scope.Kind.GroupVersion()) + if err != nil { + scope.err(err, w, req) + return + } + // TODO: rely on defaulting for the value here? + case metav1alpha1.IncludeMetadata, "": + m, err := meta.Accessor(item.Object.Object) + if err != nil { + scope.err(err, w, req) + return + } + // TODO: turn this into an internal type and do conversion in order to get object kind automatically set? + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + item.Object.Object = partial + case metav1alpha1.IncludeNone: + item.Object.Object = nil + default: + // TODO: move this to validation on the table options? + err = errors.NewBadRequest(fmt.Sprintf("unrecognized includeObject value: %q", opts.IncludeObject)) + scope.err(err, w, req) + } + } + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + scope.err(err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1alpha1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, table) + return + + default: + // this block should only be hit if scope AllowsConversion is incorrect + accepted, _ := negotiation.MediaTypesForSerializer(metainternalversion.Codecs) + err := negotiation.NewNotAcceptableError(accepted) + status := responsewriters.ErrorToAPIStatus(err) + responsewriters.WriteRawJSON(int(status.Code), status, w) + return + } + } + + responsewriters.WriteObject(statusCode, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) +} + +// errNotAcceptable indicates Accept negotiation has failed +type errNotAcceptable struct { + message string +} + +func newNotAcceptableError(message string) error { + return errNotAcceptable{message} +} + +func (e errNotAcceptable) Error() string { + return e.message +} + +func (e errNotAcceptable) Status() metav1.Status { + return metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotAcceptable, + Reason: metav1.StatusReason("NotAcceptable"), + Message: e.Error(), + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go index 92172ad7e19..2f67cb6f9d6 100755 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go @@ -30,8 +30,8 @@ type statusError interface { Status() metav1.Status } -// apiStatus converts an error to an metav1.Status object. -func apiStatus(err error) *metav1.Status { +// ErrorToAPIStatus converts an error to an metav1.Status object. +func ErrorToAPIStatus(err error) *metav1.Status { switch t := err.(type) { case statusError: status := t.Status() diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go index 2422e76c143..60168e24dbc 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go @@ -64,7 +64,7 @@ func TestAPIStatus(t *testing.T) { }, } for k, v := range cases { - actual := apiStatus(k) + actual := ErrorToAPIStatus(k) if !reflect.DeepEqual(actual, &v) { t.Errorf("%s: Expected %#v, Got %#v", k, v, actual) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go index 1e88557531f..32ea7bd6cd5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go @@ -41,11 +41,17 @@ import ( // be "application/octet-stream". All other objects are sent to standard JSON serialization. func WriteObject(ctx request.Context, statusCode int, gv schema.GroupVersion, s runtime.NegotiatedSerializer, object runtime.Object, w http.ResponseWriter, req *http.Request) { stream, ok := object.(rest.ResourceStreamer) - if !ok { - WriteObjectNegotiated(ctx, s, gv, w, req, statusCode, object) + if ok { + StreamObject(ctx, statusCode, gv, s, stream, w, req) return } + WriteObjectNegotiated(ctx, s, gv, w, req, statusCode, object) +} +// StreamObject performs input stream negotiation from a ResourceStreamer and writes that to the response. +// If the client requests a websocket upgrade, negotiate for a websocket reader protocol (because many +// browser clients cannot easily handle binary streaming protocols). +func StreamObject(ctx request.Context, statusCode int, gv schema.GroupVersion, s runtime.NegotiatedSerializer, stream rest.ResourceStreamer, w http.ResponseWriter, req *http.Request) { out, flush, contentType, err := stream.InputStream(gv.String(), req.Header.Get("Accept")) if err != nil { ErrorNegotiated(ctx, err, s, gv, w, req) @@ -78,12 +84,23 @@ func WriteObject(ctx request.Context, statusCode int, gv schema.GroupVersion, s io.Copy(writer, out) } +// SerializeObject renders an object in the content type negotiated by the client using the provided encoder. +// The context is optional and can be nil. +func SerializeObject(mediaType string, encoder runtime.Encoder, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) { + w.Header().Set("Content-Type", mediaType) + w.WriteHeader(statusCode) + + if err := encoder.Encode(object, w); err != nil { + errorJSONFatal(err, encoder, w) + } +} + // WriteObjectNegotiated renders an object in the content type negotiated by the client. // The context is optional and can be nil. func WriteObjectNegotiated(ctx request.Context, s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) { serializer, err := negotiation.NegotiateOutputSerializer(req, s) if err != nil { - status := apiStatus(err) + status := ErrorToAPIStatus(err) WriteRawJSON(int(status.Code), status, w) return } @@ -96,15 +113,13 @@ func WriteObjectNegotiated(ctx request.Context, s runtime.NegotiatedSerializer, w.WriteHeader(statusCode) encoder := s.EncoderForVersion(serializer.Serializer, gv) - if err := encoder.Encode(object, w); err != nil { - errorJSONFatal(err, encoder, w) - } + SerializeObject(serializer.MediaType, encoder, w, req, statusCode, object) } // ErrorNegotiated renders an error to the response. Returns the HTTP status code of the error. -// The context is options and may be nil. +// The context is optional and may be nil. func ErrorNegotiated(ctx request.Context, err error, s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request) int { - status := apiStatus(err) + status := ErrorToAPIStatus(err) code := int(status.Code) // when writing an error, check to see if the status indicates a retry after period if status.Details != nil && status.Details.RetryAfterSeconds > 0 { @@ -125,7 +140,7 @@ func ErrorNegotiated(ctx request.Context, err error, s runtime.NegotiatedSeriali // Returns the HTTP status code of the error. func errorJSONFatal(err error, codec runtime.Encoder, w http.ResponseWriter) int { utilruntime.HandleError(fmt.Errorf("apiserver was unable to write a JSON response: %v", err)) - status := apiStatus(err) + status := ErrorToAPIStatus(err) code := int(status.Code) output, err := runtime.Encode(codec, status) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 854091c8553..578d1c95ff2 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -32,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/conversion/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" @@ -65,6 +66,8 @@ type RequestScope struct { Typer runtime.ObjectTyper UnsafeConvertor runtime.ObjectConvertor + TableConvertor rest.TableConvertor + Resource schema.GroupVersionResource Kind schema.GroupVersionKind Subresource string @@ -77,6 +80,30 @@ func (scope *RequestScope) err(err error, w http.ResponseWriter, req *http.Reque responsewriters.ErrorNegotiated(ctx, err, scope.Serializer, scope.Kind.GroupVersion(), w, req) } +func (scope *RequestScope) AllowsConversion(gvk schema.GroupVersionKind) bool { + // TODO: this is temporary, replace with an abstraction calculated at endpoint installation time + if gvk.GroupVersion() == metav1alpha1.SchemeGroupVersion { + switch gvk.Kind { + case "Table": + return scope.TableConvertor != nil + case "PartialObjectMetadata", "PartialObjectMetadataList": + // TODO: should delineate between lists and non-list endpoints + return true + default: + return false + } + } + return false +} + +func (scope *RequestScope) AllowsServerVersion(version string) bool { + return version == scope.MetaGroupVersion.Version +} + +func (scope *RequestScope) AllowsStreamSchema(s string) bool { + return s == "watch" +} + // getterFunc performs a get request with the given context and object name. The request // may be used to deserialize an options object to pass to the getter. type getterFunc func(ctx request.Context, name string, req *http.Request, trace *utiltrace.Trace) (runtime.Object, error) @@ -115,7 +142,7 @@ func getResourceHandler(scope RequestScope, getter getterFunc) http.HandlerFunc } trace.Step("About to write a response") - responsewriters.WriteObject(ctx, http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) } } @@ -348,7 +375,7 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch } } - responsewriters.WriteObject(ctx, http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) trace.Step(fmt.Sprintf("Writing http response done (%d items)", numberOfItems)) } } @@ -447,7 +474,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.Object } trace.Step("Self-link added") - responsewriters.WriteObject(ctx, http.StatusCreated, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusCreated, result) } } @@ -547,9 +574,8 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface return } - responsewriters.WriteObject(ctx, http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) } - } type updateAdmissionFunc func(updatedObject runtime.Object, currentObject runtime.Object) error @@ -877,7 +903,8 @@ func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectType if wasCreated { status = http.StatusCreated } - responsewriters.WriteObject(ctx, status, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + + transformResponseObject(ctx, scope, req, w, status, result) } } @@ -996,7 +1023,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco } } - responsewriters.WriteObject(ctx, status, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, status, result) } } @@ -1102,7 +1129,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco } } - responsewriters.WriteObjectNegotiated(ctx, scope.Serializer, scope.Kind.GroupVersion(), w, req, http.StatusOK, result) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index f7344bdb91a..b36ef9fd4c8 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -26,6 +26,8 @@ import ( "time" "unicode" + restful "github.com/emicklei/go-restful" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" @@ -38,8 +40,6 @@ import ( "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" - - "github.com/emicklei/go-restful" ) const ( @@ -374,6 +374,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag shortNames = shortNamesProvider.ShortNames() } + tableProvider, ok := storage.(rest.TableConvertor) + if !ok { + tableProvider = rest.DefaultTableConvertor + } + var apiResource metav1.APIResource // Get the list of actions for the given scope. switch scope.Name() { @@ -525,6 +530,9 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Typer: a.group.Typer, UnsafeConvertor: a.group.UnsafeConvertor, + // TODO: Check for the interface on storage + TableConvertor: tableProvider, + // TODO: This seems wrong for cross-group subresources. It makes an assumption that a subresource and its parent are in the same group version. Revisit this. Resource: a.group.GroupVersion.WithResource(resource), Subresource: subresource, diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go index cc6613f9d6c..262e05697fa 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go @@ -23,6 +23,7 @@ import ( metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" @@ -116,6 +117,10 @@ type GetterWithOptions interface { NewGetOptions() (runtime.Object, bool, string) } +type TableConvertor interface { + ConvertToTableList(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.TableList, error) +} + // Deleter is an object that can delete a named RESTful resource. type Deleter interface { // Delete finds a resource in the storage and deletes it. diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go new file mode 100644 index 00000000000..4b48711d37f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go @@ -0,0 +1,106 @@ +/* +Copyright 2014 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 ( + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" +) + +var DefaultTableConvertor TableConvertor = defaultTableConvertor{} + +type defaultTableConvertor struct{} + +var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc() + +func (defaultTableConvertor) ConvertToTableList(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.TableList, error) { + var table metav1alpha1.TableList + fn := func(obj runtime.Object) error { + m, err := meta.Accessor(obj) + if err != nil { + // TODO: skip objects we don't recognize + return nil + } + table.Items = append(table.Items, metav1alpha1.TableListItem{ + Cells: []interface{}{m.GetClusterName(), m.GetNamespace(), m.GetName(), m.GetCreationTimestamp().Time.UTC().Format(time.RFC3339)}, + Object: runtime.RawExtension{Object: obj}, + }) + return nil + } + switch { + case meta.IsListType(object): + if err := meta.EachListItem(object, fn); err != nil { + return nil, err + } + default: + if err := fn(object); err != nil { + return nil, err + } + } + table.Headers = []metav1alpha1.TableListHeader{ + {Name: "Cluster Name", Type: "string", Description: swaggerMetadataDescriptions["clusterName"]}, + {Name: "Namespace", Type: "string", Description: swaggerMetadataDescriptions["namespace"]}, + {Name: "Name", Type: "string", Description: swaggerMetadataDescriptions["name"]}, + {Name: "Created At", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"]}, + } + // trim the left two columns if completely empty + if trimColumn(0, &table) { + trimColumn(0, &table) + } else { + trimColumn(1, &table) + } + return &table, nil +} + +func trimColumn(column int, table *metav1alpha1.TableList) bool { + for _, item := range table.Items { + switch t := item.Cells[column].(type) { + case string: + if len(t) > 0 { + return false + } + case interface{}: + if t == nil { + return false + } + } + } + if column == 0 { + table.Headers = table.Headers[1:] + } else { + for j := column; j < len(table.Headers); j++ { + table.Headers[j] = table.Headers[j+1] + } + } + for i := range table.Items { + cells := table.Items[i].Cells + if column == 0 { + table.Items[i].Cells = cells[1:] + continue + } + for j := column; j < len(cells); j++ { + cells[j] = cells[j+1] + } + table.Items[i].Cells = cells[:len(cells)-1] + } + return true +}