Client-go源码分析(一)客户端对象

Posted by Hao Liang's Blog on Friday, August 7, 2020

一、源码结构

client-go 源码结构思维导图

二、Client 客户端对象

RESTClient

restClient 封装了 RESTful 风格的 HTTP请求,用于和 apiserver 进行 HTTP 请求数据交互。通过restClient 获取 kubernetes 资源对象的流程为:读取 kubeconfig 配置信息 –》封装 HTTP 请求–》请求 apiserver –》对请求得到的结果反序列化为 kubernetes 资源对象结构体。

示例代码:

request := &corev1.PodList{}
err := restClient.Get().
Namespace("default").
Resource("pods").
VersionedParams(&metav1.ListOptions{Limit:500},schema.ParameterCodec).
//这里发送 Http 请求和 apiserver 通信
Do().
//这里将返回的结果反序列化为 corev1.PodList 对象
Into(request)

源码分析: k8s.io/client-go/rest/request.go

// 调用Do方法向apiserver发送获取资源对象的请求
func (r *Request) Do(ctx context.Context) Result {
	var result Result
	// 这里发送 http 请求
	err := r.request(ctx, func(req *http.Request, resp *http.Response) {
		result = r.transformResponse(resp, req)
	})
	if err != nil {
		return Result{err: err}
	}
	return result
}
//发送 http 请求的具体实现
func (r *Request) request(ctx context.Context, fn func(*http.Request, *http.Response)) error {
	...
	for {

		url := r.URL().String()
		req, err := http.NewRequest(r.verb, url, r.body)
		...
		// 这里发送请求
		resp, err := client.Do(req)
		...
		// 这里将apiserver返回的结果转换为资源对象
		fn(req, resp)
		return true
	}
}
// fn 函数将结果转换为资源对象的具体执行逻辑
func (r *Request) transformResponse(resp *http.Response, req *http.Request) Result {
	// 请求结果用byte切片保存
	var body []byte
	if resp.Body != nil {
		data, err := ioutil.ReadAll(resp.Body)
		switch err.(type) {
		case nil:
			// 将返回的 Response body 放入 byte 切片中
			body = data
			...
		}
	}
	...
	// 封装成 Result 结构体返回
	return Result{
		body:        body,
		contentType: contentType,
		statusCode:  resp.StatusCode,
		decoder:     decoder,
	}
}
// 将 fn 函数返回的 Result 结构体转换为 Kubernetes 的资源结构体对象
// 所有 kubernetes 资源对象都实现了 runtime.Object 接口,因此这里入参为 runtime.Object 接口
func (r Result) Into(obj runtime.Object) error {
	...
	// 将 apiserver 返回的资源对象 byte 切片反序列化成 kubernetes 的资源对象(多态)
	out, _, err := r.decoder.Decode(r.body, nil, obj)
	if err != nil || out == obj {
		return err
	}
	...
	return nil
}

ClientSet

ClientSet 封装了 RESTClient,代码上可以更方便简洁地获取资源对象,代码可读性更强,例如:

// 获取集群中 Pod 列表,发现 ClientSet 对资源和版本进行了封装,比 RESTClient 更简洁
podClient := clientSet.CoreV1().Pods(apiv1.NamespaceDefault)
list, err := podClient.List(metav1.ListOptions{Limit:500})

源码分析: k8s.io/client-go/kubernetes/typed/core/v1/pod.go

// List 方法对 RESTClient 进行了封装
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
	}
	// 看到了熟悉的 RESTClient 的代码
	result = &v1.PodList{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("pods").
		VersionedParams(&opts, scheme.ParameterCodec).
		Timeout(timeout).
		Do(ctx).
		Into(result)
	return
}

ClientSet 的源码在 k8s.io/client-go/kubernetes 包中,这个包中仅包含 kubernetes 自带资源对象的结构体实现,因此 client-go 自带的 ClientSet 只能访问 kubernetes 自带的资源对象。

DynamicClient

DynamicClient 也封装了 RESTClient,但提供了 一种用于处理无法提前预知的资源对象数据结构,因此可以用来处理自定义的资源对象(CRD)

DiscoveryClient

DynamicClient 也封装了 RESTClient,负责向 apiserver 发送请求查询 apiserver 所支持的资源组、资源版本信息,并将结果缓存在本地,减轻 apiserver 的压力。(kubecti api-versions、api-resources命令的底层实现原理) 获取 apiserver 支持的所有资源组、资源版本和资源信息:

discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
_, APIResourceList, err :=discoveryClient.ServerGroupAndResources()

源码分析:

func (d *DiscoveryClient) ServerGroups() (apiGroupList *metav1.APIGroupList, err error) {
	// 这里 d.LegacyPrefi 为 /api,因此 DiscoveryClient 通过调 apiserver 的 /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)
	}
	// 这里 DiscoveryClient 通过调 apiserver 的 /apis 接口获取所有支持的资源组信息
	apiGroupList = &metav1.APIGroupList{}
	err = d.restClient.Get().AbsPath("/apis").Do(context.TODO()).Into(apiGroupList)
	...
	return apiGroupList, nil
}

三、思考

我们平常使用 client-go 包的 client 客户端获取 kubernetes 集群中的资源对象,一般会怎么去做呢? 对于 kubernetes 自带的资源对象,我们会用 ClientSet 客户端直接获取。而对于自定义的资源对象 (CRD),很多情况下会自己去实现自定义的 ClientSet 客户端,如 Openshift 就实现了自己的 ClientSet :https://github.com/openshift/client-go/blob/master/apps/clientset/versioned/clientset.go ,从而获取自己特有的资源对象