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