Commit 125ef6fb authored by Clayton Coleman's avatar Clayton Coleman

Support content-type negotiation in the API server

A NegotiatedSerializer is passed into the API installer (and ParameterCodec, which abstracts conversion of query params) that can be used to negotiate client/server request/response serialization. All error paths are now negotiation aware, and are at least minimally version aware. Watch is specially coded to only allow application/json - a follow up change will convert it to use negotiation. Ensure the swagger scheme will include supported serializations - this now includes application/yaml as a negotiated option.
parent 6b2f70d5
...@@ -34,7 +34,6 @@ import ( ...@@ -34,7 +34,6 @@ import (
"k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/pkg/admission" "k8s.io/kubernetes/pkg/admission"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
apiutil "k8s.io/kubernetes/pkg/api/util" apiutil "k8s.io/kubernetes/pkg/api/util"
"k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apimachinery/registered"
......
...@@ -65,7 +65,13 @@ var errEmptyName = errors.NewBadRequest("name must be provided") ...@@ -65,7 +65,13 @@ var errEmptyName = errors.NewBadRequest("name must be provided")
func (a *APIInstaller) Install(ws *restful.WebService) (apiResources []unversioned.APIResource, errors []error) { func (a *APIInstaller) Install(ws *restful.WebService) (apiResources []unversioned.APIResource, errors []error) {
errors = make([]error, 0) errors = make([]error, 0)
proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info}) proxyHandler := (&ProxyHandler{
prefix: a.prefix + "/proxy/",
storage: a.group.Storage,
serializer: a.group.Serializer,
context: a.group.Context,
requestInfoResolver: a.info,
})
// Register the paths in a deterministic (sorted) order to get a deterministic swagger spec. // Register the paths in a deterministic (sorted) order to get a deterministic swagger spec.
paths := make([]string, len(a.group.Storage)) paths := make([]string, len(a.group.Storage))
...@@ -93,9 +99,11 @@ func (a *APIInstaller) NewWebService() *restful.WebService { ...@@ -93,9 +99,11 @@ func (a *APIInstaller) NewWebService() *restful.WebService {
ws.Path(a.prefix) ws.Path(a.prefix)
// a.prefix contains "prefix/group/version" // a.prefix contains "prefix/group/version"
ws.Doc("API at " + a.prefix) ws.Doc("API at " + a.prefix)
// TODO: change to restful.MIME_JSON when we set content type in client // Backwards compatibilty, we accepted objects with empty content-type at V1.
// If we stop using go-restful, we can default empty content-type to application/json on an
// endpoint by endpoint basis
ws.Consumes("*/*") ws.Consumes("*/*")
ws.Produces(restful.MIME_JSON) ws.Produces(a.group.Serializer.SupportedMediaTypes()...)
ws.ApiVersion(a.group.GroupVersion.String()) ws.ApiVersion(a.group.GroupVersion.String())
return ws return ws
...@@ -262,19 +270,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -262,19 +270,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
getOptions runtime.Object getOptions runtime.Object
versionedGetOptions runtime.Object versionedGetOptions runtime.Object
getOptionsInternalKind unversioned.GroupVersionKind getOptionsInternalKind unversioned.GroupVersionKind
getOptionsExternalKind unversioned.GroupVersionKind
getSubpath bool getSubpath bool
getSubpathKey string
) )
if isGetterWithOptions { if isGetterWithOptions {
getOptions, getSubpath, getSubpathKey = getterWithOptions.NewGetOptions() getOptions, getSubpath, _ = getterWithOptions.NewGetOptions()
getOptionsInternalKind, err = a.group.Typer.ObjectKind(getOptions) getOptionsInternalKind, err = a.group.Typer.ObjectKind(getOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO this should be a list of all the different external versions we can coerce into the internalKind
getOptionsExternalKind = optionsExternalVersion.WithKind(getOptionsInternalKind.Kind)
versionedGetOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(getOptionsInternalKind.Kind)) versionedGetOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(getOptionsInternalKind.Kind))
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -286,19 +289,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -286,19 +289,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
connectOptions runtime.Object connectOptions runtime.Object
versionedConnectOptions runtime.Object versionedConnectOptions runtime.Object
connectOptionsInternalKind unversioned.GroupVersionKind connectOptionsInternalKind unversioned.GroupVersionKind
connectOptionsExternalKind unversioned.GroupVersionKind
connectSubpath bool connectSubpath bool
connectSubpathKey string
) )
if isConnecter { if isConnecter {
connectOptions, connectSubpath, connectSubpathKey = connecter.NewConnectOptions() connectOptions, connectSubpath, _ = connecter.NewConnectOptions()
if connectOptions != nil { if connectOptions != nil {
connectOptionsInternalKind, err = a.group.Typer.ObjectKind(connectOptions) connectOptionsInternalKind, err = a.group.Typer.ObjectKind(connectOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO this should be a list of all the different external versions we can coerce into the internalKind
connectOptionsExternalKind = optionsExternalVersion.WithKind(connectOptionsInternalKind.Kind)
versionedConnectOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(connectOptionsInternalKind.Kind)) versionedConnectOptions, err = a.group.Creater.New(optionsExternalVersion.WithKind(connectOptionsInternalKind.Kind))
} }
...@@ -434,10 +433,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -434,10 +433,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
// test/integration/auth_test.go is currently the most comprehensive status code test // test/integration/auth_test.go is currently the most comprehensive status code test
reqScope := RequestScope{ reqScope := RequestScope{
ContextFunc: ctxFn, ContextFunc: ctxFn,
Creater: a.group.Creater, Serializer: a.group.Serializer,
Convertor: a.group.Convertor, ParameterCodec: a.group.ParameterCodec,
Codec: mapping.Codec, Creater: a.group.Creater,
Convertor: a.group.Convertor,
Resource: a.group.GroupVersion.WithResource(resource), Resource: a.group.GroupVersion.WithResource(resource),
Subresource: subresource, Subresource: subresource,
...@@ -454,7 +454,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -454,7 +454,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
case "GET": // Get a resource. case "GET": // Get a resource.
var handler restful.RouteFunction var handler restful.RouteFunction
if isGetterWithOptions { if isGetterWithOptions {
handler = GetResourceWithOptions(getterWithOptions, exporter, reqScope, getOptionsInternalKind, getOptionsExternalKind, getSubpath, getSubpathKey) handler = GetResourceWithOptions(getterWithOptions, reqScope)
} else { } else {
handler = GetResource(getter, exporter, reqScope) handler = GetResource(getter, exporter, reqScope)
} }
...@@ -467,7 +467,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -467,7 +467,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Doc(doc). Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("read"+namespaced+kind+strings.Title(subresource)). Operation("read"+namespaced+kind+strings.Title(subresource)).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...).
Returns(http.StatusOK, "OK", versionedObject). Returns(http.StatusOK, "OK", versionedObject).
Writes(versionedObject) Writes(versionedObject)
if isGetterWithOptions { if isGetterWithOptions {
...@@ -492,7 +492,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -492,7 +492,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Doc(doc). Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("list"+namespaced+kind+strings.Title(subresource)). Operation("list"+namespaced+kind+strings.Title(subresource)).
Produces("application/json"). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...).
Returns(http.StatusOK, "OK", versionedList). Returns(http.StatusOK, "OK", versionedList).
Writes(versionedList) Writes(versionedList)
if err := addObjectParams(ws, route, versionedListOptions); err != nil { if err := addObjectParams(ws, route, versionedListOptions); err != nil {
...@@ -524,7 +524,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -524,7 +524,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Doc(doc). Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("replace"+namespaced+kind+strings.Title(subresource)). Operation("replace"+namespaced+kind+strings.Title(subresource)).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...).
Returns(http.StatusOK, "OK", versionedObject). Returns(http.StatusOK, "OK", versionedObject).
Reads(versionedObject). Reads(versionedObject).
Writes(versionedObject) Writes(versionedObject)
...@@ -541,7 +541,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -541,7 +541,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Consumes(string(api.JSONPatchType), string(api.MergePatchType), string(api.StrategicMergePatchType)). Consumes(string(api.JSONPatchType), string(api.MergePatchType), string(api.StrategicMergePatchType)).
Operation("patch"+namespaced+kind+strings.Title(subresource)). Operation("patch"+namespaced+kind+strings.Title(subresource)).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...).
Returns(http.StatusOK, "OK", versionedObject). Returns(http.StatusOK, "OK", versionedObject).
Reads(unversioned.Patch{}). Reads(unversioned.Patch{}).
Writes(versionedObject) Writes(versionedObject)
...@@ -563,7 +563,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -563,7 +563,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Doc(doc). Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("create"+namespaced+kind+strings.Title(subresource)). Operation("create"+namespaced+kind+strings.Title(subresource)).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...).
Returns(http.StatusOK, "OK", versionedObject). Returns(http.StatusOK, "OK", versionedObject).
Reads(versionedObject). Reads(versionedObject).
Writes(versionedObject) Writes(versionedObject)
...@@ -579,7 +579,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -579,7 +579,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Doc(doc). Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("delete"+namespaced+kind+strings.Title(subresource)). Operation("delete"+namespaced+kind+strings.Title(subresource)).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...).
Writes(versionedStatus). Writes(versionedStatus).
Returns(http.StatusOK, "OK", versionedStatus) Returns(http.StatusOK, "OK", versionedStatus)
if isGracefulDeleter { if isGracefulDeleter {
...@@ -597,7 +597,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -597,7 +597,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Doc(doc). Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("deletecollection"+namespaced+kind+strings.Title(subresource)). Operation("deletecollection"+namespaced+kind+strings.Title(subresource)).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), "application/json")...). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...).
Writes(versionedStatus). Writes(versionedStatus).
Returns(http.StatusOK, "OK", versionedStatus) Returns(http.StatusOK, "OK", versionedStatus)
if err := addObjectParams(ws, route, versionedListOptions); err != nil { if err := addObjectParams(ws, route, versionedListOptions); err != nil {
...@@ -658,7 +658,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -658,7 +658,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
doc = "connect " + method + " requests to " + subresource + " of " + kind doc = "connect " + method + " requests to " + subresource + " of " + kind
} }
route := ws.Method(method).Path(action.Path). route := ws.Method(method).Path(action.Path).
To(ConnectResource(connecter, reqScope, admit, connectOptionsInternalKind, connectOptionsExternalKind, path, connectSubpath, connectSubpathKey)). To(ConnectResource(connecter, reqScope, admit, path)).
Filter(m). Filter(m).
Doc(doc). Doc(doc).
Operation("connect" + strings.Title(strings.ToLower(method)) + namespaced + kind + strings.Title(subresource)). Operation("connect" + strings.Title(strings.ToLower(method)) + namespaced + kind + strings.Title(subresource)).
......
...@@ -19,6 +19,7 @@ package apiserver ...@@ -19,6 +19,7 @@ package apiserver
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/storage" "k8s.io/kubernetes/pkg/storage"
...@@ -105,3 +106,41 @@ func IsAPIPrefixNotFound(err error) bool { ...@@ -105,3 +106,41 @@ func IsAPIPrefixNotFound(err error) bool {
_, ok := err.(*errAPIPrefixNotFound) _, ok := err.(*errAPIPrefixNotFound)
return ok return ok
} }
// errNotAcceptable indicates Accept negotiation has failed
// TODO: move to api/errors if other code needs to return this
type errNotAcceptable struct {
accepted []string
}
func (e errNotAcceptable) Error() string {
return fmt.Sprintf("only the following media types are accepted: %v", strings.Join(e.accepted, ", "))
}
func (e errNotAcceptable) Status() unversioned.Status {
return unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusNotAcceptable,
Reason: unversioned.StatusReason("NotAcceptable"),
Message: e.Error(),
}
}
// errNotAcceptable indicates Content-Type is not recognized
// TODO: move to api/errors if other code needs to return this
type errUnsupportedMediaType struct {
accepted []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, ", "))
}
func (e errUnsupportedMediaType) Status() unversioned.Status {
return unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusUnsupportedMediaType,
Reason: unversioned.StatusReason("UnsupportedMediaType"),
Message: e.Error(),
}
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 apiserver
import (
"mime"
"net/http"
"strconv"
"strings"
"bitbucket.org/ww/goautoneg"
"k8s.io/kubernetes/pkg/runtime"
)
func negotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.Serializer, string, error) {
acceptHeader := req.Header.Get("Accept")
supported := ns.SupportedMediaTypes()
if len(acceptHeader) == 0 && len(supported) > 0 {
acceptHeader = supported[0]
}
accept, ok := negotiate(acceptHeader, supported)
if !ok {
return nil, "", errNotAcceptable{supported}
}
pretty := isPrettyPrint(req)
if _, ok := accept.Params["pretty"]; !ok && pretty {
accept.Params["pretty"] = "1"
}
mediaType := accept.Type
if len(accept.SubType) > 0 {
mediaType += "/" + accept.SubType
}
if s, ok := ns.SerializerForMediaType(mediaType, accept.Params); ok {
return s, mediaType, nil
}
return nil, "", errNotAcceptable{supported}
}
func negotiateInputSerializer(req *http.Request, s runtime.NegotiatedSerializer) (runtime.Serializer, error) {
supported := s.SupportedMediaTypes()
mediaType := req.Header.Get("Content-Type")
if len(mediaType) == 0 {
mediaType = supported[0]
}
mediaType, options, err := mime.ParseMediaType(mediaType)
if err != nil {
return nil, errUnsupportedMediaType{supported}
}
out, ok := s.SerializerForMediaType(mediaType, options)
if !ok {
return nil, errUnsupportedMediaType{supported}
}
return out, nil
}
// isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent
// matches known "human" clients.
func isPrettyPrint(req *http.Request) bool {
// DEPRECATED: should be part of the content type
if req.URL != nil {
pp := req.URL.Query().Get("pretty")
if len(pp) > 0 {
pretty, _ := strconv.ParseBool(pp)
return pretty
}
}
userAgent := req.UserAgent()
// This covers basic all browers and cli http tools
if strings.HasPrefix(userAgent, "curl") || strings.HasPrefix(userAgent, "Wget") || strings.HasPrefix(userAgent, "Mozilla/5.0") {
return true
}
return false
}
// negotiate the most appropriate content type given the accept header and a list of
// alternatives.
func negotiate(header string, alternatives []string) (goautoneg.Accept, bool) {
alternates := make([][]string, 0, len(alternatives))
for _, alternate := range alternatives {
alternates = append(alternates, strings.SplitN(alternate, "/", 2))
}
for _, clause := range goautoneg.ParseAccept(header) {
for _, alternate := range alternates {
if clause.Type == alternate[0] && clause.SubType == alternate[1] {
return clause, true
}
if clause.Type == alternate[0] && clause.SubType == "*" {
clause.SubType = alternate[1]
return clause, true
}
if clause.Type == "*" && clause.SubType == "*" {
clause.Type = alternate[0]
clause.SubType = alternate[1]
return clause, true
}
}
}
return goautoneg.Accept{}, false
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 apiserver
import (
"net/http"
"net/url"
"reflect"
"testing"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/runtime"
)
type fakeNegotiater struct {
serializer runtime.Serializer
types []string
mediaType string
options map[string]string
}
func (n *fakeNegotiater) SupportedMediaTypes() []string {
return n.types
}
func (n *fakeNegotiater) SerializerForMediaType(mediaType string, options map[string]string) (runtime.Serializer, bool) {
n.mediaType = mediaType
if len(options) > 0 {
n.options = options
}
return n.serializer, n.serializer != nil
}
func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Serializer, gv unversioned.GroupVersion) runtime.Encoder {
return n.serializer
}
func (n *fakeNegotiater) DecoderToVersion(serializer runtime.Serializer, gv unversioned.GroupVersion) runtime.Decoder {
return n.serializer
}
var fakeCodec = runtime.NewCodec(runtime.NoopEncoder{}, runtime.NoopDecoder{})
func TestNegotiate(t *testing.T) {
testCases := []struct {
accept string
req *http.Request
ns *fakeNegotiater
serializer runtime.Serializer
contentType string
params map[string]string
errFn func(error) bool
}{
// pick a default
{
req: &http.Request{},
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
},
{
accept: "",
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
},
{
accept: "*/*",
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
},
{
accept: "application/*",
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
},
{
accept: "application/json",
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
},
{
accept: "application/json",
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json", "application/protobuf"}},
serializer: fakeCodec,
},
{
accept: "application/protobuf",
contentType: "application/protobuf",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json", "application/protobuf"}},
serializer: fakeCodec,
},
{
accept: "application/json; pretty=1",
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
params: map[string]string{"pretty": "1"},
},
{
accept: "unrecognized/stuff,application/json; pretty=1",
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
params: map[string]string{"pretty": "1"},
},
// query param triggers pretty
{
req: &http.Request{
Header: http.Header{"Accept": []string{"application/json"}},
URL: &url.URL{RawQuery: "pretty=1"},
},
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
params: map[string]string{"pretty": "1"},
},
// certain user agents trigger pretty
{
req: &http.Request{
Header: http.Header{
"Accept": []string{"application/json"},
"User-Agent": []string{"curl"},
},
},
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
params: map[string]string{"pretty": "1"},
},
{
req: &http.Request{
Header: http.Header{
"Accept": []string{"application/json"},
"User-Agent": []string{"Wget"},
},
},
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
params: map[string]string{"pretty": "1"},
},
{
req: &http.Request{
Header: http.Header{
"Accept": []string{"application/json"},
"User-Agent": []string{"Mozilla/5.0"},
},
},
contentType: "application/json",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application/json"}},
serializer: fakeCodec,
params: map[string]string{"pretty": "1"},
},
// "application" is not a valid media type, so the server will reject the response during
// negotiation (the server, in error, has specified an invalid media type)
{
accept: "application",
ns: &fakeNegotiater{serializer: fakeCodec, types: []string{"application"}},
errFn: func(err error) bool {
return err.Error() == "only the following media types are accepted: application"
},
},
{
ns: &fakeNegotiater{types: []string{"a/b/c"}},
errFn: func(err error) bool {
return err.Error() == "only the following media types are accepted: a/b/c"
},
},
{
ns: &fakeNegotiater{},
errFn: func(err error) bool {
return err.Error() == "only the following media types are accepted: "
},
},
{
accept: "*/*",
ns: &fakeNegotiater{},
errFn: func(err error) bool {
return err.Error() == "only the following media types are accepted: "
},
},
{
accept: "application/json",
ns: &fakeNegotiater{types: []string{"application/json"}},
errFn: func(err error) bool {
return err.Error() == "only the following media types are accepted: application/json"
},
},
}
for i, test := range testCases {
req := test.req
if req == nil {
req = &http.Request{Header: http.Header{}}
req.Header.Set("Accept", test.accept)
}
s, contentType, err := negotiateOutputSerializer(req, test.ns)
switch {
case err == nil && test.errFn != nil:
t.Errorf("%d: failed: expected error", i)
continue
case err != nil && test.errFn == nil:
t.Errorf("%d: failed: %v", i, err)
continue
case err != nil:
if !test.errFn(err) {
t.Errorf("%d: failed: %v", i, err)
}
status, ok := err.(statusError)
if !ok {
t.Errorf("%d: failed, error should be statusError: %v", i, err)
continue
}
if status.Status().Status != unversioned.StatusFailure || status.Status().Code != http.StatusNotAcceptable {
t.Errorf("%d: failed: %v", i, err)
continue
}
continue
}
if test.contentType != contentType {
t.Errorf("%d: unexpected %s %s", i, test.contentType, contentType)
}
if s != test.serializer {
t.Errorf("%d: unexpected %s %s", i, test.serializer, s)
}
if !reflect.DeepEqual(test.params, test.ns.options) {
t.Errorf("%d: unexpected %#v %#v", i, test.params, test.ns.options)
}
}
}
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/rest" "k8s.io/kubernetes/pkg/api/rest"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apiserver/metrics" "k8s.io/kubernetes/pkg/apiserver/metrics"
"k8s.io/kubernetes/pkg/httplog" "k8s.io/kubernetes/pkg/httplog"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
...@@ -44,7 +45,7 @@ import ( ...@@ -44,7 +45,7 @@ import (
type ProxyHandler struct { type ProxyHandler struct {
prefix string prefix string
storage map[string]rest.Storage storage map[string]rest.Storage
codec runtime.Codec serializer runtime.NegotiatedSerializer
context api.RequestContextMapper context api.RequestContextMapper
requestInfoResolver *RequestInfoResolver requestInfoResolver *RequestInfoResolver
} }
...@@ -98,20 +99,19 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -98,20 +99,19 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
apiResource = resource apiResource = resource
gv := unversioned.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
redirector, ok := storage.(rest.Redirector) redirector, ok := storage.(rest.Redirector)
if !ok { if !ok {
httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource) httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource)
httpCode = errorJSON(errors.NewMethodNotSupported(api.Resource(resource), "proxy"), r.codec, w) httpCode = errorNegotiated(errors.NewMethodNotSupported(api.Resource(resource), "proxy"), r.serializer, gv, w, req)
return return
} }
location, roundTripper, err := redirector.ResourceLocation(ctx, id) location, roundTripper, err := redirector.ResourceLocation(ctx, id)
if err != nil { if err != nil {
httplog.LogOf(req, w).Addf("Error getting ResourceLocation: %v", err) httplog.LogOf(req, w).Addf("Error getting ResourceLocation: %v", err)
status := errToAPIStatus(err) httpCode = errorNegotiated(err, r.serializer, gv, w, req)
code := int(status.Code)
writeJSON(code, r.codec, status, w, true)
httpCode = code
return return
} }
if location == nil { if location == nil {
...@@ -144,11 +144,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -144,11 +144,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
newReq, err := http.NewRequest(req.Method, location.String(), req.Body) newReq, err := http.NewRequest(req.Method, location.String(), req.Body)
if err != nil { if err != nil {
status := errToAPIStatus(err) httpCode = errorNegotiated(err, r.serializer, gv, w, req)
code := int(status.Code)
writeJSON(code, r.codec, status, w, true)
notFound(w, req)
httpCode = code
return return
} }
httpCode = http.StatusOK httpCode = http.StatusOK
...@@ -161,7 +157,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -161,7 +157,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO convert this entire proxy to an UpgradeAwareProxy similar to // TODO convert this entire proxy to an UpgradeAwareProxy similar to
// https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go. // https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go.
// That proxy needs to be modified to support multiple backends, not just 1. // That proxy needs to be modified to support multiple backends, not just 1.
if r.tryUpgrade(w, req, newReq, location, roundTripper) { if r.tryUpgrade(w, req, newReq, location, roundTripper, gv) {
return return
} }
...@@ -210,15 +206,13 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -210,15 +206,13 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
// tryUpgrade returns true if the request was handled. // tryUpgrade returns true if the request was handled.
func (r *ProxyHandler) tryUpgrade(w http.ResponseWriter, req, newReq *http.Request, location *url.URL, transport http.RoundTripper) bool { func (r *ProxyHandler) tryUpgrade(w http.ResponseWriter, req, newReq *http.Request, location *url.URL, transport http.RoundTripper, gv unversioned.GroupVersion) bool {
if !httpstream.IsUpgradeRequest(req) { if !httpstream.IsUpgradeRequest(req) {
return false return false
} }
backendConn, err := proxyutil.DialURL(location, transport) backendConn, err := proxyutil.DialURL(location, transport)
if err != nil { if err != nil {
status := errToAPIStatus(err) errorNegotiated(err, r.serializer, gv, w, req)
code := int(status.Code)
writeJSON(code, r.codec, status, w, true)
return true return true
} }
defer backendConn.Close() defer backendConn.Close()
...@@ -228,17 +222,13 @@ func (r *ProxyHandler) tryUpgrade(w http.ResponseWriter, req, newReq *http.Reque ...@@ -228,17 +222,13 @@ func (r *ProxyHandler) tryUpgrade(w http.ResponseWriter, req, newReq *http.Reque
// hijack, just for reference... // hijack, just for reference...
requestHijackedConn, _, err := w.(http.Hijacker).Hijack() requestHijackedConn, _, err := w.(http.Hijacker).Hijack()
if err != nil { if err != nil {
status := errToAPIStatus(err) errorNegotiated(err, r.serializer, gv, w, req)
code := int(status.Code)
writeJSON(code, r.codec, status, w, true)
return true return true
} }
defer requestHijackedConn.Close() defer requestHijackedConn.Close()
if err = newReq.Write(backendConn); err != nil { if err = newReq.Write(backendConn); err != nil {
status := errToAPIStatus(err) errorNegotiated(err, r.serializer, gv, w, req)
code := int(status.Code)
writeJSON(code, r.codec, status, w, true)
return true return true
} }
......
...@@ -28,6 +28,7 @@ import ( ...@@ -28,6 +28,7 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
apierrors "k8s.io/kubernetes/pkg/api/errors" apierrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
...@@ -155,7 +156,7 @@ func (tc *patchTestCase) Run(t *testing.T) { ...@@ -155,7 +156,7 @@ func (tc *patchTestCase) Run(t *testing.T) {
namespace := tc.startingPod.Namespace namespace := tc.startingPod.Namespace
name := tc.startingPod.Name name := tc.startingPod.Name
codec := registered.GroupOrDie(api.GroupName).Codec codec := testapi.Default.Codec()
admit := tc.admit admit := tc.admit
if admit == nil { if admit == nil {
admit = func(updatedObject runtime.Object) error { admit = func(updatedObject runtime.Object) error {
......
...@@ -65,23 +65,34 @@ func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) { ...@@ -65,23 +65,34 @@ func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
} }
// serveWatch handles serving requests to the server // serveWatch handles serving requests to the server
func serveWatch(watcher watch.Interface, scope RequestScope, w http.ResponseWriter, req *restful.Request, timeout time.Duration) { func serveWatch(watcher watch.Interface, scope RequestScope, req *restful.Request, res *restful.Response, timeout time.Duration) {
watchServer := &WatchServer{watcher, scope.Codec, func(obj runtime.Object) { s, mediaType, err := negotiateOutputSerializer(req.Request, scope.Serializer)
if err != nil {
scope.err(err, req, res)
return
}
// TODO: replace with typed serialization
if mediaType != "application/json" {
writeRawJSON(http.StatusNotAcceptable, (errNotAcceptable{[]string{"application/json"}}).Status(), res.ResponseWriter)
return
}
encoder := scope.Serializer.EncoderForVersion(s, scope.Kind.GroupVersion())
watchServer := &WatchServer{watcher, encoder, func(obj runtime.Object) {
if err := setSelfLink(obj, req, scope.Namer); err != nil { if err := setSelfLink(obj, req, scope.Namer); err != nil {
glog.V(5).Infof("Failed to set self link for object %v: %v", reflect.TypeOf(obj), err) glog.V(5).Infof("Failed to set self link for object %v: %v", reflect.TypeOf(obj), err)
} }
}, &realTimeoutFactory{timeout}} }, &realTimeoutFactory{timeout}}
if isWebsocketRequest(req.Request) { if isWebsocketRequest(req.Request) {
websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req.Request) websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(res.ResponseWriter), req.Request)
} else { } else {
watchServer.ServeHTTP(w, req.Request) watchServer.ServeHTTP(res.ResponseWriter, req.Request)
} }
} }
// WatchServer serves a watch.Interface over a websocket or vanilla HTTP. // WatchServer serves a watch.Interface over a websocket or vanilla HTTP.
type WatchServer struct { type WatchServer struct {
watching watch.Interface watching watch.Interface
codec runtime.Codec encoder runtime.Encoder
fixup func(runtime.Object) fixup func(runtime.Object)
t timeoutFactory t timeoutFactory
} }
...@@ -108,7 +119,7 @@ func (w *WatchServer) HandleWS(ws *websocket.Conn) { ...@@ -108,7 +119,7 @@ func (w *WatchServer) HandleWS(ws *websocket.Conn) {
return return
} }
w.fixup(event.Object) w.fixup(event.Object)
obj, err := watchjson.Object(w.codec, &event) obj, err := watchjson.Object(w.encoder, &event)
if err != nil { if err != nil {
// Client disconnect. // Client disconnect.
w.watching.Stop() w.watching.Stop()
...@@ -134,20 +145,21 @@ func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -134,20 +145,21 @@ func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
cn, ok := w.(http.CloseNotifier) cn, ok := w.(http.CloseNotifier)
if !ok { if !ok {
loggedW.Addf("unable to get CloseNotifier") loggedW.Addf("unable to get CloseNotifier: %#v", w)
http.NotFound(w, req) http.NotFound(w, req)
return return
} }
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
if !ok { if !ok {
loggedW.Addf("unable to get Flusher") loggedW.Addf("unable to get Flusher: %#v", w)
http.NotFound(w, req) http.NotFound(w, req)
return return
} }
w.Header().Set("Transfer-Encoding", "chunked") w.Header().Set("Transfer-Encoding", "chunked")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
flusher.Flush() flusher.Flush()
encoder := watchjson.NewEncoder(w, self.codec) // TODO: use arbitrary serialization on watch
encoder := watchjson.NewEncoder(w, self.encoder)
for { for {
select { select {
case <-cn.CloseNotify(): case <-cn.CloseNotify():
......
...@@ -169,6 +169,34 @@ func TestWatchHTTP(t *testing.T) { ...@@ -169,6 +169,34 @@ func TestWatchHTTP(t *testing.T) {
} }
} }
func TestWatchHTTPAccept(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
dest, _ := url.Parse(server.URL)
dest.Path = "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/watch/simples"
dest.RawQuery = ""
request, err := http.NewRequest("GET", dest.String(), nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
request.Header.Set("Accept", "application/yaml")
response, err := client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// TODO: once this is fixed, this test will change
if response.StatusCode != http.StatusNotAcceptable {
t.Errorf("Unexpected response %#v", response)
}
}
func TestWatchParamParsing(t *testing.T) { func TestWatchParamParsing(t *testing.T) {
simpleStorage := &SimpleRESTStorage{} simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{ handler := handle(map[string]rest.Storage{
......
...@@ -148,6 +148,15 @@ type APIGroupInfo struct { ...@@ -148,6 +148,15 @@ type APIGroupInfo struct {
// If nil, defaults to groupMeta.GroupVersion. // If nil, defaults to groupMeta.GroupVersion.
// TODO: Remove this when https://github.com/kubernetes/kubernetes/issues/19018 is fixed. // TODO: Remove this when https://github.com/kubernetes/kubernetes/issues/19018 is fixed.
OptionsExternalVersion *unversioned.GroupVersion OptionsExternalVersion *unversioned.GroupVersion
// Scheme includes all of the types used by this group and how to convert between them (or
// to convert objects from outside of this group that are accepted in this API).
// TODO: replace with interfaces
Scheme *runtime.Scheme
// NegotiatedSerializer controls how this group encodes and decodes data
NegotiatedSerializer runtime.NegotiatedSerializer
// ParameterCodec performs conversions for query parameters passed to API calls
ParameterCodec runtime.ParameterCodec
} }
// Config is a structure used to configure a GenericAPIServer. // Config is a structure used to configure a GenericAPIServer.
...@@ -275,6 +284,10 @@ type GenericAPIServer struct { ...@@ -275,6 +284,10 @@ type GenericAPIServer struct {
// storage contains the RESTful endpoints exposed by this GenericAPIServer // storage contains the RESTful endpoints exposed by this GenericAPIServer
storage map[string]rest.Storage storage map[string]rest.Storage
// Serializer controls how common API objects not in a group/version prefix are serialized for this server.
// Individual APIGroups may define their own serializers.
Serializer runtime.NegotiatedSerializer
// "Outputs" // "Outputs"
Handler http.Handler Handler http.Handler
InsecureHandler http.Handler InsecureHandler http.Handler
......
...@@ -94,6 +94,7 @@ func TestInstallAPIGroups(t *testing.T) { ...@@ -94,6 +94,7 @@ func TestInstallAPIGroups(t *testing.T) {
config.ProxyTLSClientConfig = &tls.Config{} config.ProxyTLSClientConfig = &tls.Config{}
config.APIPrefix = "/apiPrefix" config.APIPrefix = "/apiPrefix"
config.APIGroupPrefix = "/apiGroupPrefix" config.APIGroupPrefix = "/apiGroupPrefix"
config.Serializer = latest.Codecs
s := New(&config) s := New(&config)
apiGroupMeta := registered.GroupOrDie(api.GroupName) apiGroupMeta := registered.GroupOrDie(api.GroupName)
......
...@@ -51,3 +51,73 @@ func TestWatchSucceedsWithoutArgs(t *testing.T) { ...@@ -51,3 +51,73 @@ func TestWatchSucceedsWithoutArgs(t *testing.T) {
} }
resp.Body.Close() resp.Body.Close()
} }
func TestAccept(t *testing.T) {
_, s := framework.RunAMaster(t)
defer s.Close()
resp, err := http.Get(s.URL + "/api/")
if err != nil {
t.Fatalf("unexpected error getting api: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("got status %v instead of 200 OK", resp.StatusCode)
}
body, _ := ioutil.ReadAll(resp.Body)
if resp.Header.Get("Content-Type") != "application/json" {
t.Errorf("unexpected content: %s", body)
}
if err := json.Unmarshal(body, &map[string]interface{}{}); err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("GET", s.URL+"/api/", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "application/yaml")
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
body, _ = ioutil.ReadAll(resp.Body)
if resp.Header.Get("Content-Type") != "application/yaml" {
t.Errorf("unexpected content: %s", body)
}
t.Logf("body: %s", body)
if err := yaml.Unmarshal(body, &map[string]interface{}{}); err != nil {
t.Fatal(err)
}
req, err = http.NewRequest("GET", s.URL+"/api/", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "application/json, application/yaml")
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
body, _ = ioutil.ReadAll(resp.Body)
if resp.Header.Get("Content-Type") != "application/json" {
t.Errorf("unexpected content: %s", body)
}
t.Logf("body: %s", body)
if err := yaml.Unmarshal(body, &map[string]interface{}{}); err != nil {
t.Fatal(err)
}
req, err = http.NewRequest("GET", s.URL+"/api/", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "application") // not a valid media type
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusNotAcceptable {
t.Errorf("unexpected error from the server")
}
}
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