blob: 41b60d7319e636f511024db28343caa391b9ed06 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
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}