Kubernetes Clientset

姚永年
2023-12-01

Kubernetes Clientset

资源类型 Scheme

Clienset 和 apiserver 通信时,需要根据资源对象的类型生成 Resource URL、对 Wire-data 进行编解码(序列化/反序列化)

资源类型的 Group、Version、Kind、go struct 定义、编解码(序列化/反序列化) 等内容构成了它的 Scheme

K8S 内置资源类型的 Scheme 位于 k8s.io/api/<group>/<version> 目录下,以 Deployment 为例:

$ pwd
/Users/zhangjun/go/src/gitlab.4pd.io/pht3/aol/vendor/k8s.io/api/extensions/v1beta1

$ ls -l
total 1048
-rw-r--r--  1 zhangjun  staff     642 Jan 22 15:16 doc.go
-rw-r--r--  1 zhangjun  staff  308747 Jan 22 15:16 generated.pb.go
-rw-r--r--  1 zhangjun  staff   49734 Jan 22 15:16 generated.proto
-rw-r--r--  1 zhangjun  staff    2042 Jan 22 15:16 register.go
-rw-r--r--  1 zhangjun  staff   69022 Jan 23 22:30 types.go
-rw-r--r--  1 zhangjun  staff   47996 Jan 22 15:16 types_swagger_doc_generated.go
-rw-r--r--  1 zhangjun  staff   41555 Jan 22 15:16 zz_generated.deepcopy.go

可以暂时忽略无关的文件,我们主要分析 types.gozz_generated.deepcopy.goregister.go 三个文件。

  1. types.go:定义本 <group>/<version> 下所有的资源类型和 codegen 注释;
  2. zz_generated.deepcopy.go:deepcopy-gen 工具创建的、定义各资源类型 DeepCopyObject() 方法的文件;
  3. register.go:定义了 AddToScheme() 函数,用于将本 <group>/<version> 下的各资源类型注册到 Clientset 使用的 Scheme 对象中(k8s.io/client-go/kubernetes/scheme/);
  4. 对于自定义资源类型,需要在 doc.go 中添加注释 // +groupName=<group_name>,否则后续自动生成的 fake clientset 的 Group 不对;

types.go 文件

该文件包含资源类型的 go struct 定义及 codegen 命令行工具使用的注释:

// 来源于:k8s.io/api/extensions/v1beta1/types.go

// +genclient
// +genclient:method=GetScale,verb=get,subresource=scale,result=Scale
// +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// DEPRECATED - This group version of Deployment is deprecated by apps/v1beta2/Deployment. See the release notes for
// more information.
// Deployment enables declarative updates for Pods and ReplicaSets.
type Deployment struct {
	metav1.TypeMeta `json:",inline"`
	// Standard object metadata.
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// Specification of the desired behavior of the Deployment.
	// +optional
	Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`

	// Most recently observed status of the Deployment.
	// +optional
	Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

// DeploymentSpec is the specification of the desired behavior of the Deployment.
type DeploymentSpec struct {
	Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"`
	Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,2,opt,name=selector"`
	Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"`
	Strategy DeploymentStrategy `json:"strategy,omitempty" patchStrategy:"retainKeys" protobuf:"bytes,4,opt,name=strategy"`
	MinReadySeconds int32 `json:"minReadySeconds,omitempty" protobuf:"varint,5,opt,name=minReadySeconds"`
	RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty" protobuf:"varint,6,opt,name=revisionHistoryLimit"`
	Paused bool `json:"paused,omitempty" protobuf:"varint,7,opt,name=paused"`
	RollbackTo *RollbackConfig `json:"rollbackTo,omitempty" protobuf:"bytes,8,opt,name=rollbackTo"`
	ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty" protobuf:"varint,9,opt,name=progressDeadlineSeconds"`
}

zz_generated.deepcopy.go 文件

所有注册到 Scheme 的资源类型都要实现 runtime.Object 接口:

// 来源于:k8s.io/apimachinery/pkg/runtime/interfaces.go
type Object interface {
	GetObjectKind() schema.ObjectKind
	DeepCopyObject() Object
}

K8S 各资源类型的 go struct 定义都嵌入了 metav1.TypeMeta 类型,而该类型实现了 GetObjectKind() 方法,故各资源类型只需要实现 DeepCopyObject() 方法。

我们不需要手动为各资源类型定义 DeepCopyObject() 方法,而是使用 deepcopy-gen 工具命令统一、自动地生成该方法。

deepcopy-gen 工具读取 types.go 文件中的 +k8s:deepcopy-gen 注释,如:

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

然后将生成的结果保存到 zz_generated.deepcopy.go 文件。

Deployment 类型为例:

// 来源于:k8s.io/api/extensions/v1beta1/zz_generated.deepcopy.go
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Deployment.
func (in *Deployment) DeepCopy() *Deployment {
	if in == nil {
		return nil
	}
	out := new(Deployment)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Deployment) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

register.go 文件

使用 deepcopy-gen 工具命令统一、自动地为各资源类型生成 DeepCopyObject() 方法后,各资源类型就满足了 runtime.Object 接口,进而可以注册到 Scheme 中。

各 API Group/Version 目录下都有一个 register.go 文件,该文件对外提供的 AddToScheme() 方法用于将本 Group/Version 下的各资源类型注册到传入的 Scheme 对象中(k8s.io/client-go/kubernetes/scheme/register.go 中创建该 Scheme 对象),然后 Clientset 就可以使用它进行 Wired-data 和对象之间的转换了。

// 来源于:k8s.io/api/extensions/v1beta1/register.go
// 本 package 的 Group 名称
const GroupName = "extensions"

// 注册时提供的 Group/Version 信息
// 一个 Group 目录下,有可能有多个 Version 的子目录
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"}

// Resource 实际就是资源类型的完整路径 <Group>/<Version>/<Plural>,如 extensions/v1beta1/deployments
// Plural 是资源类型的复数形式
func Resource(resource string) schema.GroupResource {
	return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
	SchemeBuilder      = runtime.NewSchemeBuilder(addKnownTypes)
	localSchemeBuilder = &SchemeBuilder
	// 对外暴露的 AddToScheme() 方法用于注册该 Group/Verion 下的所有资源类型
	AddToScheme        = localSchemeBuilder.AddToScheme
)

// 将本 Group/Version 下的所有资源类型注册到传入的 scheme
func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&Deployment{},
		&DeploymentList{},
		...
	)
	// Add the watch version that applies
	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
	return nil
}

注册所有内置资源类型到 Scheme 对象

需要将 k8s.io/api/<group>/<version> 目录下的各资源类型注册到全局 Scheme 对象,这样 Clienset 才能识别和使用它们。

client-go 的 scheme packagek8s.io/client-go/kubernetes/scheme/)定义了这个全局 Scheme 对象,并将各 k8s.io/api/<Group>/<Version> 目录下的资源类型注册到它上面。

Scheme 对象还被用于创建另外两个外部对象:

  1. 对资源类型对象进行编解码(序列化/反序列化)的工厂对象 Codecs,后续使用它配置 rest.Config.NegotiatedSerializer
  2. 参数编解码对象 ParameterCodec,后续调用 RestFul 的方法时使用,如 VersionedParams(&options, scheme.ParameterCodec)
// 来源于 k8s.io/client-go/kubernetes/scheme/register.go
// 新建一个 Scheme,后续所有 K8S 类型均添加到该 Scheme;
var Scheme = runtime.NewScheme()
// 为 Scheme 中的所有类型创建一个编解码工厂;
var Codecs = serializer.NewCodecFactory(Scheme)
// 为 Scheme 中的所有类型创建一个参数编解码工厂
var ParameterCodec = runtime.NewParameterCodec(Scheme)

// 将各 `k8s.io/api/<Group>/<Version>` 目录下的资源类型的 AddToScheme() 方法注册到 SchemeBuilder 中
var localSchemeBuilder = runtime.SchemeBuilder{
    ...
	extensionsv1beta1.AddToScheme,
    ...
}
var AddToScheme = localSchemeBuilder.AddToScheme

func init() {
    v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
	// 调用 SchemeBuilder 中各资源对象的 AddToScheme() 方法,将它们注册到到 Scheme 对象。
	utilruntime.Must(AddToScheme(Scheme))
}

创建和使用 Kubernetes Clientset

经过前面的铺垫分析后,我们开始分析 Kubernetes Clientset 的创建过程。

先从使用者的角度看看如何创建和使用 Kubernetes Clientset:

var err error
var config *rest.Config
// 使用 ServiceAccount 创建集群配置
if config, err = rest.InClusterConfig(); err != nil {
	// 使用 kubeConfig 指向的配置文件创建集群配置
	if config, err = clientcmd.BuildConfigFromFlags("", *kubeConfig); err != nil {
		panic(err.Error())
	}
}

// 创建 k8s clientset
clientset, err = kubernetes.NewForConfig(config)
if err != nil {
	panic(err.Error())
}

// 使用 clienset 创建一个 Deploy
deploy, err := c.kubeclientset.ExtensionsV1beta1().Deployments(aolDeploy.ObjectMeta.Namespace).Create(myDeploy)
  1. 使用 Kubeconfig 文件或 ServiceAccount 创建 Kubernetes 的 RestFul 配置参数;
  2. 使用 Kubernetes 的 RestFul 配置参数,创建 Clientset;
  3. 调用 Clientset 的方法对资源对象进行 CRUD;

创建支持所有资源类型的全局 Clientset

k8s.io/client-go/kubernetes/clientset.go 文件中创建的 Clientset 实际上是对各资源类型的 Clientset 做了一次封装:

  1. 调用各资源类型的 NewForConfig() 函数创建对应的 Clientset;
  2. 后续可以使用 Clientset.(),如 Clientset.ExtensionsV1beta1() 来调用具体资源类型的 Clientset;
// 来源于 k8s.io/client-go/kubernetes/clientset.go
// 传入的 rest.Config 包含 apiserver 服务器和认证信息
func NewForConfig(c *rest.Config) (*Clientset, error) {
	configShallowCopy := *c
    ...
    // 透传 rest.Config,调用具体分组和版本的资源类型的 ClientSet 构造函数
	cs.extensionsV1beta1, err = extensionsv1beta1.NewForConfig(&configShallowCopy)
	if err != nil {
		return nil, err
	}
    ...
	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
	if err != nil {
		return nil, err
	}
	return &cs, nil
}

// ExtensionsV1beta1 retrieves the ExtensionsV1beta1Client
func (c *Clientset) ExtensionsV1beta1() extensionsv1beta1.ExtensionsV1beta1Interface {
	return c.extensionsV1beta1
}

各资源类型的 Clientset

各资源类型的 Clientset 定义位于 k8s.io/client-go/kubernetes/typed/<group>/<version>/<plug>_client.go 文件中,如
k8s.io/client-go/kubernetes/typed/extensions/v1beta1/extensions_client.go

比较关键的是 setConfigDefaults() 函数,它负责为 Clientset 配置参数:

  1. 资源对象的 GroupVersion;
  2. 资源对象的 root path;
  3. 对 wired data 进行编解码(序列化/反序列化)的 NegotiatedSerializer,使用的 scheme.Codecs 为前面介绍过的 scheme package

RESTClient 根据配置的 root path 和 GroupVersion,构造 Resource 地址(格式为 /apis/<group>/<version>/<name>)。

// 来源于 k8s.io/client-go/kubernetes/typed/extensions/v1beta1/extensions_client.go
// 传入的 rest.Config 包含 apiserver 服务器和认证信息
func NewForConfig(c *rest.Config) (*ExtensionsV1beta1Client, error) {
    config := *c
    // 为 rest.Config 设置资源对象相关的参数
	if err := setConfigDefaults(&config); err != nil {
		return nil, err
    }
    // 创建 ExtensionsV1beta1 的 RestClient
	client, err := rest.RESTClientFor(&config)
	if err != nil {
		return nil, err
	}
	return &ExtensionsV1beta1Client{client}, nil
}

func setConfigDefaults(config *rest.Config) error {
    // 资源对象的 GroupVersion
	gv := v1beta1.SchemeGroupVersion
    config.GroupVersion = &gv
    // 资源对象的 root path
    config.APIPath = "/apis"
    // 使用注册的资源类型 Schema 对请求和响应进行编解码
    // scheme 为前面分析过的 k8s.io/client-go/kubernetes/scheme package
	config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}

	if config.UserAgent == "" {
		config.UserAgent = rest.DefaultKubernetesUserAgent()
	}

	return nil
}

func (c *ExtensionsV1beta1Client) Deployments(namespace string) DeploymentInterface {
	return newDeployments(c, namespace)
}

各资源类型的 RestFul 方法

使用各资源类型的 Clientset 创建特定资源类型的 RestFul 方法,参数的编解码工厂 scheme.ParameterCodec 来源于前面介绍的 scheme package 中。

// 来源于 k8s.io/client-go/kubernetes/typed/extensions/v1beta1/deployment.go
// newDeployments returns a Deployments
func newDeployments(c *ExtensionsV1beta1Client, namespace string) *deployments {
	return &deployments{
		client: c.RESTClient(),
		ns:     namespace,
	}
}

// Get takes name of the deployment, and returns the corresponding deployment object, and an error if there is any.
func (c *deployments) Get(name string, options v1.GetOptions) (result *v1beta1.Deployment, err error) {
	result = &v1beta1.Deployment{}
	// 发起实际的 RestFul 请求;
	err = c.client.Get().
		Namespace(c.ns).
		Resource("deployments").
		Name(name).
		VersionedParams(&options, scheme.ParameterCodec).
		Do().
		Into(result)
	return
}

使用资源类型的 Clientset 创建 Informer 和 Lister

使用 codegen 工具生成资源类型的 Clientset、Informer 和 Lister

目录结构:

zhangjun:core zhangjun$ pwd
/Users/zhangjun/go/src/gitlab.4pd.io/pht3/aol/pkg/apis/core/v1alpha1

zhangjun:v1alpha1 zhangjun$ ls -l
total 976
-rw-r--r--  1 zhangjun  staff     643 Jan 28 21:14 doc.go
-rw-r--r--  1 zhangjun  staff    1200 Jan 28 21:37 objectreference.go
-rw-r--r--  1 zhangjun  staff    2741 Jan 28 21:39 register.go
-rw-r--r--  1 zhangjun  staff  273720 Jan 28 21:40 types.go
-rw-r--r--  1 zhangjun  staff  154116 Jan 28 21:41 zz_generated.deepcopy.go

doc.go 文件里添加 // +k8s:deepcopy-gen=package,否则后续不会为 types.go 中的类型生成 DeepCopy() 方法:

// 来源于 doc.go

// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true

package v1alpha1

参考

Kubernetes Deep Dive: Code Generation for CustomResources

 类似资料:

相关阅读

相关文章

相关问答