Merge pull request #2059 from smarterclayton/rest_mapping

Define a mapping between REST resource name and kind/apiVersion
This commit is contained in:
Daniel Smith 2014-11-03 15:08:21 -08:00
commit 7c2b7b55e7
6 changed files with 538 additions and 53 deletions

View File

@ -20,6 +20,7 @@ import (
"fmt"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
@ -59,23 +60,21 @@ var ResourceVersioner runtime.ResourceVersioner = accessor
// to go through the InterfacesFor method below.
var SelfLinker runtime.SelfLinker = accessor
// VersionInterfaces contains the interfaces one should use for dealing with types of a particular version.
type VersionInterfaces struct {
runtime.Codec
meta.MetadataAccessor
}
// RESTMapper provides the default mapping between REST paths and the objects declared in api.Scheme and all known
// Kubernetes versions.
var RESTMapper meta.RESTMapper
// InterfacesFor returns the default Codec and ResourceVersioner for a given version
// string, or an error if the version is not known.
func InterfacesFor(version string) (*VersionInterfaces, error) {
func InterfacesFor(version string) (*meta.VersionInterfaces, error) {
switch version {
case "v1beta1":
return &VersionInterfaces{
return &meta.VersionInterfaces{
Codec: v1beta1.Codec,
MetadataAccessor: accessor,
}, nil
case "v1beta2":
return &VersionInterfaces{
return &meta.VersionInterfaces{
Codec: v1beta2.Codec,
MetadataAccessor: accessor,
}, nil
@ -83,3 +82,19 @@ func InterfacesFor(version string) (*VersionInterfaces, error) {
return nil, fmt.Errorf("unsupported storage version: %s (valid: %s)", version, strings.Join(Versions, ", "))
}
}
func init() {
mapper := meta.NewDefaultRESTMapper(
Versions,
func(version string) (*meta.VersionInterfaces, bool) {
interfaces, err := InterfacesFor(version)
if err != nil {
return nil, false
}
return interfaces, true
},
)
mapper.Add(api.Scheme, true, Versions...)
// TODO: when v1beta3 is added it will not use mixed case.
RESTMapper = mapper
}

View File

@ -186,3 +186,40 @@ func TestInterfacesFor(t *testing.T) {
}
}
}
func TestRESTMapper(t *testing.T) {
if v, k, err := RESTMapper.VersionAndKindForResource("replicationControllers"); err != nil || v != Version || k != "ReplicationController" {
t.Errorf("unexpected version mapping: %s %s %v", v, k, err)
}
if v, k, err := RESTMapper.VersionAndKindForResource("replicationcontrollers"); err != nil || v != Version || k != "ReplicationController" {
t.Errorf("unexpected version mapping: %s %s %v", v, k, err)
}
for _, version := range Versions {
mapping, err := RESTMapper.RESTMapping(version, "ReplicationController")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if mapping.Resource != "replicationControllers" && mapping.Resource != "replicationcontrollers" {
t.Errorf("incorrect resource name: %#v", mapping)
}
if mapping.APIVersion != version {
t.Errorf("incorrect version: %v", mapping)
}
interfaces, _ := InterfacesFor(version)
if mapping.Codec != interfaces.Codec {
t.Errorf("unexpected codec: %#v", mapping)
}
rc := &internal.ReplicationController{ObjectMeta: internal.ObjectMeta{Name: "foo"}}
name, err := mapping.MetadataAccessor.Name(rc)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if name != "foo" {
t.Errorf("unable to retrieve object meta with: %v", mapping.MetadataAccessor)
}
}
}

View File

@ -0,0 +1,97 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package meta
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
// VersionInterfaces contains the interfaces one should use for dealing with types of a particular version.
type VersionInterfaces struct {
runtime.Codec
MetadataAccessor
}
// Interface lets you work with object and list metadata from any of the versioned or
// internal API objects. Attempting to set or retrieve a field on an object that does
// not support that field (Name, UID, Namespace on lists) will be a no-op and return
// a default value.
type Interface interface {
Namespace() string
SetNamespace(namespace string)
Name() string
SetName(name string)
UID() string
SetUID(uid string)
APIVersion() string
SetAPIVersion(version string)
Kind() string
SetKind(kind string)
ResourceVersion() string
SetResourceVersion(version string)
SelfLink() string
SetSelfLink(selfLink string)
}
// MetadataAccessor lets you work with object and list metadata from any of the versioned or
// internal API objects. Attempting to set or retrieve a field on an object that does
// not support that field (Name, UID, Namespace on lists) will be a no-op and return
// a default value.
//
// MetadataAccessor exposes Interface in a way that can be used with multiple objects.
type MetadataAccessor interface {
APIVersion(obj runtime.Object) (string, error)
SetAPIVersion(obj runtime.Object, version string) error
Kind(obj runtime.Object) (string, error)
SetKind(obj runtime.Object, kind string) error
Namespace(obj runtime.Object) (string, error)
SetNamespace(obj runtime.Object, namespace string) error
Name(obj runtime.Object) (string, error)
SetName(obj runtime.Object, name string) error
UID(obj runtime.Object) (string, error)
SetUID(obj runtime.Object, uid string) error
SelfLink(obj runtime.Object) (string, error)
SetSelfLink(obj runtime.Object, selfLink string) error
runtime.ResourceVersioner
}
// RESTMapping contains the information needed to deal with objects of a specific
// resource and kind in a RESTful manner.
type RESTMapping struct {
// Resource is a string representing the name of this resource as a REST client would see it
Resource string
// APIVersion represents the APIVersion that represents the resource as presented. It is provided
// for convenience for passing around a consistent mapping.
APIVersion string
runtime.Codec
MetadataAccessor
}
// RESTMapper allows clients to map resources to kind, and map kind and version
// to interfaces for manipulating those objects. It is primarily intended for
// consumers of Kubernetes compatible REST APIs as defined in docs/api-conventions.md.
type RESTMapper interface {
VersionAndKindForResource(resource string) (defaultVersion, kind string, err error)
RESTMapping(version, kind string) (*RESTMapping, error)
}

View File

@ -24,27 +24,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
// Interface lets you work with object and list metadata from any of the versioned or
// internal API objects. Attempting to set or retrieve a field on an object that does
// not support that field (Name, UID, Namespace on lists) will be a no-op and return
// a default value.
type Interface interface {
Namespace() string
SetNamespace(namespace string)
Name() string
SetName(name string)
UID() string
SetUID(uid string)
APIVersion() string
SetAPIVersion(version string)
Kind() string
SetKind(kind string)
ResourceVersion() string
SetResourceVersion(version string)
SelfLink() string
SetSelfLink(selfLink string)
}
// Accessor takes an arbitary object pointer and returns meta.Interface.
// obj must be a pointer to an API type. An error is returned if the minimum
// required fields are missing. Fields that are not required return the default
@ -94,30 +73,6 @@ func Accessor(obj interface{}) (Interface, error) {
return a, nil
}
// MetadataAccessor lets you work with object metadata from any of the versioned or
// internal API objects.
type MetadataAccessor interface {
APIVersion(obj runtime.Object) (string, error)
SetAPIVersion(obj runtime.Object, version string) error
Kind(obj runtime.Object) (string, error)
SetKind(obj runtime.Object, kind string) error
Namespace(obj runtime.Object) (string, error)
SetNamespace(obj runtime.Object, namespace string) error
Name(obj runtime.Object) (string, error)
SetName(obj runtime.Object, name string) error
UID(obj runtime.Object) (string, error)
SetUID(obj runtime.Object, uid string) error
SelfLink(obj runtime.Object) (string, error)
SetSelfLink(obj runtime.Object, selfLink string) error
runtime.ResourceVersioner
}
// NewAccessor returns a MetadataAccessor that can retrieve
// or manipulate resource version on objects derived from core API
// metadata concepts.

165
pkg/api/meta/restmapper.go Normal file
View File

@ -0,0 +1,165 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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
// API object.
type typeMeta struct {
APIVersion string
Kind string
}
// RESTMapper exposes mappings between the types defined in a
// runtime.Scheme. It assumes that all types defined the provided scheme
// can be mapped with the provided MetadataAccessor and Codec interfaces.
//
// The resource name of a Kind is defined as the lowercase,
// English-plural version of the Kind string in v1beta3 and onwards,
// and as the camelCase version of the name in v1beta1 and v1beta2.
// When converting from resource to Kind, the singular version of the
// resource name is also accepted for convenience.
//
// TODO: Only accept plural for some operations for increased control?
// (`get pod bar` vs `get pods bar`)
type DefaultRESTMapper struct {
mapping map[string]typeMeta
reverse map[typeMeta]string
versions []string
interfacesFunc VersionInterfacesFunc
}
// VersionInterfacesFunc returns the appropriate codec and metadata accessor for a
// given api version, or false if no such api version exists.
type VersionInterfacesFunc func(apiVersion string) (*VersionInterfaces, bool)
// NewDefaultRESTMapper initializes a mapping between Kind and APIVersion
// to a resource name and back based on the objects in a runtime.Scheme
// and the Kubernetes API conventions. Takes a priority list of the versions to
// search when an object has no default version (set empty to return an error)
// and a function that retrieves the correct codec and metadata for a given version.
func NewDefaultRESTMapper(versions []string, f VersionInterfacesFunc) *DefaultRESTMapper {
mapping := make(map[string]typeMeta)
reverse := make(map[typeMeta]string)
// TODO: verify name mappings work correctly when versions differ
return &DefaultRESTMapper{
mapping: mapping,
reverse: reverse,
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
}
}
}
// kindToResource converts Kind to a resource name.
func kindToResource(kind string, mixedCase bool) (plural, singular string) {
if mixedCase {
// Legacy support for mixed case names
singular = strings.ToLower(kind[:1]) + kind[1:]
} else {
singular = strings.ToLower(kind)
}
if !strings.HasSuffix(singular, "s") {
plural = singular + "s"
} else {
plural = singular
}
return
}
// VersionAndKindForResource implements RESTMapper
func (m *DefaultRESTMapper) VersionAndKindForResource(resource string) (defaultVersion, kind string, err error) {
meta, ok := m.mapping[resource]
if !ok {
return "", "", fmt.Errorf("no resource %q has been defined", resource)
}
return meta.APIVersion, meta.Kind, nil
}
// RESTMapping returns a struct representing the resource path and conversion interfaces a
// RESTClient should use to operate on the provided version and kind. If a version is not
// provided, the search order provided to DefaultRESTMapper will be used to resolve which
// APIVersion should be used to access the named kind.
func (m *DefaultRESTMapper) RESTMapping(version, kind string) (*RESTMapping, error) {
// Default to a version with this kind
if len(version) == 0 {
for _, v := range m.versions {
if _, ok := m.reverse[typeMeta{APIVersion: v, Kind: kind}]; ok {
version = v
break
}
}
if len(version) == 0 {
return nil, fmt.Errorf("no object named %q is registered.", kind)
}
}
// Ensure we have a REST mapping
resource, ok := m.reverse[typeMeta{APIVersion: version, Kind: kind}]
if !ok {
found := []string{}
for _, v := range m.versions {
if _, ok := m.reverse[typeMeta{APIVersion: v, Kind: kind}]; ok {
found = append(found, v)
}
}
if len(found) > 0 {
return nil, fmt.Errorf("object with kind %q exists in versions %q, not %q", kind, strings.Join(found, ", "), version)
}
return nil, fmt.Errorf("the provided version %q and kind %q cannot be mapped to a supported object", version, kind)
}
interfaces, ok := m.interfacesFunc(version)
if !ok {
return nil, fmt.Errorf("the provided version %q has no relevant versions", version)
}
return &RESTMapping{
Resource: resource,
APIVersion: version,
Codec: interfaces.Codec,
MetadataAccessor: interfaces.MetadataAccessor,
}, nil
}

View File

@ -0,0 +1,216 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package meta
import (
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
type fakeCodec struct{}
func (fakeCodec) Encode(runtime.Object) ([]byte, error) {
return []byte{}, nil
}
func (fakeCodec) Decode([]byte) (runtime.Object, error) {
return nil, nil
}
func (fakeCodec) DecodeInto([]byte, runtime.Object) error {
return nil
}
var validCodec = fakeCodec{}
var validAccessor = resourceAccessor{}
func fakeInterfaces(version string) (*VersionInterfaces, bool) {
return &VersionInterfaces{Codec: validCodec, MetadataAccessor: validAccessor}, true
}
func unmatchedVersionInterfaces(version string) (*VersionInterfaces, bool) {
return nil, false
}
func TestRESTMapperVersionAndKindForResource(t *testing.T) {
testCases := []struct {
Resource string
Kind, APIVersion string
MixedCase bool
Err bool
}{
{Resource: "internalobjec", Err: true},
{Resource: "internalObjec", Err: true},
{Resource: "internalobject", Kind: "InternalObject", APIVersion: "test"},
{Resource: "internalobjects", Kind: "InternalObject", APIVersion: "test"},
{Resource: "internalobject", MixedCase: true, Kind: "InternalObject", APIVersion: "test"},
{Resource: "internalobjects", MixedCase: true, Kind: "InternalObject", APIVersion: "test"},
{Resource: "internalObject", MixedCase: true, Kind: "InternalObject", APIVersion: "test"},
{Resource: "internalObjects", MixedCase: true, Kind: "InternalObject", APIVersion: "test"},
}
for i, testCase := range testCases {
mapper := NewDefaultRESTMapper([]string{"test"}, fakeInterfaces)
scheme := runtime.NewScheme()
scheme.AddKnownTypes("test", &InternalObject{})
mapper.Add(scheme, testCase.MixedCase, "test")
v, k, err := mapper.VersionAndKindForResource(testCase.Resource)
hasErr := err != nil
if hasErr != testCase.Err {
t.Errorf("%d: unexpected error behavior %f: %v", i, testCase.Err, err)
continue
}
if v != testCase.APIVersion || k != testCase.Kind {
t.Errorf("%d: unexpected version and kind: %s %s", i, v, k)
}
}
}
func TestKindToResource(t *testing.T) {
testCases := []struct {
Kind string
MixedCase bool
Plural, Singular string
}{
{Kind: "Pod", MixedCase: true, Plural: "pods", Singular: "pod"},
{Kind: "Pod", MixedCase: true, Plural: "pods", Singular: "pod"},
{Kind: "Pod", MixedCase: false, Plural: "pods", Singular: "pod"},
{Kind: "ReplicationController", MixedCase: true, Plural: "replicationControllers", Singular: "replicationController"},
{Kind: "ReplicationController", MixedCase: true, Plural: "replicationControllers", Singular: "replicationController"},
// API convention changed with regard to capitalization for v1beta3
{Kind: "ReplicationController", MixedCase: false, Plural: "replicationcontrollers", Singular: "replicationcontroller"},
{Kind: "lowercase", MixedCase: false, Plural: "lowercases", Singular: "lowercase"},
// Don't add extra s if the original object is already plural
{Kind: "lowercases", MixedCase: false, Plural: "lowercases", Singular: "lowercases"},
}
for i, testCase := range testCases {
plural, singular := kindToResource(testCase.Kind, testCase.MixedCase)
if singular != testCase.Singular || plural != testCase.Plural {
t.Errorf("%d: unexpected plural and signular: %s %s", i, plural, singular)
}
}
}
func TestRESTMapperRESTMapping(t *testing.T) {
testCases := []struct {
Kind, APIVersion string
MixedCase bool
Resource string
Version string
Err bool
}{
{Kind: "Unknown", APIVersion: "", Err: true},
{Kind: "InternalObject", APIVersion: "test", Resource: "internalobjects"},
{Kind: "InternalObject", APIVersion: "test", Resource: "internalobjects"},
{Kind: "InternalObject", APIVersion: "", Resource: "internalobjects", Version: "test"},
{Kind: "InternalObject", APIVersion: "test", Resource: "internalobjects"},
{Kind: "InternalObject", APIVersion: "test", MixedCase: true, Resource: "internalObjects"},
// TODO: add test for a resource that exists in one version but not another
}
for i, testCase := range testCases {
mapper := NewDefaultRESTMapper([]string{"test"}, fakeInterfaces)
scheme := runtime.NewScheme()
scheme.AddKnownTypes("test", &InternalObject{})
mapper.Add(scheme, testCase.MixedCase, "test")
mapping, err := mapper.RESTMapping(testCase.APIVersion, testCase.Kind)
hasErr := err != nil
if hasErr != testCase.Err {
t.Errorf("%d: unexpected error behavior %f: %v", i, testCase.Err, err)
}
if hasErr {
continue
}
if mapping.Resource != testCase.Resource {
t.Errorf("%d: unexpected resource: %#v", i, mapping)
}
version := testCase.Version
if version == "" {
version = testCase.APIVersion
}
if mapping.APIVersion != version {
t.Errorf("%d: unexpected version: %#v", i, mapping)
}
if mapping.Codec == nil || mapping.MetadataAccessor == nil {
t.Errorf("%d: missing codec and accessor: %#v", i, mapping)
}
}
}
func TestRESTMapperRESTMappingSelectsVersion(t *testing.T) {
mapper := NewDefaultRESTMapper([]string{"test1", "test2"}, fakeInterfaces)
scheme := runtime.NewScheme()
scheme.AddKnownTypes("test1", &InternalObject{})
scheme.AddKnownTypeWithName("test2", "OtherObject", &InternalObject{})
scheme.AddKnownTypeWithName("test3", "OtherObject", &InternalObject{})
mapper.Add(scheme, false, "test1", "test2")
// pick default matching object kind based on search order
mapping, err := mapper.RESTMapping("", "OtherObject")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if mapping.Resource != "otherobjects" || mapping.APIVersion != "test2" {
t.Errorf("unexpected mapping: %#v", mapping)
}
mapping, err = mapper.RESTMapping("", "InternalObject")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if mapping.Resource != "internalobjects" || mapping.APIVersion != "test1" {
t.Errorf("unexpected mapping: %#v", mapping)
}
// mismatch of version
mapping, err = mapper.RESTMapping("test2", "InternalObject")
if err == nil {
t.Errorf("unexpected non-error")
}
mapping, err = mapper.RESTMapping("test1", "OtherObject")
if err == nil {
t.Errorf("unexpected non-error")
}
// not in the search versions
mapping, err = mapper.RESTMapping("test3", "OtherObject")
if err == nil {
t.Errorf("unexpected non-error")
}
}
func TestRESTMapperReportsErrorOnBadVersion(t *testing.T) {
mapper := NewDefaultRESTMapper([]string{"test1", "test2"}, unmatchedVersionInterfaces)
scheme := runtime.NewScheme()
scheme.AddKnownTypes("test1", &InternalObject{})
mapper.Add(scheme, false, "test1")
_, err := mapper.RESTMapping("test1", "InternalObject")
if err == nil {
t.Errorf("unexpected non-error")
}
}