From 71ec444d63665744f03251ceab6835f8f0d81a1e Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Thu, 29 Jan 2015 11:35:06 -0500 Subject: [PATCH] Make a RESTMapper scope aware --- pkg/api/latest/latest.go | 59 ++++++++++++++++++++++++++++++++-- pkg/api/meta/interfaces.go | 18 +++++++++++ pkg/api/meta/restmapper.go | 45 +++++++++++++------------- pkg/apiserver/apiserver.go | 36 ++++++++++++--------- pkg/kubectl/resource/mapper.go | 3 ++ 5 files changed, 122 insertions(+), 39 deletions(-) diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index 2a13f1a356..24a1600aba 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -103,7 +103,62 @@ func init() { return interfaces, true }, ) - mapper.Add(api.Scheme, true, "v1beta1", "v1beta2") - mapper.Add(api.Scheme, false, "v1beta3") + // scopes that are used to qualify resources in the API + namespaceAsQueryParam := meta.RESTScope{ + Name: "namespace", + ParamName: "namespace", + ParamPath: false, + ParamDescription: "object name and auth scope, such as for teams and projects", + } + namespaceAsPathParam := meta.RESTScope{ + Name: "namespace", + ParamName: "ns", + ParamPath: true, + ParamDescription: "object name and auth scope, such as for teams and projects", + } + rootScope := meta.RESTScope{ + Name: "root", + ParamName: "", + ParamPath: true, + } + + // list of versions we support on the server + versions := []string{"v1beta1", "v1beta2", "v1beta3"} + + // versions that used mixed case URL formats + versionMixedCase := map[string]bool{ + "v1beta1": true, + "v1beta2": true, + } + + // backwards compatibility, prior to v1beta3, we identified the namespace as a query parameter + versionToNamespaceScope := map[string]meta.RESTScope{ + "v1beta1": namespaceAsQueryParam, + "v1beta2": namespaceAsQueryParam, + "v1beta3": namespaceAsPathParam, + } + + // the list of kinds that are scoped at the root of the api hierarchy + // if a kind is not enumerated here, it is assumed to have a namespace scope + kindToRootScope := map[string]bool{ + "Node": true, + "Minion": true, + } + + // enumerate all supported versions, get the kinds, and register with the mapper how to address our resources + for _, version := range versions { + for kind := range api.Scheme.KnownTypes(version) { + mixedCase, found := versionMixedCase[version] + if !found { + mixedCase = false + } + scope := versionToNamespaceScope[version] + _, found = kindToRootScope[kind] + if found { + scope = rootScope + } + mapper.Add(scope, kind, version, mixedCase) + } + } RESTMapper = mapper } diff --git a/pkg/api/meta/interfaces.go b/pkg/api/meta/interfaces.go index 17161d1e47..b56dcf802a 100644 --- a/pkg/api/meta/interfaces.go +++ b/pkg/api/meta/interfaces.go @@ -94,6 +94,21 @@ type MetadataAccessor interface { runtime.ResourceVersioner } +// RESTScope contains the information needed to deal with REST Resources that are in a resource hierarchy +type RESTScope struct { + // Name of the scope (e.g. "cluster", "namespace", etc.) + Name string + // ParamName is the optional name of the parameter that should be inserted in the resource url + // If empty, no param will be inserted + ParamName string + // ParamPath is a boolean that controls how the parameter is manifested in resource paths + // If true, this parameter is encoded in path (i.e. /{paramName}/{paramValue}) + // If false, this parameter is encoded in query (i.e. ?{paramName}={paramValue}) + ParamPath bool + // ParamDescription is the optional description to use to document the parameter in api documentation + ParamDescription string +} + // RESTMapping contains the information needed to deal with objects of a specific // resource and kind in a RESTful manner. type RESTMapping struct { @@ -104,6 +119,9 @@ type RESTMapping struct { APIVersion string Kind string + // Scope contains the information needed to deal with REST Resources that are in a resource hierarchy + Scope RESTScope + runtime.Codec runtime.ObjectConvertor MetadataAccessor diff --git a/pkg/api/meta/restmapper.go b/pkg/api/meta/restmapper.go index 9ce592e439..e4c751cb16 100644 --- a/pkg/api/meta/restmapper.go +++ b/pkg/api/meta/restmapper.go @@ -19,8 +19,6 @@ package meta import ( "fmt" "strings" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) // typeMeta is used as a key for lookup in the mapping between REST path and @@ -45,6 +43,7 @@ type typeMeta struct { type DefaultRESTMapper struct { mapping map[string]typeMeta reverse map[typeMeta]string + scopes map[typeMeta]RESTScope versions []string interfacesFunc VersionInterfacesFunc } @@ -61,36 +60,31 @@ type VersionInterfacesFunc func(apiVersion string) (*VersionInterfaces, bool) func NewDefaultRESTMapper(versions []string, f VersionInterfacesFunc) *DefaultRESTMapper { mapping := make(map[string]typeMeta) reverse := make(map[typeMeta]string) + scopes := make(map[typeMeta]RESTScope) // TODO: verify name mappings work correctly when versions differ return &DefaultRESTMapper{ - mapping: mapping, - reverse: reverse, - + mapping: mapping, + reverse: reverse, + scopes: scopes, versions: versions, interfacesFunc: f, } } -// Add adds objects from a runtime.Scheme and its named versions to this map. -// If mixedCase is true, the legacy v1beta1/v1beta2 Kubernetes resource naming convention -// will be applied (camelCase vs lowercase). -func (m *DefaultRESTMapper) Add(scheme *runtime.Scheme, mixedCase bool, versions ...string) { - for _, version := range versions { - for kind := range scheme.KnownTypes(version) { - plural, singular := kindToResource(kind, mixedCase) - meta := typeMeta{APIVersion: version, Kind: kind} - if _, ok := m.mapping[plural]; !ok { - m.mapping[plural] = meta - m.mapping[singular] = meta - if strings.ToLower(plural) != plural { - m.mapping[strings.ToLower(plural)] = meta - m.mapping[strings.ToLower(singular)] = meta - } - } - m.reverse[meta] = plural +func (m *DefaultRESTMapper) Add(scope RESTScope, kind string, version string, mixedCase bool) { + plural, singular := kindToResource(kind, mixedCase) + meta := typeMeta{APIVersion: version, Kind: kind} + if _, ok := m.mapping[plural]; !ok { + m.mapping[plural] = meta + m.mapping[singular] = meta + if strings.ToLower(plural) != plural { + m.mapping[strings.ToLower(plural)] = meta + m.mapping[strings.ToLower(singular)] = meta } } + m.reverse[meta] = plural + m.scopes[meta] = scope } // kindToResource converts Kind to a resource name. @@ -167,6 +161,12 @@ func (m *DefaultRESTMapper) RESTMapping(kind string, versions ...string) (*RESTM return nil, fmt.Errorf("the provided version %q and kind %q cannot be mapped to a supported object", version, kind) } + // Ensure we have a REST scope + scope, ok := m.scopes[typeMeta{APIVersion: version, Kind: kind}] + if !ok { + return nil, fmt.Errorf("the provided version %q and kind %q cannot be mapped to a supported scope", version, kind) + } + interfaces, ok := m.interfacesFunc(version) if !ok { return nil, fmt.Errorf("the provided version %q has no relevant versions", version) @@ -176,6 +176,7 @@ func (m *DefaultRESTMapper) RESTMapping(kind string, versions ...string) (*RESTM Resource: resource, APIVersion: version, Kind: kind, + Scope: scope, Codec: interfaces.Codec, ObjectConvertor: interfaces.ObjectConvertor, diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 54aad06a9e..1b15dadfb7 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -29,6 +29,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -99,7 +100,7 @@ func indirectArbitraryPointer(ptrToObject interface{}) interface{} { return reflect.Indirect(reflect.ValueOf(ptrToObject)).Interface() } -func registerResourceHandlers(ws *restful.WebService, version string, path string, storage RESTStorage, h restful.RouteFunction, namespaceScope bool) error { +func registerResourceHandlers(ws *restful.WebService, version string, path string, storage RESTStorage, h restful.RouteFunction) error { object := storage.New() _, kind, err := api.Scheme.ObjectVersionAndKind(object) if err != nil { @@ -111,21 +112,31 @@ func registerResourceHandlers(ws *restful.WebService, version string, path strin } versionedObject := indirectArbitraryPointer(versionedPtr) + mapper := latest.RESTMapper + mapping, err := mapper.RESTMapping(kind, version) + if err != nil { + glog.V(1).Infof("OH NOES kind %s version %s err: %v", kind, version, err) + return err + } + // See github.com/emicklei/go-restful/blob/master/jsr311.go for routing logic // and status-code behavior - if namespaceScope { - path = "ns/{namespace}/" + path + // check if this + scope := mapping.Scope + var scopeParam *restful.Parameter + if len(scope.ParamName) > 0 && scope.ParamPath { + path = scope.ParamName + "/{" + scope.ParamName + "}/" + path + scopeParam = ws.PathParameter(scope.ParamName, scope.ParamDescription).DataType("string") } glog.V(5).Infof("Installing version=/%s, kind=/%s, path=/%s", version, kind, path) nameParam := ws.PathParameter("name", "name of the "+kind).DataType("string") - namespaceParam := ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string") createRoute := ws.POST(path).To(h). Doc("create a " + kind). Operation("create" + kind) - addParamIf(createRoute, namespaceParam, namespaceScope) + addParamIf(createRoute, scopeParam, scopeParam != nil) if _, ok := storage.(RESTCreater); ok { ws.Route(createRoute.Reads(versionedObject)) // from the request } else { @@ -135,7 +146,7 @@ func registerResourceHandlers(ws *restful.WebService, version string, path strin listRoute := ws.GET(path).To(h). Doc("list objects of kind " + kind). Operation("list" + kind) - addParamIf(listRoute, namespaceParam, namespaceScope) + addParamIf(listRoute, scopeParam, scopeParam != nil) if lister, ok := storage.(RESTLister); ok { list := lister.NewList() _, listKind, err := api.Scheme.ObjectVersionAndKind(list) @@ -154,7 +165,7 @@ func registerResourceHandlers(ws *restful.WebService, version string, path strin Doc("read the specified " + kind). Operation("read" + kind). Param(nameParam) - addParamIf(getRoute, namespaceParam, namespaceScope) + addParamIf(getRoute, scopeParam, scopeParam != nil) if _, ok := storage.(RESTGetter); ok { ws.Route(getRoute.Writes(versionedObject)) // on the response } else { @@ -165,7 +176,7 @@ func registerResourceHandlers(ws *restful.WebService, version string, path strin Doc("update the specified " + kind). Operation("update" + kind). Param(nameParam) - addParamIf(updateRoute, namespaceParam, namespaceScope) + addParamIf(updateRoute, scopeParam, scopeParam != nil) if _, ok := storage.(RESTUpdater); ok { ws.Route(updateRoute.Reads(versionedObject)) // from the request } else { @@ -177,7 +188,7 @@ func registerResourceHandlers(ws *restful.WebService, version string, path strin Doc("delete the specified " + kind). Operation("delete" + kind). Param(nameParam) - addParamIf(deleteRoute, namespaceParam, namespaceScope) + addParamIf(deleteRoute, scopeParam, scopeParam != nil) if _, ok := storage.(RESTDeleter); ok { ws.Route(deleteRoute) } else { @@ -252,12 +263,7 @@ func (g *APIGroupVersion) InstallREST(container *restful.Container, mux Mux, roo registrationErrors := make([]error, 0) for path, storage := range g.handler.storage { - // register legacy patterns where namespace is optional in path - if err := registerResourceHandlers(ws, version, path, storage, h, false); err != nil { - registrationErrors = append(registrationErrors, err) - } - // register pattern where namespace is required in path - if err := registerResourceHandlers(ws, version, path, storage, h, true); err != nil { + if err := registerResourceHandlers(ws, version, path, storage, h); err != nil { registrationErrors = append(registrationErrors, err) } } diff --git a/pkg/kubectl/resource/mapper.go b/pkg/kubectl/resource/mapper.go index 874eddbe87..0d58572b62 100644 --- a/pkg/kubectl/resource/mapper.go +++ b/pkg/kubectl/resource/mapper.go @@ -54,6 +54,9 @@ func (m *Mapper) InfoForData(data []byte, source string) (*Info, error) { } name, _ := mapping.MetadataAccessor.Name(obj) namespace, _ := mapping.MetadataAccessor.Namespace(obj) + if mapping.Scope.Name != "namespace" { + namespace = "" + } resourceVersion, _ := mapping.MetadataAccessor.ResourceVersion(obj) return &Info{ Mapping: mapping,