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.

Kubernetes-commit: f203e42cb98ed4bac7ad8ebbed717d3bd42f55b6
This commit is contained in:
Clayton Coleman 2017-05-29 15:08:17 -04:00 committed by Kubernetes Publisher
parent 42d367c84c
commit e1228ec319
13 changed files with 700 additions and 86 deletions

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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, ", "))
}

View File

@ -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,

View File

@ -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(),
}
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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.

106
pkg/registry/rest/table.go Normal file
View File

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