| /* |
| Copyright 2014 The Kubernetes Authors. |
| |
| 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. |
| */ |
| |
| // TODO: move everything in this file to pkg/api/rest |
| package meta |
| |
| import ( |
| "fmt" |
| "sort" |
| "strings" |
| |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/apimachinery/pkg/runtime/schema" |
| ) |
| |
| // Implements RESTScope interface |
| type restScope struct { |
| name RESTScopeName |
| } |
| |
| func (r *restScope) Name() RESTScopeName { |
| return r.name |
| } |
| |
| var RESTScopeNamespace = &restScope{ |
| name: RESTScopeNameNamespace, |
| } |
| |
| var RESTScopeRoot = &restScope{ |
| name: RESTScopeNameRoot, |
| } |
| |
| // DefaultRESTMapper 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. |
| // 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 { |
| defaultGroupVersions []schema.GroupVersion |
| |
| resourceToKind map[schema.GroupVersionResource]schema.GroupVersionKind |
| kindToPluralResource map[schema.GroupVersionKind]schema.GroupVersionResource |
| kindToScope map[schema.GroupVersionKind]RESTScope |
| singularToPlural map[schema.GroupVersionResource]schema.GroupVersionResource |
| pluralToSingular map[schema.GroupVersionResource]schema.GroupVersionResource |
| } |
| |
| func (m *DefaultRESTMapper) String() string { |
| return fmt.Sprintf("DefaultRESTMapper{kindToPluralResource=%v}", m.kindToPluralResource) |
| } |
| |
| var _ RESTMapper = &DefaultRESTMapper{} |
| |
| // 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 group name, 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 metadata for a given version. |
| func NewDefaultRESTMapper(defaultGroupVersions []schema.GroupVersion) *DefaultRESTMapper { |
| resourceToKind := make(map[schema.GroupVersionResource]schema.GroupVersionKind) |
| kindToPluralResource := make(map[schema.GroupVersionKind]schema.GroupVersionResource) |
| kindToScope := make(map[schema.GroupVersionKind]RESTScope) |
| singularToPlural := make(map[schema.GroupVersionResource]schema.GroupVersionResource) |
| pluralToSingular := make(map[schema.GroupVersionResource]schema.GroupVersionResource) |
| // TODO: verify name mappings work correctly when versions differ |
| |
| return &DefaultRESTMapper{ |
| resourceToKind: resourceToKind, |
| kindToPluralResource: kindToPluralResource, |
| kindToScope: kindToScope, |
| defaultGroupVersions: defaultGroupVersions, |
| singularToPlural: singularToPlural, |
| pluralToSingular: pluralToSingular, |
| } |
| } |
| |
| func (m *DefaultRESTMapper) Add(kind schema.GroupVersionKind, scope RESTScope) { |
| plural, singular := UnsafeGuessKindToResource(kind) |
| m.AddSpecific(kind, plural, singular, scope) |
| } |
| |
| func (m *DefaultRESTMapper) AddSpecific(kind schema.GroupVersionKind, plural, singular schema.GroupVersionResource, scope RESTScope) { |
| m.singularToPlural[singular] = plural |
| m.pluralToSingular[plural] = singular |
| |
| m.resourceToKind[singular] = kind |
| m.resourceToKind[plural] = kind |
| |
| m.kindToPluralResource[kind] = plural |
| m.kindToScope[kind] = scope |
| } |
| |
| // unpluralizedSuffixes is a list of resource suffixes that are the same plural and singular |
| // This is only is only necessary because some bits of code are lazy and don't actually use the RESTMapper like they should. |
| // TODO eliminate this so that different callers can correctly map to resources. This probably means updating all |
| // callers to use the RESTMapper they mean. |
| var unpluralizedSuffixes = []string{ |
| "endpoints", |
| } |
| |
| // UnsafeGuessKindToResource converts Kind to a resource name. |
| // Broken. This method only "sort of" works when used outside of this package. It assumes that Kinds and Resources match |
| // and they aren't guaranteed to do so. |
| func UnsafeGuessKindToResource(kind schema.GroupVersionKind) ( /*plural*/ schema.GroupVersionResource /*singular*/, schema.GroupVersionResource) { |
| kindName := kind.Kind |
| if len(kindName) == 0 { |
| return schema.GroupVersionResource{}, schema.GroupVersionResource{} |
| } |
| singularName := strings.ToLower(kindName) |
| singular := kind.GroupVersion().WithResource(singularName) |
| |
| for _, skip := range unpluralizedSuffixes { |
| if strings.HasSuffix(singularName, skip) { |
| return singular, singular |
| } |
| } |
| |
| switch string(singularName[len(singularName)-1]) { |
| case "s": |
| return kind.GroupVersion().WithResource(singularName + "es"), singular |
| case "y": |
| return kind.GroupVersion().WithResource(strings.TrimSuffix(singularName, "y") + "ies"), singular |
| } |
| |
| return kind.GroupVersion().WithResource(singularName + "s"), singular |
| } |
| |
| // ResourceSingularizer implements RESTMapper |
| // It converts a resource name from plural to singular (e.g., from pods to pod) |
| func (m *DefaultRESTMapper) ResourceSingularizer(resourceType string) (string, error) { |
| partialResource := schema.GroupVersionResource{Resource: resourceType} |
| resources, err := m.ResourcesFor(partialResource) |
| if err != nil { |
| return resourceType, err |
| } |
| |
| singular := schema.GroupVersionResource{} |
| for _, curr := range resources { |
| currSingular, ok := m.pluralToSingular[curr] |
| if !ok { |
| continue |
| } |
| if singular.Empty() { |
| singular = currSingular |
| continue |
| } |
| |
| if currSingular.Resource != singular.Resource { |
| return resourceType, fmt.Errorf("multiple possible singular resources (%v) found for %v", resources, resourceType) |
| } |
| } |
| |
| if singular.Empty() { |
| return resourceType, fmt.Errorf("no singular of resource %v has been defined", resourceType) |
| } |
| |
| return singular.Resource, nil |
| } |
| |
| // coerceResourceForMatching makes the resource lower case and converts internal versions to unspecified (legacy behavior) |
| func coerceResourceForMatching(resource schema.GroupVersionResource) schema.GroupVersionResource { |
| resource.Resource = strings.ToLower(resource.Resource) |
| if resource.Version == runtime.APIVersionInternal { |
| resource.Version = "" |
| } |
| |
| return resource |
| } |
| |
| func (m *DefaultRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { |
| resource := coerceResourceForMatching(input) |
| |
| hasResource := len(resource.Resource) > 0 |
| hasGroup := len(resource.Group) > 0 |
| hasVersion := len(resource.Version) > 0 |
| |
| if !hasResource { |
| return nil, fmt.Errorf("a resource must be present, got: %v", resource) |
| } |
| |
| ret := []schema.GroupVersionResource{} |
| switch { |
| case hasGroup && hasVersion: |
| // fully qualified. Find the exact match |
| for plural, singular := range m.pluralToSingular { |
| if singular == resource { |
| ret = append(ret, plural) |
| break |
| } |
| if plural == resource { |
| ret = append(ret, plural) |
| break |
| } |
| } |
| |
| case hasGroup: |
| // given a group, prefer an exact match. If you don't find one, resort to a prefix match on group |
| foundExactMatch := false |
| requestedGroupResource := resource.GroupResource() |
| for plural, singular := range m.pluralToSingular { |
| if singular.GroupResource() == requestedGroupResource { |
| foundExactMatch = true |
| ret = append(ret, plural) |
| } |
| if plural.GroupResource() == requestedGroupResource { |
| foundExactMatch = true |
| ret = append(ret, plural) |
| } |
| } |
| |
| // if you didn't find an exact match, match on group prefixing. This allows storageclass.storage to match |
| // storageclass.storage.k8s.io |
| if !foundExactMatch { |
| for plural, singular := range m.pluralToSingular { |
| if !strings.HasPrefix(plural.Group, requestedGroupResource.Group) { |
| continue |
| } |
| if singular.Resource == requestedGroupResource.Resource { |
| ret = append(ret, plural) |
| } |
| if plural.Resource == requestedGroupResource.Resource { |
| ret = append(ret, plural) |
| } |
| } |
| |
| } |
| |
| case hasVersion: |
| for plural, singular := range m.pluralToSingular { |
| if singular.Version == resource.Version && singular.Resource == resource.Resource { |
| ret = append(ret, plural) |
| } |
| if plural.Version == resource.Version && plural.Resource == resource.Resource { |
| ret = append(ret, plural) |
| } |
| } |
| |
| default: |
| for plural, singular := range m.pluralToSingular { |
| if singular.Resource == resource.Resource { |
| ret = append(ret, plural) |
| } |
| if plural.Resource == resource.Resource { |
| ret = append(ret, plural) |
| } |
| } |
| } |
| |
| if len(ret) == 0 { |
| return nil, &NoResourceMatchError{PartialResource: resource} |
| } |
| |
| sort.Sort(resourceByPreferredGroupVersion{ret, m.defaultGroupVersions}) |
| return ret, nil |
| } |
| |
| func (m *DefaultRESTMapper) ResourceFor(resource schema.GroupVersionResource) (schema.GroupVersionResource, error) { |
| resources, err := m.ResourcesFor(resource) |
| if err != nil { |
| return schema.GroupVersionResource{}, err |
| } |
| if len(resources) == 1 { |
| return resources[0], nil |
| } |
| |
| return schema.GroupVersionResource{}, &AmbiguousResourceError{PartialResource: resource, MatchingResources: resources} |
| } |
| |
| func (m *DefaultRESTMapper) KindsFor(input schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { |
| resource := coerceResourceForMatching(input) |
| |
| hasResource := len(resource.Resource) > 0 |
| hasGroup := len(resource.Group) > 0 |
| hasVersion := len(resource.Version) > 0 |
| |
| if !hasResource { |
| return nil, fmt.Errorf("a resource must be present, got: %v", resource) |
| } |
| |
| ret := []schema.GroupVersionKind{} |
| switch { |
| // fully qualified. Find the exact match |
| case hasGroup && hasVersion: |
| kind, exists := m.resourceToKind[resource] |
| if exists { |
| ret = append(ret, kind) |
| } |
| |
| case hasGroup: |
| foundExactMatch := false |
| requestedGroupResource := resource.GroupResource() |
| for currResource, currKind := range m.resourceToKind { |
| if currResource.GroupResource() == requestedGroupResource { |
| foundExactMatch = true |
| ret = append(ret, currKind) |
| } |
| } |
| |
| // if you didn't find an exact match, match on group prefixing. This allows storageclass.storage to match |
| // storageclass.storage.k8s.io |
| if !foundExactMatch { |
| for currResource, currKind := range m.resourceToKind { |
| if !strings.HasPrefix(currResource.Group, requestedGroupResource.Group) { |
| continue |
| } |
| if currResource.Resource == requestedGroupResource.Resource { |
| ret = append(ret, currKind) |
| } |
| } |
| |
| } |
| |
| case hasVersion: |
| for currResource, currKind := range m.resourceToKind { |
| if currResource.Version == resource.Version && currResource.Resource == resource.Resource { |
| ret = append(ret, currKind) |
| } |
| } |
| |
| default: |
| for currResource, currKind := range m.resourceToKind { |
| if currResource.Resource == resource.Resource { |
| ret = append(ret, currKind) |
| } |
| } |
| } |
| |
| if len(ret) == 0 { |
| return nil, &NoResourceMatchError{PartialResource: input} |
| } |
| |
| sort.Sort(kindByPreferredGroupVersion{ret, m.defaultGroupVersions}) |
| return ret, nil |
| } |
| |
| func (m *DefaultRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { |
| kinds, err := m.KindsFor(resource) |
| if err != nil { |
| return schema.GroupVersionKind{}, err |
| } |
| if len(kinds) == 1 { |
| return kinds[0], nil |
| } |
| |
| return schema.GroupVersionKind{}, &AmbiguousResourceError{PartialResource: resource, MatchingKinds: kinds} |
| } |
| |
| type kindByPreferredGroupVersion struct { |
| list []schema.GroupVersionKind |
| sortOrder []schema.GroupVersion |
| } |
| |
| func (o kindByPreferredGroupVersion) Len() int { return len(o.list) } |
| func (o kindByPreferredGroupVersion) Swap(i, j int) { o.list[i], o.list[j] = o.list[j], o.list[i] } |
| func (o kindByPreferredGroupVersion) Less(i, j int) bool { |
| lhs := o.list[i] |
| rhs := o.list[j] |
| if lhs == rhs { |
| return false |
| } |
| |
| if lhs.GroupVersion() == rhs.GroupVersion() { |
| return lhs.Kind < rhs.Kind |
| } |
| |
| // otherwise, the difference is in the GroupVersion, so we need to sort with respect to the preferred order |
| lhsIndex := -1 |
| rhsIndex := -1 |
| |
| for i := range o.sortOrder { |
| if o.sortOrder[i] == lhs.GroupVersion() { |
| lhsIndex = i |
| } |
| if o.sortOrder[i] == rhs.GroupVersion() { |
| rhsIndex = i |
| } |
| } |
| |
| if rhsIndex == -1 { |
| return true |
| } |
| |
| return lhsIndex < rhsIndex |
| } |
| |
| type resourceByPreferredGroupVersion struct { |
| list []schema.GroupVersionResource |
| sortOrder []schema.GroupVersion |
| } |
| |
| func (o resourceByPreferredGroupVersion) Len() int { return len(o.list) } |
| func (o resourceByPreferredGroupVersion) Swap(i, j int) { o.list[i], o.list[j] = o.list[j], o.list[i] } |
| func (o resourceByPreferredGroupVersion) Less(i, j int) bool { |
| lhs := o.list[i] |
| rhs := o.list[j] |
| if lhs == rhs { |
| return false |
| } |
| |
| if lhs.GroupVersion() == rhs.GroupVersion() { |
| return lhs.Resource < rhs.Resource |
| } |
| |
| // otherwise, the difference is in the GroupVersion, so we need to sort with respect to the preferred order |
| lhsIndex := -1 |
| rhsIndex := -1 |
| |
| for i := range o.sortOrder { |
| if o.sortOrder[i] == lhs.GroupVersion() { |
| lhsIndex = i |
| } |
| if o.sortOrder[i] == rhs.GroupVersion() { |
| rhsIndex = i |
| } |
| } |
| |
| if rhsIndex == -1 { |
| return true |
| } |
| |
| return lhsIndex < rhsIndex |
| } |
| |
| // RESTMapping returns a struct representing the resource path and conversion interfaces a |
| // RESTClient should use to operate on the provided group/kind in order of versions. If a version search |
| // order is not provided, the search order provided to DefaultRESTMapper will be used to resolve which |
| // version should be used to access the named group/kind. |
| func (m *DefaultRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*RESTMapping, error) { |
| mappings, err := m.RESTMappings(gk, versions...) |
| if err != nil { |
| return nil, err |
| } |
| if len(mappings) == 0 { |
| return nil, &NoKindMatchError{GroupKind: gk, SearchedVersions: versions} |
| } |
| // since we rely on RESTMappings method |
| // take the first match and return to the caller |
| // as this was the existing behavior. |
| return mappings[0], nil |
| } |
| |
| // RESTMappings returns the RESTMappings for the provided group kind. If a version search order |
| // is not provided, the search order provided to DefaultRESTMapper will be used. |
| func (m *DefaultRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*RESTMapping, error) { |
| mappings := make([]*RESTMapping, 0) |
| potentialGVK := make([]schema.GroupVersionKind, 0) |
| hadVersion := false |
| |
| // Pick an appropriate version |
| for _, version := range versions { |
| if len(version) == 0 || version == runtime.APIVersionInternal { |
| continue |
| } |
| currGVK := gk.WithVersion(version) |
| hadVersion = true |
| if _, ok := m.kindToPluralResource[currGVK]; ok { |
| potentialGVK = append(potentialGVK, currGVK) |
| break |
| } |
| } |
| // Use the default preferred versions |
| if !hadVersion && len(potentialGVK) == 0 { |
| for _, gv := range m.defaultGroupVersions { |
| if gv.Group != gk.Group { |
| continue |
| } |
| potentialGVK = append(potentialGVK, gk.WithVersion(gv.Version)) |
| } |
| } |
| |
| if len(potentialGVK) == 0 { |
| return nil, &NoKindMatchError{GroupKind: gk, SearchedVersions: versions} |
| } |
| |
| for _, gvk := range potentialGVK { |
| //Ensure we have a REST mapping |
| res, ok := m.kindToPluralResource[gvk] |
| if !ok { |
| continue |
| } |
| |
| // Ensure we have a REST scope |
| scope, ok := m.kindToScope[gvk] |
| if !ok { |
| return nil, fmt.Errorf("the provided version %q and kind %q cannot be mapped to a supported scope", gvk.GroupVersion(), gvk.Kind) |
| } |
| |
| mappings = append(mappings, &RESTMapping{ |
| Resource: res, |
| GroupVersionKind: gvk, |
| Scope: scope, |
| }) |
| } |
| |
| if len(mappings) == 0 { |
| return nil, &NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: gk.Group, Resource: gk.Kind}} |
| } |
| return mappings, nil |
| } |