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 +}