Convert List query parameters via object conversion

Convert url.Values -> an object, with appropriate versioning. ListOptions
should also expose parameter names to swagger.
This commit is contained in:
Clayton Coleman
2015-03-22 17:43:00 -04:00
parent a2801a5a18
commit 1618c39a46
29 changed files with 417 additions and 98 deletions

View File

@@ -29,6 +29,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/emicklei/go-restful"
@@ -60,10 +61,12 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) {
// Initialize the custom handlers.
watchHandler := (&WatchHandler{
storage: a.group.Storage,
codec: a.group.Codec,
linker: a.group.Linker,
info: a.info,
storage: a.group.Storage,
mapper: a.group.Mapper,
convertor: a.group.Convertor,
codec: a.group.Codec,
linker: a.group.Linker,
info: a.info,
})
redirectHandler := (&RedirectHandler{a.group.Storage, a.group.Codec, a.group.Context, a.info})
proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info})
@@ -99,6 +102,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
admit := a.group.Admit
context := a.group.Context
serverVersion := a.group.ServerVersion
if len(serverVersion) == 0 {
serverVersion = a.group.Version
}
var resource, subresource string
switch parts := strings.Split(path, "/"); len(parts) {
case 2:
@@ -152,10 +160,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
storageMeta = defaultStorageMetadata{}
}
versionedListOptions, err := a.group.Creater.New(serverVersion, "ListOptions")
if err != nil {
return err
}
var versionedDeleterObject runtime.Object
switch {
case isGracefulDeleter:
object, err := a.group.Creater.New(a.group.Version, "DeleteOptions")
object, err := a.group.Creater.New(serverVersion, "DeleteOptions")
if err != nil {
return err
}
@@ -288,11 +301,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
// test/integration/auth_test.go is currently the most comprehensive status code test
reqScope := RequestScope{
ContextFunc: ctxFn,
Codec: mapping.Codec,
APIVersion: a.group.Version,
Resource: resource,
Kind: kind,
ContextFunc: ctxFn,
Creater: a.group.Creater,
Convertor: a.group.Convertor,
Codec: mapping.Codec,
APIVersion: a.group.Version,
ServerAPIVersion: serverVersion,
Resource: resource,
Kind: kind,
}
for _, action := range actions {
reqScope.Namer = action.Namer
@@ -314,6 +330,9 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Operation("list" + kind).
Produces("application/json").
Writes(versionedList)
if err := addObjectParams(ws, route, versionedListOptions); err != nil {
return err
}
addParams(route, action.Params)
ws.Route(route)
case "PUT": // Update a resource.
@@ -651,6 +670,39 @@ func addParams(route *restful.RouteBuilder, params []*restful.Parameter) {
}
}
func addObjectParams(ws *restful.WebService, route *restful.RouteBuilder, obj runtime.Object) error {
sv, err := conversion.EnforcePtr(obj)
if err != nil {
return err
}
st := sv.Type()
switch st.Kind() {
case reflect.Struct:
for i := 0; i < st.NumField(); i++ {
name := st.Field(i).Name
sf, ok := st.FieldByName(name)
if !ok {
continue
}
switch sf.Type.Kind() {
case reflect.Interface, reflect.Struct:
default:
jsonTag := sf.Tag.Get("json")
if len(jsonTag) == 0 {
continue
}
jsonName := strings.SplitN(jsonTag, ",", 2)[0]
if len(jsonName) == 0 {
continue
}
desc := sf.Tag.Get("description")
route.Param(ws.QueryParameter(jsonName, desc).DataType(sf.Type.Name()))
}
}
}
return nil
}
// defaultStorageMetadata provides default answers to rest.StorageMetadata.
type defaultStorageMetadata struct{}

View File

@@ -102,12 +102,19 @@ type APIGroupVersion struct {
Root string
Version string
// ServerVersion controls the Kubernetes APIVersion used for common objects in the apiserver
// schema like api.Status, api.DeleteOptions, and api.ListOptions. Other implementors may
// define a version "v1beta1" but want to use the Kubernetes "v1beta3" internal objects. If
// empty, defaults to Version.
ServerVersion string
Mapper meta.RESTMapper
Codec runtime.Codec
Typer runtime.ObjectTyper
Creater runtime.ObjectCreater
Linker runtime.SelfLinker
Codec runtime.Codec
Typer runtime.ObjectTyper
Creater runtime.ObjectCreater
Convertor runtime.ObjectConvertor
Linker runtime.SelfLinker
Admit admission.Interface
Context api.RequestContextMapper

View File

@@ -37,6 +37,7 @@ import (
apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@@ -96,11 +97,10 @@ func init() {
// api.Status is returned in errors
// "internal" version
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{},
&api.Status{})
api.Scheme.AddKnownTypes("", &Simple{}, &SimpleList{}, &api.Status{}, &api.ListOptions{})
// "version" version
// TODO: Use versioned api objects?
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &api.DeleteOptions{}, &api.Status{})
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &v1beta1.DeleteOptions{}, &v1beta1.Status{}, &v1beta1.ListOptions{})
nsMapper := newMapper()
legacyNsMapper := newMapper()
@@ -156,10 +156,11 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
Root: "/api",
Version: testVersion,
Creater: api.Scheme,
Typer: api.Scheme,
Codec: codec,
Linker: selfLinker,
Creater: api.Scheme,
Convertor: api.Scheme,
Typer: api.Scheme,
Codec: codec,
Linker: selfLinker,
Admit: admissionControl,
Context: requestContextMapper,
@@ -244,6 +245,8 @@ func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, f
result := &SimpleList{
Items: storage.list,
}
storage.requestedLabelSelector = label
storage.requestedFieldSelector = field
return result, storage.errors["list"]
}
@@ -522,15 +525,60 @@ func TestList(t *testing.T) {
namespace string
selfLink string
legacy bool
label string
field string
}{
{"/api/version/simple", "", "/api/version/simple?namespace=", true},
{"/api/version/simple?namespace=other", "other", "/api/version/simple?namespace=other", true},
{
url: "/api/version/simple",
namespace: "",
selfLink: "/api/version/simple?namespace=",
legacy: true,
},
{
url: "/api/version/simple?namespace=other",
namespace: "other",
selfLink: "/api/version/simple?namespace=other",
legacy: true,
},
{
url: "/api/version/simple?namespace=other&labels=a%3Db&fields=c%3Dd",
namespace: "other",
selfLink: "/api/version/simple?namespace=other",
legacy: true,
label: "a=b",
field: "c=d",
},
// list items across all namespaces
{"/api/version/simple?namespace=", "", "/api/version/simple?namespace=", true},
{"/api/version/namespaces/default/simple", "default", "/api/version/namespaces/default/simple", false},
{"/api/version/namespaces/other/simple", "other", "/api/version/namespaces/other/simple", false},
{
url: "/api/version/simple?namespace=",
namespace: "",
selfLink: "/api/version/simple?namespace=",
legacy: true,
},
// list items in a namespace, v1beta3+
{
url: "/api/version/namespaces/default/simple",
namespace: "default",
selfLink: "/api/version/namespaces/default/simple",
},
{
url: "/api/version/namespaces/other/simple",
namespace: "other",
selfLink: "/api/version/namespaces/other/simple",
},
{
url: "/api/version/namespaces/other/simple?labels=a%3Db&fields=c%3Dd",
namespace: "other",
selfLink: "/api/version/namespaces/other/simple",
label: "a=b",
field: "c=d",
},
// list items across all namespaces
{"/api/version/simple", "", "/api/version/simple", false},
{
url: "/api/version/simple",
namespace: "",
selfLink: "/api/version/simple",
},
}
for i, testCase := range testCases {
storage := map[string]rest.Storage{}
@@ -567,6 +615,12 @@ func TestList(t *testing.T) {
} else if simpleStorage.actualNamespace != testCase.namespace {
t.Errorf("%d: unexpected resource namespace: %s", i, simpleStorage.actualNamespace)
}
if simpleStorage.requestedLabelSelector == nil || simpleStorage.requestedLabelSelector.String() != testCase.label {
t.Errorf("%d: unexpected label selector: %v", i, simpleStorage.requestedLabelSelector)
}
if simpleStorage.requestedFieldSelector == nil || simpleStorage.requestedFieldSelector.String() != testCase.field {
t.Errorf("%d: unexpected field selector: %v", i, simpleStorage.requestedFieldSelector)
}
}
}

View File

@@ -19,7 +19,6 @@ package apiserver
import (
"fmt"
"net/http"
"net/url"
gpath "path"
"time"
@@ -27,8 +26,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/emicklei/go-restful"
@@ -64,9 +61,15 @@ type RequestScope struct {
Namer ScopeNamer
ContextFunc
runtime.Codec
Creater runtime.ObjectCreater
Convertor runtime.ObjectConvertor
Resource string
Kind string
APIVersion string
// The version of apiserver resources to use
ServerAPIVersion string
}
// GetResource returns a function that handles retrieving a single resource from a rest.Storage object.
@@ -94,24 +97,6 @@ func GetResource(r rest.Getter, scope RequestScope) restful.RouteFunction {
}
}
func parseSelectorQueryParams(query url.Values, version, apiResource string) (label labels.Selector, field fields.Selector, err error) {
labelString := query.Get(api.LabelSelectorQueryParam(version))
label, err = labels.Parse(labelString)
if err != nil {
return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'labels' selector parameter (%s) could not be parsed: %v", labelString, err))
}
convertToInternalVersionFunc := func(label, value string) (newLabel, newValue string, err error) {
return api.Scheme.ConvertFieldLabel(version, apiResource, label, value)
}
fieldString := query.Get(api.FieldSelectorQueryParam(version))
field, err = fields.ParseAndTransformSelector(fieldString, convertToInternalVersionFunc)
if err != nil {
return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'fields' selector parameter (%s) could not be parsed: %v", fieldString, err))
}
return label, field, nil
}
// ListResource returns a function that handles retrieving a list of resources from a rest.Storage object.
func ListResource(r rest.Lister, scope RequestScope) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
@@ -125,13 +110,38 @@ func ListResource(r rest.Lister, scope RequestScope) restful.RouteFunction {
ctx := scope.ContextFunc(req)
ctx = api.WithNamespace(ctx, namespace)
label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), scope.APIVersion, scope.Resource)
// TODO: extract me into a method
query := req.Request.URL.Query()
versioned, err := scope.Creater.New(scope.ServerAPIVersion, "ListOptions")
if err != nil {
// programmer error
errorJSON(err, scope.Codec, w)
return
}
if err := scope.Convertor.Convert(&query, versioned); err != nil {
// bad request
errorJSON(err, scope.Codec, w)
return
}
out, err := scope.Convertor.ConvertToVersion(versioned, "")
if err != nil {
// programmer error
errorJSON(err, scope.Codec, w)
return
}
opts := *out.(*api.ListOptions)
// transform fields
fn := func(label, value string) (newLabel, newValue string, err error) {
return scope.Convertor.ConvertFieldLabel(scope.APIVersion, scope.Kind, label, value)
}
if opts.FieldSelector, err = opts.FieldSelector.Transform(fn); err != nil {
// invalid field
errorJSON(err, scope.Codec, w)
return
}
result, err := r.List(ctx, label, field)
result, err := r.List(ctx, opts.LabelSelector, opts.FieldSelector)
if err != nil {
errorJSON(err, scope.Codec, w)
return

View File

@@ -19,6 +19,7 @@ package apiserver
import (
"fmt"
"net/http"
"net/url"
"path"
"regexp"
"strings"
@@ -26,8 +27,11 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
@@ -36,11 +40,14 @@ import (
"golang.org/x/net/websocket"
)
// TODO: convert me to resthandler custom verb
type WatchHandler struct {
storage map[string]rest.Storage
codec runtime.Codec
linker runtime.SelfLinker
info *APIRequestInfoResolver
storage map[string]rest.Storage
mapper meta.RESTMapper
convertor runtime.ObjectConvertor
codec runtime.Codec
linker runtime.SelfLinker
info *APIRequestInfoResolver
}
// setSelfLinkAddName sets the self link, appending the object's name to the canonical path & type.
@@ -96,8 +103,24 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
httpCode = errorJSON(errors.NewMethodNotSupported(requestInfo.Resource, "watch"), h.codec, w)
return
}
kind := requestInfo.Kind
if len(kind) == 0 {
if _, kind, err = h.mapper.VersionAndKindForResource(apiResource); err != nil {
glog.Errorf("No kind found for %s: %v", apiResource, err)
}
}
label, field, err := parseSelectorQueryParams(req.URL.Query(), requestInfo.APIVersion, apiResource)
scope := RequestScope{
Convertor: h.convertor,
Kind: kind,
Resource: apiResource,
APIVersion: requestInfo.APIVersion,
// TODO: this must be parameterized per version, and is incorrect for implementors
// outside of Kubernetes. Fix by refactoring watch under resthandler as a custome
// resource.
ServerAPIVersion: requestInfo.APIVersion,
}
label, field, err := parseSelectorQueryParams(req.URL.Query(), scope)
if err != nil {
httpCode = errorJSON(err, h.codec, w)
return
@@ -125,6 +148,26 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
// TODO: remove when watcher is refactored to fit under api_installer
func parseSelectorQueryParams(query url.Values, scope RequestScope) (label labels.Selector, field fields.Selector, err error) {
labelString := query.Get(api.LabelSelectorQueryParam(scope.ServerAPIVersion))
label, err = labels.Parse(labelString)
if err != nil {
return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'labels' selector parameter (%s) could not be parsed: %v", labelString, err))
}
fn := func(label, value string) (newLabel, newValue string, err error) {
return scope.Convertor.ConvertFieldLabel(scope.APIVersion, scope.Kind, label, value)
}
fieldString := query.Get(api.FieldSelectorQueryParam(scope.ServerAPIVersion))
field, err = fields.ParseAndTransformSelector(fieldString, fn)
if err != nil {
return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'fields' selector parameter (%s) could not be parsed: %v", fieldString, err))
}
glog.Infof("Found %#v %#v from %v in scope %#v", label, field, query, scope)
return label, field, nil
}
// WatchServer serves a watch.Interface over a websocket or vanilla HTTP.
type WatchServer struct {
watching watch.Interface

View File

@@ -25,6 +25,7 @@ import (
"testing"
"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/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
@@ -48,16 +49,24 @@ var watchTestTable = []struct {
{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) {
simpleStorage := &SimpleRESTStorage{}
_ = rest.Watcher(simpleStorage) // Give compile error if this doesn't work.
handler := handle(map[string]rest.Storage{"foo": simpleStorage})
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
dest, _ := url.Parse(server.URL)
dest.Scheme = "ws" // Required by websocket, though the server never sees it.
dest.Path = "/api/version/watch/foo"
dest.Path = "/api/version/watch/simples"
dest.RawQuery = ""
ws, err := websocket.Dial(dest.String(), "", "http://localhost")
@@ -103,13 +112,13 @@ func TestWatchWebsocket(t *testing.T) {
func TestWatchHTTP(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{"foo": simpleStorage})
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
dest, _ := url.Parse(server.URL)
dest.Path = "/api/version/watch/foo"
dest.Path = "/api/version/watch/simples"
dest.RawQuery = ""
request, err := http.NewRequest("GET", dest.String(), nil)
@@ -163,17 +172,13 @@ func TestWatchHTTP(t *testing.T) {
}
func TestWatchParamParsing(t *testing.T) {
api.Scheme.AddFieldLabelConversionFunc(testVersion, "foo",
func(label, value string) (string, string, error) {
return label, value, nil
})
simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{"foo": simpleStorage})
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
dest, _ := url.Parse(server.URL)
dest.Path = "/api/" + testVersion + "/watch/foo"
dest.Path = "/api/" + testVersion + "/watch/simples"
table := []struct {
rawQuery string
@@ -238,14 +243,14 @@ func TestWatchParamParsing(t *testing.T) {
func TestWatchProtocolSelection(t *testing.T) {
simpleStorage := &SimpleRESTStorage{}
handler := handle(map[string]rest.Storage{"foo": simpleStorage})
handler := handle(map[string]rest.Storage{"simples": simpleStorage})
server := httptest.NewServer(handler)
defer server.Close()
defer server.CloseClientConnections()
client := http.Client{}
dest, _ := url.Parse(server.URL)
dest.Path = "/api/version/watch/foo"
dest.Path = "/api/version/watch/simples"
dest.RawQuery = ""
table := []struct {