1
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
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
52 defaultRetries = 2
53
54 openAPIV2mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf"
55
56
57
58 defaultTimeout = 32 * time.Second
59
60
61 defaultBurst = 300
62
63 AcceptV1 = runtime.ContentTypeJSON
64
65
66
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
70 acceptDiscoveryFormats = AcceptV2 + "," + AcceptV2Beta1 + "," + AcceptV1
71 )
72
73
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
78
79 type DiscoveryInterface interface {
80 RESTClient() restclient.Interface
81 ServerGroupsInterface
82 ServerResourcesInterface
83 ServerVersionInterface
84 OpenAPISchemaInterface
85 OpenAPIV3SchemaInterface
86
87
88
89 WithLegacy() DiscoveryInterface
90 }
91
92
93
94
95 type AggregatedDiscoveryInterface interface {
96 DiscoveryInterface
97
98 GroupsAndMaybeResources() (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error)
99 }
100
101
102
103
104
105 type CachedDiscoveryInterface interface {
106 DiscoveryInterface
107
108
109
110
111
112 Fresh() bool
113
114
115 Invalidate()
116 }
117
118
119 type ServerGroupsInterface interface {
120
121
122 ServerGroups() (*metav1.APIGroupList, error)
123 }
124
125
126 type ServerResourcesInterface interface {
127
128 ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error)
129
130
131
132
133 ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)
134
135
136
137
138
139 ServerPreferredResources() ([]*metav1.APIResourceList, error)
140
141
142
143
144
145 ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error)
146 }
147
148
149 type ServerVersionInterface interface {
150
151 ServerVersion() (*version.Info, error)
152 }
153
154
155 type OpenAPISchemaInterface interface {
156
157 OpenAPISchema() (*openapi_v2.Document, error)
158 }
159
160 type OpenAPIV3SchemaInterface interface {
161 OpenAPIV3() openapi.Client
162 }
163
164
165
166 type DiscoveryClient struct {
167 restClient restclient.Interface
168
169 LegacyPrefix string
170
171 UseLegacyDiscovery bool
172 }
173
174 var _ AggregatedDiscoveryInterface = &DiscoveryClient{}
175
176
177
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
189 apiGroup.PreferredVersion = groupVersions[0]
190 return
191 }
192
193
194
195
196
197
198
199
200 func (d *DiscoveryClient) GroupsAndMaybeResources() (
201 *metav1.APIGroupList,
202 map[schema.GroupVersion]*metav1.APIResourceList,
203 map[schema.GroupVersion]error,
204 error) {
205
206
207 groups, resources, failedGVs, err := d.downloadLegacy()
208 if err != nil {
209 return nil, nil, nil, err
210 }
211
212 apiGroups, apiResources, failedApisGVs, aerr := d.downloadAPIs()
213 if aerr != nil {
214 return nil, nil, nil, aerr
215 }
216
217 for _, group := range apiGroups.Groups {
218 groups.Groups = append(groups.Groups, group)
219 }
220
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
229 for gv, err := range failedApisGVs {
230 failedGVs[gv] = err
231 }
232 return groups, resources, failedGVs, err
233 }
234
235
236
237
238
239
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
260 if errors.IsNotFound(err) {
261
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
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
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
303
304
305
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
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
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
356
357
358
359
360
361
362
363
364
365
366
367
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
381
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
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
407
408
409 if groupVersion == "v1" && errors.IsNotFound(err) {
410 return resources, nil
411 }
412 return nil, err
413 }
414 return resources, nil
415 }
416
417
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
425 type ErrGroupDiscoveryFailed struct {
426
427 Groups map[schema.GroupVersion]error
428 }
429
430
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
441 func (e *ErrGroupDiscoveryFailed) Is(target error) bool {
442 _, ok := target.(*ErrGroupDiscoveryFailed)
443 return ok
444 }
445
446
447
448 func IsGroupDiscoveryFailedError(err error) bool {
449 _, ok := err.(*ErrGroupDiscoveryFailed)
450 return err != nil && ok
451 }
452
453
454
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
470
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
489 if resources != nil {
490
491
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
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
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
527
528
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
539 if groupVersionResources == nil {
540 groupVersionResources, failedGroups = fetchGroupVersionResources(d, serverGroupList)
541 }
542
543 result := []*metav1.APIResourceList{}
544 grVersions := map[schema.GroupResource]string{}
545 grAPIResources := map[schema.GroupResource]*metav1.APIResource{}
546 gvAPIResourceLists := map[schema.GroupVersion]*metav1.APIResourceList{}
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
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
572 continue
573 }
574 grVersions[gv] = version.Version
575 grAPIResources[gv] = apiResource
576 }
577 }
578 }
579
580
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
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
613 resultLock.Lock()
614 defer resultLock.Unlock()
615
616 if err != nil {
617
618 failedGroups[groupVersion] = err
619 }
620 if apiResourceList != nil {
621
622 groupVersionResources[groupVersion] = apiResourceList
623 }
624 }()
625 }
626 }
627 wg.Wait()
628
629 return groupVersionResources, failedGroups
630 }
631
632
633
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
643
644 func (d *DiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
645 return ServerPreferredNamespacedResources(d)
646 }
647
648
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
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
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
689
690 func (d *DiscoveryClient) WithLegacy() DiscoveryInterface {
691 client := *d
692 client.UseLegacyDiscovery = true
693 return &client
694 }
695
696
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
720 if config.Burst == 0 {
721
722
723
724
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
736
737
738
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
752
753
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
764
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
775 func NewDiscoveryClient(c restclient.Interface) *DiscoveryClient {
776 return &DiscoveryClient{restClient: c, LegacyPrefix: "/api", UseLegacyDiscovery: false}
777 }
778
779
780
781 func (d *DiscoveryClient) RESTClient() restclient.Interface {
782 if d == nil {
783 return nil
784 }
785 return d.restClient
786 }
787
View as plain text