diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 47bb8ad3c82..235b8dc9167 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -221,7 +221,11 @@ func serviceErrorHandler(s runtime.NegotiatedSerializer, requestResolver *Reques func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Container, apiPrefix string, getAPIVersionsFunc func(req *restful.Request) *unversioned.APIVersions) { // TODO: InstallREST should register each version automatically - versionHandler := APIVersionHandler(s, getAPIVersionsFunc) + // Because in release 1.1, /api returns response with empty APIVersion, we + // use StripVersionNegotiatedSerializer to keep the response backwards + // compatible. + ss := StripVersionNegotiatedSerializer{s} + versionHandler := APIVersionHandler(ss, getAPIVersionsFunc) ws := new(restful.WebService) ws.Path(apiPrefix) ws.Doc("get available API versions") @@ -233,9 +237,52 @@ func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Contain container.Add(ws) } +// stripVersionEncoder strips APIVersion field from the encoding output. It's +// used to keep the responses at the discovery endpoints backward compatible +// with release-1.1, when the responses have empty APIVersion. +type stripVersionEncoder struct { + encoder runtime.Encoder + serializer runtime.Serializer +} + +func (c stripVersionEncoder) EncodeToStream(obj runtime.Object, w io.Writer, overrides ...unversioned.GroupVersion) error { + buf := bytes.NewBuffer([]byte{}) + err := c.encoder.EncodeToStream(obj, buf, overrides...) + if err != nil { + return err + } + roundTrippedObj, gvk, err := c.serializer.Decode(buf.Bytes(), nil, nil) + if err != nil { + return err + } + gvk.Group = "" + gvk.Version = "" + roundTrippedObj.GetObjectKind().SetGroupVersionKind(gvk) + return c.serializer.EncodeToStream(roundTrippedObj, w) +} + +// StripVersionNegotiatedSerializer will return stripVersionEncoder when +// EncoderForVersion is called. See comments for stripVersionEncoder. +type StripVersionNegotiatedSerializer struct { + runtime.NegotiatedSerializer +} + +func (n StripVersionNegotiatedSerializer) EncoderForVersion(serializer runtime.Serializer, gv unversioned.GroupVersion) runtime.Encoder { + encoder := n.NegotiatedSerializer.EncoderForVersion(serializer, gv) + return stripVersionEncoder{encoder, serializer} +} + +func keepUnversioned(group string) bool { + return group == "" || group == "extensions" +} + // Adds a service to return the supported api versions at /apis. func AddApisWebService(s runtime.NegotiatedSerializer, container *restful.Container, apiPrefix string, f func(req *restful.Request) []unversioned.APIGroup) { - rootAPIHandler := RootAPIHandler(s, f) + // Because in release 1.1, /apis returns response with empty APIVersion, we + // use StripVersionNegotiatedSerializer to keep the response backwards + // compatible. + ss := StripVersionNegotiatedSerializer{s} + rootAPIHandler := RootAPIHandler(ss, f) ws := new(restful.WebService) ws.Path(apiPrefix) ws.Doc("get available API versions") @@ -250,7 +297,14 @@ func AddApisWebService(s runtime.NegotiatedSerializer, container *restful.Contai // Adds a service to return the supported versions, preferred version, and name // of a group. E.g., a such web service will be registered at /apis/extensions. func AddGroupWebService(s runtime.NegotiatedSerializer, container *restful.Container, path string, group unversioned.APIGroup) { - groupHandler := GroupHandler(s, group) + ss := s + if keepUnversioned(group.Name) { + // Because in release 1.1, /apis/extensions returns response with empty + // APIVersion, we use StripVersionNegotiatedSerializer to keep the + // response backwards compatible. + ss = StripVersionNegotiatedSerializer{s} + } + groupHandler := GroupHandler(ss, group) ws := new(restful.WebService) ws.Path(path) ws.Doc("get information of a group") @@ -265,7 +319,14 @@ func AddGroupWebService(s runtime.NegotiatedSerializer, container *restful.Conta // Adds a service to return the supported resources, E.g., a such web service // will be registered at /apis/extensions/v1. func AddSupportedResourcesWebService(s runtime.NegotiatedSerializer, ws *restful.WebService, groupVersion unversioned.GroupVersion, apiResources []unversioned.APIResource) { - resourceHandler := SupportedResourcesHandler(s, groupVersion, apiResources) + ss := s + if keepUnversioned(groupVersion.Group) { + // Because in release 1.1, /apis/extensions/v1beta1 returns response + // with empty APIVersion, we use StripVersionNegotiatedSerializer to + // keep the response backwards compatible. + ss = StripVersionNegotiatedSerializer{s} + } + resourceHandler := SupportedResourcesHandler(ss, groupVersion, apiResources) ws.Route(ws.GET("/").To(resourceHandler). Doc("get available resources"). Operation("getAPIResources"). diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index f108c75d48d..60369d1cfcb 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -260,6 +260,82 @@ func TestGetNodeAddresses(t *testing.T) { assert.Equal([]string{"127.0.0.2", "127.0.0.2"}, addrs) } +// Because we need to be backwards compatible with release 1.1, at endpoints +// that exist in release 1.1, the responses should have empty APIVersion. +func TestAPIVersionOfDiscoveryEndpoints(t *testing.T) { + master, etcdserver, _, assert := newMaster(t) + defer etcdserver.Terminate(t) + + server := httptest.NewServer(master.HandlerContainer.ServeMux) + + // /api exists in release-1.1 + resp, err := http.Get(server.URL + "/api") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + apiVersions := unversioned.APIVersions{} + assert.NoError(decodeResponse(resp, &apiVersions)) + assert.Equal(apiVersions.APIVersion, "") + + // /api/v1 exists in release-1.1 + resp, err = http.Get(server.URL + "/api/v1") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourceList := unversioned.APIResourceList{} + assert.NoError(decodeResponse(resp, &resourceList)) + assert.Equal(resourceList.APIVersion, "") + + // /apis exists in release-1.1 + resp, err = http.Get(server.URL + "/apis") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + groupList := unversioned.APIGroupList{} + assert.NoError(decodeResponse(resp, &groupList)) + assert.Equal(groupList.APIVersion, "") + + // /apis/extensions exists in release-1.1 + resp, err = http.Get(server.URL + "/apis/extensions") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + group := unversioned.APIGroup{} + assert.NoError(decodeResponse(resp, &group)) + assert.Equal(group.APIVersion, "") + + // /apis/extensions/v1beta1 exists in release-1.1 + resp, err = http.Get(server.URL + "/apis/extensions/v1beta1") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourceList = unversioned.APIResourceList{} + assert.NoError(decodeResponse(resp, &resourceList)) + assert.Equal(resourceList.APIVersion, "") + + // /apis/autoscaling doesn't exist in release-1.1, so the APIVersion field + // should be non-empty in the results returned by the server. + resp, err = http.Get(server.URL + "/apis/autoscaling") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + group = unversioned.APIGroup{} + assert.NoError(decodeResponse(resp, &group)) + assert.Equal(group.APIVersion, "v1") + + // apis/autoscaling/v1 doesn't exist in release-1.1, so the APIVersion field + // should be non-empty in the results returned by the server. + + resp, err = http.Get(server.URL + "/apis/autoscaling/v1") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourceList = unversioned.APIResourceList{} + assert.NoError(decodeResponse(resp, &resourceList)) + assert.Equal(resourceList.APIVersion, "v1") + +} + func TestDiscoveryAtAPIS(t *testing.T) { master, etcdserver, config, assert := newMaster(t) defer etcdserver.Terminate(t)