client-go之metadata包(上篇)源码分析

588 阅读7分钟

@TOC

温馨提示

阅读本文,可以参考分析dynamic文章,两篇类似,区别是操作对象侧重点不同

metadata包

用于获取gvr对应的metadata(gvr的TypeMeta和ObjectMeta)的informer/lister/client

interface.go

  • 接口.go
    // ResourceInterface的工厂模式,方法Resource可以获取对应gvr的ResourceInterface接口
    type Interface interface {
    	Resource(resource schema.GroupVersionResource) Getter
    }
    // ResourceInterface 包含一组方法,这些方法可以通过对象的元数据在对象上调用。
    // 服务器不支持更新操作,但patch可用于更新操作。
    type ResourceInterface interface {
    	Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error
    	DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error
    	Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*metav1.PartialObjectMetadata, error)
    	List(ctx context.Context, opts metav1.ListOptions) (*metav1.PartialObjectMetadataList, error)
    	Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
    	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*metav1.PartialObjectMetadata, error)
    }
    // 限定了namespace的ResourceInterface
    type Getter interface {
    	Namespace(string) ResourceInterface
    	ResourceInterface
    }
    

metadata.go 对于上面提到的接口的简单实现及实现kubernetes/apimachinery的编解码(ListOptions/GetOptions/DeleteOptions/CreateOptions/UpdateOptions/PatchOptions),供ResourceClient使用。

  • 接口实现相关函数
    // ConfigFor 返回提供的配置的副本,并设置了适当的元数据客户端默认值。
    func ConfigFor(inConfig *rest.Config) *rest.Config {
      // 复制参数配置文件
    	config := rest.CopyConfig(inConfig)
      // 设置默认属性
      //设置可接受contenttype,用于实际请求时作第一层header拦截(判断请求是否在接受列表中)
    	config.AcceptContentTypes = "application/vnd.kubernetes.protobuf,application/json" 
      // 设置contenttype(注意和AcceptContentTypes的区别),用于从下面序列化列表中选取一种序列化器
    	config.ContentType = "application/vnd.kubernetes.protobuf"
      // 序列化器列表,用于序列化request body和返回response使用
    	config.NegotiatedSerializer = metainternalversionscheme.Codecs.WithoutConversion()
    	if config.UserAgent == "" {
    		config.UserAgent = rest.DefaultKubernetesUserAgent()
    	}
    	return config
    }
    
    // NewForConfig 创建了一个新的元数据客户端,它可以以检索任何 Kubernetes 对象(核心、聚合或基于自定义资源)的元数据详细信息(PartialObjectMetadata 对象的形式),或者返回错误。
    func NewForConfig(inConfig *rest.Config) (Interface, error) {
      // 复制并设置默认的属性
    	config := ConfigFor(inConfig)
    	// 用于序列化选项
    	config.GroupVersion = &schema.GroupVersion{}
    	config.APIPath = "/this-value-should-never-be-sent"
      // 获取包装了http.client的client,操作obj,后面章节讲解
    	restClient, err := rest.RESTClientFor(config)
    	if err != nil {
    		return nil, err
    	}
    	return &dynamicClient{client: restClient}, nil
    }
    
    // NewForConfigOrDie 为给定的配置创建一个新的元数据客户端,如果配置中有错误, 会发生panic。
    func NewForConfigOrDie(c *rest.Config) Interface {
    	ret, err := NewForConfig(c)
    	if err != nil {
    		panic(err)
    	}
    	return ret
    }
    
  • 接口实现相关函数
    // 构造组成请求url的各部分uri,以slice返回
    func (c *client) makeURLSegments(name string) []string {
      // 组成请求url的各部分uri
    	url := []string{}
      // 判断group是否为空
    	if len(c.resource.Group) == 0 {
          // 为空则是核心api resource, /api形式
    		url = append(url, "api")
    	} else {
          // 不为空 则是非核心api resource,/apis/${group}形式
    		url = append(url, "apis", c.resource.Group)
    	}
      // 追加版本
    	url = append(url, c.resource.Version)
      // 如果namespace不为空
    	if len(c.namespace) > 0 {
          // 表示是命名空间下的资源操作。形式为/namespaces/${namespace}
    		url = append(url, "namespaces", c.namespace)
    	}
      // 追加资源
    	url = append(url, c.resource.Resource)
      // 如果资源名称不为空
    	if len(name) > 0 {
          // 追加资源名称,表示形式/${name}
    		url = append(url, name)
    	}
    
    	return url
    }
    
    // 判断是否可能是ObjectMeta类型
    func isLikelyObjectMetadata(meta *metav1.PartialObjectMetadata) bool {
      // 这里也是一种可能性判断。根据meta.UID不为空或meta.CreationTimestamp.IsZero|meta.Name不为空|meta.GenerateName不为空
    	return len(meta.UID) > 0 || !meta.CreationTimestamp.IsZero() || len(meta.Name) > 0 || len(meta.GenerateName) > 0
    }
    
  • 接口实现相关结构体
    // rest.RESTClient(操作obj的具体执行client)的包装结构体,实现了Interface接口
    type Client struct {
    	client *rest.RESTClient
    }
    
    // Resource 返回一个接口,该接口可以访问集群或命名空间范围的资源实例。
    func (c *Client) Resource(resource schema.GroupVersionResource) Getter {
    	return &client{client: c, resource: resource}
    }
    
    // 上面Client的代理client
    type client struct {
    	client    *Client
    	namespace string
    	resource  schema.GroupVersionResource
    }
    
    // 实现Getter接口的Namespace方法,用于限定该client的作用范围(集群或者命名空间)
    func (c *client) Namespace(ns string) ResourceInterface {
    	ret := *c
    	ret.namespace = ns
    	return &ret
    }
    
    // 下面分析几个操作接口(DeleteCollection和Delete类似,List类似Get),其他不做分析
    // 从服务器中删除提供的资源。这里其实和dynamicclient的删除操作一样,都是整个gvr(注意不是单纯gvr的metadata数据)
    func (c *client) Delete(ctx context.Context, name string, opts metav1.DeleteOptions, subresources ...string) error {
      // 限定resource name不能为空
    	if len(name) == 0 {
    		return fmt.Errorf("name is required")
    	}
      // 解码参数DeleteOptions(这里从工厂模式中获取得是group:"",version:v1,代表是core resource)
    	deleteOptionsByte, err := runtime.Encode(deleteOptionsCodec.LegacyCodec(schema.GroupVersion{Version: "v1"}), &opts)
    	if err != nil {
    		return err
    	}
      //这里只是建立了tcp长链接和解码相应,后面再rest包做具体分析(具体的执行逻辑还是在k8s api-server中调用k8s api的etcd相关接口)
    	result := c.client.client.
    		Delete().
    		AbsPath(append(c.makeURLSegments(name), subresources...)...).
    		Body(deleteOptionsByte).
    		Do(ctx)
    	return result.Error()
    }
    
    // Get 返回指定范围(命名空间或集群)中具有该名称的资源。
    func (c *client) Get(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
      // 限定资源名称不能为空
    	if len(name) == 0 {
    		return nil, fmt.Errorf("name is required")
    	}
      //这里只是建立了tcp长链接和解码相应,后面再rest包做具体分析(具体的执行逻辑还是在k8s api-server中调用k8s api的etcd相关接口)
    	result := c.client.client.Get().AbsPath(append(c.makeURLSegments(name), subresources...)...).
          // 这里设置的header要注意(application/vnd.kubernetes.protobuf;as=PartialObjectMetadata会被解析为Accept(Type:application  SubType:vnd.kubernetes.protobuf  Params:key为as  value为PartialObjectMetadata))
          // k8s api-server以那总类型(json/protobug/yaml)何种gvr返回
    		SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json").
    		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
    		Do(ctx)
    	if err := result.Error(); err != nil {
    		return nil, err
    	}
      // 获取请求结果的obj
    	obj, err := result.Get()
      // 如果PartialObjectMetadata没有注册到scheme  为true 
    	if runtime.IsNotRegisteredError(err) {
    		klog.V(5).Infof("Unable to retrieve PartialObjectMetadata: %#v", err)
          // 获取请求结果body的字节数组
    		rawBytes, err := result.Raw()
    		if err != nil {
    			return nil, err
    		}
          // 最终要转化的对象(PartialObjectMetadata类型,包含了TypeMeta和ObjectMeta)
    		var partial metav1.PartialObjectMetadata
          // json格式解组字节数组为PartialObjectMetadata类型对象(出现失败两种情况 1.不是PartialObjectMetadata类型 2.字节数组不是经过json.Marshal形成的字节数组)
    		if err := json.Unmarshal(rawBytes, &partial); err != nil {
    			return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadata: %v", err)
    		}
          // 判断转化后的partial是否可能是ObjectMeta(根据ObjectMeta的UID/CreationTimestamp/Name/GenerateName,具体参考该方法)
    		if !isLikelyObjectMetadata(&partial) {
    			return nil, fmt.Errorf("object does not appear to match the ObjectMeta schema: %#v", partial)
    		}
          // 把partial对应的TypeMeta对象置为空对象
    		partial.TypeMeta = metav1.TypeMeta{}
    		return &partial, nil
    	}
      // 有err则返回
    	if err != nil {
    		return nil, err
    	}
      // 类型转化
    	partial, ok := obj.(*metav1.PartialObjectMetadata)
    	if !ok {
    		return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj)
    	}
    	return partial, nil
    }
    
    // Watch 查找指定范围(命名空间或集群)中所有更改的资源。
    func (c *client) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
      // 设置超时时长
    	var timeout time.Duration
    	if opts.TimeoutSeconds != nil {
    		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
    	}
      // 设置标识 表示此请求是watch类型(以为和get请求的区别  只能通过该参数来区分)
    	opts.Watch = true
      // 这里获取监听接口(List和watch方法),后面再rest包做具体分析(具体的执行逻辑还是在k8s api-server中调用k8s api的etcd相关接口)
    	return c.client.client.Get().
    		AbsPath(c.makeURLSegments("")...).
    		SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json").
    		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
    		Timeout(timeout).
    		Watch(ctx)
    }
    
    // Patch 修改指定范围(命名空间或集群)中的命名资源(根据名称修改)。
    func (c *client) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
      // 限定资源名称不能为空
    	if len(name) == 0 {
    		return nil, fmt.Errorf("name is required")
    	}
      // 这里通过tcp长连接(可重用),后面再rest包做具体分析(具体的执行逻辑还是在k8s api-server中调用k8s api的etcd相关接口)
    	result := c.client.client.
    		Patch(pt).
    		AbsPath(append(c.makeURLSegments(name), subresources...)...).
    		Body(data).
    		SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1,application/json").
    		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
    		Do(ctx)
    	if err := result.Error(); err != nil {
    		return nil, err
    	}
      // 获取请求结果的obj
    	obj, err := result.Get()
      // 如果PartialObjectMetadata没有注册到scheme  为true 
    	if runtime.IsNotRegisteredError(err) {
          // 获取请求结果body的字节数组
    		rawBytes, err := result.Raw()
    		if err != nil {
    			return nil, err
    		}
          // 最终要转化的对象(PartialObjectMetadata类型,包含了TypeMeta和ObjectMeta)
    		var partial metav1.PartialObjectMetadata
    		if err := json.Unmarshal(rawBytes, &partial); err != nil {
    			return nil, fmt.Errorf("unable to decode returned object as PartialObjectMetadata: %v", err)
    		}
          // 判断转化后的partial是否可能是ObjectMeta(根据ObjectMeta的UID/CreationTimestamp/Name/GenerateName,具体参考该方法)
    		if !isLikelyObjectMetadata(&partial) {
    			return nil, fmt.Errorf("object does not appear to match the ObjectMeta schema")
    		}
          // 本身通过该方法请求的就是只需要ObjectMeta对象,所以清空TypeMeta
    		partial.TypeMeta = metav1.TypeMeta{}
    		return &partial, nil
    	}
      // 有err则返回
    	if err != nil {
    		return nil, err
    	}
      // 类型转化
    	partial, ok := obj.(*metav1.PartialObjectMetadata)
    	if !ok {
    		return nil, fmt.Errorf("unexpected object, expected PartialObjectMetadata but got %T", obj)
    	}
    	return partial, nil
    }
    
  • 定义scheme相关变量及相关编解码器
    // 为上面ResourceClient删除操作的Options创建删除CodecFactory(编解码器工厂),提供Scheme参数
    var deleteScheme = runtime.NewScheme()
    // 为上面ResourceClient各种操作创建参数CodecFactory(编解码器工厂),提供Scheme参数
    var parameterScheme = runtime.NewScheme()
    // 创建删除CodecFactory(编解码器工厂),编解码DeleteOptions对象
    var deleteOptionsCodec = serializer.NewCodecFactory(deleteScheme)
    // 创建参数CodecFactory(编解码器工厂),编解码ListOptions/GetOptions/CreateOptions/UpdateOptions/PatchOptions对象,作为请求参数
    var dynamicParameterCodec = runtime.NewParameterCodec(parameterScheme)
    // 定义gv
    var versionV1 = schema.GroupVersion{Version: "v1"}
    // 添加ListOptions/GetOptions/DeleteOptions/CreateOptions/UpdateOptions/PatchOptions,初始化各个schema
    func init() {
    	metav1.AddToGroupVersion(parameterScheme, versionV1)
      metav1.AddToGroupVersion(deleteScheme, versionV1)
    }