k8s.io/client-go@v0.31.1/discovery/discovery_client.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package discovery 18 19 import ( 20 "context" 21 "encoding/json" 22 goerrors "errors" 23 "fmt" 24 "mime" 25 "net/http" 26 "net/url" 27 "sort" 28 "strings" 29 "sync" 30 "time" 31 32 //nolint:staticcheck // SA1019 Keep using module since it's still being maintained and the api of google.golang.org/protobuf/proto differs 33 "github.com/golang/protobuf/proto" 34 openapi_v2 "github.com/google/gnostic-models/openapiv2" 35 36 apidiscoveryv2 "k8s.io/api/apidiscovery/v2" 37 apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" 38 "k8s.io/apimachinery/pkg/api/errors" 39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 "k8s.io/apimachinery/pkg/runtime" 41 "k8s.io/apimachinery/pkg/runtime/schema" 42 "k8s.io/apimachinery/pkg/runtime/serializer" 43 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 44 "k8s.io/apimachinery/pkg/version" 45 "k8s.io/client-go/kubernetes/scheme" 46 "k8s.io/client-go/openapi" 47 restclient "k8s.io/client-go/rest" 48 ) 49 50 const ( 51 // defaultRetries is the number of times a resource discovery is repeated if an api group disappears on the fly (e.g. CustomResourceDefinitions). 52 defaultRetries = 2 53 // protobuf mime type 54 openAPIV2mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf" 55 56 // defaultTimeout is the maximum amount of time per request when no timeout has been set on a RESTClient. 57 // Defaults to 32s in order to have a distinguishable length of time, relative to other timeouts that exist. 58 defaultTimeout = 32 * time.Second 59 60 // defaultBurst is the default burst to be used with the discovery client's token bucket rate limiter 61 defaultBurst = 300 62 63 AcceptV1 = runtime.ContentTypeJSON 64 // Aggregated discovery content-type (v2beta1). NOTE: content-type parameters 65 // MUST be ordered (g, v, as) for server in "Accept" header (BUT we are resilient 66 // to ordering when comparing returned values in "Content-Type" header). 67 AcceptV2Beta1 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" 68 AcceptV2 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList" 69 // Prioritize aggregated discovery by placing first in the order of discovery accept types. 70 acceptDiscoveryFormats = AcceptV2 + "," + AcceptV2Beta1 + "," + AcceptV1 71 ) 72 73 // Aggregated discovery content-type GVK. 74 var v2Beta1GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2beta1", Kind: "APIGroupDiscoveryList"} 75 var v2GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2", Kind: "APIGroupDiscoveryList"} 76 77 // DiscoveryInterface holds the methods that discover server-supported API groups, 78 // versions and resources. 79 type DiscoveryInterface interface { 80 RESTClient() restclient.Interface 81 ServerGroupsInterface 82 ServerResourcesInterface 83 ServerVersionInterface 84 OpenAPISchemaInterface 85 OpenAPIV3SchemaInterface 86 // Returns copy of current discovery client that will only 87 // receive the legacy discovery format, or pointer to current 88 // discovery client if it does not support legacy-only discovery. 89 WithLegacy() DiscoveryInterface 90 } 91 92 // AggregatedDiscoveryInterface extends DiscoveryInterface to include a method to possibly 93 // return discovery resources along with the discovery groups, which is what the newer 94 // aggregated discovery format does (APIGroupDiscoveryList). 95 type AggregatedDiscoveryInterface interface { 96 DiscoveryInterface 97 98 GroupsAndMaybeResources() (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error) 99 } 100 101 // CachedDiscoveryInterface is a DiscoveryInterface with cache invalidation and freshness. 102 // Note that If the ServerResourcesForGroupVersion method returns a cache miss 103 // error, the user needs to explicitly call Invalidate to clear the cache, 104 // otherwise the same cache miss error will be returned next time. 105 type CachedDiscoveryInterface interface { 106 DiscoveryInterface 107 // Fresh is supposed to tell the caller whether or not to retry if the cache 108 // fails to find something (false = retry, true = no need to retry). 109 // 110 // TODO: this needs to be revisited, this interface can't be locked properly 111 // and doesn't make a lot of sense. 112 Fresh() bool 113 // Invalidate enforces that no cached data that is older than the current time 114 // is used. 115 Invalidate() 116 } 117 118 // ServerGroupsInterface has methods for obtaining supported groups on the API server 119 type ServerGroupsInterface interface { 120 // ServerGroups returns the supported groups, with information like supported versions and the 121 // preferred version. 122 ServerGroups() (*metav1.APIGroupList, error) 123 } 124 125 // ServerResourcesInterface has methods for obtaining supported resources on the API server 126 type ServerResourcesInterface interface { 127 // ServerResourcesForGroupVersion returns the supported resources for a group and version. 128 ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) 129 // ServerGroupsAndResources returns the supported groups and resources for all groups and versions. 130 // 131 // The returned group and resource lists might be non-nil with partial results even in the 132 // case of non-nil error. 133 ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) 134 // ServerPreferredResources returns the supported resources with the version preferred by the 135 // server. 136 // 137 // The returned group and resource lists might be non-nil with partial results even in the 138 // case of non-nil error. 139 ServerPreferredResources() ([]*metav1.APIResourceList, error) 140 // ServerPreferredNamespacedResources returns the supported namespaced resources with the 141 // version preferred by the server. 142 // 143 // The returned resource list might be non-nil with partial results even in the case of 144 // non-nil error. 145 ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) 146 } 147 148 // ServerVersionInterface has a method for retrieving the server's version. 149 type ServerVersionInterface interface { 150 // ServerVersion retrieves and parses the server's version (git version). 151 ServerVersion() (*version.Info, error) 152 } 153 154 // OpenAPISchemaInterface has a method to retrieve the open API schema. 155 type OpenAPISchemaInterface interface { 156 // OpenAPISchema retrieves and parses the swagger API schema the server supports. 157 OpenAPISchema() (*openapi_v2.Document, error) 158 } 159 160 type OpenAPIV3SchemaInterface interface { 161 OpenAPIV3() openapi.Client 162 } 163 164 // DiscoveryClient implements the functions that discover server-supported API groups, 165 // versions and resources. 166 type DiscoveryClient struct { 167 restClient restclient.Interface 168 169 LegacyPrefix string 170 // Forces the client to request only "unaggregated" (legacy) discovery. 171 UseLegacyDiscovery bool 172 } 173 174 var _ AggregatedDiscoveryInterface = &DiscoveryClient{} 175 176 // Convert metav1.APIVersions to metav1.APIGroup. APIVersions is used by legacy v1, so 177 // group would be "". 178 func apiVersionsToAPIGroup(apiVersions *metav1.APIVersions) (apiGroup metav1.APIGroup) { 179 groupVersions := []metav1.GroupVersionForDiscovery{} 180 for _, version := range apiVersions.Versions { 181 groupVersion := metav1.GroupVersionForDiscovery{ 182 GroupVersion: version, 183 Version: version, 184 } 185 groupVersions = append(groupVersions, groupVersion) 186 } 187 apiGroup.Versions = groupVersions 188 // There should be only one groupVersion returned at /api 189 apiGroup.PreferredVersion = groupVersions[0] 190 return 191 } 192 193 // GroupsAndMaybeResources returns the discovery groups, and (if new aggregated 194 // discovery format) the resources keyed by group/version. Merges discovery groups 195 // and resources from /api and /apis (either aggregated or not). Legacy groups 196 // must be ordered first. The server will either return both endpoints (/api, /apis) 197 // as aggregated discovery format or legacy format. For safety, resources will only 198 // be returned if both endpoints returned resources. Returned "failedGVs" can be 199 // empty, but will only be nil in the case an error is returned. 200 func (d *DiscoveryClient) GroupsAndMaybeResources() ( 201 *metav1.APIGroupList, 202 map[schema.GroupVersion]*metav1.APIResourceList, 203 map[schema.GroupVersion]error, 204 error) { 205 // Legacy group ordered first (there is only one -- core/v1 group). Returned groups must 206 // be non-nil, but it could be empty. Returned resources, apiResources map could be nil. 207 groups, resources, failedGVs, err := d.downloadLegacy() 208 if err != nil { 209 return nil, nil, nil, err 210 } 211 // Discovery groups and (possibly) resources downloaded from /apis. 212 apiGroups, apiResources, failedApisGVs, aerr := d.downloadAPIs() 213 if aerr != nil { 214 return nil, nil, nil, aerr 215 } 216 // Merge apis groups into the legacy groups. 217 for _, group := range apiGroups.Groups { 218 groups.Groups = append(groups.Groups, group) 219 } 220 // For safety, only return resources if both endpoints returned resources. 221 if resources != nil && apiResources != nil { 222 for gv, resourceList := range apiResources { 223 resources[gv] = resourceList 224 } 225 } else if resources != nil { 226 resources = nil 227 } 228 // Merge failed GroupVersions from /api and /apis 229 for gv, err := range failedApisGVs { 230 failedGVs[gv] = err 231 } 232 return groups, resources, failedGVs, err 233 } 234 235 // downloadLegacy returns the discovery groups and possibly resources 236 // for the legacy v1 GVR at /api, or an error if one occurred. It is 237 // possible for the resource map to be nil if the server returned 238 // the unaggregated discovery. Returned "failedGVs" can be empty, but 239 // will only be nil in the case of a returned error. 240 func (d *DiscoveryClient) downloadLegacy() ( 241 *metav1.APIGroupList, 242 map[schema.GroupVersion]*metav1.APIResourceList, 243 map[schema.GroupVersion]error, 244 error) { 245 accept := acceptDiscoveryFormats 246 if d.UseLegacyDiscovery { 247 accept = AcceptV1 248 } 249 var responseContentType string 250 body, err := d.restClient.Get(). 251 AbsPath("/api"). 252 SetHeader("Accept", accept). 253 Do(context.TODO()). 254 ContentType(&responseContentType). 255 Raw() 256 apiGroupList := &metav1.APIGroupList{} 257 failedGVs := map[schema.GroupVersion]error{} 258 if err != nil { 259 // Tolerate 404, since aggregated api servers can return it. 260 if errors.IsNotFound(err) { 261 // Return empty structures and no error. 262 emptyGVMap := map[schema.GroupVersion]*metav1.APIResourceList{} 263 return apiGroupList, emptyGVMap, failedGVs, nil 264 } else { 265 return nil, nil, nil, err 266 } 267 } 268 269 var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList 270 // Based on the content-type server responded with: aggregated or unaggregated. 271 if isGVK, _ := ContentTypeIsGVK(responseContentType, v2GVK); isGVK { 272 var aggregatedDiscovery apidiscoveryv2.APIGroupDiscoveryList 273 err = json.Unmarshal(body, &aggregatedDiscovery) 274 if err != nil { 275 return nil, nil, nil, err 276 } 277 apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery) 278 } else if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK { 279 var aggregatedDiscovery apidiscoveryv2beta1.APIGroupDiscoveryList 280 err = json.Unmarshal(body, &aggregatedDiscovery) 281 if err != nil { 282 return nil, nil, nil, err 283 } 284 apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResourcesV2Beta1(aggregatedDiscovery) 285 } else { 286 // Default is unaggregated discovery v1. 287 var v metav1.APIVersions 288 err = json.Unmarshal(body, &v) 289 if err != nil { 290 return nil, nil, nil, err 291 } 292 apiGroup := metav1.APIGroup{} 293 if len(v.Versions) != 0 { 294 apiGroup = apiVersionsToAPIGroup(&v) 295 } 296 apiGroupList.Groups = []metav1.APIGroup{apiGroup} 297 } 298 299 return apiGroupList, resourcesByGV, failedGVs, nil 300 } 301 302 // downloadAPIs returns the discovery groups and (if aggregated format) the 303 // discovery resources. The returned groups will always exist, but the 304 // resources map may be nil. Returned "failedGVs" can be empty, but will 305 // only be nil in the case of a returned error. 306 func (d *DiscoveryClient) downloadAPIs() ( 307 *metav1.APIGroupList, 308 map[schema.GroupVersion]*metav1.APIResourceList, 309 map[schema.GroupVersion]error, 310 error) { 311 accept := acceptDiscoveryFormats 312 if d.UseLegacyDiscovery { 313 accept = AcceptV1 314 } 315 var responseContentType string 316 body, err := d.restClient.Get(). 317 AbsPath("/apis"). 318 SetHeader("Accept", accept). 319 Do(context.TODO()). 320 ContentType(&responseContentType). 321 Raw() 322 if err != nil { 323 return nil, nil, nil, err 324 } 325 326 apiGroupList := &metav1.APIGroupList{} 327 failedGVs := map[schema.GroupVersion]error{} 328 var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList 329 // Based on the content-type server responded with: aggregated or unaggregated. 330 if isGVK, _ := ContentTypeIsGVK(responseContentType, v2GVK); isGVK { 331 var aggregatedDiscovery apidiscoveryv2.APIGroupDiscoveryList 332 err = json.Unmarshal(body, &aggregatedDiscovery) 333 if err != nil { 334 return nil, nil, nil, err 335 } 336 apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery) 337 } else if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK { 338 var aggregatedDiscovery apidiscoveryv2beta1.APIGroupDiscoveryList 339 err = json.Unmarshal(body, &aggregatedDiscovery) 340 if err != nil { 341 return nil, nil, nil, err 342 } 343 apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResourcesV2Beta1(aggregatedDiscovery) 344 } else { 345 // Default is unaggregated discovery v1. 346 err = json.Unmarshal(body, apiGroupList) 347 if err != nil { 348 return nil, nil, nil, err 349 } 350 } 351 352 return apiGroupList, resourcesByGV, failedGVs, nil 353 } 354 355 // ContentTypeIsGVK checks of the content-type string is both 356 // "application/json" and matches the provided GVK. An error 357 // is returned if the content type string is malformed. 358 // NOTE: This function is resilient to the ordering of the 359 // content-type parameters, as well as parameters added by 360 // intermediaries such as proxies or gateways. Examples: 361 // 362 // ("application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil) 363 // ("application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil) 364 // ("application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io;charset=utf-8", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil) 365 // ("application/json", any GVK) = (false, nil) 366 // ("application/json; charset=UTF-8", any GVK) = (false, nil) 367 // ("malformed content type string", any GVK) = (false, error) 368 func ContentTypeIsGVK(contentType string, gvk schema.GroupVersionKind) (bool, error) { 369 base, params, err := mime.ParseMediaType(contentType) 370 if err != nil { 371 return false, err 372 } 373 gvkMatch := runtime.ContentTypeJSON == base && 374 params["g"] == gvk.Group && 375 params["v"] == gvk.Version && 376 params["as"] == gvk.Kind 377 return gvkMatch, nil 378 } 379 380 // ServerGroups returns the supported groups, with information like supported versions and the 381 // preferred version. 382 func (d *DiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { 383 groups, _, _, err := d.GroupsAndMaybeResources() 384 if err != nil { 385 return nil, err 386 } 387 return groups, nil 388 } 389 390 // ServerResourcesForGroupVersion returns the supported resources for a group and version. 391 func (d *DiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (resources *metav1.APIResourceList, err error) { 392 url := url.URL{} 393 if len(groupVersion) == 0 { 394 return nil, fmt.Errorf("groupVersion shouldn't be empty") 395 } 396 if len(d.LegacyPrefix) > 0 && groupVersion == "v1" { 397 url.Path = d.LegacyPrefix + "/" + groupVersion 398 } else { 399 url.Path = "/apis/" + groupVersion 400 } 401 resources = &metav1.APIResourceList{ 402 GroupVersion: groupVersion, 403 } 404 err = d.restClient.Get().AbsPath(url.String()).Do(context.TODO()).Into(resources) 405 if err != nil { 406 // Tolerate core/v1 not found response by returning empty resource list; 407 // this probably should not happen. But we should verify all callers are 408 // not depending on this toleration before removal. 409 if groupVersion == "v1" && errors.IsNotFound(err) { 410 return resources, nil 411 } 412 return nil, err 413 } 414 return resources, nil 415 } 416 417 // ServerGroupsAndResources returns the supported resources for all groups and versions. 418 func (d *DiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 419 return withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 420 return ServerGroupsAndResources(d) 421 }) 422 } 423 424 // ErrGroupDiscoveryFailed is returned if one or more API groups fail to load. 425 type ErrGroupDiscoveryFailed struct { 426 // Groups is a list of the groups that failed to load and the error cause 427 Groups map[schema.GroupVersion]error 428 } 429 430 // Error implements the error interface 431 func (e *ErrGroupDiscoveryFailed) Error() string { 432 var groups []string 433 for k, v := range e.Groups { 434 groups = append(groups, fmt.Sprintf("%s: %v", k, v)) 435 } 436 sort.Strings(groups) 437 return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(groups, ", ")) 438 } 439 440 // Is makes it possible for the callers to use `errors.Is(` helper on errors wrapped with ErrGroupDiscoveryFailed error. 441 func (e *ErrGroupDiscoveryFailed) Is(target error) bool { 442 _, ok := target.(*ErrGroupDiscoveryFailed) 443 return ok 444 } 445 446 // IsGroupDiscoveryFailedError returns true if the provided error indicates the server was unable to discover 447 // a complete list of APIs for the client to use. 448 func IsGroupDiscoveryFailedError(err error) bool { 449 _, ok := err.(*ErrGroupDiscoveryFailed) 450 return err != nil && ok 451 } 452 453 // GroupDiscoveryFailedErrorGroups returns true if the error is an ErrGroupDiscoveryFailed error, 454 // along with the map of group versions that failed discovery. 455 func GroupDiscoveryFailedErrorGroups(err error) (map[schema.GroupVersion]error, bool) { 456 var groupDiscoveryError *ErrGroupDiscoveryFailed 457 if err != nil && goerrors.As(err, &groupDiscoveryError) { 458 return groupDiscoveryError.Groups, true 459 } 460 return nil, false 461 } 462 463 func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 464 var sgs *metav1.APIGroupList 465 var resources []*metav1.APIResourceList 466 var failedGVs map[schema.GroupVersion]error 467 var err error 468 469 // If the passed discovery object implements the wider AggregatedDiscoveryInterface, 470 // then attempt to retrieve aggregated discovery with both groups and the resources. 471 if ad, ok := d.(AggregatedDiscoveryInterface); ok { 472 var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList 473 sgs, resourcesByGV, failedGVs, err = ad.GroupsAndMaybeResources() 474 for _, resourceList := range resourcesByGV { 475 resources = append(resources, resourceList) 476 } 477 } else { 478 sgs, err = d.ServerGroups() 479 } 480 481 if sgs == nil { 482 return nil, nil, err 483 } 484 resultGroups := []*metav1.APIGroup{} 485 for i := range sgs.Groups { 486 resultGroups = append(resultGroups, &sgs.Groups[i]) 487 } 488 // resources is non-nil if aggregated discovery succeeded. 489 if resources != nil { 490 // Any stale Group/Versions returned by aggregated discovery 491 // must be surfaced to the caller as failed Group/Versions. 492 var ferr error 493 if len(failedGVs) > 0 { 494 ferr = &ErrGroupDiscoveryFailed{Groups: failedGVs} 495 } 496 return resultGroups, resources, ferr 497 } 498 499 groupVersionResources, failedGroups := fetchGroupVersionResources(d, sgs) 500 501 // order results by group/version discovery order 502 result := []*metav1.APIResourceList{} 503 for _, apiGroup := range sgs.Groups { 504 for _, version := range apiGroup.Versions { 505 gv := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version} 506 if resources, ok := groupVersionResources[gv]; ok { 507 result = append(result, resources) 508 } 509 } 510 } 511 512 if len(failedGroups) == 0 { 513 return resultGroups, result, nil 514 } 515 516 return resultGroups, result, &ErrGroupDiscoveryFailed{Groups: failedGroups} 517 } 518 519 // ServerPreferredResources uses the provided discovery interface to look up preferred resources 520 func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) { 521 var serverGroupList *metav1.APIGroupList 522 var failedGroups map[schema.GroupVersion]error 523 var groupVersionResources map[schema.GroupVersion]*metav1.APIResourceList 524 var err error 525 526 // If the passed discovery object implements the wider AggregatedDiscoveryInterface, 527 // then it is attempt to retrieve both the groups and the resources. "failedGroups" 528 // are Group/Versions returned as stale in AggregatedDiscovery format. 529 ad, ok := d.(AggregatedDiscoveryInterface) 530 if ok { 531 serverGroupList, groupVersionResources, failedGroups, err = ad.GroupsAndMaybeResources() 532 } else { 533 serverGroupList, err = d.ServerGroups() 534 } 535 if err != nil { 536 return nil, err 537 } 538 // Non-aggregated discovery must fetch resources from Groups. 539 if groupVersionResources == nil { 540 groupVersionResources, failedGroups = fetchGroupVersionResources(d, serverGroupList) 541 } 542 543 result := []*metav1.APIResourceList{} 544 grVersions := map[schema.GroupResource]string{} // selected version of a GroupResource 545 grAPIResources := map[schema.GroupResource]*metav1.APIResource{} // selected APIResource for a GroupResource 546 gvAPIResourceLists := map[schema.GroupVersion]*metav1.APIResourceList{} // blueprint for a APIResourceList for later grouping 547 548 for _, apiGroup := range serverGroupList.Groups { 549 for _, version := range apiGroup.Versions { 550 groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version} 551 552 apiResourceList, ok := groupVersionResources[groupVersion] 553 if !ok { 554 continue 555 } 556 557 // create empty list which is filled later in another loop 558 emptyAPIResourceList := metav1.APIResourceList{ 559 GroupVersion: version.GroupVersion, 560 } 561 gvAPIResourceLists[groupVersion] = &emptyAPIResourceList 562 result = append(result, &emptyAPIResourceList) 563 564 for i := range apiResourceList.APIResources { 565 apiResource := &apiResourceList.APIResources[i] 566 if strings.Contains(apiResource.Name, "/") { 567 continue 568 } 569 gv := schema.GroupResource{Group: apiGroup.Name, Resource: apiResource.Name} 570 if _, ok := grAPIResources[gv]; ok && version.Version != apiGroup.PreferredVersion.Version { 571 // only override with preferred version 572 continue 573 } 574 grVersions[gv] = version.Version 575 grAPIResources[gv] = apiResource 576 } 577 } 578 } 579 580 // group selected APIResources according to GroupVersion into APIResourceLists 581 for groupResource, apiResource := range grAPIResources { 582 version := grVersions[groupResource] 583 groupVersion := schema.GroupVersion{Group: groupResource.Group, Version: version} 584 apiResourceList := gvAPIResourceLists[groupVersion] 585 apiResourceList.APIResources = append(apiResourceList.APIResources, *apiResource) 586 } 587 588 if len(failedGroups) == 0 { 589 return result, nil 590 } 591 592 return result, &ErrGroupDiscoveryFailed{Groups: failedGroups} 593 } 594 595 // fetchServerResourcesForGroupVersions uses the discovery client to fetch the resources for the specified groups in parallel. 596 func fetchGroupVersionResources(d DiscoveryInterface, apiGroups *metav1.APIGroupList) (map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error) { 597 groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList) 598 failedGroups := make(map[schema.GroupVersion]error) 599 600 wg := &sync.WaitGroup{} 601 resultLock := &sync.Mutex{} 602 for _, apiGroup := range apiGroups.Groups { 603 for _, version := range apiGroup.Versions { 604 groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version} 605 wg.Add(1) 606 go func() { 607 defer wg.Done() 608 defer utilruntime.HandleCrash() 609 610 apiResourceList, err := d.ServerResourcesForGroupVersion(groupVersion.String()) 611 612 // lock to record results 613 resultLock.Lock() 614 defer resultLock.Unlock() 615 616 if err != nil { 617 // TODO: maybe restrict this to NotFound errors 618 failedGroups[groupVersion] = err 619 } 620 if apiResourceList != nil { 621 // even in case of error, some fallback might have been returned 622 groupVersionResources[groupVersion] = apiResourceList 623 } 624 }() 625 } 626 } 627 wg.Wait() 628 629 return groupVersionResources, failedGroups 630 } 631 632 // ServerPreferredResources returns the supported resources with the version preferred by the 633 // server. 634 func (d *DiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { 635 _, rs, err := withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 636 rs, err := ServerPreferredResources(d) 637 return nil, rs, err 638 }) 639 return rs, err 640 } 641 642 // ServerPreferredNamespacedResources returns the supported namespaced resources with the 643 // version preferred by the server. 644 func (d *DiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { 645 return ServerPreferredNamespacedResources(d) 646 } 647 648 // ServerPreferredNamespacedResources uses the provided discovery interface to look up preferred namespaced resources 649 func ServerPreferredNamespacedResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) { 650 all, err := ServerPreferredResources(d) 651 return FilteredBy(ResourcePredicateFunc(func(groupVersion string, r *metav1.APIResource) bool { 652 return r.Namespaced 653 }), all), err 654 } 655 656 // ServerVersion retrieves and parses the server's version (git version). 657 func (d *DiscoveryClient) ServerVersion() (*version.Info, error) { 658 body, err := d.restClient.Get().AbsPath("/version").Do(context.TODO()).Raw() 659 if err != nil { 660 return nil, err 661 } 662 var info version.Info 663 err = json.Unmarshal(body, &info) 664 if err != nil { 665 return nil, fmt.Errorf("unable to parse the server version: %v", err) 666 } 667 return &info, nil 668 } 669 670 // OpenAPISchema fetches the open api v2 schema using a rest client and parses the proto. 671 func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) { 672 data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", openAPIV2mimePb).Do(context.TODO()).Raw() 673 if err != nil { 674 return nil, err 675 } 676 document := &openapi_v2.Document{} 677 err = proto.Unmarshal(data, document) 678 if err != nil { 679 return nil, err 680 } 681 return document, nil 682 } 683 684 func (d *DiscoveryClient) OpenAPIV3() openapi.Client { 685 return openapi.NewClient(d.restClient) 686 } 687 688 // WithLegacy returns copy of current discovery client that will only 689 // receive the legacy discovery format. 690 func (d *DiscoveryClient) WithLegacy() DiscoveryInterface { 691 client := *d 692 client.UseLegacyDiscovery = true 693 return &client 694 } 695 696 // withRetries retries the given recovery function in case the groups supported by the server change after ServerGroup() returns. 697 func withRetries(maxRetries int, f func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { 698 var result []*metav1.APIResourceList 699 var resultGroups []*metav1.APIGroup 700 var err error 701 for i := 0; i < maxRetries; i++ { 702 resultGroups, result, err = f() 703 if err == nil { 704 return resultGroups, result, nil 705 } 706 if _, ok := err.(*ErrGroupDiscoveryFailed); !ok { 707 return nil, nil, err 708 } 709 } 710 return resultGroups, result, err 711 } 712 713 func setDiscoveryDefaults(config *restclient.Config) error { 714 config.APIPath = "" 715 config.GroupVersion = nil 716 if config.Timeout == 0 { 717 config.Timeout = defaultTimeout 718 } 719 // if a burst limit is not already configured 720 if config.Burst == 0 { 721 // discovery is expected to be bursty, increase the default burst 722 // to accommodate looking up resource info for many API groups. 723 // matches burst set by ConfigFlags#ToDiscoveryClient(). 724 // see https://issue.k8s.io/86149 725 config.Burst = defaultBurst 726 } 727 codec := runtime.NoopEncoder{Decoder: scheme.Codecs.UniversalDecoder()} 728 config.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}) 729 if len(config.UserAgent) == 0 { 730 config.UserAgent = restclient.DefaultKubernetesUserAgent() 731 } 732 return nil 733 } 734 735 // NewDiscoveryClientForConfig creates a new DiscoveryClient for the given config. This client 736 // can be used to discover supported resources in the API server. 737 // NewDiscoveryClientForConfig is equivalent to NewDiscoveryClientForConfigAndClient(c, httpClient), 738 // where httpClient was generated with rest.HTTPClientFor(c). 739 func NewDiscoveryClientForConfig(c *restclient.Config) (*DiscoveryClient, error) { 740 config := *c 741 if err := setDiscoveryDefaults(&config); err != nil { 742 return nil, err 743 } 744 httpClient, err := restclient.HTTPClientFor(&config) 745 if err != nil { 746 return nil, err 747 } 748 return NewDiscoveryClientForConfigAndClient(&config, httpClient) 749 } 750 751 // NewDiscoveryClientForConfigAndClient creates a new DiscoveryClient for the given config. This client 752 // can be used to discover supported resources in the API server. 753 // Note the http client provided takes precedence over the configured transport values. 754 func NewDiscoveryClientForConfigAndClient(c *restclient.Config, httpClient *http.Client) (*DiscoveryClient, error) { 755 config := *c 756 if err := setDiscoveryDefaults(&config); err != nil { 757 return nil, err 758 } 759 client, err := restclient.UnversionedRESTClientForConfigAndClient(&config, httpClient) 760 return &DiscoveryClient{restClient: client, LegacyPrefix: "/api", UseLegacyDiscovery: false}, err 761 } 762 763 // NewDiscoveryClientForConfigOrDie creates a new DiscoveryClient for the given config. If 764 // there is an error, it panics. 765 func NewDiscoveryClientForConfigOrDie(c *restclient.Config) *DiscoveryClient { 766 client, err := NewDiscoveryClientForConfig(c) 767 if err != nil { 768 panic(err) 769 } 770 return client 771 772 } 773 774 // NewDiscoveryClient returns a new DiscoveryClient for the given RESTClient. 775 func NewDiscoveryClient(c restclient.Interface) *DiscoveryClient { 776 return &DiscoveryClient{restClient: c, LegacyPrefix: "/api", UseLegacyDiscovery: false} 777 } 778 779 // RESTClient returns a RESTClient that is used to communicate 780 // with API server by this client implementation. 781 func (d *DiscoveryClient) RESTClient() restclient.Interface { 782 if d == nil { 783 return nil 784 } 785 return d.restClient 786 }