1/*
2Copyright 2014 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// TODO: move everything in this file to pkg/api/rest
18package meta
19
20import (
21	"fmt"
22	"sort"
23	"strings"
24
25	"k8s.io/apimachinery/pkg/runtime"
26	"k8s.io/apimachinery/pkg/runtime/schema"
27)
28
29// Implements RESTScope interface
30type restScope struct {
31	name RESTScopeName
32}
33
34func (r *restScope) Name() RESTScopeName {
35	return r.name
36}
37
38var RESTScopeNamespace = &restScope{
39	name: RESTScopeNameNamespace,
40}
41
42var RESTScopeRoot = &restScope{
43	name: RESTScopeNameRoot,
44}
45
46// DefaultRESTMapper exposes mappings between the types defined in a
47// runtime.Scheme. It assumes that all types defined the provided scheme
48// can be mapped with the provided MetadataAccessor and Codec interfaces.
49//
50// The resource name of a Kind is defined as the lowercase,
51// English-plural version of the Kind string.
52// When converting from resource to Kind, the singular version of the
53// resource name is also accepted for convenience.
54//
55// TODO: Only accept plural for some operations for increased control?
56// (`get pod bar` vs `get pods bar`)
57type DefaultRESTMapper struct {
58	defaultGroupVersions []schema.GroupVersion
59
60	resourceToKind       map[schema.GroupVersionResource]schema.GroupVersionKind
61	kindToPluralResource map[schema.GroupVersionKind]schema.GroupVersionResource
62	kindToScope          map[schema.GroupVersionKind]RESTScope
63	singularToPlural     map[schema.GroupVersionResource]schema.GroupVersionResource
64	pluralToSingular     map[schema.GroupVersionResource]schema.GroupVersionResource
65}
66
67func (m *DefaultRESTMapper) String() string {
68	return fmt.Sprintf("DefaultRESTMapper{kindToPluralResource=%v}", m.kindToPluralResource)
69}
70
71var _ RESTMapper = &DefaultRESTMapper{}
72
73// NewDefaultRESTMapper initializes a mapping between Kind and APIVersion
74// to a resource name and back based on the objects in a runtime.Scheme
75// and the Kubernetes API conventions. Takes a group name, a priority list of the versions
76// to search when an object has no default version (set empty to return an error),
77// and a function that retrieves the correct metadata for a given version.
78func NewDefaultRESTMapper(defaultGroupVersions []schema.GroupVersion) *DefaultRESTMapper {
79	resourceToKind := make(map[schema.GroupVersionResource]schema.GroupVersionKind)
80	kindToPluralResource := make(map[schema.GroupVersionKind]schema.GroupVersionResource)
81	kindToScope := make(map[schema.GroupVersionKind]RESTScope)
82	singularToPlural := make(map[schema.GroupVersionResource]schema.GroupVersionResource)
83	pluralToSingular := make(map[schema.GroupVersionResource]schema.GroupVersionResource)
84	// TODO: verify name mappings work correctly when versions differ
85
86	return &DefaultRESTMapper{
87		resourceToKind:       resourceToKind,
88		kindToPluralResource: kindToPluralResource,
89		kindToScope:          kindToScope,
90		defaultGroupVersions: defaultGroupVersions,
91		singularToPlural:     singularToPlural,
92		pluralToSingular:     pluralToSingular,
93	}
94}
95
96func (m *DefaultRESTMapper) Add(kind schema.GroupVersionKind, scope RESTScope) {
97	plural, singular := UnsafeGuessKindToResource(kind)
98	m.AddSpecific(kind, plural, singular, scope)
99}
100
101func (m *DefaultRESTMapper) AddSpecific(kind schema.GroupVersionKind, plural, singular schema.GroupVersionResource, scope RESTScope) {
102	m.singularToPlural[singular] = plural
103	m.pluralToSingular[plural] = singular
104
105	m.resourceToKind[singular] = kind
106	m.resourceToKind[plural] = kind
107
108	m.kindToPluralResource[kind] = plural
109	m.kindToScope[kind] = scope
110}
111
112// unpluralizedSuffixes is a list of resource suffixes that are the same plural and singular
113// This is only is only necessary because some bits of code are lazy and don't actually use the RESTMapper like they should.
114// TODO eliminate this so that different callers can correctly map to resources.  This probably means updating all
115// callers to use the RESTMapper they mean.
116var unpluralizedSuffixes = []string{
117	"endpoints",
118}
119
120// UnsafeGuessKindToResource converts Kind to a resource name.
121// Broken. This method only "sort of" works when used outside of this package.  It assumes that Kinds and Resources match
122// and they aren't guaranteed to do so.
123func UnsafeGuessKindToResource(kind schema.GroupVersionKind) ( /*plural*/ schema.GroupVersionResource /*singular*/, schema.GroupVersionResource) {
124	kindName := kind.Kind
125	if len(kindName) == 0 {
126		return schema.GroupVersionResource{}, schema.GroupVersionResource{}
127	}
128	singularName := strings.ToLower(kindName)
129	singular := kind.GroupVersion().WithResource(singularName)
130
131	for _, skip := range unpluralizedSuffixes {
132		if strings.HasSuffix(singularName, skip) {
133			return singular, singular
134		}
135	}
136
137	switch string(singularName[len(singularName)-1]) {
138	case "s":
139		return kind.GroupVersion().WithResource(singularName + "es"), singular
140	case "y":
141		return kind.GroupVersion().WithResource(strings.TrimSuffix(singularName, "y") + "ies"), singular
142	}
143
144	return kind.GroupVersion().WithResource(singularName + "s"), singular
145}
146
147// ResourceSingularizer implements RESTMapper
148// It converts a resource name from plural to singular (e.g., from pods to pod)
149func (m *DefaultRESTMapper) ResourceSingularizer(resourceType string) (string, error) {
150	partialResource := schema.GroupVersionResource{Resource: resourceType}
151	resources, err := m.ResourcesFor(partialResource)
152	if err != nil {
153		return resourceType, err
154	}
155
156	singular := schema.GroupVersionResource{}
157	for _, curr := range resources {
158		currSingular, ok := m.pluralToSingular[curr]
159		if !ok {
160			continue
161		}
162		if singular.Empty() {
163			singular = currSingular
164			continue
165		}
166
167		if currSingular.Resource != singular.Resource {
168			return resourceType, fmt.Errorf("multiple possible singular resources (%v) found for %v", resources, resourceType)
169		}
170	}
171
172	if singular.Empty() {
173		return resourceType, fmt.Errorf("no singular of resource %v has been defined", resourceType)
174	}
175
176	return singular.Resource, nil
177}
178
179// coerceResourceForMatching makes the resource lower case and converts internal versions to unspecified (legacy behavior)
180func coerceResourceForMatching(resource schema.GroupVersionResource) schema.GroupVersionResource {
181	resource.Resource = strings.ToLower(resource.Resource)
182	if resource.Version == runtime.APIVersionInternal {
183		resource.Version = ""
184	}
185
186	return resource
187}
188
189func (m *DefaultRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
190	resource := coerceResourceForMatching(input)
191
192	hasResource := len(resource.Resource) > 0
193	hasGroup := len(resource.Group) > 0
194	hasVersion := len(resource.Version) > 0
195
196	if !hasResource {
197		return nil, fmt.Errorf("a resource must be present, got: %v", resource)
198	}
199
200	ret := []schema.GroupVersionResource{}
201	switch {
202	case hasGroup && hasVersion:
203		// fully qualified.  Find the exact match
204		for plural, singular := range m.pluralToSingular {
205			if singular == resource {
206				ret = append(ret, plural)
207				break
208			}
209			if plural == resource {
210				ret = append(ret, plural)
211				break
212			}
213		}
214
215	case hasGroup:
216		// given a group, prefer an exact match.  If you don't find one, resort to a prefix match on group
217		foundExactMatch := false
218		requestedGroupResource := resource.GroupResource()
219		for plural, singular := range m.pluralToSingular {
220			if singular.GroupResource() == requestedGroupResource {
221				foundExactMatch = true
222				ret = append(ret, plural)
223			}
224			if plural.GroupResource() == requestedGroupResource {
225				foundExactMatch = true
226				ret = append(ret, plural)
227			}
228		}
229
230		// if you didn't find an exact match, match on group prefixing. This allows storageclass.storage to match
231		// storageclass.storage.k8s.io
232		if !foundExactMatch {
233			for plural, singular := range m.pluralToSingular {
234				if !strings.HasPrefix(plural.Group, requestedGroupResource.Group) {
235					continue
236				}
237				if singular.Resource == requestedGroupResource.Resource {
238					ret = append(ret, plural)
239				}
240				if plural.Resource == requestedGroupResource.Resource {
241					ret = append(ret, plural)
242				}
243			}
244
245		}
246
247	case hasVersion:
248		for plural, singular := range m.pluralToSingular {
249			if singular.Version == resource.Version && singular.Resource == resource.Resource {
250				ret = append(ret, plural)
251			}
252			if plural.Version == resource.Version && plural.Resource == resource.Resource {
253				ret = append(ret, plural)
254			}
255		}
256
257	default:
258		for plural, singular := range m.pluralToSingular {
259			if singular.Resource == resource.Resource {
260				ret = append(ret, plural)
261			}
262			if plural.Resource == resource.Resource {
263				ret = append(ret, plural)
264			}
265		}
266	}
267
268	if len(ret) == 0 {
269		return nil, &NoResourceMatchError{PartialResource: resource}
270	}
271
272	sort.Sort(resourceByPreferredGroupVersion{ret, m.defaultGroupVersions})
273	return ret, nil
274}
275
276func (m *DefaultRESTMapper) ResourceFor(resource schema.GroupVersionResource) (schema.GroupVersionResource, error) {
277	resources, err := m.ResourcesFor(resource)
278	if err != nil {
279		return schema.GroupVersionResource{}, err
280	}
281	if len(resources) == 1 {
282		return resources[0], nil
283	}
284
285	return schema.GroupVersionResource{}, &AmbiguousResourceError{PartialResource: resource, MatchingResources: resources}
286}
287
288func (m *DefaultRESTMapper) KindsFor(input schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
289	resource := coerceResourceForMatching(input)
290
291	hasResource := len(resource.Resource) > 0
292	hasGroup := len(resource.Group) > 0
293	hasVersion := len(resource.Version) > 0
294
295	if !hasResource {
296		return nil, fmt.Errorf("a resource must be present, got: %v", resource)
297	}
298
299	ret := []schema.GroupVersionKind{}
300	switch {
301	// fully qualified.  Find the exact match
302	case hasGroup && hasVersion:
303		kind, exists := m.resourceToKind[resource]
304		if exists {
305			ret = append(ret, kind)
306		}
307
308	case hasGroup:
309		foundExactMatch := false
310		requestedGroupResource := resource.GroupResource()
311		for currResource, currKind := range m.resourceToKind {
312			if currResource.GroupResource() == requestedGroupResource {
313				foundExactMatch = true
314				ret = append(ret, currKind)
315			}
316		}
317
318		// if you didn't find an exact match, match on group prefixing. This allows storageclass.storage to match
319		// storageclass.storage.k8s.io
320		if !foundExactMatch {
321			for currResource, currKind := range m.resourceToKind {
322				if !strings.HasPrefix(currResource.Group, requestedGroupResource.Group) {
323					continue
324				}
325				if currResource.Resource == requestedGroupResource.Resource {
326					ret = append(ret, currKind)
327				}
328			}
329
330		}
331
332	case hasVersion:
333		for currResource, currKind := range m.resourceToKind {
334			if currResource.Version == resource.Version && currResource.Resource == resource.Resource {
335				ret = append(ret, currKind)
336			}
337		}
338
339	default:
340		for currResource, currKind := range m.resourceToKind {
341			if currResource.Resource == resource.Resource {
342				ret = append(ret, currKind)
343			}
344		}
345	}
346
347	if len(ret) == 0 {
348		return nil, &NoResourceMatchError{PartialResource: input}
349	}
350
351	sort.Sort(kindByPreferredGroupVersion{ret, m.defaultGroupVersions})
352	return ret, nil
353}
354
355func (m *DefaultRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
356	kinds, err := m.KindsFor(resource)
357	if err != nil {
358		return schema.GroupVersionKind{}, err
359	}
360	if len(kinds) == 1 {
361		return kinds[0], nil
362	}
363
364	return schema.GroupVersionKind{}, &AmbiguousResourceError{PartialResource: resource, MatchingKinds: kinds}
365}
366
367type kindByPreferredGroupVersion struct {
368	list      []schema.GroupVersionKind
369	sortOrder []schema.GroupVersion
370}
371
372func (o kindByPreferredGroupVersion) Len() int      { return len(o.list) }
373func (o kindByPreferredGroupVersion) Swap(i, j int) { o.list[i], o.list[j] = o.list[j], o.list[i] }
374func (o kindByPreferredGroupVersion) Less(i, j int) bool {
375	lhs := o.list[i]
376	rhs := o.list[j]
377	if lhs == rhs {
378		return false
379	}
380
381	if lhs.GroupVersion() == rhs.GroupVersion() {
382		return lhs.Kind < rhs.Kind
383	}
384
385	// otherwise, the difference is in the GroupVersion, so we need to sort with respect to the preferred order
386	lhsIndex := -1
387	rhsIndex := -1
388
389	for i := range o.sortOrder {
390		if o.sortOrder[i] == lhs.GroupVersion() {
391			lhsIndex = i
392		}
393		if o.sortOrder[i] == rhs.GroupVersion() {
394			rhsIndex = i
395		}
396	}
397
398	if rhsIndex == -1 {
399		return true
400	}
401
402	return lhsIndex < rhsIndex
403}
404
405type resourceByPreferredGroupVersion struct {
406	list      []schema.GroupVersionResource
407	sortOrder []schema.GroupVersion
408}
409
410func (o resourceByPreferredGroupVersion) Len() int      { return len(o.list) }
411func (o resourceByPreferredGroupVersion) Swap(i, j int) { o.list[i], o.list[j] = o.list[j], o.list[i] }
412func (o resourceByPreferredGroupVersion) Less(i, j int) bool {
413	lhs := o.list[i]
414	rhs := o.list[j]
415	if lhs == rhs {
416		return false
417	}
418
419	if lhs.GroupVersion() == rhs.GroupVersion() {
420		return lhs.Resource < rhs.Resource
421	}
422
423	// otherwise, the difference is in the GroupVersion, so we need to sort with respect to the preferred order
424	lhsIndex := -1
425	rhsIndex := -1
426
427	for i := range o.sortOrder {
428		if o.sortOrder[i] == lhs.GroupVersion() {
429			lhsIndex = i
430		}
431		if o.sortOrder[i] == rhs.GroupVersion() {
432			rhsIndex = i
433		}
434	}
435
436	if rhsIndex == -1 {
437		return true
438	}
439
440	return lhsIndex < rhsIndex
441}
442
443// RESTMapping returns a struct representing the resource path and conversion interfaces a
444// RESTClient should use to operate on the provided group/kind in order of versions. If a version search
445// order is not provided, the search order provided to DefaultRESTMapper will be used to resolve which
446// version should be used to access the named group/kind.
447func (m *DefaultRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*RESTMapping, error) {
448	mappings, err := m.RESTMappings(gk, versions...)
449	if err != nil {
450		return nil, err
451	}
452	if len(mappings) == 0 {
453		return nil, &NoKindMatchError{GroupKind: gk, SearchedVersions: versions}
454	}
455	// since we rely on RESTMappings method
456	// take the first match and return to the caller
457	// as this was the existing behavior.
458	return mappings[0], nil
459}
460
461// RESTMappings returns the RESTMappings for the provided group kind. If a version search order
462// is not provided, the search order provided to DefaultRESTMapper will be used.
463func (m *DefaultRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*RESTMapping, error) {
464	mappings := make([]*RESTMapping, 0)
465	potentialGVK := make([]schema.GroupVersionKind, 0)
466	hadVersion := false
467
468	// Pick an appropriate version
469	for _, version := range versions {
470		if len(version) == 0 || version == runtime.APIVersionInternal {
471			continue
472		}
473		currGVK := gk.WithVersion(version)
474		hadVersion = true
475		if _, ok := m.kindToPluralResource[currGVK]; ok {
476			potentialGVK = append(potentialGVK, currGVK)
477			break
478		}
479	}
480	// Use the default preferred versions
481	if !hadVersion && len(potentialGVK) == 0 {
482		for _, gv := range m.defaultGroupVersions {
483			if gv.Group != gk.Group {
484				continue
485			}
486			potentialGVK = append(potentialGVK, gk.WithVersion(gv.Version))
487		}
488	}
489
490	if len(potentialGVK) == 0 {
491		return nil, &NoKindMatchError{GroupKind: gk, SearchedVersions: versions}
492	}
493
494	for _, gvk := range potentialGVK {
495		//Ensure we have a REST mapping
496		res, ok := m.kindToPluralResource[gvk]
497		if !ok {
498			continue
499		}
500
501		// Ensure we have a REST scope
502		scope, ok := m.kindToScope[gvk]
503		if !ok {
504			return nil, fmt.Errorf("the provided version %q and kind %q cannot be mapped to a supported scope", gvk.GroupVersion(), gvk.Kind)
505		}
506
507		mappings = append(mappings, &RESTMapping{
508			Resource:         res,
509			GroupVersionKind: gvk,
510			Scope:            scope,
511		})
512	}
513
514	if len(mappings) == 0 {
515		return nil, &NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: gk.Group, Resource: gk.Kind}}
516	}
517	return mappings, nil
518}
519