(四)clientset、dynamic client源码阅读

总结:以clientset podWatch为例

	apiserver对外提供服务,客户端访问apiserver都是通过url来访问的,所以client的各种client的最终原理都是用户提供一些设置,然后client-go把这些配置组装成一个最终的url,然后请求该URL,然后接收响应,然后解码成指定对象,clientset和dynamic client的区别在于clientset只能处理固定的,也就是他的url某些字段已经固定了,所以只能请求特定的资源,这些资源都是k8s内建的,而dynamic所有字段都是自己设置的,也就是我们可以完全操作url的每一个字段,所以dynamic可以用来处理所有的资源,包括crd。
	对于crd的informer,我们了解到informer内部会有一个listwatch,所以我们要用informer去获取crd,我们只需要手动设置一个访问特定url的listwatch就行了,然后返回的也是unstructed结构的对象,对于unstructed形式的对象,我们需要手动去把unstructed对象解码成指定对象,我们可以选择在调用addResourceHandler的时候手动解析,我们也可以直接把unstructed对象丢到queue,然后在controller中去解析
	因为是unstructed对象,也就是未知对象,所以我们是手动解析,是直接解析http响应,直接把他解析成指定版本的struct,这个解析过程是我们自己控制的,解析得到的就是指定版本的struct,如果要转换成其他版本,比如内部版本,那么也需要我们自己手动转换。
	有了crd informer,有了controller,那么我们也就可以猜到operator的实现了:
		1:创建crd,定义自定义资源
		2:创建crd informer,监听crd变化,这里是监听crd定义的资源变化,不是crd的变化。比如定义了一个crd,这个crd定义了叫一种叫x的资源,那么informer就是监听x(比如说pod)的变化(add/update/delete x资源)
		3:创建crd controller,处理crd的变化事件。
	综上可以知道operator本质就是一个informer+一个controller,实现流程和普通的controller一模样,所以即使不会用code-gen,对于少量的代码,直接手动编码就行了

文章内容:
	1.1:clientSet创建过程
	1.2:clientset podList
	1.3:clientset podWatch
	2.1:dynamic clientset创建
	2.2:dynamic podlist
	2.3:informer crd

0:client-go四种客户端

restclient:最底层的,clientset、dynamic client、discovery client都是对restclient的封装
clientest:对restclient进行了封装,更易用,不过url中的有些字段是写死在代码里面的,只能存取k8s内建资源
dynamic client:对restclient的封装,更易用,不过url中的所有字段都是我们自己可定义的,所以能存取所有资源
discovery client:略

1.1:clientset创建过程

kubernetes.NewForConfig									  //流程就是
														  //1:创建一个httpClient用于网络请求
														  //2:为每种资源对象创建XXClient,会把httpClient传给新创建的XXClient,XXClient进行组装url而httpClient负责最终的网络请求
														  //3:把所有的XXClient都组装到一个最终的ClientSet对象,我们用的就是这个ClientSet对象
  rest.HTTPClientFor
  kubernetes.NewForConfigAndClient                  
	v1.NewForConfigAndClient	                          //为每个版本的内建资源创建xxclient对象,每种资源对应一个包,每个包都有一个叫做NewForCOnfigAndClient的函数
	  v1.setConfigDefaults                                //设置请求url里面的apiPath、group、version、agent,即/apis or api/${group}/${version},不同的资源对应不同的group、version
	  rest.RESTClientForConfigAndClient                   //所有的xxclient底层都是一个RestClient,不同包里面的NewForCOnfigAndClient函数只是进行不同的配置
		rest.DefaultServerUrlFor                          //组装baseurl,不同资源的baseurl不同:baseUrl:=http://hostIp:port/apis/${group}/${version},如果开启https就是https
		rest.NewRESTClient                                //创建restClient,xxClient就是对restClient的一个包装,而restClient又是httpClient的一个包装

	admissionregistrationv1.NewForConfigAndClient
	admissionregistrationv1alpha1.NewForConfigAndClient
	...还有很多xxclient,就不一一列举了...

1.2:pod list

//kubernetes.ClientSet.CoreV1().Pods(namespace).List()
//CoreV1()返回clientset内部的corev1对象
//Pods()返回一个PodInterface接口,实际就是创建一个v1.pods对象,并把clientset内部的RestClient传给给这个pod对象,并封装了一个namespace参数
//List()默认查所有,可以在listoption中设置分页查询
v1.pods.List()                                       //就是构造一个list请求,然后请求apiserver,接收响应。
  rest.
	RestClient.
	Get().                                          //创建一个rest.Request请求对象并设置请求方法为Get
	Namespace(c.ns).                                //设置命名空间参数为Pods(namespace)中的namespace
	Resource("pods").                               //设置资源参数为pods
	VersionedParams(&opts, scheme.ParameterCodec).  //把参数ListOptions转换为map[string]string,即转换为key=value
	Timeout(timeout).                               //设置请求超时参数
	Do(ctx).                                        //发送请求和获取响应
	  rest.Request.request                          //构造请求、执行请求、返回响应
		rest.Request.newHTTPRequest                 //构造请求
          rest.Request.URL                          //组装url,[]表示如果没有设置,那么url中就不含有该字段,url的规则是如果参数不为空,则加上该字段,如果参数为空则不加上该字段
                                                    //URL=http://hostIp:port/apis/${group}/${version}[/namespace/${namespace}][/${resource}][/${resourceName}]?arg1=v1
													//URL=http://hostIp:port/api/${version}[/namespace/${namespace}][/${resource}][/${resourceName}]?arg1=v1
													//apis表示group字段不为空,api表示group字段为空
                                                    //listoptions的参数会当做请求参数来查询,${resourceName}表示资源名字,比如说${resource}=pod,那么${resourceName}就是pod名字
		  http.NewRequestWithContext                //构造http.Request请求
		http.Client.Do                              //执行请求 
	    rest.Request.transformResponse              //读取响应并获取解码器
		  io.ReadAll                                //读取响应
			http2.transportResponseBody.Read        //body是一个io对象,是decoder解码器的输入源,解码器读取值的时候如果没有消息就会阻塞
													//json decoder是内部有一个reader,数据来源于reader,而reader可以是磁盘io、可以是内存bufio、也可以是网络io
			  http2.pipe.Read
				http2.dataBuffer.Read
				sync.Cond.Wait
		  runtime.clientNegotiator                  //创建解码器,k8s返回的请求一般是json格式的,所以一般按照json格式解码就行(还支持yaml、probuf等格式)
		  return rest.Result{xx}                    //这里不解码,只是把解码器和待解码的数据封装成一个rest.result对象
	Into(xx)                                        //调用解码器把响应解码成对应的对象
	  runtime.WithoutVersionDecoder.Decode    
		json.Serializer.Decode                      //解码对象


1.3:pod watch

总结:

podWatch原理就是构造一个http长链接,然后据此构建一个网络io对象,并把这个io对象封装到一个解码器里面,然后开两个线程,一个解码器线程和一个事件发送线程。解码器线程负责不断调用io对象的read方法读取输入,如果watch没有消息,那么网络io对象的read方法就会阻塞,如果有数据,就解码数据,然后发送给事件发送线程,事件发送线程收到后封装成一个Event{Action,Obj}对象然后再把消息转发给watch.ResultChan,之后在watch.ResultChan监听的进程就会收到消息并作出相应的处理

代码:

v1.pods.Watch
  rest.RestClient.Get().Namespace(c.ns).Resource("pods").VersionedParams(&opts, scheme.ParameterCodec).Timeout(timeout).Watch(ctx)
	rest.Request.newHTTPRequest
	http.Client.Do 
    rest.Request.newStreamWatcher 
	  runtime.clientNegotiator.StreamDecoder                     //获取解码器
	  streaming.NewDecoder                                       //创建解码器,该解码器的输入为http.Resp.Body,即http.Resp.Body是一个网络io对象
	  watch.NewStreamWatcher                                     //创建watch对象,并启动解码器线程和事件转发线程
		go watch.StreamWatcher.receive                           //启动解码器线程、
		  for{
		    version.Decoder.Decode                               //这个解码器内部含有一个我们前面通过streaming.NewDecoder创建的解码器
			  streaming.decoder.Decode  
				framer.jsonFrameReader.Read                      //
				  streaming.decoder.Decode
					framer.jsonFrameReader.Read                  //读取数据到缓冲区
					  json.Decoder.Decode
						json.Decoder.readValue                   //从输入源读取数据
						  json.Decoder.refill                    //填充数据到缓冲区
							http2.transportResponseBody.Read     //这里就是从body io对象读取数据了,如果没有数据就会阻塞
					json.Serializer.Decode                       //把二进制数据解码成指定版本的对象(v1.WatchEvent)
		    runtime.Decode                                       //v1.WatchEvent含有一个Raw字段,这是未解码的Object,所以需要解码
			  runtime.WithoutVersionDecoder.Decode 
				json.Serializer.Decode                           //解码对象,把WatchEvent的Raw字段解码成对应版本的对象,版本应该同Event(未验证)

		    result <- Event{                                     //封装成Event对象发给resultChan,应用程序会监听这个resultChan
			  Type:   action,
			  Object: obj,
		    }
		  }

//我们自己写的应用程序
func main() {
	...略...
	podWatch:=clientset.CoreV1().Pods("crane-system").Watch(ctx, metav1.ListOptions{}) //这里会启动watch
	go func() {
		for event := range podWatch.ResultChan() {                                      //这里会接收watch写到resultChan的事件
			switch event.Type {
				...略...
			}
		}
	}()
	stopChan := make(chan struct{})
	<-stopChan
}

1:k8s api和apis路径有什么区别,api是最开始的,apis是后面的
dynamic client中通过group字段是否为空来判断是不是core,如果group字段为空,则是core,那么路径就是api,反之则是apis
if len(c.resource.Group) == 0 {
	url = append(url, "api")
} else {
	url = append(url, "apis", c.resource.Group)
}

2:
clientset是创建clientset的时候就设置好了路径,而dynamicset则是abspath时组装资源路径,list的时候才即时创建url即把hostip和资源路径组装起来,然后再请求

2.1:dynamic client创建

dynamic.NewForConfig                       //只初始化一个最基础的rest.HTTPClient,并没有初始化资源的gvk等东西,而clientset则是创建的时候就把基础URL都设置好了
  rest.HTTPClientFor  
  dynamic.NewForConfigAndClient
    rest.RESTClientForConfigAndClient  

2.2:dynamic client podlist

分两步,第一步获取unstructedList形式的对象,此时每个对象都是unstructed的即未知的;第二步是手动把unstructedList中的unstructed对象解析成指定对象

//dynamic.Resource(gvr).Namespace("crane-system").List()
//resource创建一个resource对象,namespace设置resource对象的namespace,list函数设置gvr和baseurl(api或apis)、组装资源路径(不包括资源名)、组装最终url

//1:获取
dynamic.dynamicResourceClient.List
  dynamic.dynamicResourceClient.makeURLSegments     //获取路径每一段的值并放到数组里
									                //URL=http://hostIp:port/apis/${group}/${version}[/namespace/${namespace}][/${resource}][/${resourceName}]?arg1=v1
													//URL=http://hostIp:port/api/${version}[/namespace/${namespace}][/${resource}][/${resourceName}]?arg1=v1
													//apis表示group字段不为空,api表示group字段为空
  rest.RESTClient.Get()                             //创建一个rest.Request请求对象并设置请求方法为Get   
	.AbsPath(c.makeURLSegments("")...)              //组装资源绝对路径,即把makeURLSegments中的资源路径组装成最终的请求地址
	.SpecificallyVersionedParams                    //把listOptions中的字段转换为map
	.Do(ctx)                                        //把apiserver地址、资源绝对路径、请求参数组装成最终地址并发出请求
  rest.Result.Raw                                   //获取响应中结果对应的原始字节
  runtime.Decode					                //把字节解析成unstructedList或者unstructed对象
	unstructed.unstructuredJSONScheme.Decode        //把字节解析成unstructedList或者unstructed对象,并返回对象的gvk
	if obj!=nil:
	  unstructed.unstructuredJSONScheme.decodeInto	              //解析成指定类型
	    unstructed.unstructuredJSONScheme.decodeToUnstructured    //根据类型选择是否解析成unstructed对象
		  json.Unmarshal                                          //解析成map[string]interface{}对象并赋给一个unstructed对象,unstructed对象内部含有一个map对象
	    unstructed.unstructuredJSONScheme.decodeToList            //根据类型选择是否解析成unstructedList对象
		  json.Unmarshal                                          //解析成unstructedList对象
		  delete                                                  //从unstructedList的Object字段中删除item字段,因为后面会把map[item]中的对象存放到unstructedList的items字段
	      unstructed.unstructuredJSONScheme.decodeToUnstructured  //把unstructedList对象的items字段中的每一个元素解析成一个unstructed对象
          unstructed.UnstructuredList.GetAPIVersion               //获取unstructed对象的gvk字段
          unstructed.UnstructuredList.Kind                        //如果unstructed对象的vk字段没有设置则根据设置unstruectedList对象的gv字段来设置unstruected对象的gv字段
															      //unstructed无需知道资源的group字段,所以这里就没有设置,只需要设置apiversion和kind就行,kind表示哪个结构体
          unstructed.Unstructured.SetKind
          unstructed.Unstructured.SetAPIVersion
		json.Unmarshal                                            //都不是则直接解析成指定对象
	else:
	  unstructed.unstructuredJSONScheme.decode
		json.Unmarshal                                            //这里并不完全解析对象,而是简单解析成一个简单对象,只是用来看一下是否含有items字段
		unstructed.unstructuredJSONScheme.decodeToUnstructured    //如果不含有items字段,则解析成unstructed对象
	    unstructed.unstructuredJSONScheme.decodeToList            //如果含有items字段,则解析成unstructedList对象
  unstructed.Unstructured.ToList                                  //如果是unstructed对象,那么就把他转换为unstructedList对象


//2:手动解析
runtime.unstructuredConverter.FromUnstructured
  runtime.unstructuredConverter.FromUnstructuredWithValidation   //通过反射把unstructed对象解析成指定结构体



//3:crd样例yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: kinddcf3s.groupdcf3.example.com
spec:
  group: groupdcf3.example.com
  names:
    kind: kinddcf3
    listKind: kinddcf3List
    plural: kinddcf3s
    singular: kinddcf3
  scope: Namespaced
  versions:
  - name: versiondcf3
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              name:
                type: string
              info:
                type: object
                properties:
                  weight:
                    type: string

//4:crd实例yaml
apiVersion: groupdcf3.example.com/versiondcf3
kind: kinddcf3
metadata:
  name: dcf-instance
  namespace: my-system
spec:
  name: dcf
  info:
    weight: "71kg"

//5: 结构体需要自定义,因为是我们直接手动解析,不通过scheme.codec去解析,所以不需要再去通过code-gen弄什么defaulter、converter
type KindDcf3 struct {
	metav1.Object
	Spec Spec
}
type Spec struct {
	Name string
	Info Info
}
type Info struct {
	Weight string
}

//6:手动获取和手动解析
instance, err := dynamicClient.Resource(gvr).Namespace("my-system").Get(context.TODO(), "dcf-instance", metav1.GetOptions{})
if err != nil {
	fmt.Println("get crd kinddcf3 instance failed:", err.Error())
} else {
	obj := &KindDcf3{}
	err := runtime.DefaultUnstructuredConverter.FromUnstructured(instance.Object, obj) //直接解析,不用去管什么gv
	if err != nil {
		fmt.Println("decode crd instance dcf-instance failed:", err.Error())
	} else {
		fmt.Println("decord crd instance dcf-instance ok:", obj.Spec)
	}
}

2.3:informer crd

listWatcher := cache.NewListWatchFromClient(client, resource.GetAPIVersion(), resource.GetKind(), "") //自定义listwatch
cache.NewSharedIndexInformer(listWatcher, &unstructured.Unstructured{}, time.Second*30,cache.Indexers{}) //收到的响应会被解析成unstructed对象
  • 8
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值