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/api/testing:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion: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: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/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels: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:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema: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:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/streaming: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/diff:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets: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" apitesting "k8s.io/apimachinery/pkg/api/testing"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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/fields"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
@ -754,6 +757,15 @@ func (storage *SimpleTypedStorage) checkContext(ctx request.Context) {
storage.actualNamespace, storage.namespacePresent = request.NamespaceFrom(ctx) 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) { func extractBody(response *http.Response, object runtime.Object) (string, error) {
return extractBodyDecoder(response, object, codec) 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) 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) { func TestNotFound(t *testing.T) {
type T struct { type T struct {
Method string 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) { func TestGetBinary(t *testing.T) {
simpleStorage := SimpleRESTStorage{ simpleStorage := SimpleRESTStorage{
stream: &SimpleStream{ stream: &SimpleStream{
@ -2952,12 +3190,13 @@ func TestUpdateChecksDecode(t *testing.T) {
} }
type setTestSelfLinker struct { type setTestSelfLinker struct {
t *testing.T t *testing.T
expectedSet string expectedSet string
name string alternativeSet sets.String
namespace string name string
called bool namespace string
err error called bool
err error
} }
func (s *setTestSelfLinker) Namespace(runtime.Object) (string, error) { return s.namespace, s.err } 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) SelfLink(runtime.Object) (string, error) { return "", s.err }
func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error { func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error {
if e, a := s.expectedSet, selfLink; e != a { 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 s.called = true
return s.err return s.err

View File

@ -38,6 +38,7 @@ go_library(
"namer.go", "namer.go",
"patch.go", "patch.go",
"proxy.go", "proxy.go",
"response.go",
"rest.go", "rest.go",
"watch.go", "watch.go",
], ],
@ -50,6 +51,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_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/internalversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1: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/conversion/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",

View File

@ -29,6 +29,10 @@ type errNotAcceptable struct {
accepted []string accepted []string
} }
func NewNotAcceptableError(accepted []string) error {
return errNotAcceptable{accepted}
}
func (e errNotAcceptable) Error() string { func (e errNotAcceptable) Error() string {
return fmt.Sprintf("only the following media types are accepted: %v", strings.Join(e.accepted, ", ")) return fmt.Sprintf("only the following media types are accepted: %v", strings.Join(e.accepted, ", "))
} }
@ -47,6 +51,10 @@ type errUnsupportedMediaType struct {
accepted []string accepted []string
} }
func NewUnsupportedMediaTypeError(accepted []string) error {
return errUnsupportedMediaType{accepted}
}
func (e errUnsupportedMediaType) Error() string { 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, ", ")) 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 return mediaTypes, streamMediaTypes
} }
func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (MediaTypeOptions, runtime.SerializerInfo, error) {
mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions) mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), restrictions)
if !ok { if !ok {
supported, _ := MediaTypesForSerializer(ns) supported, _ := MediaTypesForSerializer(ns)
return runtime.SerializerInfo{}, errNotAcceptable{supported} return mediaType, runtime.SerializerInfo{}, NewNotAcceptableError(supported)
} }
// TODO: move into resthandler // TODO: move into resthandler
info := mediaType.accepted.Serializer info := mediaType.Accepted.Serializer
if (mediaType.pretty || isPrettyPrint(req)) && info.PrettySerializer != nil { if (mediaType.Pretty || isPrettyPrint(req)) && info.PrettySerializer != nil {
info.Serializer = info.PrettySerializer 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) { func NegotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) {
mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions) mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), DefaultEndpointRestrictions)
if !ok || mediaType.accepted.Serializer.StreamSerializer == nil { if !ok || mediaType.Accepted.Serializer.StreamSerializer == nil {
_, supported := MediaTypesForSerializer(ns) _, 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) { 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) mediaType, _, err := mime.ParseMediaType(mediaType)
if err != nil { if err != nil {
_, supported := MediaTypesForSerializer(ns) _, supported := MediaTypesForSerializer(ns)
return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported)
} }
for _, info := range mediaTypes { for _, info := range mediaTypes {
@ -83,7 +88,7 @@ func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer
} }
_, supported := MediaTypesForSerializer(ns) _, 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 // 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 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 // to verify server support for specific options
type endpointRestrictions interface { type EndpointRestrictions interface {
// AllowsConversion should return true if the specified group version kind // AllowsConversion should return true if the specified group version kind
// is an allowed target object. // is an allowed target object.
AllowsConversion(schema.GroupVersionKind) bool AllowsConversion(schema.GroupVersionKind) bool
@ -145,7 +150,7 @@ type endpointRestrictions interface {
AllowsStreamSchema(schema string) bool AllowsStreamSchema(schema string) bool
} }
var defaultEndpointRestrictions = emptyEndpointRestrictions{} var DefaultEndpointRestrictions = emptyEndpointRestrictions{}
type emptyEndpointRestrictions struct{} type emptyEndpointRestrictions struct{}
@ -153,9 +158,9 @@ func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool
func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false } func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false }
func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" } 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. // server can serialize.
type acceptedMediaType struct { type AcceptedMediaType struct {
// Type is the first part of the media type ("application") // Type is the first part of the media type ("application")
Type string Type string
// SubType is the second part of the media type ("json") // SubType is the second part of the media type ("json")
@ -164,40 +169,40 @@ type acceptedMediaType struct {
Serializer runtime.SerializerInfo 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 // the server response
type mediaTypeOptions struct { type MediaTypeOptions struct {
// pretty is true if the requested representation should be formatted for human // pretty is true if the requested representation should be formatted for human
// viewing // viewing
pretty bool Pretty bool
// stream, if set, indicates that a streaming protocol variant of this encoding // stream, if set, indicates that a streaming protocol variant of this encoding
// is desired. The only currently supported value is watch which returns versioned // is desired. The only currently supported value is watch which returns versioned
// events. In the future, this may refer to other stream protocols. // 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 // convert is a request to alter the type of object returned by the server from the
// normal response // normal response
convert *schema.GroupVersionKind Convert *schema.GroupVersionKind
// useServerVersion is an optional version for the server group // 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 // export is true if the representation requested should exclude fields the server
// has set // has set
export bool Export bool
// unrecognized is a list of all unrecognized keys // unrecognized is a list of all unrecognized keys
unrecognized []string Unrecognized []string
// the accepted media type from the client // the accepted media type from the client
accepted *acceptedMediaType Accepted *AcceptedMediaType
} }
// acceptMediaTypeOptions returns an options object that matches the provided media type params. If // 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 // 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. // parameters are unversioned and may not be changed.
func acceptMediaTypeOptions(params map[string]string, accepts *acceptedMediaType, endpoint endpointRestrictions) (mediaTypeOptions, bool) { func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
var options mediaTypeOptions var options MediaTypeOptions
// extract all known parameters // extract all known parameters
for k, v := range params { for k, v := range params {
@ -205,66 +210,65 @@ func acceptMediaTypeOptions(params map[string]string, accepts *acceptedMediaType
// controls transformation of the object when returned // controls transformation of the object when returned
case "as": case "as":
if options.convert == nil { if options.Convert == nil {
options.convert = &schema.GroupVersionKind{} options.Convert = &schema.GroupVersionKind{}
} }
options.convert.Kind = v options.Convert.Kind = v
case "g": case "g":
if options.convert == nil { if options.Convert == nil {
options.convert = &schema.GroupVersionKind{} options.Convert = &schema.GroupVersionKind{}
} }
options.convert.Group = v options.Convert.Group = v
case "v": case "v":
if options.convert == nil { if options.Convert == nil {
options.convert = &schema.GroupVersionKind{} options.Convert = &schema.GroupVersionKind{}
} }
options.convert.Version = v options.Convert.Version = v
// controls the streaming schema // controls the streaming schema
case "stream": case "stream":
if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) { 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 // controls the version of the server API group used
// for generic output // for generic output
case "sv": case "sv":
if len(v) > 0 && !endpoint.AllowsServerVersion(v) { 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 // if specified, the server should transform the returned
// output and remove fields that are always server specified, // output and remove fields that are always server specified,
// or which fit the default behavior. // or which fit the default behavior.
case "export": case "export":
options.export = v == "1" options.Export = v == "1"
// if specified, the pretty serializer will be used // if specified, the pretty serializer will be used
case "pretty": case "pretty":
options.pretty = v == "1" options.Pretty = v == "1"
default: default:
options.unrecognized = append(options.unrecognized, k) options.Unrecognized = append(options.Unrecognized, k)
} }
} }
if options.convert != nil && !endpoint.AllowsConversion(*options.convert) { if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert) {
return mediaTypeOptions{}, false return MediaTypeOptions{}, false
} }
options.accepted = accepts options.Accepted = accepts
return options, true 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. // 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 { if len(header) == 0 && len(accepted) > 0 {
return mediaTypeOptions{ return MediaTypeOptions{
accepted: &accepted[0], Accepted: &accepted[0],
}, true }, 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. // allowed media types the server exposes.
func acceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []acceptedMediaType { func AcceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []AcceptedMediaType {
var acceptedMediaTypes []acceptedMediaType var acceptedMediaTypes []AcceptedMediaType
for _, info := range ns.SupportedMediaTypes() { for _, info := range ns.SupportedMediaTypes() {
segments := strings.SplitN(info.MediaType, "/", 2) segments := strings.SplitN(info.MediaType, "/", 2)
if len(segments) == 1 { if len(segments) == 1 {
segments = append(segments, "*") segments = append(segments, "*")
} }
t := acceptedMediaType{ t := AcceptedMediaType{
Type: segments[0], Type: segments[0],
SubType: segments[1], SubType: segments[1],
Serializer: info, 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 Status() metav1.Status
} }
// apiStatus converts an error to an metav1.Status object. // ErrorToAPIStatus converts an error to an metav1.Status object.
func apiStatus(err error) *metav1.Status { func ErrorToAPIStatus(err error) *metav1.Status {
switch t := err.(type) { switch t := err.(type) {
case statusError: case statusError:
status := t.Status() status := t.Status()

View File

@ -64,7 +64,7 @@ func TestAPIStatus(t *testing.T) {
}, },
} }
for k, v := range cases { for k, v := range cases {
actual := apiStatus(k) actual := ErrorToAPIStatus(k)
if !reflect.DeepEqual(actual, &v) { if !reflect.DeepEqual(actual, &v) {
t.Errorf("%s: Expected %#v, Got %#v", k, v, actual) 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. // 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) { 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) stream, ok := object.(rest.ResourceStreamer)
if !ok { if ok {
WriteObjectNegotiated(ctx, s, gv, w, req, statusCode, object) StreamObject(ctx, statusCode, gv, s, stream, w, req)
return 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")) out, flush, contentType, err := stream.InputStream(gv.String(), req.Header.Get("Accept"))
if err != nil { if err != nil {
ErrorNegotiated(ctx, err, s, gv, w, req) 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) 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. // WriteObjectNegotiated renders an object in the content type negotiated by the client.
// The context is optional and can be nil. // 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) { 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) serializer, err := negotiation.NegotiateOutputSerializer(req, s)
if err != nil { if err != nil {
status := apiStatus(err) status := ErrorToAPIStatus(err)
WriteRawJSON(int(status.Code), status, w) WriteRawJSON(int(status.Code), status, w)
return return
} }
@ -96,15 +113,13 @@ func WriteObjectNegotiated(ctx request.Context, s runtime.NegotiatedSerializer,
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
encoder := s.EncoderForVersion(serializer.Serializer, gv) encoder := s.EncoderForVersion(serializer.Serializer, gv)
if err := encoder.Encode(object, w); err != nil { SerializeObject(serializer.MediaType, encoder, w, req, statusCode, object)
errorJSONFatal(err, encoder, w)
}
} }
// ErrorNegotiated renders an error to the response. Returns the HTTP status code of the error. // 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 { 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) code := int(status.Code)
// when writing an error, check to see if the status indicates a retry after period // when writing an error, check to see if the status indicates a retry after period
if status.Details != nil && status.Details.RetryAfterSeconds > 0 { 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. // Returns the HTTP status code of the error.
func errorJSONFatal(err error, codec runtime.Encoder, w http.ResponseWriter) int { 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)) utilruntime.HandleError(fmt.Errorf("apiserver was unable to write a JSON response: %v", err))
status := apiStatus(err) status := ErrorToAPIStatus(err)
code := int(status.Code) code := int(status.Code)
output, err := runtime.Encode(codec, status) output, err := runtime.Encode(codec, status)
if err != nil { if err != nil {

View File

@ -32,6 +32,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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/conversion/unstructured"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -65,6 +66,8 @@ type RequestScope struct {
Typer runtime.ObjectTyper Typer runtime.ObjectTyper
UnsafeConvertor runtime.ObjectConvertor UnsafeConvertor runtime.ObjectConvertor
TableConvertor rest.TableConvertor
Resource schema.GroupVersionResource Resource schema.GroupVersionResource
Kind schema.GroupVersionKind Kind schema.GroupVersionKind
Subresource string 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) 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 // 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. // 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) 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") 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)) 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") 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 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 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 { if wasCreated {
status = http.StatusCreated 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" "time"
"unicode" "unicode"
restful "github.com/emicklei/go-restful"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/conversion"
@ -38,8 +40,6 @@ import (
"k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"github.com/emicklei/go-restful"
) )
const ( const (
@ -374,6 +374,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
shortNames = shortNamesProvider.ShortNames() shortNames = shortNamesProvider.ShortNames()
} }
tableProvider, ok := storage.(rest.TableConvertor)
if !ok {
tableProvider = rest.DefaultTableConvertor
}
var apiResource metav1.APIResource var apiResource metav1.APIResource
// Get the list of actions for the given scope. // Get the list of actions for the given scope.
switch scope.Name() { switch scope.Name() {
@ -525,6 +530,9 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Typer: a.group.Typer, Typer: a.group.Typer,
UnsafeConvertor: a.group.UnsafeConvertor, 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. // 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), Resource: a.group.GroupVersion.WithResource(resource),
Subresource: subresource, Subresource: subresource,

View File

@ -23,6 +23,7 @@ import (
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
@ -116,6 +117,10 @@ type GetterWithOptions interface {
NewGetOptions() (runtime.Object, bool, string) 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. // Deleter is an object that can delete a named RESTful resource.
type Deleter interface { type Deleter interface {
// Delete finds a resource in the storage and deletes it. // 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
}