diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index e2aa4987a3e..64303b1b29b 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -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 +} diff --git a/pkg/api/latest/latest_test.go b/pkg/api/latest/latest_test.go index 0739b20abdc..301f2ff05d8 100644 --- a/pkg/api/latest/latest_test.go +++ b/pkg/api/latest/latest_test.go @@ -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) + } + } +} diff --git a/pkg/api/meta/interfaces.go b/pkg/api/meta/interfaces.go new file mode 100644 index 00000000000..cfc0a4cfa97 --- /dev/null +++ b/pkg/api/meta/interfaces.go @@ -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) +} diff --git a/pkg/api/meta/meta.go b/pkg/api/meta/meta.go index be855574201..89a6e672b4f 100644 --- a/pkg/api/meta/meta.go +++ b/pkg/api/meta/meta.go @@ -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. diff --git a/pkg/api/meta/restmapper.go b/pkg/api/meta/restmapper.go new file mode 100644 index 00000000000..11541b61ecf --- /dev/null +++ b/pkg/api/meta/restmapper.go @@ -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 +} diff --git a/pkg/api/meta/restmapper_test.go b/pkg/api/meta/restmapper_test.go new file mode 100644 index 00000000000..3935101face --- /dev/null +++ b/pkg/api/meta/restmapper_test.go @@ -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") + } +}