Make a RESTMapper scope aware

pull/6/head
derekwaynecarr 2015-01-29 11:35:06 -05:00
parent e335e2d3e2
commit 71ec444d63
5 changed files with 122 additions and 39 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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,