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