总结:以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对象