discovery_client.go 26 KB

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