本文基于Kubernetes v1.22.4版本进行源码学习,对应的client-go版本为v0.22.4
1、client-go源码结构
client-go的代码库已经集成到Kubernetes源码中了,源码结构示例如下:
$ tree vendor/k8s.io/client-go -L 1
vendor/k8s.io/client-go
├── discovery
├── dynamic
├── informers
├── kubernetes
├── listers
├── plugin
├── rest
├── scale
├── tools
├── transport
└── util
源码目录 | 说明 |
discovery | 提供DiscoveryClient发现客户端 |
dynamic | 提供DynamicClient动态客户端 |
informers | 每种Kubernetes资源的Informer实现 |
kubernetes | 提供ClientSet客户端 |
listers | 为每一个Kubernetes资源提供Lister功能,该功能对Get和List请求提供只读的缓存数据 |
plugin | 提供OpenStack、GCP和Azure等云服务商授权插件 |
rest | 提供RESTClient客户端,对Kubernetes API Server执行RESTful操作 |
scale | 提供ScaleClient客户端,用于扩容或缩容Deployment、ReplicaSet、Replication Controller等资源对象 |
tools | 提供常用工具,例如SharedInformer、Reflector、DealtFIFO及Indexers。提供Client查询和缓存机制,以减少向kube-apiserver发起的请求数等 |
transport | 提供安全的TCP连接,支持Http Stream,某些操作需要在客户端和容器之间传输二进制流,例如exec、attach等操作。该功能由内部的spdy包提供支持 |
util | 提供常用方法,例如WorkQueue工作队列、Certificate证书管理等 |
2、Client客户端对象
client-go支持4种Client客户端对象与Kubernetes API Server交互的方式,Client交互对象如下图所示:
- RESTClient:最基础的客户端,对HTTP Request进行了封装,实现了RESTful风格的API。ClientSet、DynamicClient及DiscoveryClient客户端都是基于RESTClient实现的
- ClientSet:在RESTClient的基础上封装了对Resource和Version的管理方法。每一个Resource可以理解为一个客户端,而ClientSet则是多个客户端的集合,每一个Resource和Version都以函数的方式暴露给开发者。ClientSet只能够处理内置资源,它是通过client-gen代码生成器自动生成的
- DynamicClient:ClientSet仅能访问Kubernetes自带的资源(即Client集合内的资源),不能直接访问CRD自定义资源。DynamicClient能够处理Kubernetes中的所有资源对象,包括Kubernetes内置资源与CRD自定义资源
- DiscoveryClient:用于发现kube-apiserver所支持的资源组、资源版本、资源信息(即Group、Versions、Resources)
1)、RESTClient
RESTClient是最基础的客户端,对HTTP Request进行了封装,实现了RESTful风格的API。ClientSet、DynamicClient及DiscoveryClient客户端都是基于RESTClient实现的
1)示例代码
使用RESTClient列出default命名空间下的所有Pod资源对象的相关信息,代码如下:
package main
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// 加载kubeconfig配置信息
config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
if err != nil {
panic(err)
}
// 设置config.APIPath请求的HTTP路径
config.APIPath = "/api"
// 设置config.GroupVersion请求的资源组/资源版本
config.GroupVersion = &corev1.SchemeGroupVersion
// 设置config.NegotiatedSerializer数据的编解码器
config.NegotiatedSerializer = scheme.Codecs
// 通过kubeconfig配置信息实例化RESTClient对象
restClient, err := rest.RESTClientFor(config)
if err != nil {
panic(err)
}
result := &corev1.PodList{}
// RESTClient对象构建HTTP请求参数
err = restClient.Get().
// 设置请求的命名空间
Namespace("default").
// 设置请求的资源名称
Resource("pods").
// VersionedParams函数将一些查询选项(如limit、TimeoutSeconds等)添加到请求参数中
VersionedParams(&metav1.ListOptions{Limit: 500}, scheme.ParameterCodec).
// 通过Do函数执行该请求
Do(context.TODO()).
// 将kube-apiserver返回的结果(Result对象)解析到corev1.PodList对象中
Into(result)
if err != nil {
panic(err)
}
for _, pod := range result.Items {
fmt.Printf("namespcae:%v,name:%v,status:%v\n", pod.Namespace, pod.Name, pod.Status.Phase)
}
}
2)源码解析
RESTClient中的Request结构体代码如下,当调用Get()
、Namespace()
、Resource()
、VersionedParams()
等函数时,实际是对Request的属性进行赋值:
// vendor/k8s.io/client-go/rest/request.go
type Request struct {
c *RESTClient
warningHandler WarningHandler
rateLimiter flowcontrol.RateLimiter
backoff BackoffManager
timeout time.Duration
// generic components accessible via method setters
verb string
pathPrefix string
subpath string
params url.Values
headers http.Header
// structural elements of the request that are part of the Kubernetes API conventions
namespace string
namespaceSet bool
resource string
resourceName string
subresource string
// output
err error
body io.Reader
retry WithRetry
}
RESTClient发送请求的过程对Go标准库net/http
进行了封装,在Do函数中真正执行请求,相关源码如下:
// vendor/k8s.io/client-go/rest/request.go
func (r *Request) Do(ctx context.Context) Result {
var result Result
// 调用request方法执行请求
err := r.request(ctx, func(req *http.Request, resp *http.Response) {
// 将response转换为Result对象
result = r.transformResponse(resp, req)
})
if err != nil {
return Result{err: err}
}
return result
}
func (r *Request) request(ctx context.Context, fn func(*http.Request, *http.Response)) error {
//Metrics for total request latency
start := time.Now()
defer func() {
metrics.RequestLatency.Observe(ctx, r.verb, r.finalURLTemplate(), time.Since(start))
}()
if r.err != nil {
klog.V(4).Infof("Error in request: %v", r.err)
return r.err
}
if err := r.requestPreflightCheck(); err != nil {
return err
}
client := r.c.Client
if client == nil {
client = http.DefaultClient
}
// Throttle the first try before setting up the timeout configured on the
// client. We don't want a throttled client to return timeouts to callers
// before it makes a single request.
if err := r.tryThrottle(ctx); err != nil {
return err
}
if r.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.timeout)
defer cancel()
}
// Right now we make about ten retry attempts if we get a Retry-After response.
var retryAfter *RetryAfter
for {
// 1)初始化request对象
req, err := r.newHTTPRequest(ctx)
if err != nil {
return err
}
r.backoff.Sleep(r.backoff.CalculateBackoff(r.URL()))
if retryAfter != nil {
// We are retrying the request that we already send to apiserver
// at least once before.
// This request should also be throttled with the client-internal rate limiter.
if err := r.tryThrottleWithInfo(ctx, retryAfter.Reason); err != nil {
return err
}
retryAfter = nil
}
// 2)发送请求
resp, err := client.Do(req)
updateURLMetrics(ctx, r, resp, err)
if err != nil {
r.backoff.UpdateBackoff(r.URL(), err, 0)
} else {
r.backoff.UpdateBackoff(r.URL(), err, resp.StatusCode)
}
done := func() bool {
defer readAndCloseResponseBody(resp)
// if the the server returns an error in err, the response will be nil.
f := func(req *http.Request, resp *http.Response) {
if resp == nil {
return
}
// 3)fn函数(即transformResponse)将response转换为Result对象
fn(req, resp)
}
var retry bool
retryAfter, retry = r.retry.NextRetry(req, resp, err, func(req *http.Request, err error) bool {
// "Connection reset by peer" or "apiserver is shutting down" are usually a transient errors.
// Thus in case of "GET" operations, we simply retry it.
// We are not automatically retrying "write" operations, as they are not idempotent.
if r.verb != "GET" {
return false
}
// For connection errors and apiserver shutdown errors retry.
if net.IsConnectionReset(err) || net.IsProbableEOF(err) {
return true
}
return false
})
if retry {
err := r.retry.BeforeNextRetry(ctx, r.backoff, retryAfter, req.URL.String(), r.body)
if err == nil {
return false
}
klog.V(4).Infof("Could not retry request - %v", err)
}
f(req, resp)
return true
}()
if done {
return err
}
}
}
核心函数request逻辑如下:
- 代码1)处初始化request对象,其中会调用
r.URL().String()
生成请求的RESTful URL,实际是使用RESTClient中Request对象的属性进行拼接,在RESTClient示例代码中生成请求的URL为http://127.0.0.1:8001/api/v1/namespaces/default/pods?limit=500
- 代码2)处通过Go标准库
net/http
向kube-apiserver发送请求,请求得到的结果放到response对象中 - 代码3)处fn函数(即transformResponse)将response转换为Result对象
2)、ClientSet
ClientSet在RESTClient的基础上封装了对Resource和Version的管理方法。每一个Resource可以理解为一个客户端,而ClientSet则是多个客户端的集合,每一个Resource和Version都以函数的方式暴露给开发者。多ClientSet多资源集合如下图所示:
ClientSet仅能访问Kubernetes自身内置的资源(即客户端集合内的资源),不能直接访问CRD自定义资源。如果需要ClientSet访问CRD自定义资源,可以通过client-gen代码生成器重新生成ClientSet,在ClientSet集合中自动生成与CRD操作相关的接口
1)示例代码
使用ClientSet列出default命名空间下的所有Pod资源对象的相关信息,代码如下:
package main
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// 加载kubeconfig配置信息
config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
if err != nil {
panic(err)
}
// 通过kubeconfig配置信息实例化Clientset对象,该对象用于管理所有Resource的客户端
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
// clientset.CoreV1().Pods表示请求core资源组的v1资源版本下的Pod资源对象
// Pods函数是一个资源接口对象,用于Pod资源对象的管理
podClient := clientset.CoreV1().Pods(corev1.NamespaceDefault)
// podClient.List函数通过RESTClient获得Pod列表
result, err := podClient.List(context.TODO(), metav1.ListOptions{Limit: 500})
if err != nil {
panic(err)
}
for _, pod := range result.Items {
fmt.Printf("namespcae:%v,name:%v,status:%v\n", pod.Namespace, pod.Name, pod.Status.Phase)
}
}
2)源码解析
Clientset中包含了一组Client,每个Group Version都有一个Client,Clientset结构体定义如下:
// vendor/k8s.io/client-go/kubernetes/clientset.go
type Clientset struct {
*discovery.DiscoveryClient
admissionregistrationV1 *admissionregistrationv1.AdmissionregistrationV1Client
admissionregistrationV1beta1 *admissionregistrationv1beta1.AdmissionregistrationV1beta1Client
internalV1alpha1 *internalv1alpha1.InternalV1alpha1Client
appsV1 *appsv1.AppsV1Client
appsV1beta1 *appsv1beta1.AppsV1beta1Client
appsV1beta2 *appsv1beta2.AppsV1beta2Client
authenticationV1 *authenticationv1.AuthenticationV1Client
authenticationV1beta1 *authenticationv1beta1.AuthenticationV1beta1Client
authorizationV1 *authorizationv1.AuthorizationV1Client
authorizationV1beta1 *authorizationv1beta1.AuthorizationV1beta1Client
autoscalingV1 *autoscalingv1.AutoscalingV1Client
autoscalingV2beta1 *autoscalingv2beta1.AutoscalingV2beta1Client
autoscalingV2beta2 *autoscalingv2beta2.AutoscalingV2beta2Client
batchV1 *batchv1.BatchV1Client
batchV1beta1 *batchv1beta1.BatchV1beta1Client
certificatesV1 *certificatesv1.CertificatesV1Client
certificatesV1beta1 *certificatesv1beta1.CertificatesV1beta1Client
coordinationV1beta1 *coordinationv1beta1.CoordinationV1beta1Client
coordinationV1 *coordinationv1.CoordinationV1Client
coreV1 *corev1.CoreV1Client
discoveryV1 *discoveryv1.DiscoveryV1Client
discoveryV1beta1 *discoveryv1beta1.DiscoveryV1beta1Client
eventsV1 *eventsv1.EventsV1Client
eventsV1beta1 *eventsv1beta1.EventsV1beta1Client
extensionsV1beta1 *extensionsv1beta1.ExtensionsV1beta1Client
flowcontrolV1alpha1 *flowcontrolv1alpha1.FlowcontrolV1alpha1Client
flowcontrolV1beta1 *flowcontrolv1beta1.FlowcontrolV1beta1Client
networkingV1 *networkingv1.NetworkingV1Client
networkingV1beta1 *networkingv1beta1.NetworkingV1beta1Client
nodeV1 *nodev1.NodeV1Client
nodeV1alpha1 *nodev1alpha1.NodeV1alpha1Client
nodeV1beta1 *nodev1beta1.NodeV1beta1Client
policyV1 *policyv1.PolicyV1Client
policyV1beta1 *policyv1beta1.PolicyV1beta1Client
rbacV1 *rbacv1.RbacV1Client
rbacV1beta1 *rbacv1beta1.RbacV1beta1Client
rbacV1alpha1 *rbacv1alpha1.RbacV1alpha1Client
schedulingV1alpha1 *schedulingv1alpha1.SchedulingV1alpha1Client
schedulingV1beta1 *schedulingv1beta1.SchedulingV1beta1Client
schedulingV1 *schedulingv1.SchedulingV1Client
storageV1beta1 *storagev1beta1.StorageV1beta1Client
storageV1 *storagev1.StorageV1Client
storageV1alpha1 *storagev1alpha1.StorageV1alpha1Client
}
clientset.CoreV1().Pods
函数表示请求core资源组的v1资源版本下的Pod资源对象,其内部设置了APIPath请求的HTTP路径,GroupVersion请求的资源组、资源版本,NegotiatedSerializer数据的编解码器
其中,Pods函数是一个资源接口对象,用于Pod资源对象的管理,例如,对Pod资源执行Create、Update、Delete、Get、List、Watch、Patch等操作,这些操作实际上是对RESTClient进行了封装,可以设置选项(如Limit、TimeoutSeconds等)。podClient.List
函数通过RESTClient获得Pod列表,代码如下:
// vendor/k8s.io/client-go/kubernetes/typed/core/v1/pod.go
func (c *pods) List(ctx context.Context, opts metav1.ListOptions) (result *v1.PodList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &v1.PodList{}
err = c.client.Get().
Namespace(c.ns).
Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do(ctx).
Into(result)
return
}
3)、DynamicClient
DynamicClient是一种动态客户端,它可以对任意Kubernetes资源进行RESTful操作,包括CRD自定义资源。DynamicClient与ClientSet操作类似,同样封装了RESTClient,同样提供了Create、Update、Delete、Get、List、Watch、Patch等方法
DynamicClient与ClientSet最大的不同之处是,ClientSet仅能访问Kubernetes自带的资源(即Client集合内的资源),不能直接访问CRD自定义资源。ClientSet需要预先实现每种Resource和Version的操作,其内部的数据都是结构化数据(即已知数据结构)。而DynamicClient内部实现了Unstructured,用于处理非结构化数据结构(即无法提前预知数据结构),这也是DynamicClient能够处理CRD自定义资源的关键
DynamicClient的处理过程将Resource(例如PodList)转换成Unstructured结构类型,Kubernetes的所有Resource都可以转换成该结构类型。处理完成后,再将Unstructured转换成PodList。整个过程类似于Go的interface{}
断言转换过程。另外,Unstructured结构类型是通过map[string]interface{}
转换的
1)示例代码
使用DynamicClient列出default命名空间下的所有Pod资源对象的相关信息,代码如下:
package main
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// 加载kubeconfig配置信息
config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
if err != nil {
panic(err)
}
// 通过kubeconfig配置信息实例化dynamicClient对象,该对象用于管理Kubernetes的所有Resource的客户端
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
panic(err)
}
gvr := schema.GroupVersionResource{Version: "v1", Resource: "pods"}
unstructObj, err := dynamicClient.
// 设置请求的资源组、资源版本、资源名称
Resource(gvr).
// 设置请求的命名空间
Namespace(corev1.NamespaceDefault).
// 获取Pod列表,得到的Pod列表为unstructured.UnstructuredList指针类型
List(context.TODO(), metav1.ListOptions{Limit: 500})
if err != nil {
panic(err)
}
podList := &corev1.PodList{}
// 将unstructured.UnstructuredList转换成PodList类型
err = runtime.DefaultUnstructuredConverter.FromUnstructured(
unstructObj.UnstructuredContent(), podList)
if err != nil {
panic(err)
}
for _, pod := range podList.Items {
fmt.Printf("namespcae:%v,name:%v,status:%v\n", pod.Namespace, pod.Name, pod.Status.Phase)
}
}
2)源码解析
DynamicClient对RESTClient进行了封装,List函数代码如下:
// vendor/k8s.io/client-go/dynamic/simple.go
func (c *dynamicResourceClient) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
result := c.client.client.Get().AbsPath(c.makeURLSegments("")...).SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).Do(ctx)
if err := result.Error(); err != nil {
return nil, err
}
retBytes, err := result.Raw()
if err != nil {
return nil, err
}
uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, retBytes)
if err != nil {
return nil, err
}
if list, ok := uncastObj.(*unstructured.UnstructuredList); ok {
return list, nil
}
list, err := uncastObj.(*unstructured.Unstructured).ToList()
if err != nil {
return nil, err
}
return list, nil
}
c.client.client.Get().AbsPath(c.makeURLSegments("")...).SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1)
使用RESTClient来构建Request对象,对请求的RESTful URL进行拼接,最终调用RESTClient的Do函数真正执行请求
4)、DiscoveryClient
DiscoveryClient是发现客户端,用于发现Kubernetes API Server所支持的资源组、资源版本、资源信息,还可以将这些信息存储到本地,用于本地缓存,以减轻对Kubernetes API Server访问的压力,缓存信息默认存储于~/.kube/cache
下
kubectl的api-versions和api-resources命令输出也是通过DiscoveryClient实现的。另外,DiscoveryClient同样在RESTClient的基础上进行了封装
1)示例代码
package main
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// 加载kubeconfig配置信息
config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
if err != nil {
panic(err)
}
// 通过kubeconfig配置信息实例化DiscoveryClient对象,该对象用于发现Kubernetes API Server所支持的资源组、资源版本、资源信息的客户端
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
panic(err)
}
// 获取Kubernetes API Server所支持的资源组、资源版本、资源信息
_, apiResourceList, err := discoveryClient.ServerGroupsAndResources()
if err != nil {
panic(err)
}
for _, list := range apiResourceList {
gv, err := schema.ParseGroupVersion(list.GroupVersion)
if err != nil {
panic(err)
}
fmt.Printf("group:%v,version:%v\nresources:\n", gv.Group, gv.Version)
for _, resource := range list.APIResources {
fmt.Printf("%v\n", resource.Name)
}
fmt.Println()
}
}
2)源码解析
// vendor/k8s.io/client-go/discovery/discovery_client.go
func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
// 1)获取Kubernetes API Server所支持的GroupVersion
sgs, err := d.ServerGroups()
if sgs == nil {
return nil, nil, err
}
resultGroups := []*metav1.APIGroup{}
for i := range sgs.Groups {
resultGroups = append(resultGroups, &sgs.Groups[i])
}
// 2)获取Kubernetes API Server所支持的GroupVersionResources
groupVersionResources, failedGroups := fetchGroupVersionResources(d, sgs)
// order results by group/version discovery order
result := []*metav1.APIResourceList{}
for _, apiGroup := range sgs.Groups {
for _, version := range apiGroup.Versions {
gv := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
if resources, ok := groupVersionResources[gv]; ok {
result = append(result, resources)
}
}
}
if len(failedGroups) == 0 {
return resultGroups, result, nil
}
return resultGroups, result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
}
ServerGroups函数实现如下:
// vendor/k8s.io/client-go/discovery/discovery_client.go
func (d *DiscoveryClient) ServerGroups() (apiGroupList *metav1.APIGroupList, err error) {
// Get the groupVersions exposed at /api
v := &metav1.APIVersions{}
err = d.restClient.Get().AbsPath(d.LegacyPrefix).Do(context.TODO()).Into(v)
apiGroup := metav1.APIGroup{}
if err == nil && len(v.Versions) != 0 {
apiGroup = apiVersionsToAPIGroup(v)
}
if err != nil && !errors.IsNotFound(err) && !errors.IsForbidden(err) {
return nil, err
}
// Get the groupVersions exposed at /apis
apiGroupList = &metav1.APIGroupList{}
err = d.restClient.Get().AbsPath("/apis").Do(context.TODO()).Into(apiGroupList)
if err != nil && !errors.IsNotFound(err) && !errors.IsForbidden(err) {
return nil, err
}
// to be compatible with a v1.0 server, if it's a 403 or 404, ignore and return whatever we got from /api
if err != nil && (errors.IsNotFound(err) || errors.IsForbidden(err)) {
apiGroupList = &metav1.APIGroupList{}
}
// prepend the group retrieved from /api to the list if not empty
if len(v.Versions) != 0 {
apiGroupList.Groups = append([]metav1.APIGroup{apiGroup}, apiGroupList.Groups...)
}
return apiGroupList, nil
}
通过RESTClient分别请求/api和/apis接口获取Kubernetes API Server所支持的资源组及对应的版本
Kubernetes中支持两类资源组,分别是拥有组名的资源组和没有组名的资源组
- 拥有组名的资源组:其表现形式为
<group>/<version>/<resource>
,例如apps/v1/deployments
- 没有组名的资源组:被称为Core Groups(核心资源组),其表现形式为
/<version>/<resource>
,例如/v1/pods
两类资源组表现形式不同,形成的HTTP PATH路径也不同。拥有组名的资源组的HTTP PATH以/apis为前缀,其表现形式为
/apis/<group>/<version>/<resource>
,例如http://127.0.0.1:8001/apis/app/v1/deployments
。没有组名的资源组的HTTP PATH以/api为前缀,其表现形式为/api/<version>/<resource>
,例如http://127.0.0.1:8001/api/v1/pods
fetchGroupVersionResources函数中遍历所有的GroupVersion获取其对应的资源信息,实现如下:
// vendor/k8s.io/client-go/discovery/discovery_client.go
func fetchGroupVersionResources(d DiscoveryInterface, apiGroups *metav1.APIGroupList) (map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error) {
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
failedGroups := make(map[schema.GroupVersion]error)
wg := &sync.WaitGroup{}
resultLock := &sync.Mutex{}
for _, apiGroup := range apiGroups.Groups {
for _, version := range apiGroup.Versions {
groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
wg.Add(1)
go func() {
defer wg.Done()
defer utilruntime.HandleCrash()
// 获取GroupVersion对应的资源信息
apiResourceList, err := d.ServerResourcesForGroupVersion(groupVersion.String())
// lock to record results
resultLock.Lock()
defer resultLock.Unlock()
if err != nil {
// TODO: maybe restrict this to NotFound errors
failedGroups[groupVersion] = err
}
if apiResourceList != nil {
// even in case of error, some fallback might have been returned
groupVersionResources[groupVersion] = apiResourceList
}
}()
}
}
wg.Wait()
return groupVersionResources, failedGroups
}