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:
parent
42d367c84c
commit
e1228ec319
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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, ", "))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue