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.
parent 3f41f8fc
...@@ -43,6 +43,7 @@ go_library( ...@@ -43,6 +43,7 @@ go_library(
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion: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",
......
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
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" "k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
...@@ -89,6 +90,35 @@ func Accessor(obj interface{}) (metav1.Object, error) { ...@@ -89,6 +90,35 @@ func Accessor(obj interface{}) (metav1.Object, error) {
} }
} }
// AsPartialObjectMetadata takes the metav1 interface and returns a partial object.
// TODO: consider making this solely a conversion action.
func AsPartialObjectMetadata(m metav1.Object) *metav1alpha1.PartialObjectMetadata {
switch t := m.(type) {
case *metav1.ObjectMeta:
return &metav1alpha1.PartialObjectMetadata{ObjectMeta: *t}
default:
return &metav1alpha1.PartialObjectMetadata{
ObjectMeta: metav1.ObjectMeta{
Name: m.GetName(),
GenerateName: m.GetGenerateName(),
Namespace: m.GetNamespace(),
SelfLink: m.GetSelfLink(),
UID: m.GetUID(),
ResourceVersion: m.GetResourceVersion(),
Generation: m.GetGeneration(),
CreationTimestamp: m.GetCreationTimestamp(),
DeletionTimestamp: m.GetDeletionTimestamp(),
DeletionGracePeriodSeconds: m.GetDeletionGracePeriodSeconds(),
Labels: m.GetLabels(),
Annotations: m.GetAnnotations(),
OwnerReferences: m.GetOwnerReferences(),
Finalizers: m.GetFinalizers(),
ClusterName: m.GetClusterName(),
},
}
}
}
// TypeAccessor returns an interface that allows retrieving and modifying the APIVersion // TypeAccessor returns an interface that allows retrieving and modifying the APIVersion
// and Kind of an in-memory internal object. // and Kind of an in-memory internal object.
// TODO: this interface is used to test code that does not have ObjectMeta or ListMeta // TODO: this interface is used to test code that does not have ObjectMeta or ListMeta
......
...@@ -28,12 +28,15 @@ go_test( ...@@ -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",
......
...@@ -38,6 +38,7 @@ go_library( ...@@ -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( ...@@ -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",
......
...@@ -29,6 +29,10 @@ type errNotAcceptable struct { ...@@ -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 { ...@@ -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, ", "))
} }
......
/*
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 { ...@@ -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()
......
...@@ -64,7 +64,7 @@ func TestAPIStatus(t *testing.T) { ...@@ -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)
} }
......
...@@ -41,11 +41,17 @@ import ( ...@@ -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 ...@@ -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, ...@@ -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 ...@@ -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 {
......
...@@ -32,6 +32,7 @@ import ( ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ( ...@@ -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 ( ...@@ -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 ...@@ -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 ...@@ -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,
......
...@@ -23,6 +23,7 @@ import ( ...@@ -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 { ...@@ -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.
......
/*
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
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment