diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index e4980809181..3e7e3ed5261 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -545,7 +545,7 @@ func StartControllers(s *options.CMServer, kubeconfig *restclient.Config, rootCl config := restclient.AddUserAgent(kubeconfig, "generic-garbage-collector") config.ContentConfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: metaonly.NewMetadataCodecFactory()} metaOnlyClientPool := dynamic.NewClientPool(config, restMapper, dynamic.LegacyAPIPathResolverFunc) - config.ContentConfig.NegotiatedSerializer = nil + config.ContentConfig = dynamic.ContentConfig() clientPool := dynamic.NewClientPool(config, restMapper, dynamic.LegacyAPIPathResolverFunc) garbageCollector, err := garbagecollector.NewGarbageCollector(metaOnlyClientPool, clientPool, restMapper, groupVersionResources) if err != nil { diff --git a/pkg/api/serialization_proto_test.go b/pkg/api/serialization_proto_test.go index 4ccbb5d03df..5400f37ec44 100644 --- a/pkg/api/serialization_proto_test.go +++ b/pkg/api/serialization_proto_test.go @@ -54,12 +54,12 @@ func TestUniversalDeserializer(t *testing.T) { expected := &v1.Pod{ObjectMeta: v1.ObjectMeta{Name: "test"}} d := api.Codecs.UniversalDeserializer() for _, mediaType := range []string{"application/json", "application/yaml", "application/vnd.kubernetes.protobuf"} { - e, ok := api.Codecs.SerializerForMediaType(mediaType, nil) + info, ok := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), mediaType) if !ok { t.Fatal(mediaType) } buf := &bytes.Buffer{} - if err := e.Encode(expected, buf); err != nil { + if err := info.Serializer.Encode(expected, buf); err != nil { t.Fatalf("%s: %v", mediaType, err) } obj, _, err := d.Decode(buf.Bytes(), &unversioned.GroupVersionKind{Kind: "Pod", Version: "v1"}, nil) diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 657b7de82a0..3fbab963531 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -380,12 +380,15 @@ func TestObjectWatchFraming(t *testing.T) { secret.Data["long"] = bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x00}, 1000) converted, _ := api.Scheme.ConvertToVersion(secret, v1.SchemeGroupVersion) v1secret := converted.(*v1.Secret) - for _, streamingMediaType := range api.Codecs.SupportedStreamingMediaTypes() { - s, _ := api.Codecs.StreamingSerializerForMediaType(streamingMediaType, nil) + for _, info := range api.Codecs.SupportedMediaTypes() { + if info.StreamSerializer == nil { + continue + } + s := info.StreamSerializer framer := s.Framer - embedded := s.Embedded.Serializer + embedded := info.Serializer if embedded == nil { - t.Errorf("no embedded serializer for %s", streamingMediaType) + t.Errorf("no embedded serializer for %s", info.MediaType) continue } innerDecode := api.Codecs.DecoderToVersion(embedded, api.SchemeGroupVersion) @@ -442,7 +445,7 @@ func TestObjectWatchFraming(t *testing.T) { } if !api.Semantic.DeepEqual(secret, outEvent.Object.Object) { - t.Fatalf("%s: did not match after frame decoding: %s", streamingMediaType, diff.ObjectGoPrintDiff(secret, outEvent.Object.Object)) + t.Fatalf("%s: did not match after frame decoding: %s", info.MediaType, diff.ObjectGoPrintDiff(secret, outEvent.Object.Object)) } } } diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go index 343591dc391..3bdaea35eb3 100644 --- a/pkg/api/testapi/testapi.go +++ b/pkg/api/testapi/testapi.go @@ -93,11 +93,11 @@ type TestGroup struct { func init() { if apiMediaType := os.Getenv("KUBE_TEST_API_TYPE"); len(apiMediaType) > 0 { var ok bool - mediaType, options, err := mime.ParseMediaType(apiMediaType) + mediaType, _, err := mime.ParseMediaType(apiMediaType) if err != nil { panic(err) } - serializer, ok = api.Codecs.SerializerForMediaType(mediaType, options) + serializer, ok = runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), mediaType) if !ok { panic(fmt.Sprintf("no serializer for %s", apiMediaType)) } @@ -105,11 +105,11 @@ func init() { if storageMediaType := StorageMediaType(); len(storageMediaType) > 0 { var ok bool - mediaType, options, err := mime.ParseMediaType(storageMediaType) + mediaType, _, err := mime.ParseMediaType(storageMediaType) if err != nil { panic(err) } - storageSerializer, ok = api.Codecs.SerializerForMediaType(mediaType, options) + storageSerializer, ok = runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), mediaType) if !ok { panic(fmt.Sprintf("no serializer for %s", storageMediaType)) } @@ -312,7 +312,7 @@ func (g TestGroup) Codec() runtime.Codec { if serializer.Serializer == nil { return api.Codecs.LegacyCodec(g.externalGroupVersion) } - return api.Codecs.CodecForVersions(serializer, api.Codecs.UniversalDeserializer(), unversioned.GroupVersions{g.externalGroupVersion}, nil) + return api.Codecs.CodecForVersions(serializer.Serializer, api.Codecs.UniversalDeserializer(), unversioned.GroupVersions{g.externalGroupVersion}, nil) } // NegotiatedSerializer returns the negotiated serializer for the server. @@ -452,11 +452,11 @@ func GetCodecForObject(obj runtime.Object) (runtime.Codec, error) { } // Codec used for unversioned types if api.Scheme.Recognizes(kind) { - serializer, ok := api.Codecs.SerializerForFileExtension("json") + serializer, ok := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { return nil, fmt.Errorf("no serializer registered for json") } - return serializer, nil + return serializer.Serializer, nil } return nil, fmt.Errorf("unexpected kind: %v", kind) } diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 85e536cfeef..a3920f2632c 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -102,7 +102,8 @@ func (a *APIInstaller) NewWebService() *restful.WebService { // If we stop using go-restful, we can default empty content-type to application/json on an // endpoint by endpoint basis ws.Consumes("*/*") - ws.Produces(a.group.Serializer.SupportedMediaTypes()...) + mediaTypes, streamMediaTypes := mediaTypesForSerializer(a.group.Serializer) + ws.Produces(append(mediaTypes, streamMediaTypes...)...) ws.ApiVersion(a.group.GroupVersion.String()) return ws @@ -472,6 +473,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag // // test/integration/auth_test.go is currently the most comprehensive status code test + mediaTypes, streamMediaTypes := mediaTypesForSerializer(a.group.Serializer) + allMediaTypes := append(mediaTypes, streamMediaTypes...) + ws.Produces(allMediaTypes...) + reqScope := RequestScope{ ContextFunc: ctxFn, Serializer: a.group.Serializer, @@ -517,7 +522,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("read"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", versionedObject). Writes(versionedObject) if isGetterWithOptions { @@ -542,7 +547,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("list"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), allMediaTypes...)...). Returns(http.StatusOK, "OK", versionedList). Writes(versionedList) if err := addObjectParams(ws, route, versionedListOptions); err != nil { @@ -574,7 +579,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("replace"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", versionedObject). Reads(versionedObject). Writes(versionedObject) @@ -591,7 +596,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Consumes(string(api.JSONPatchType), string(api.MergePatchType), string(api.StrategicMergePatchType)). Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", versionedObject). Reads(unversioned.Patch{}). Writes(versionedObject) @@ -613,7 +618,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", versionedObject). Reads(versionedObject). Writes(versionedObject) @@ -629,7 +634,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("delete"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Writes(versionedStatus). Returns(http.StatusOK, "OK", versionedStatus) if isGracefulDeleter { @@ -647,7 +652,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("deletecollection"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(append(storageMeta.ProducesMIMETypes(action.Verb), a.group.Serializer.SupportedMediaTypes()...)...). + Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Writes(versionedStatus). Returns(http.StatusOK, "OK", versionedStatus) if err := addObjectParams(ws, route, versionedListOptions); err != nil { @@ -666,7 +671,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("watch"+namespaced+kind+strings.Title(subresource)+operationSuffix). - Produces(a.group.Serializer.SupportedStreamingMediaTypes()...). + Produces(allMediaTypes...). Returns(http.StatusOK, "OK", versionedWatchEvent). Writes(versionedWatchEvent) if err := addObjectParams(ws, route, versionedListOptions); err != nil { @@ -685,7 +690,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Operation("watch"+namespaced+kind+strings.Title(subresource)+"List"+operationSuffix). - Produces(a.group.Serializer.SupportedStreamingMediaTypes()...). + Produces(allMediaTypes...). Returns(http.StatusOK, "OK", versionedWatchEvent). Writes(versionedWatchEvent) if err := addObjectParams(ws, route, versionedListOptions); err != nil { diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f293700b6b8..b1e54bf359a 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -211,6 +211,7 @@ func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Contain // Because in release 1.1, /api returns response with empty APIVersion, we // use StripVersionNegotiatedSerializer to keep the response backwards // compatible. + mediaTypes, _ := mediaTypesForSerializer(s) ss := StripVersionNegotiatedSerializer{s} versionHandler := APIVersionHandler(ss, getAPIVersionsFunc) ws := new(restful.WebService) @@ -219,8 +220,8 @@ func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Contain ws.Route(ws.GET("/").To(versionHandler). Doc("get available API versions"). Operation("getAPIVersions"). - Produces(s.SupportedMediaTypes()...). - Consumes(s.SupportedMediaTypes()...). + Produces(mediaTypes...). + Consumes(mediaTypes...). Writes(unversioned.APIVersions{})) container.Add(ws) } @@ -277,6 +278,7 @@ func NewApisWebService(s runtime.NegotiatedSerializer, apiPrefix string, f func( // use StripVersionNegotiatedSerializer to keep the response backwards // compatible. ss := StripVersionNegotiatedSerializer{s} + mediaTypes, _ := mediaTypesForSerializer(s) rootAPIHandler := RootAPIHandler(ss, f) ws := new(restful.WebService) ws.Path(apiPrefix) @@ -284,8 +286,8 @@ func NewApisWebService(s runtime.NegotiatedSerializer, apiPrefix string, f func( ws.Route(ws.GET("/").To(rootAPIHandler). Doc("get available API versions"). Operation("getAPIVersions"). - Produces(s.SupportedMediaTypes()...). - Consumes(s.SupportedMediaTypes()...). + Produces(mediaTypes...). + Consumes(mediaTypes...). Writes(unversioned.APIGroupList{})) return ws } @@ -300,6 +302,7 @@ func NewGroupWebService(s runtime.NegotiatedSerializer, path string, group unver // response backwards compatible. ss = StripVersionNegotiatedSerializer{s} } + mediaTypes, _ := mediaTypesForSerializer(s) groupHandler := GroupHandler(ss, group) ws := new(restful.WebService) ws.Path(path) @@ -307,8 +310,8 @@ func NewGroupWebService(s runtime.NegotiatedSerializer, path string, group unver ws.Route(ws.GET("/").To(groupHandler). Doc("get information of a group"). Operation("getAPIGroup"). - Produces(s.SupportedMediaTypes()...). - Consumes(s.SupportedMediaTypes()...). + Produces(mediaTypes...). + Consumes(mediaTypes...). Writes(unversioned.APIGroup{})) return ws } @@ -323,12 +326,13 @@ func AddSupportedResourcesWebService(s runtime.NegotiatedSerializer, ws *restful // keep the response backwards compatible. ss = StripVersionNegotiatedSerializer{s} } + mediaTypes, _ := mediaTypesForSerializer(s) resourceHandler := SupportedResourcesHandler(ss, groupVersion, lister) ws.Route(ws.GET("/").To(resourceHandler). Doc("get available resources"). Operation("getAPIResources"). - Produces(s.SupportedMediaTypes()...). - Consumes(s.SupportedMediaTypes()...). + Produces(mediaTypes...). + Consumes(mediaTypes...). Writes(unversioned.APIResourceList{})) } @@ -417,7 +421,7 @@ func writeNegotiated(s runtime.NegotiatedSerializer, gv unversioned.GroupVersion w.Header().Set("Content-Type", serializer.MediaType) w.WriteHeader(statusCode) - encoder := s.EncoderForVersion(serializer, gv) + encoder := s.EncoderForVersion(serializer.Serializer, gv) if err := encoder.Encode(object, w); err != nil { errorJSONFatal(err, encoder, w) } diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index e150796b269..e16987c85cb 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -1224,11 +1224,12 @@ func TestMetadata(t *testing.T) { matches[s] = i + 1 } } + if matches["text/plain,application/json,application/yaml,application/vnd.kubernetes.protobuf"] == 0 || - matches["application/json,application/json;stream=watch,application/vnd.kubernetes.protobuf,application/vnd.kubernetes.protobuf;stream=watch"] == 0 || + matches["application/json,application/yaml,application/vnd.kubernetes.protobuf,application/json;stream=watch,application/vnd.kubernetes.protobuf;stream=watch"] == 0 || matches["application/json,application/yaml,application/vnd.kubernetes.protobuf"] == 0 || matches["*/*"] == 0 || - len(matches) != 4 { + len(matches) != 5 { t.Errorf("unexpected mime types: %v", matches) } } @@ -1321,6 +1322,89 @@ func TestGet(t *testing.T) { } } +func TestGetPretty(t *testing.T) { + storage := map[string]rest.Storage{} + simpleStorage := SimpleRESTStorage{ + item: apiservertesting.Simple{ + Other: "foo", + }, + } + 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() + + tests := []struct { + accept string + userAgent string + params url.Values + pretty bool + }{ + {accept: runtime.ContentTypeJSON}, + {accept: runtime.ContentTypeJSON + ";pretty=0"}, + {accept: runtime.ContentTypeJSON, userAgent: "kubectl"}, + {accept: runtime.ContentTypeJSON, params: url.Values{"pretty": {"0"}}}, + + {pretty: true, accept: runtime.ContentTypeJSON, userAgent: "curl"}, + {pretty: true, accept: runtime.ContentTypeJSON, userAgent: "Mozilla/5.0"}, + {pretty: true, accept: runtime.ContentTypeJSON, userAgent: "Wget"}, + {pretty: true, accept: runtime.ContentTypeJSON + ";pretty=1"}, + {pretty: true, accept: runtime.ContentTypeJSON, params: url.Values{"pretty": {"1"}}}, + {pretty: true, accept: runtime.ContentTypeJSON, params: url.Values{"pretty": {"true"}}}, + } + 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) + req.Header.Set("User-Agent", test.userAgent) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK { + t.Fatal(err) + } + var itemOut apiservertesting.Simple + body, err := extractBody(resp, &itemOut) + if err != nil { + t.Fatal(err) + } + // to get stable ordering we need to use a go type + unstructured := apiservertesting.Simple{} + if err := json.Unmarshal([]byte(body), &unstructured); err != nil { + t.Fatal(err) + } + var expect string + if test.pretty { + out, err := json.MarshalIndent(unstructured, "", " ") + if err != nil { + t.Fatal(err) + } + expect = string(out) + } else { + out, err := json.Marshal(unstructured) + if err != nil { + t.Fatal(err) + } + expect = string(out) + "\n" + } + if expect != body { + t.Errorf("%d: body did not match expected:\n%s\n%s", i, body, expect) + } + } +} + func TestGetBinary(t *testing.T) { simpleStorage := SimpleRESTStorage{ stream: &SimpleStream{ @@ -2719,12 +2803,12 @@ func TestCreateYAML(t *testing.T) { simple := &apiservertesting.Simple{ Other: "bar", } - serializer, ok := api.Codecs.SerializerForMediaType("application/yaml", nil) + info, ok := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), "application/yaml") if !ok { t.Fatal("No yaml serializer") } - encoder := api.Codecs.EncoderForVersion(serializer, testGroupVersion) - decoder := api.Codecs.DecoderToVersion(serializer, testInternalGroupVersion) + encoder := api.Codecs.EncoderForVersion(info.Serializer, testGroupVersion) + decoder := api.Codecs.DecoderToVersion(info.Serializer, testInternalGroupVersion) data, err := runtime.Encode(encoder, simple) if err != nil { @@ -3216,7 +3300,7 @@ func BenchmarkUpdateProtobuf(b *testing.B) { dest.Path = "/" + prefix + "/" + newGroupVersion.Group + "/" + newGroupVersion.Version + "/namespaces/foo/simples/bar" dest.RawQuery = "" - info, _ := api.Codecs.SerializerForMediaType("application/vnd.kubernetes.protobuf", nil) + info, _ := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), "application/vnd.kubernetes.protobuf") e := api.Codecs.EncoderForVersion(info.Serializer, newGroupVersion) data, err := runtime.Encode(e, &items[0]) if err != nil { diff --git a/pkg/apiserver/negotiate.go b/pkg/apiserver/negotiate.go index f51e68ad275..6656e5c34e6 100644 --- a/pkg/apiserver/negotiate.go +++ b/pkg/apiserver/negotiate.go @@ -24,71 +24,66 @@ import ( "bitbucket.org/ww/goautoneg" + "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/runtime" ) -func negotiateOutput(req *http.Request, supported []string) (string, map[string]string, error) { - acceptHeader := req.Header.Get("Accept") - if len(acceptHeader) == 0 && len(supported) > 0 { - acceptHeader = supported[0] +// mediaTypesForSerializer returns a list of media and stream media types for the server. +func mediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, streamMediaTypes []string) { + for _, info := range ns.SupportedMediaTypes() { + mediaTypes = append(mediaTypes, info.MediaType) + if info.StreamSerializer != nil { + // stream=watch is the existing mime-type parameter for watch + streamMediaTypes = append(streamMediaTypes, info.MediaType+";stream=watch") + } } - 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 - } - - return mediaType, accept.Params, nil + return mediaTypes, streamMediaTypes } func negotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - supported := ns.SupportedMediaTypes() - mediaType, params, err := negotiateOutput(req, supported) - if err != nil { - return runtime.SerializerInfo{}, err + mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions) + if !ok { + supported, _ := mediaTypesForSerializer(ns) + return runtime.SerializerInfo{}, errNotAcceptable{supported} } - if s, ok := ns.SerializerForMediaType(mediaType, params); ok { - return s, nil + // TODO: move into resthandler + info := mediaType.accepted.Serializer + if (mediaType.pretty || isPrettyPrint(req)) && info.PrettySerializer != nil { + info.Serializer = info.PrettySerializer } - return runtime.SerializerInfo{}, errNotAcceptable{supported} + return info, nil } -func negotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.StreamSerializerInfo, error) { - supported := ns.SupportedMediaTypes() - mediaType, params, err := negotiateOutput(req, supported) - if err != nil { - return runtime.StreamSerializerInfo{}, 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 { + _, supported := mediaTypesForSerializer(ns) + return runtime.SerializerInfo{}, errNotAcceptable{supported} } - if s, ok := ns.StreamingSerializerForMediaType(mediaType, params); ok { - return s, nil - } - return runtime.StreamSerializerInfo{}, errNotAcceptable{supported} + return mediaType.accepted.Serializer, nil } -func negotiateInputSerializer(req *http.Request, s runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - supported := s.SupportedMediaTypes() +func negotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { + mediaTypes := ns.SupportedMediaTypes() mediaType := req.Header.Get("Content-Type") if len(mediaType) == 0 { - mediaType = supported[0] + mediaType = mediaTypes[0].MediaType } - mediaType, options, err := mime.ParseMediaType(mediaType) + mediaType, _, err := mime.ParseMediaType(mediaType) if err != nil { + _, supported := mediaTypesForSerializer(ns) return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} } - out, ok := s.SerializerForMediaType(mediaType, options) - if !ok { - return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} + + for _, info := range mediaTypes { + if info.MediaType != mediaType { + continue + } + return info, nil } - return out, nil + + _, supported := mediaTypesForSerializer(ns) + return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} } // isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent @@ -135,3 +130,176 @@ func negotiate(header string, alternatives []string) (goautoneg.Accept, bool) { } return goautoneg.Accept{}, false } + +// endpointRestrictions is an interface that allows content-type negotiation +// to verify server support for specific options +type endpointRestrictions interface { + // AllowsConversion should return true if the specified group version kind + // is an allowed target object. + AllowsConversion(unversioned.GroupVersionKind) bool + // AllowsServerVersion should return true if the specified version is valid + // for the server group. + AllowsServerVersion(version string) bool + // AllowsStreamSchema should return true if the specified stream schema is + // valid for the server group. + AllowsStreamSchema(schema string) bool +} + +var defaultEndpointRestrictions = emptyEndpointRestrictions{} + +type emptyEndpointRestrictions struct{} + +func (emptyEndpointRestrictions) AllowsConversion(unversioned.GroupVersionKind) bool { return false } +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 +// server can serialize. +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") + SubType string + // Serializer is the serialization info this object accepts + Serializer runtime.SerializerInfo +} + +// mediaTypeOptions describes information for a given media type that may alter +// the server response +type mediaTypeOptions struct { + // pretty is true if the requested representation should be formatted for human + // viewing + 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 + + // convert is a request to alter the type of object returned by the server from the + // normal response + convert *unversioned.GroupVersionKind + // useServerVersion is an optional version for the server group + useServerVersion string + + // export is true if the representation requested should exclude fields the server + // has set + export bool + + // unrecognized is a list of all unrecognized keys + unrecognized []string + + // the accepted media type from the client + 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 + + // extract all known parameters + for k, v := range params { + switch k { + + // controls transformation of the object when returned + case "as": + if options.convert == nil { + options.convert = &unversioned.GroupVersionKind{} + } + options.convert.Kind = v + case "g": + if options.convert == nil { + options.convert = &unversioned.GroupVersionKind{} + } + options.convert.Group = v + case "v": + if options.convert == nil { + options.convert = &unversioned.GroupVersionKind{} + } + options.convert.Version = v + + // controls the streaming schema + case "stream": + if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) { + return mediaTypeOptions{}, false + } + 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 + } + 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" + + // if specified, the pretty serializer will be used + case "pretty": + options.pretty = v == "1" + + default: + options.unrecognized = append(options.unrecognized, k) + } + } + + if options.convert != nil && !endpoint.AllowsConversion(*options.convert) { + return mediaTypeOptions{}, false + } + + options.accepted = accepts + + return options, true +} + +// 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) { + if len(header) == 0 && len(accepted) > 0 { + return mediaTypeOptions{ + accepted: &accepted[0], + }, true + } + + clauses := goautoneg.ParseAccept(header) + for _, clause := range clauses { + for i := range accepted { + accepts := &accepted[i] + switch { + case clause.Type == accepts.Type && clause.SubType == accepts.SubType, + clause.Type == accepts.Type && clause.SubType == "*", + clause.Type == "*" && clause.SubType == "*": + // TODO: should we prefer the first type with no unrecognized options? Do we need to ignore unrecognized + // parameters. + return acceptMediaTypeOptions(clause.Params, accepts, endpoint) + } + } + } + return mediaTypeOptions{}, false +} + +// 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 + for _, info := range ns.SupportedMediaTypes() { + segments := strings.SplitN(info.MediaType, "/", 2) + if len(segments) == 1 { + segments = append(segments, "*") + } + t := acceptedMediaType{ + Type: segments[0], + SubType: segments[1], + Serializer: info, + } + acceptedMediaTypes = append(acceptedMediaTypes, t) + } + return acceptedMediaTypes +} diff --git a/pkg/apiserver/negotiate_test.go b/pkg/apiserver/negotiate_test.go index 8187a1761a2..514b359c2da 100644 --- a/pkg/apiserver/negotiate_test.go +++ b/pkg/apiserver/negotiate_test.go @@ -19,7 +19,6 @@ package apiserver import ( "net/http" "net/url" - "reflect" "testing" "k8s.io/kubernetes/pkg/api/unversioned" @@ -30,38 +29,24 @@ type fakeNegotiater struct { serializer, streamSerializer runtime.Serializer framer runtime.Framer types, streamTypes []string - mediaType, streamMediaType string - options, streamOptions map[string]string } -func (n *fakeNegotiater) SupportedMediaTypes() []string { - return n.types -} -func (n *fakeNegotiater) SupportedStreamingMediaTypes() []string { - return n.streamTypes -} - -func (n *fakeNegotiater) SerializerForMediaType(mediaType string, options map[string]string) (runtime.SerializerInfo, bool) { - n.mediaType = mediaType - if len(options) > 0 { - n.options = options +func (n *fakeNegotiater) SupportedMediaTypes() []runtime.SerializerInfo { + var out []runtime.SerializerInfo + for _, s := range n.types { + info := runtime.SerializerInfo{Serializer: n.serializer, MediaType: s, EncodesAsText: true} + for _, t := range n.streamTypes { + if t == s { + info.StreamSerializer = &runtime.StreamSerializerInfo{ + EncodesAsText: true, + Framer: n.framer, + Serializer: n.streamSerializer, + } + } + } + out = append(out, info) } - return runtime.SerializerInfo{Serializer: n.serializer, MediaType: n.mediaType, EncodesAsText: true}, n.serializer != nil -} - -func (n *fakeNegotiater) StreamingSerializerForMediaType(mediaType string, options map[string]string) (runtime.StreamSerializerInfo, bool) { - n.streamMediaType = mediaType - if len(options) > 0 { - n.streamOptions = options - } - return runtime.StreamSerializerInfo{ - SerializerInfo: runtime.SerializerInfo{ - Serializer: n.serializer, - MediaType: mediaType, - EncodesAsText: true, - }, - Framer: n.framer, - }, n.streamSerializer != nil + return out } func (n *fakeNegotiater) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { @@ -201,12 +186,6 @@ func TestNegotiate(t *testing.T) { 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 { @@ -220,13 +199,6 @@ func TestNegotiate(t *testing.T) { 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 { @@ -264,8 +236,5 @@ func TestNegotiate(t *testing.T) { if s.Serializer != test.serializer { t.Errorf("%d: unexpected %s %s", i, test.serializer, s.Serializer) } - if !reflect.DeepEqual(test.params, test.ns.options) { - t.Errorf("%d: unexpected %#v %#v", i, test.params, test.ns.options) - } } } diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index ba5b48ca7af..4182547cef6 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -364,7 +364,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.Object scope.err(err, res.ResponseWriter, req.Request) return } - decoder := scope.Serializer.DecoderToVersion(s, unversioned.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}) + decoder := scope.Serializer.DecoderToVersion(s.Serializer, unversioned.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}) body, err := readBody(req.Request) if err != nil { @@ -480,15 +480,15 @@ func PatchResource(r rest.Patcher, scope RequestScope, typer runtime.ObjectTyper return } - s, ok := scope.Serializer.SerializerForMediaType("application/json", nil) + s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { scope.err(fmt.Errorf("no serializer defined for JSON"), res.ResponseWriter, req.Request) return } gv := scope.Kind.GroupVersion() codec := runtime.NewCodec( - scope.Serializer.EncoderForVersion(s, gv), - scope.Serializer.DecoderToVersion(s, unversioned.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}), + scope.Serializer.EncoderForVersion(s.Serializer, gv), + scope.Serializer.DecoderToVersion(s.Serializer, unversioned.GroupVersion{Group: gv.Group, Version: runtime.APIVersionInternal}), ) updateAdmit := func(updatedObject runtime.Object, currentObject runtime.Object) error { @@ -685,7 +685,7 @@ func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectType defaultGVK := scope.Kind original := r.New() trace.Step("About to convert to expected version") - obj, gvk, err := scope.Serializer.DecoderToVersion(s, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, original) + obj, gvk, err := scope.Serializer.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, original) if err != nil { err = transformDecodeError(typer, err, original, gvk, body) scope.err(err, res.ResponseWriter, req.Request) @@ -772,7 +772,7 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, scope RequestScope, return } defaultGVK := scope.Kind.GroupVersion().WithKind("DeleteOptions") - obj, _, err := scope.Serializer.DecoderToVersion(s, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) + obj, _, err := scope.Serializer.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return @@ -889,7 +889,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco return } defaultGVK := scope.Kind.GroupVersion().WithKind("DeleteOptions") - obj, _, err := scope.Serializer.DecoderToVersion(s, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) + obj, _, err := scope.Serializer.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) if err != nil { scope.err(err, res.ResponseWriter, req.Request) return diff --git a/pkg/apiserver/watch.go b/pkg/apiserver/watch.go index 2aa5409a193..119e4353716 100755 --- a/pkg/apiserver/watch.go +++ b/pkg/apiserver/watch.go @@ -68,24 +68,33 @@ func serveWatch(watcher watch.Interface, scope RequestScope, req *restful.Reques scope.err(err, res.ResponseWriter, req.Request) return } - if serializer.Framer == nil { + framer := serializer.StreamSerializer.Framer + streamSerializer := serializer.StreamSerializer.Serializer + embedded := serializer.Serializer + if framer == nil { scope.err(fmt.Errorf("no framer defined for %q available for embedded encoding", serializer.MediaType), res.ResponseWriter, req.Request) return } - encoder := scope.Serializer.EncoderForVersion(serializer.Serializer, scope.Kind.GroupVersion()) + encoder := scope.Serializer.EncoderForVersion(streamSerializer, scope.Kind.GroupVersion()) useTextFraming := serializer.EncodesAsText // find the embedded serializer matching the media type - embeddedEncoder := scope.Serializer.EncoderForVersion(serializer.Embedded.Serializer, scope.Kind.GroupVersion()) + embeddedEncoder := scope.Serializer.EncoderForVersion(embedded, scope.Kind.GroupVersion()) + + // TODO: next step, get back mediaTypeOptions from negotiate and return the exact value here + mediaType := serializer.MediaType + if mediaType != runtime.ContentTypeJSON { + mediaType += ";stream=watch" + } server := &WatchServer{ watching: watcher, scope: scope, useTextFraming: useTextFraming, - mediaType: serializer.MediaType, - framer: serializer.Framer, + mediaType: mediaType, + framer: framer, encoder: encoder, embeddedEncoder: embeddedEncoder, fixup: func(obj runtime.Object) { diff --git a/pkg/apiserver/watch_test.go b/pkg/apiserver/watch_test.go index 7844deccf04..83b3312bbc5 100644 --- a/pkg/apiserver/watch_test.go +++ b/pkg/apiserver/watch_test.go @@ -234,7 +234,8 @@ func TestWatchRead(t *testing.T) { } if response.StatusCode != http.StatusOK { - t.Fatalf("Unexpected response %#v", response) + b, _ := ioutil.ReadAll(response.Body) + t.Fatalf("Unexpected response for accept: %q: %#v\n%s", accept, response, string(b)) } return response.Body, response.Header.Get("Content-Type") } @@ -264,6 +265,11 @@ func TestWatchRead(t *testing.T) { ExpectedContentType: "application/json", MediaType: "application/json", }, + { + Accept: "application/json;stream=watch", + ExpectedContentType: "application/json", // legacy behavior + MediaType: "application/json", + }, // TODO: yaml stream serialization requires that RawExtension.MarshalJSON // be able to understand nested encoding (since yaml calls json.Marshal // rather than yaml.Marshal, which results in the raw bytes being in yaml). @@ -295,10 +301,11 @@ func TestWatchRead(t *testing.T) { for _, protocol := range protocols { for _, test := range testCases { - serializer, ok := api.Codecs.StreamingSerializerForMediaType(test.MediaType, nil) - if !ok { - t.Fatal(serializer) + info, ok := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), test.MediaType) + if !ok || info.StreamSerializer == nil { + t.Fatal(info) } + streamSerializer := info.StreamSerializer r, contentType := protocol.fn(test.Accept) defer r.Close() @@ -306,17 +313,13 @@ func TestWatchRead(t *testing.T) { if contentType != "__default__" && contentType != test.ExpectedContentType { t.Errorf("Unexpected content type: %#v", contentType) } - objectSerializer, ok := api.Codecs.SerializerForMediaType(test.MediaType, nil) - if !ok { - t.Fatal(objectSerializer) - } - objectCodec := api.Codecs.DecoderToVersion(objectSerializer, testInternalGroupVersion) + objectCodec := api.Codecs.DecoderToVersion(info.Serializer, testInternalGroupVersion) var fr io.ReadCloser = r if !protocol.selfFraming { - fr = serializer.Framer.NewFrameReader(r) + fr = streamSerializer.Framer.NewFrameReader(r) } - d := streaming.NewDecoder(fr, serializer) + d := streaming.NewDecoder(fr, streamSerializer.Serializer) var w *watch.FakeWatcher for w == nil { @@ -568,10 +571,11 @@ func TestWatchHTTPTimeout(t *testing.T) { timeoutCh := make(chan time.Time) done := make(chan struct{}) - serializer, ok := api.Codecs.StreamingSerializerForMediaType("application/json", nil) - if !ok { - t.Fatal(serializer) + info, ok := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + if !ok || info.StreamSerializer == nil { + t.Fatal(info) } + serializer := info.StreamSerializer // Setup a new watchserver watchServer := &WatchServer{ diff --git a/pkg/client/restclient/client.go b/pkg/client/restclient/client.go index 9a22ddc139b..52bf13d9368 100644 --- a/pkg/client/restclient/client.go +++ b/pkg/client/restclient/client.go @@ -18,6 +18,7 @@ package restclient import ( "fmt" + "mime" "net/http" "net/url" "os" @@ -153,34 +154,48 @@ func readExpBackoffConfig() BackoffManager { } // createSerializers creates all necessary serializers for given contentType. +// TODO: the negotiated serializer passed to this method should probably return +// serializers that control decoding and versioning without this package +// being aware of the types. Depends on whether RESTClient must deal with +// generic infrastructure. func createSerializers(config ContentConfig) (*Serializers, error) { - negotiated := config.NegotiatedSerializer + mediaTypes := config.NegotiatedSerializer.SupportedMediaTypes() contentType := config.ContentType - info, ok := negotiated.SerializerForMediaType(contentType, nil) - if !ok { - return nil, fmt.Errorf("serializer for %s not registered", contentType) + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + return nil, fmt.Errorf("the content type specified in the client configuration is not recognized: %v", err) } - streamInfo, ok := negotiated.StreamingSerializerForMediaType(contentType, nil) + info, ok := runtime.SerializerInfoForMediaType(mediaTypes, mediaType) if !ok { - return nil, fmt.Errorf("streaming serializer for %s not registered", contentType) + if len(contentType) != 0 || len(mediaTypes) == 0 { + return nil, fmt.Errorf("no serializers registered for %s", contentType) + } + info = mediaTypes[0] } + internalGV := unversioned.GroupVersion{ Group: config.GroupVersion.Group, Version: runtime.APIVersionInternal, } - return &Serializers{ - Encoder: negotiated.EncoderForVersion(info.Serializer, *config.GroupVersion), - Decoder: negotiated.DecoderToVersion(info.Serializer, internalGV), - StreamingSerializer: streamInfo.Serializer, - Framer: streamInfo.Framer, + + s := &Serializers{ + Encoder: config.NegotiatedSerializer.EncoderForVersion(info.Serializer, *config.GroupVersion), + Decoder: config.NegotiatedSerializer.DecoderToVersion(info.Serializer, internalGV), + RenegotiatedDecoder: func(contentType string, params map[string]string) (runtime.Decoder, error) { - renegotiated, ok := negotiated.SerializerForMediaType(contentType, params) + info, ok := runtime.SerializerInfoForMediaType(mediaTypes, contentType) if !ok { return nil, fmt.Errorf("serializer for %s not registered", contentType) } - return negotiated.DecoderToVersion(renegotiated.Serializer, internalGV), nil + return config.NegotiatedSerializer.DecoderToVersion(info.Serializer, internalGV), nil }, - }, nil + } + if info.StreamSerializer != nil { + s.StreamingSerializer = info.StreamSerializer.Serializer + s.Framer = info.StreamSerializer.Framer + } + + return s, nil } // Verb begins a request with a verb (GET, POST, PUT, DELETE). diff --git a/pkg/client/restclient/config_test.go b/pkg/client/restclient/config_test.go index 667b8e60771..e1518ace24f 100644 --- a/pkg/client/restclient/config_test.go +++ b/pkg/client/restclient/config_test.go @@ -153,20 +153,8 @@ var fakeWrapperFunc = func(http.RoundTripper) http.RoundTripper { type fakeNegotiatedSerializer struct{} -func (n *fakeNegotiatedSerializer) SupportedMediaTypes() []string { - return []string{} -} - -func (n *fakeNegotiatedSerializer) SerializerForMediaType(mediaType string, params map[string]string) (s runtime.SerializerInfo, ok bool) { - return runtime.SerializerInfo{}, true -} - -func (n *fakeNegotiatedSerializer) SupportedStreamingMediaTypes() []string { - return []string{} -} - -func (n *fakeNegotiatedSerializer) StreamingSerializerForMediaType(mediaType string, params map[string]string) (s runtime.StreamSerializerInfo, ok bool) { - return runtime.StreamSerializerInfo{}, true +func (n *fakeNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { + return nil } func (n *fakeNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { diff --git a/pkg/client/typed/discovery/discovery_client.go b/pkg/client/typed/discovery/discovery_client.go index 0cb7da4f42e..1b4cec5a1d2 100644 --- a/pkg/client/typed/discovery/discovery_client.go +++ b/pkg/client/typed/discovery/discovery_client.go @@ -306,10 +306,7 @@ func setDiscoveryDefaults(config *restclient.Config) error { config.APIPath = "" config.GroupVersion = nil codec := runtime.NoopEncoder{Decoder: api.Codecs.UniversalDecoder()} - config.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper( - runtime.SerializerInfo{Serializer: codec}, - runtime.StreamSerializerInfo{}, - ) + config.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}) if len(config.UserAgent) == 0 { config.UserAgent = restclient.DefaultKubernetesUserAgent() } diff --git a/pkg/client/typed/dynamic/client.go b/pkg/client/typed/dynamic/client.go index 1565261c5e9..362fce5f4c9 100644 --- a/pkg/client/typed/dynamic/client.go +++ b/pkg/client/typed/dynamic/client.go @@ -241,13 +241,22 @@ func (dynamicCodec) Encode(obj runtime.Object, w io.Writer) error { // ContentConfig returns a restclient.ContentConfig for dynamic types. func ContentConfig() restclient.ContentConfig { - // TODO: it's questionable that this should be using anything other than unstructured schema and JSON - codec := dynamicCodec{} - streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil) + var jsonInfo runtime.SerializerInfo + // TODO: api.Codecs here should become "pkg/apis/server/scheme" which is the minimal core you need + // to talk to a kubernetes server + for _, info := range api.Codecs.SupportedMediaTypes() { + if info.MediaType == runtime.ContentTypeJSON { + jsonInfo = info + break + } + } + + jsonInfo.Serializer = dynamicCodec{} + jsonInfo.PrettySerializer = nil return restclient.ContentConfig{ AcceptContentTypes: runtime.ContentTypeJSON, ContentType: runtime.ContentTypeJSON, - NegotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}, streamingInfo), + NegotiatedSerializer: serializer.NegotiatedSerializerWrapper(jsonInfo), } } diff --git a/pkg/client/typed/dynamic/client_pool.go b/pkg/client/typed/dynamic/client_pool.go index e7ba944dc8b..85f59cac507 100644 --- a/pkg/client/typed/dynamic/client_pool.go +++ b/pkg/client/typed/dynamic/client_pool.go @@ -19,12 +19,9 @@ package dynamic import ( "sync" - "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/client/restclient" - "k8s.io/kubernetes/pkg/runtime" - "k8s.io/kubernetes/pkg/runtime/serializer" ) // ClientPool manages a pool of dynamic clients. @@ -64,6 +61,7 @@ type clientPoolImpl struct { // resources or groups. func NewClientPool(config *restclient.Config, mapper meta.RESTMapper, apiPathResolverFunc APIPathResolverFunc) ClientPool { confCopy := *config + return &clientPoolImpl{ config: &confCopy, clients: map[unversioned.GroupVersion]*Client{}, @@ -108,11 +106,6 @@ func (c *clientPoolImpl) ClientForGroupVersionKind(kind unversioned.GroupVersion // we need to make a client conf.GroupVersion = &gv - if conf.NegotiatedSerializer == nil { - streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil) - conf.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: dynamicCodec{}}, streamingInfo) - } - dynamicClient, err := NewClient(conf) if err != nil { return nil, err diff --git a/pkg/client/typed/dynamic/client_test.go b/pkg/client/typed/dynamic/client_test.go index d0dbe0b7563..04449ae0f13 100644 --- a/pkg/client/typed/dynamic/client_test.go +++ b/pkg/client/typed/dynamic/client_test.go @@ -529,6 +529,7 @@ func TestPatch(t *testing.T) { return } + w.Header().Set("Content-Type", "application/json") w.Write(data) }) if err != nil { diff --git a/pkg/client/unversioned/fake/fake.go b/pkg/client/unversioned/fake/fake.go index ac7833c239c..462566d3ff3 100644 --- a/pkg/client/unversioned/fake/fake.go +++ b/pkg/client/unversioned/fake/fake.go @@ -92,25 +92,25 @@ func (c *RESTClient) request(verb string) *restclient.Request { GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion, NegotiatedSerializer: c.NegotiatedSerializer, } - ns := c.NegotiatedSerializer - serializer, _ := ns.SerializerForMediaType(runtime.ContentTypeJSON, nil) - streamingSerializer, _ := ns.StreamingSerializerForMediaType(runtime.ContentTypeJSON, nil) groupName := api.GroupName if c.GroupName != "" { groupName = c.GroupName } - + ns := c.NegotiatedSerializer + info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) internalVersion := unversioned.GroupVersion{ Group: registered.GroupOrDie(groupName).GroupVersion.Group, Version: runtime.APIVersionInternal, } internalVersion.Version = runtime.APIVersionInternal serializers := restclient.Serializers{ - Encoder: ns.EncoderForVersion(serializer, registered.GroupOrDie(api.GroupName).GroupVersion), - Decoder: ns.DecoderToVersion(serializer, internalVersion), - StreamingSerializer: streamingSerializer, - Framer: streamingSerializer.Framer, + Encoder: ns.EncoderForVersion(info.Serializer, registered.GroupOrDie(api.GroupName).GroupVersion), + Decoder: ns.DecoderToVersion(info.Serializer, internalVersion), + } + if info.StreamSerializer != nil { + serializers.StreamingSerializer = info.StreamSerializer.Serializer + serializers.Framer = info.StreamSerializer.Framer } return restclient.NewRequest(c, verb, &url.URL{Host: "localhost"}, "", config, serializers, nil, nil) } diff --git a/pkg/controller/garbagecollector/garbagecollector_test.go b/pkg/controller/garbagecollector/garbagecollector_test.go index 78b2864e4a7..54174a78ec3 100644 --- a/pkg/controller/garbagecollector/garbagecollector_test.go +++ b/pkg/controller/garbagecollector/garbagecollector_test.go @@ -94,6 +94,7 @@ func (f *fakeActionHandler) ServeHTTP(response http.ResponseWriter, request *htt fakeResponse.statusCode = 200 fakeResponse.content = []byte("{\"kind\": \"List\"}") } + response.Header().Set("Content-Type", "application/json") response.WriteHeader(fakeResponse.statusCode) response.Write(fakeResponse.content) } diff --git a/pkg/controller/garbagecollector/metaonly/metaonly_test.go b/pkg/controller/garbagecollector/metaonly/metaonly_test.go index aad13ff57a7..7c6e879e758 100644 --- a/pkg/controller/garbagecollector/metaonly/metaonly_test.go +++ b/pkg/controller/garbagecollector/metaonly/metaonly_test.go @@ -87,11 +87,11 @@ func verfiyMetadata(description string, t *testing.T, in *MetadataOnlyObject) { func TestDecodeToMetadataOnlyObject(t *testing.T) { data := getPodJson(t) cf := serializer.DirectCodecFactory{CodecFactory: NewMetadataCodecFactory()} - serializer, ok := cf.SerializerForMediaType(runtime.ContentTypeJSON, nil) + info, ok := runtime.SerializerInfoForMediaType(cf.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { t.Fatalf("expected to get a JSON serializer") } - codec := cf.DecoderToVersion(serializer, unversioned.GroupVersion{Group: "SOMEGROUP", Version: "SOMEVERSION"}) + codec := cf.DecoderToVersion(info.Serializer, unversioned.GroupVersion{Group: "SOMEGROUP", Version: "SOMEVERSION"}) // decode with into into := &MetadataOnlyObject{} ret, _, err := codec.Decode(data, nil, into) @@ -133,11 +133,11 @@ func verifyListMetadata(t *testing.T, metaOnlyList *MetadataOnlyObjectList) { func TestDecodeToMetadataOnlyObjectList(t *testing.T) { data := getPodListJson(t) cf := serializer.DirectCodecFactory{CodecFactory: NewMetadataCodecFactory()} - serializer, ok := cf.SerializerForMediaType(runtime.ContentTypeJSON, nil) + info, ok := runtime.SerializerInfoForMediaType(cf.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { t.Fatalf("expected to get a JSON serializer") } - codec := cf.DecoderToVersion(serializer, unversioned.GroupVersion{Group: "SOMEGROUP", Version: "SOMEVERSION"}) + codec := cf.DecoderToVersion(info.Serializer, unversioned.GroupVersion{Group: "SOMEGROUP", Version: "SOMEVERSION"}) // decode with into into := &MetadataOnlyObjectList{} ret, _, err := codec.Decode(data, nil, into) diff --git a/pkg/genericapiserver/storage_factory.go b/pkg/genericapiserver/storage_factory.go index 989bf1c1815..efef1bc3bc3 100644 --- a/pkg/genericapiserver/storage_factory.go +++ b/pkg/genericapiserver/storage_factory.go @@ -243,11 +243,11 @@ func (s *DefaultStorageFactory) Backends() []string { // NewStorageCodec assembles a storage codec for the provided storage media type, the provided serializer, and the requested // storage and memory versions. func NewStorageCodec(storageMediaType string, ns runtime.StorageSerializer, storageVersion, memoryVersion unversioned.GroupVersion, config storagebackend.Config) (runtime.Codec, error) { - mediaType, options, err := mime.ParseMediaType(storageMediaType) + mediaType, _, err := mime.ParseMediaType(storageMediaType) if err != nil { return nil, fmt.Errorf("%q is not a valid mime-type", storageMediaType) } - serializer, ok := ns.SerializerForMediaType(mediaType, options) + serializer, ok := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), mediaType) if !ok { return nil, fmt.Errorf("unable to find serializer for %q", storageMediaType) } diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index ee726cb83d0..f005a9773b1 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -193,9 +193,7 @@ func TestGetUnknownSchemaObjectListGeneric(t *testing.T) { } f, tf, codec := cmdtesting.NewMixedFactory(regularClient) - negotiatedSerializer := serializer.NegotiatedSerializerWrapper( - runtime.SerializerInfo{Serializer: codec}, - runtime.StreamSerializerInfo{}) + negotiatedSerializer := serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}) tf.Printer = &testPrinter{} tf.Client = &fake.RESTClient{ NegotiatedSerializer: negotiatedSerializer, diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index 711ae9516d7..355452b0a67 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -157,9 +157,7 @@ func NewTestFactory() (cmdutil.Factory, *TestFactory, runtime.Codec, runtime.Neg Mapper: mapper, Typer: scheme, } - negotiatedSerializer := serializer.NegotiatedSerializerWrapper( - runtime.SerializerInfo{Serializer: codec}, - runtime.StreamSerializerInfo{}) + negotiatedSerializer := serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}) return &FakeFactory{ tf: t, Codec: codec, diff --git a/pkg/kubelet/config/common_test.go b/pkg/kubelet/config/common_test.go index 082f619ac90..f1ee4ede87d 100644 --- a/pkg/kubelet/config/common_test.go +++ b/pkg/kubelet/config/common_test.go @@ -71,8 +71,8 @@ func TestDecodeSinglePod(t *testing.T) { } for _, gv := range registered.EnabledVersionsForGroup(api.GroupName) { - s, _ := api.Codecs.SerializerForFileExtension("yaml") - encoder := api.Codecs.EncoderForVersion(s, gv) + info, _ := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), "application/yaml") + encoder := api.Codecs.EncoderForVersion(info.Serializer, gv) yaml, err := runtime.Encode(encoder, pod) if err != nil { t.Errorf("unexpected error: %v", err) @@ -134,8 +134,8 @@ func TestDecodePodList(t *testing.T) { } for _, gv := range registered.EnabledVersionsForGroup(api.GroupName) { - s, _ := api.Codecs.SerializerForFileExtension("yaml") - encoder := api.Codecs.EncoderForVersion(s, gv) + info, _ := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), "application/yaml") + encoder := api.Codecs.EncoderForVersion(info.Serializer, gv) yaml, err := runtime.Encode(encoder, podList) if err != nil { t.Errorf("unexpected error: %v", err) diff --git a/pkg/registry/extensions/thirdpartyresourcedata/codec.go b/pkg/registry/extensions/thirdpartyresourcedata/codec.go index 061e350250d..a64ad974e6c 100644 --- a/pkg/registry/extensions/thirdpartyresourcedata/codec.go +++ b/pkg/registry/extensions/thirdpartyresourcedata/codec.go @@ -33,7 +33,6 @@ import ( "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" "k8s.io/kubernetes/pkg/runtime" - "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/yaml" "k8s.io/kubernetes/pkg/watch/versioned" ) @@ -206,32 +205,13 @@ func NewNegotiatedSerializer(s runtime.NegotiatedSerializer, kind string, encode } } -func (t *thirdPartyResourceDataCodecFactory) SupportedMediaTypes() []string { - supported := sets.NewString(t.delegate.SupportedMediaTypes()...) - return supported.Intersection(sets.NewString("application/json", "application/yaml")).List() -} - -func (t *thirdPartyResourceDataCodecFactory) SerializerForMediaType(mediaType string, params map[string]string) (runtime.SerializerInfo, bool) { - switch mediaType { - case "application/json", "application/yaml": - return t.delegate.SerializerForMediaType(mediaType, params) - default: - return runtime.SerializerInfo{}, false - } -} - -func (t *thirdPartyResourceDataCodecFactory) SupportedStreamingMediaTypes() []string { - supported := sets.NewString(t.delegate.SupportedStreamingMediaTypes()...) - return supported.Intersection(sets.NewString("application/json", "application/json;stream=watch")).List() -} - -func (t *thirdPartyResourceDataCodecFactory) StreamingSerializerForMediaType(mediaType string, params map[string]string) (runtime.StreamSerializerInfo, bool) { - switch mediaType { - case "application/json", "application/json;stream=watch": - return t.delegate.StreamingSerializerForMediaType(mediaType, params) - default: - return runtime.StreamSerializerInfo{}, false +func (t *thirdPartyResourceDataCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { + for _, info := range t.delegate.SupportedMediaTypes() { + if info.MediaType == runtime.ContentTypeJSON { + return []runtime.SerializerInfo{info} + } } + return nil } func (t *thirdPartyResourceDataCodecFactory) EncoderForVersion(s runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { diff --git a/pkg/runtime/codec.go b/pkg/runtime/codec.go index d359787a1ea..345dabce4fd 100644 --- a/pkg/runtime/codec.go +++ b/pkg/runtime/codec.go @@ -217,6 +217,22 @@ func (s base64Serializer) Decode(data []byte, defaults *unversioned.GroupVersion return s.Serializer.Decode(out[:n], defaults, into) } +// SerializerInfoForMediaType returns the first info in types that has a matching media type (which cannot +// include media-type parameters), or the first info with an empty media type, or false if no type matches. +func SerializerInfoForMediaType(types []SerializerInfo, mediaType string) (SerializerInfo, bool) { + for _, info := range types { + if info.MediaType == mediaType { + return info, true + } + } + for _, info := range types { + if len(info.MediaType) == 0 { + return info, true + } + } + return SerializerInfo{}, false +} + var ( // InternalGroupVersioner will always prefer the internal version for a given group version kind. InternalGroupVersioner GroupVersioner = internalGroupVersioner{} diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index 22b129300df..e3f03c399e5 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -89,20 +89,28 @@ type Framer interface { // SerializerInfo contains information about a specific serialization format type SerializerInfo struct { - Serializer - // EncodesAsText indicates this serializer can be encoded to UTF-8 safely. - EncodesAsText bool // MediaType is the value that represents this serializer over the wire. MediaType string + // EncodesAsText indicates this serializer can be encoded to UTF-8 safely. + EncodesAsText bool + // Serializer is the individual object serializer for this media type. + Serializer Serializer + // PrettySerializer, if set, can serialize this object in a form biased towards + // readability. + PrettySerializer Serializer + // StreamSerializer, if set, describes the streaming serialization format + // for this media type. + StreamSerializer *StreamSerializerInfo } // StreamSerializerInfo contains information about a specific stream serialization format type StreamSerializerInfo struct { - SerializerInfo + // EncodesAsText indicates this serializer can be encoded to UTF-8 safely. + EncodesAsText bool + // Serializer is the top level object serializer for this type when streaming + Serializer // Framer is the factory for retrieving streams that separate objects on the wire Framer - // Embedded is the type of the nested serialization that should be used. - Embedded SerializerInfo } // NegotiatedSerializer is an interface used for obtaining encoders, decoders, and serializers @@ -110,21 +118,7 @@ type StreamSerializerInfo struct { // that performs HTTP content negotiation to accept multiple formats. type NegotiatedSerializer interface { // SupportedMediaTypes is the media types supported for reading and writing single objects. - SupportedMediaTypes() []string - // SerializerForMediaType returns a serializer for the provided media type. params is the set of - // parameters applied to the media type that may modify the resulting output. ok will be false - // if no serializer matched the media type. - SerializerForMediaType(mediaType string, params map[string]string) (s SerializerInfo, ok bool) - - // SupportedStreamingMediaTypes returns the media types of the supported streaming serializers. - // Streaming serializers control how multiple objects are written to a stream output. - SupportedStreamingMediaTypes() []string - // StreamingSerializerForMediaType returns a serializer for the provided media type that supports - // reading and writing multiple objects to a stream. It returns a framer and serializer, or an - // error if no such serializer can be created. Params is the set of parameters applied to the - // media type that may modify the resulting output. ok will be false if no serializer matched - // the media type. - StreamingSerializerForMediaType(mediaType string, params map[string]string) (s StreamSerializerInfo, ok bool) + SupportedMediaTypes() []SerializerInfo // EncoderForVersion returns an encoder that ensures objects being written to the provided // serializer are in the provided group version. @@ -138,9 +132,8 @@ type NegotiatedSerializer interface { // that can read and write data at rest. This would commonly be used by client tools that must // read files, or server side storage interfaces that persist restful objects. type StorageSerializer interface { - // SerializerForMediaType returns a serializer for the provided media type. Options is a set of - // parameters applied to the media type that may modify the resulting output. - SerializerForMediaType(mediaType string, options map[string]string) (SerializerInfo, bool) + // SupportedMediaTypes are the media types supported for reading and writing objects. + SupportedMediaTypes() []SerializerInfo // UniversalDeserializer returns a Serializer that can read objects in multiple supported formats // by introspecting the data at rest. diff --git a/pkg/runtime/scheme_test.go b/pkg/runtime/scheme_test.go index 06cd5242354..2d71d412714 100644 --- a/pkg/runtime/scheme_test.go +++ b/pkg/runtime/scheme_test.go @@ -84,7 +84,8 @@ func TestScheme(t *testing.T) { codecs := serializer.NewCodecFactory(scheme) codec := codecs.LegacyCodec(externalGV) - jsonserializer, _ := codecs.SerializerForFileExtension("json") + info, _ := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + jsonserializer := info.Serializer simple := &InternalSimple{ TestString: "foo", @@ -150,7 +151,8 @@ func TestScheme(t *testing.T) { func TestBadJSONRejection(t *testing.T) { scheme := runtime.NewScheme() codecs := serializer.NewCodecFactory(scheme) - jsonserializer, _ := codecs.SerializerForFileExtension("json") + info, _ := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + jsonserializer := info.Serializer badJSONMissingKind := []byte(`{ }`) if _, err := runtime.Decode(jsonserializer, badJSONMissingKind); err == nil { diff --git a/pkg/runtime/serializer/codec_factory.go b/pkg/runtime/serializer/codec_factory.go index 4743d30019c..d58c6cce070 100644 --- a/pkg/runtime/serializer/codec_factory.go +++ b/pkg/runtime/serializer/codec_factory.go @@ -36,13 +36,6 @@ type serializerType struct { Serializer runtime.Serializer PrettySerializer runtime.Serializer - // RawSerializer serializes an object without adding a type wrapper. Some serializers, like JSON - // automatically include identifying type information with the JSON. Others, like Protobuf, need - // a wrapper object that includes type information. This serializer should be set if the serializer - // can serialize / deserialize objects without type info. Note that this serializer will always - // be expected to pass into or a gvk to Decode, since no type information will be available on - // the object itself. - RawSerializer runtime.Serializer AcceptStreamContentTypes []string StreamContentType string @@ -65,10 +58,8 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory) []seri Serializer: jsonSerializer, PrettySerializer: jsonPrettySerializer, - AcceptStreamContentTypes: []string{"application/json", "application/json;stream=watch"}, - StreamContentType: "application/json", - Framer: json.Framer, - StreamSerializer: jsonSerializer, + Framer: json.Framer, + StreamSerializer: jsonSerializer, }, { AcceptContentTypes: []string{"application/yaml"}, @@ -76,13 +67,6 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory) []seri FileExtensions: []string{"yaml"}, EncodesAsText: true, Serializer: yamlSerializer, - - // TODO: requires runtime.RawExtension to properly distinguish when the nested content is - // yaml, because the yaml encoder invokes MarshalJSON first - //AcceptStreamContentTypes: []string{"application/yaml", "application/yaml;stream=watch"}, - //StreamContentType: "application/yaml;stream=watch", - //Framer: json.YAMLFramer, - //StreamSerializer: yamlSerializer, }, } @@ -97,11 +81,10 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory) []seri // CodecFactory provides methods for retrieving codecs and serializers for specific // versions and content types. type CodecFactory struct { - scheme *runtime.Scheme - serializers []serializerType - universal runtime.Decoder - accepts []string - streamingAccepts []string + scheme *runtime.Scheme + serializers []serializerType + universal runtime.Decoder + accepts []runtime.SerializerInfo legacySerializer runtime.Serializer } @@ -120,7 +103,7 @@ func NewCodecFactory(scheme *runtime.Scheme) CodecFactory { // newCodecFactory is a helper for testing that allows a different metafactory to be specified. func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) CodecFactory { decoders := make([]runtime.Decoder, 0, len(serializers)) - accepts := []string{} + var accepts []runtime.SerializerInfo alreadyAccepted := make(map[string]struct{}) var legacySerializer runtime.Serializer @@ -131,8 +114,21 @@ func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) Codec continue } alreadyAccepted[mediaType] = struct{}{} - accepts = append(accepts, mediaType) - if mediaType == "application/json" { + info := runtime.SerializerInfo{ + MediaType: d.ContentType, + EncodesAsText: d.EncodesAsText, + Serializer: d.Serializer, + PrettySerializer: d.PrettySerializer, + } + if d.StreamSerializer != nil { + info.StreamSerializer = &runtime.StreamSerializerInfo{ + Serializer: d.StreamSerializer, + EncodesAsText: d.EncodesAsText, + Framer: d.Framer, + } + } + accepts = append(accepts, info) + if mediaType == runtime.ContentTypeJSON { legacySerializer = d.Serializer } } @@ -141,45 +137,22 @@ func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) Codec legacySerializer = serializers[0].Serializer } - streamAccepts := []string{} - alreadyAccepted = make(map[string]struct{}) - for _, d := range serializers { - if len(d.StreamContentType) == 0 { - continue - } - for _, mediaType := range d.AcceptStreamContentTypes { - if _, ok := alreadyAccepted[mediaType]; ok { - continue - } - alreadyAccepted[mediaType] = struct{}{} - streamAccepts = append(streamAccepts, mediaType) - } - } - return CodecFactory{ scheme: scheme, serializers: serializers, universal: recognizer.NewDecoder(decoders...), - accepts: accepts, - streamingAccepts: streamAccepts, + accepts: accepts, legacySerializer: legacySerializer, } } -var _ runtime.NegotiatedSerializer = &CodecFactory{} - // SupportedMediaTypes returns the RFC2046 media types that this factory has serializers for. -func (f CodecFactory) SupportedMediaTypes() []string { +func (f CodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { return f.accepts } -// SupportedStreamingMediaTypes returns the RFC2046 media types that this factory has stream serializers for. -func (f CodecFactory) SupportedStreamingMediaTypes() []string { - return f.streamingAccepts -} - // LegacyCodec encodes output to a given API versions, and decodes output into the internal form from // any recognized source. The returned codec will always encode output to JSON. If a type is not // found in the list of versions an error will be returned. @@ -242,64 +215,6 @@ func (f CodecFactory) EncoderForVersion(encoder runtime.Encoder, gv runtime.Grou return f.CodecForVersions(encoder, nil, gv, nil) } -// SerializerForMediaType returns a serializer that matches the provided RFC2046 mediaType, or false if no such -// serializer exists -func (f CodecFactory) SerializerForMediaType(mediaType string, params map[string]string) (runtime.SerializerInfo, bool) { - for _, s := range f.serializers { - for _, accepted := range s.AcceptContentTypes { - if accepted == mediaType { - // legacy support for ?pretty=1 continues, but this is more formally defined - if v, ok := params["pretty"]; ok && v == "1" && s.PrettySerializer != nil { - return runtime.SerializerInfo{Serializer: s.PrettySerializer, MediaType: s.ContentType, EncodesAsText: s.EncodesAsText}, true - } - - // return the base variant - return runtime.SerializerInfo{Serializer: s.Serializer, MediaType: s.ContentType, EncodesAsText: s.EncodesAsText}, true - } - } - } - return runtime.SerializerInfo{}, false -} - -// StreamingSerializerForMediaType returns a serializer that matches the provided RFC2046 mediaType, or false if no such -// serializer exists -func (f CodecFactory) StreamingSerializerForMediaType(mediaType string, params map[string]string) (runtime.StreamSerializerInfo, bool) { - for _, s := range f.serializers { - for _, accepted := range s.AcceptStreamContentTypes { - if accepted == mediaType { - // TODO: accept params - nested, ok := f.SerializerForMediaType(s.ContentType, nil) - if !ok { - panic("no serializer defined for internal content type") - } - - return runtime.StreamSerializerInfo{ - SerializerInfo: runtime.SerializerInfo{ - Serializer: s.StreamSerializer, - MediaType: s.StreamContentType, - EncodesAsText: s.EncodesAsText, - }, - Framer: s.Framer, - Embedded: nested, - }, true - } - } - } - return runtime.StreamSerializerInfo{}, false -} - -// SerializerForFileExtension returns a serializer for the provided extension, or false if no serializer matches. -func (f CodecFactory) SerializerForFileExtension(extension string) (runtime.Serializer, bool) { - for _, s := range f.serializers { - for _, ext := range s.FileExtensions { - if extension == ext { - return s.Serializer, true - } - } - } - return nil, false -} - // DirectCodecFactory provides methods for retrieving "DirectCodec"s, which do not do conversion. type DirectCodecFactory struct { CodecFactory diff --git a/pkg/runtime/serializer/codec_test.go b/pkg/runtime/serializer/codec_test.go index 87158a0c23e..2d3cbee0980 100644 --- a/pkg/runtime/serializer/codec_test.go +++ b/pkg/runtime/serializer/codec_test.go @@ -252,7 +252,8 @@ func TestTypes(t *testing.T) { func TestVersionedEncoding(t *testing.T) { s, _ := GetTestScheme() cf := newCodecFactory(s, newSerializersForScheme(s, testMetaFactory{})) - encoder, _ := cf.SerializerForFileExtension("json") + info, _ := runtime.SerializerInfoForMediaType(cf.SupportedMediaTypes(), runtime.ContentTypeJSON) + encoder := info.Serializer codec := cf.CodecForVersions(encoder, nil, unversioned.GroupVersion{Version: "v2"}, nil) out, err := runtime.Encode(codec, &TestType1{}) @@ -415,7 +416,8 @@ func GetDirectCodecTestScheme() *runtime.Scheme { func TestDirectCodec(t *testing.T) { s := GetDirectCodecTestScheme() cf := newCodecFactory(s, newSerializersForScheme(s, testMetaFactory{})) - serializer, _ := cf.SerializerForFileExtension("json") + info, _ := runtime.SerializerInfoForMediaType(cf.SupportedMediaTypes(), runtime.ContentTypeJSON) + serializer := info.Serializer df := DirectCodecFactory{cf} ignoredGV, err := unversioned.ParseGroupVersion("ignored group/ignored version") if err != nil { diff --git a/pkg/runtime/serializer/negotiated_codec.go b/pkg/runtime/serializer/negotiated_codec.go index d89177259ff..20337fc40fd 100644 --- a/pkg/runtime/serializer/negotiated_codec.go +++ b/pkg/runtime/serializer/negotiated_codec.go @@ -20,31 +20,18 @@ import ( "k8s.io/kubernetes/pkg/runtime" ) -// TODO: We should figure out what happens when someone asks -// encoder for version and it conflicts with the raw serializer. +// TODO: We should split negotiated serializers that we can change versions on from those we can change +// serialization formats on type negotiatedSerializerWrapper struct { - info runtime.SerializerInfo - streamInfo runtime.StreamSerializerInfo + info runtime.SerializerInfo } -func NegotiatedSerializerWrapper(info runtime.SerializerInfo, streamInfo runtime.StreamSerializerInfo) runtime.NegotiatedSerializer { - return &negotiatedSerializerWrapper{info, streamInfo} +func NegotiatedSerializerWrapper(info runtime.SerializerInfo) runtime.NegotiatedSerializer { + return &negotiatedSerializerWrapper{info} } -func (n *negotiatedSerializerWrapper) SupportedMediaTypes() []string { - return []string{} -} - -func (n *negotiatedSerializerWrapper) SerializerForMediaType(mediaType string, options map[string]string) (runtime.SerializerInfo, bool) { - return n.info, true -} - -func (n *negotiatedSerializerWrapper) SupportedStreamingMediaTypes() []string { - return []string{} -} - -func (n *negotiatedSerializerWrapper) StreamingSerializerForMediaType(mediaType string, options map[string]string) (runtime.StreamSerializerInfo, bool) { - return n.streamInfo, true +func (n *negotiatedSerializerWrapper) SupportedMediaTypes() []runtime.SerializerInfo { + return []runtime.SerializerInfo{n.info} } func (n *negotiatedSerializerWrapper) EncoderForVersion(e runtime.Encoder, _ runtime.GroupVersioner) runtime.Encoder { diff --git a/pkg/runtime/serializer/protobuf_extension.go b/pkg/runtime/serializer/protobuf_extension.go index 5846d94d532..a50dae54089 100644 --- a/pkg/runtime/serializer/protobuf_extension.go +++ b/pkg/runtime/serializer/protobuf_extension.go @@ -26,8 +26,7 @@ const ( // depending on it unintentionally. // TODO: potentially move to pkg/api (since it's part of the Kube public API) and pass it in to the // CodecFactory on initialization. - contentTypeProtobuf = "application/vnd.kubernetes.protobuf" - contentTypeProtobufWatch = contentTypeProtobuf + ";stream=watch" + contentTypeProtobuf = "application/vnd.kubernetes.protobuf" ) func protobufSerializer(scheme *runtime.Scheme) (serializerType, bool) { @@ -38,12 +37,9 @@ func protobufSerializer(scheme *runtime.Scheme) (serializerType, bool) { ContentType: contentTypeProtobuf, FileExtensions: []string{"pb"}, Serializer: serializer, - RawSerializer: raw, - AcceptStreamContentTypes: []string{contentTypeProtobuf, contentTypeProtobufWatch}, - StreamContentType: contentTypeProtobufWatch, - Framer: protobuf.LengthDelimitedFramer, - StreamSerializer: raw, + Framer: protobuf.LengthDelimitedFramer, + StreamSerializer: raw, }, true } diff --git a/plugin/pkg/webhook/webhook.go b/plugin/pkg/webhook/webhook.go index 3c6cc89341e..9dccbcf4499 100755 --- a/plugin/pkg/webhook/webhook.go +++ b/plugin/pkg/webhook/webhook.go @@ -56,10 +56,7 @@ func NewGenericWebhook(kubeConfigFile string, groupVersions []unversioned.GroupV return nil, err } codec := api.Codecs.LegacyCodec(groupVersions...) - clientConfig.ContentConfig.NegotiatedSerializer = runtimeserializer.NegotiatedSerializerWrapper( - runtime.SerializerInfo{Serializer: codec}, - runtime.StreamSerializerInfo{}, - ) + clientConfig.ContentConfig.NegotiatedSerializer = runtimeserializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}) restClient, err := restclient.UnversionedRESTClientFor(clientConfig) if err != nil { diff --git a/test/integration/framework/master_utils.go b/test/integration/framework/master_utils.go index bd0a6c0a7a1..f2ec3449978 100644 --- a/test/integration/framework/master_utils.go +++ b/test/integration/framework/master_utils.go @@ -303,45 +303,46 @@ func NewMasterConfig() *master.Config { Prefix: uuid.New(), } - negotiatedSerializer := NewSingleContentTypeSerializer(api.Scheme, testapi.Default.Codec(), runtime.ContentTypeJSON) + info, _ := runtime.SerializerInfoForMediaType(api.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + ns := NewSingleContentTypeSerializer(api.Scheme, info) - storageFactory := genericapiserver.NewDefaultStorageFactory(config, runtime.ContentTypeJSON, negotiatedSerializer, genericapiserver.NewDefaultResourceEncodingConfig(), master.DefaultAPIResourceConfigSource()) + storageFactory := genericapiserver.NewDefaultStorageFactory(config, runtime.ContentTypeJSON, ns, genericapiserver.NewDefaultResourceEncodingConfig(), master.DefaultAPIResourceConfigSource()) storageFactory.SetSerializer( unversioned.GroupResource{Group: api.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Default.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: autoscaling.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Autoscaling.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: batch.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Batch.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: apps.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Apps.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: extensions.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Extensions.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: policy.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Policy.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: rbac.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Rbac.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: certificates.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Certificates.Codec(), runtime.ContentTypeJSON)) + ns) storageFactory.SetSerializer( unversioned.GroupResource{Group: storage.GroupName, Resource: genericapiserver.AllResources}, "", - NewSingleContentTypeSerializer(api.Scheme, testapi.Storage.Codec(), runtime.ContentTypeJSON)) + ns) genericConfig := genericapiserver.NewConfig() kubeVersion := version.Get() diff --git a/test/integration/framework/serializer.go b/test/integration/framework/serializer.go index 7808d9976cd..28a3982b4e2 100644 --- a/test/integration/framework/serializer.go +++ b/test/integration/framework/serializer.go @@ -22,39 +22,26 @@ import ( ) // NewSingleContentTypeSerializer wraps a serializer in a NegotiatedSerializer that handles one content type -func NewSingleContentTypeSerializer(scheme *runtime.Scheme, serializer runtime.Serializer, contentType string) runtime.StorageSerializer { +func NewSingleContentTypeSerializer(scheme *runtime.Scheme, info runtime.SerializerInfo) runtime.StorageSerializer { return &wrappedSerializer{ - scheme: scheme, - serializer: serializer, - contentType: contentType, + scheme: scheme, + info: info, } } type wrappedSerializer struct { - scheme *runtime.Scheme - serializer runtime.Serializer - contentType string + scheme *runtime.Scheme + info runtime.SerializerInfo } var _ runtime.StorageSerializer = &wrappedSerializer{} -func (s *wrappedSerializer) SupportedMediaTypes() []string { - return []string{s.contentType} -} -func (s *wrappedSerializer) SerializerForMediaType(mediaType string, options map[string]string) (runtime.SerializerInfo, bool) { - if mediaType != s.contentType { - return runtime.SerializerInfo{}, false - } - - return runtime.SerializerInfo{ - Serializer: s.serializer, - MediaType: mediaType, - EncodesAsText: true, // TODO: this should be parameterized - }, true +func (s *wrappedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { + return []runtime.SerializerInfo{s.info} } func (s *wrappedSerializer) UniversalDeserializer() runtime.Decoder { - return s.serializer + return s.info.Serializer } func (s *wrappedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {