一、源码结构
二、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 ,从而获取自己特有的资源对象