Commit 870da687 authored by Clayton Coleman's avatar Clayton Coleman

Add more extensive tests to apiserver for variations in version

Formalize v1beta1 and v1beta3 style APIs in our test cases.
parent eb0eff69
...@@ -670,6 +670,12 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) { ...@@ -670,6 +670,12 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
} }
} }
// addObjectParams converts a runtime.Object into a set of go-restful Param() definitions on the route.
// The object must be a pointer to a struct; only fields at the top level of the struct that are not
// themselves interfaces or structs are used; only fields with a json tag that is non empty (the standard
// Go JSON behavior for omitting a field) become query parameters. The name of the query parameter is
// the JSON field name. If a description struct tag is set on the field, that description is used on the
// query parameter. In essence, it converts a standard JSON top level object into a query param schema.
func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj runtime.Object) error { func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj runtime.Object) error {
sv, err := conversion.EnforcePtr(obj) sv, err := conversion.EnforcePtr(obj)
if err != nil { if err != nil {
......
...@@ -204,7 +204,11 @@ func APIVersionHandler(versions ...string) restful.RouteFunction { ...@@ -204,7 +204,11 @@ func APIVersionHandler(versions ...string) restful.RouteFunction {
} }
} }
// write renders a returned runtime.Object to the response as a stream or an encoded object. // write renders a returned runtime.Object to the response as a stream or an encoded object. If the object
// returned by the response implements rest.ResourceStreamer that interface will be used to render the
// response. The Accept header and current API version will be passed in, and the output will be copied
// directly to the response body. If content type is returned it is used, otherwise the content type will
// be "application/octet-stream". All other objects are sent to standard JSON serialization.
func write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) { func write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) {
if stream, ok := object.(rest.ResourceStreamer); ok { if stream, ok := object.(rest.ResourceStreamer); ok {
out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept")) out, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept"))
......
...@@ -38,6 +38,7 @@ import ( ...@@ -38,6 +38,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
...@@ -54,11 +55,21 @@ func convert(obj runtime.Object) (runtime.Object, error) { ...@@ -54,11 +55,21 @@ func convert(obj runtime.Object) (runtime.Object, error) {
return obj, nil return obj, nil
} }
// This creates a fake API version, similar to api/latest.go // This creates a fake API version, similar to api/latest.go for a v1beta1 equivalent api. It is distinct
// from the Kubernetes API versions to allow clients to properly distinguish the two.
const testVersion = "version" const testVersion = "version"
var versions = []string{testVersion} // The equivalent of the Kubernetes v1beta3 API.
var codec = runtime.CodecFor(api.Scheme, testVersion) const testVersion2 = "version2"
var versions = []string{testVersion, testVersion2}
var legacyCodec = runtime.CodecFor(api.Scheme, testVersion)
var codec = runtime.CodecFor(api.Scheme, testVersion2)
// these codecs reflect ListOptions/DeleteOptions coming from the serverAPIversion
var versionServerCodec = runtime.CodecFor(api.Scheme, "v1beta1")
var version2ServerCodec = runtime.CodecFor(api.Scheme, "v1beta3")
var accessor = meta.NewAccessor() var accessor = meta.NewAccessor()
var versioner runtime.ResourceVersioner = accessor var versioner runtime.ResourceVersioner = accessor
var selfLinker runtime.SelfLinker = accessor var selfLinker runtime.SelfLinker = accessor
...@@ -70,6 +81,12 @@ func interfacesFor(version string) (*meta.VersionInterfaces, error) { ...@@ -70,6 +81,12 @@ func interfacesFor(version string) (*meta.VersionInterfaces, error) {
switch version { switch version {
case testVersion: case testVersion:
return &meta.VersionInterfaces{ return &meta.VersionInterfaces{
Codec: legacyCodec,
ObjectConvertor: api.Scheme,
MetadataAccessor: accessor,
}, nil
case testVersion2:
return &meta.VersionInterfaces{
Codec: codec, Codec: codec,
ObjectConvertor: api.Scheme, ObjectConvertor: api.Scheme,
MetadataAccessor: accessor, MetadataAccessor: accessor,
...@@ -100,7 +117,10 @@ func init() { ...@@ -100,7 +117,10 @@ func init() {
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, &api.Status{}, &api.ListOptions{}) api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, &api.Status{}, &api.ListOptions{})
// "version" version // "version" version
// TODO: Use versioned api objects? // TODO: Use versioned api objects?
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &v1beta1.DeleteOptions{}, &v1beta1.Status{}, &v1beta1.ListOptions{}) api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &v1beta1.Status{})
// "version2" version
// TODO: Use versioned api objects?
api.Scheme.AddKnownTypes(testVersion2, &Simple{}, &SimpleList{}, &v1beta3.Status{})
nsMapper := newMapper() nsMapper := newMapper()
legacyNsMapper := newMapper() legacyNsMapper := newMapper()
...@@ -118,6 +138,18 @@ func init() { ...@@ -118,6 +138,18 @@ func init() {
namespaceMapper = nsMapper namespaceMapper = nsMapper
admissionControl = admit.NewAlwaysAdmit() admissionControl = admit.NewAlwaysAdmit()
requestContextMapper = api.NewRequestContextMapper() requestContextMapper = api.NewRequestContextMapper()
//mapper.(*meta.DefaultRESTMapper).Add(meta.RESTScopeNamespaceLegacy, "Simple", testVersion, false)
api.Scheme.AddFieldLabelConversionFunc(testVersion, "Simple",
func(label, value string) (string, string, error) {
return label, value, nil
},
)
api.Scheme.AddFieldLabelConversionFunc(testVersion2, "Simple",
func(label, value string) (string, string, error) {
return label, value, nil
},
)
} }
// defaultAPIServer exposes nested objects for testability. // defaultAPIServer exposes nested objects for testability.
...@@ -129,46 +161,61 @@ type defaultAPIServer struct { ...@@ -129,46 +161,61 @@ type defaultAPIServer struct {
// uses the default settings // uses the default settings
func handle(storage map[string]rest.Storage) http.Handler { func handle(storage map[string]rest.Storage) http.Handler {
return handleInternal(storage, admissionControl, mapper, selfLinker) return handleInternal(true, storage, admissionControl, selfLinker)
}
// uses the default settings for a v1beta3 compatible api
func handleNew(storage map[string]rest.Storage) http.Handler {
return handleInternal(false, storage, admissionControl, selfLinker)
} }
// tests with a deny admission controller // tests with a deny admission controller
func handleDeny(storage map[string]rest.Storage) http.Handler { func handleDeny(storage map[string]rest.Storage) http.Handler {
return handleInternal(storage, deny.NewAlwaysDeny(), mapper, selfLinker) return handleInternal(true, storage, deny.NewAlwaysDeny(), selfLinker)
} }
// tests using the new namespace scope mechanism // tests using the new namespace scope mechanism
func handleNamespaced(storage map[string]rest.Storage) http.Handler { func handleNamespaced(storage map[string]rest.Storage) http.Handler {
return handleInternal(storage, admissionControl, namespaceMapper, selfLinker) return handleInternal(false, storage, admissionControl, selfLinker)
} }
// tests using a custom self linker // tests using a custom self linker
func handleLinker(storage map[string]rest.Storage, selfLinker runtime.SelfLinker) http.Handler { func handleLinker(storage map[string]rest.Storage, selfLinker runtime.SelfLinker) http.Handler {
return handleInternal(storage, admissionControl, mapper, selfLinker) return handleInternal(true, storage, admissionControl, selfLinker)
} }
func handleInternal(storage map[string]rest.Storage, admissionControl admission.Interface, mapper meta.RESTMapper, selfLinker runtime.SelfLinker) http.Handler { func handleInternal(legacy bool, storage map[string]rest.Storage, admissionControl admission.Interface, selfLinker runtime.SelfLinker) http.Handler {
group := &APIGroupVersion{ group := &APIGroupVersion{
Storage: storage, Storage: storage,
Mapper: mapper, Root: "/api",
Root: "/api",
Version: testVersion,
Creater: api.Scheme, Creater: api.Scheme,
Convertor: api.Scheme, Convertor: api.Scheme,
Typer: api.Scheme, Typer: api.Scheme,
Codec: codec,
Linker: selfLinker, Linker: selfLinker,
Admit: admissionControl, Admit: admissionControl,
Context: requestContextMapper, Context: requestContextMapper,
} }
if legacy {
group.Version = testVersion
group.ServerVersion = "v1beta1"
group.Codec = legacyCodec
group.Mapper = legacyNamespaceMapper
} else {
group.Version = testVersion2
group.ServerVersion = "v1beta3"
group.Codec = codec
group.Mapper = namespaceMapper
}
container := restful.NewContainer() container := restful.NewContainer()
container.Router(restful.CurlyRouter{}) container.Router(restful.CurlyRouter{})
mux := container.ServeMux mux := container.ServeMux
group.InstallREST(container) if err := group.InstallREST(container); err != nil {
panic(fmt.Sprintf("unable to install container %s: %v", group.Version, err))
}
ws := new(restful.WebService) ws := new(restful.WebService)
InstallSupport(mux, ws) InstallSupport(mux, ws)
container.Add(ws) container.Add(ws)
...@@ -557,27 +604,27 @@ func TestList(t *testing.T) { ...@@ -557,27 +604,27 @@ func TestList(t *testing.T) {
}, },
// list items in a namespace, v1beta3+ // list items in a namespace, v1beta3+
{ {
url: "/api/version/namespaces/default/simple", url: "/api/version2/namespaces/default/simple",
namespace: "default", namespace: "default",
selfLink: "/api/version/namespaces/default/simple", selfLink: "/api/version2/namespaces/default/simple",
}, },
{ {
url: "/api/version/namespaces/other/simple", url: "/api/version2/namespaces/other/simple",
namespace: "other", namespace: "other",
selfLink: "/api/version/namespaces/other/simple", selfLink: "/api/version2/namespaces/other/simple",
}, },
{ {
url: "/api/version/namespaces/other/simple?labels=a%3Db&fields=c%3Dd", url: "/api/version2/namespaces/other/simple?labelSelector=a%3Db&fieldSelector=c%3Dd",
namespace: "other", namespace: "other",
selfLink: "/api/version/namespaces/other/simple", selfLink: "/api/version2/namespaces/other/simple",
label: "a=b", label: "a=b",
field: "c=d", field: "c=d",
}, },
// list items across all namespaces // list items across all namespaces
{ {
url: "/api/version/simple", url: "/api/version2/simple",
namespace: "", namespace: "",
selfLink: "/api/version/simple", selfLink: "/api/version2/simple",
}, },
} }
for i, testCase := range testCases { for i, testCase := range testCases {
...@@ -593,7 +640,7 @@ func TestList(t *testing.T) { ...@@ -593,7 +640,7 @@ func TestList(t *testing.T) {
if testCase.legacy { if testCase.legacy {
handler = handleLinker(storage, selfLinker) handler = handleLinker(storage, selfLinker)
} else { } else {
handler = handleInternal(storage, admissionControl, namespaceMapper, selfLinker) handler = handleInternal(false, storage, admissionControl, selfLinker)
} }
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
...@@ -605,6 +652,9 @@ func TestList(t *testing.T) { ...@@ -605,6 +652,9 @@ func TestList(t *testing.T) {
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
t.Errorf("%d: unexpected status: %d, Expected: %d, %#v", i, resp.StatusCode, http.StatusOK, resp) t.Errorf("%d: unexpected status: %d, Expected: %d, %#v", i, resp.StatusCode, http.StatusOK, resp)
body, _ := ioutil.ReadAll(resp.Body)
t.Logf("%d: body: %s", string(body))
continue
} }
// TODO: future, restore get links // TODO: future, restore get links
if !selfLinker.called { if !selfLinker.called {
...@@ -875,16 +925,16 @@ func TestGetNamespaceSelfLink(t *testing.T) { ...@@ -875,16 +925,16 @@ func TestGetNamespaceSelfLink(t *testing.T) {
} }
selfLinker := &setTestSelfLinker{ selfLinker := &setTestSelfLinker{
t: t, t: t,
expectedSet: "/api/version/namespaces/foo/simple/id", expectedSet: "/api/version2/namespaces/foo/simple/id",
name: "id", name: "id",
namespace: "foo", namespace: "foo",
} }
storage["simple"] = &simpleStorage storage["simple"] = &simpleStorage
handler := handleInternal(storage, admissionControl, namespaceMapper, selfLinker) handler := handleInternal(false, storage, admissionControl, selfLinker)
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
resp, err := http.Get(server.URL + "/api/version/namespaces/foo/simple/id") resp, err := http.Get(server.URL + "/api/version2/namespaces/foo/simple/id")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
...@@ -959,7 +1009,7 @@ func TestDeleteWithOptions(t *testing.T) { ...@@ -959,7 +1009,7 @@ func TestDeleteWithOptions(t *testing.T) {
item := &api.DeleteOptions{ item := &api.DeleteOptions{
GracePeriodSeconds: &grace, GracePeriodSeconds: &grace,
} }
body, err := codec.Encode(item) body, err := versionServerCodec.Encode(item)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
...@@ -1020,7 +1070,7 @@ func TestLegacyDeleteIgnoresOptions(t *testing.T) { ...@@ -1020,7 +1070,7 @@ func TestLegacyDeleteIgnoresOptions(t *testing.T) {
defer server.Close() defer server.Close()
item := api.NewDeleteOptions(300) item := api.NewDeleteOptions(300)
body, err := codec.Encode(item) body, err := versionServerCodec.Encode(item)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
...@@ -1629,7 +1679,7 @@ func TestCreateInvokesAdmissionControl(t *testing.T) { ...@@ -1629,7 +1679,7 @@ func TestCreateInvokesAdmissionControl(t *testing.T) {
namespace: "other", namespace: "other",
expectedSet: "/api/version/foo/bar?namespace=other", expectedSet: "/api/version/foo/bar?namespace=other",
} }
handler := handleInternal(map[string]rest.Storage{"foo": &storage}, deny.NewAlwaysDeny(), mapper, selfLinker) handler := handleInternal(true, map[string]rest.Storage{"foo": &storage}, deny.NewAlwaysDeny(), selfLinker)
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
defer server.Close() defer server.Close()
client := http.Client{} client := http.Client{}
......
...@@ -294,7 +294,7 @@ func TestProxy(t *testing.T) { ...@@ -294,7 +294,7 @@ func TestProxy(t *testing.T) {
server *httptest.Server server *httptest.Server
proxyTestPattern string proxyTestPattern string
}{ }{
{namespaceServer, "/api/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, {namespaceServer, "/api/version2/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path},
{legacyNamespaceServer, "/api/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace}, {legacyNamespaceServer, "/api/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace},
} }
...@@ -348,7 +348,7 @@ func TestProxyUpgrade(t *testing.T) { ...@@ -348,7 +348,7 @@ func TestProxyUpgrade(t *testing.T) {
server := httptest.NewServer(namespaceHandler) server := httptest.NewServer(namespaceHandler)
defer server.Close() defer server.Close()
ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/api/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/") ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/api/version2/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/")
if err != nil { if err != nil {
t.Fatalf("websocket dial err: %s", err) t.Fatalf("websocket dial err: %s", err)
} }
......
...@@ -105,7 +105,7 @@ func TestRedirectWithNamespaces(t *testing.T) { ...@@ -105,7 +105,7 @@ func TestRedirectWithNamespaces(t *testing.T) {
for _, item := range table { for _, item := range table {
simpleStorage.errors["resourceLocation"] = item.err simpleStorage.errors["resourceLocation"] = item.err
simpleStorage.resourceLocation = &url.URL{Host: item.id} simpleStorage.resourceLocation = &url.URL{Host: item.id}
resp, err := client.Get(server.URL + "/api/version/redirect/namespaces/other/foo/" + item.id) resp, err := client.Get(server.URL + "/api/version2/redirect/namespaces/other/foo/" + item.id)
if resp == nil { if resp == nil {
t.Fatalf("Unexpected nil resp") t.Fatalf("Unexpected nil resp")
} }
......
...@@ -25,7 +25,6 @@ import ( ...@@ -25,7 +25,6 @@ import (
"testing" "testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
...@@ -49,14 +48,6 @@ var watchTestTable = []struct { ...@@ -49,14 +48,6 @@ var watchTestTable = []struct {
{watch.Deleted, &Simple{ObjectMeta: api.ObjectMeta{Name: "bar"}}}, {watch.Deleted, &Simple{ObjectMeta: api.ObjectMeta{Name: "bar"}}},
} }
func init() {
mapper.(*meta.DefaultRESTMapper).Add(meta.RESTScopeNamespaceLegacy, "Simple", testVersion, false)
api.Scheme.AddFieldLabelConversionFunc(testVersion, "Simple",
func(label, value string) (string, string, error) {
return label, value, nil
})
}
func TestWatchWebsocket(t *testing.T) { func TestWatchWebsocket(t *testing.T) {
simpleStorage := &SimpleRESTStorage{} simpleStorage := &SimpleRESTStorage{}
_ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work. _ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work.
......
...@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and ...@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// Defines conversions between generic types and structs to map query strings
// to struct objects.
package runtime package runtime
import ( import (
......
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