k8s client-go快速入门教程及源代码阅读之静态客户端

文章目录

client-go的客户端可以分为两类,一类称为静态客户端,另一类称为动态客户端,前者的好处在接口已经固定,在写代码的时候有很好的提示,坏处是无法访问非k8s内置资源,后者的好处是够灵活,但是需要使用者知道GVK/GVR等前置知识。两者各有好处,需要根据自己需要选择。

其实还有一个发现客户端DiscoveryClient,但是它是用于发现集群资源版本的,而不是操作资源的客户端。

虽然分为两类,但是,只是两者暴露的接口不同而已,两者底层都是使用的rest.RESTClient对象,所以最终代码的深入都会到同一个地方,为了不造成文章的冗余,静态客户端和动态客户端的源码阅读部分都不会深入到发送请求,构造请求的层面,所以静态/动态客户端的源代码解读可能不够

快速入门

在之前的文章已经贴过使用静态客户端的增删改查的代码,这里就不贴完整的代码了,还是以deployment对象/资源为例, 代码删减如下:

func main() {
	var kubeconfig *string
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	clientset, err := kubernetes.NewForConfig(config)
	deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)

    deployment := &appsv1.Deployment{}

	// Create Deployment
	fmt.Println("Creating deployment...")
	result, err := deploymentsClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
	fmt.Printf("Created deployment %q.\n", result.GetObjectMeta().GetName())
    
	fmt.Println("Updating deployment...")
	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		result, getErr := deploymentsClient.Get(context.TODO(), "demo-deployment", metav1.GetOptions{})
		result.Spec.Replicas = int32Ptr(1)            // reduce replica count
		result.Spec.Template.Spec.Containers[0].Image = "nginx:1.13" // change nginx version
		_, updateErr := deploymentsClient.Update(context.TODO(), result, metav1.UpdateOptions{})
		return updateErr
	})
	fmt.Println("Updated deployment...")
    
	fmt.Printf("Listing deployments in namespace %q:\n", apiv1.NamespaceDefault)
	list, err := deploymentsClient.List(context.TODO(), metav1.ListOptions{})
	for _, d := range list.Items {
		fmt.Printf(" * %s (%d replicas)\n", d.Name, *d.Spec.Replicas)
	}

	fmt.Println("Deleting deployment...")
	deletePolicy := metav1.DeletePropagationForeground
	if err := deploymentsClient.Delete(context.TODO(), "demo-deployment", metav1.DeleteOptions{
		PropagationPolicy: &deletePolicy,
	}); err != nil {
		panic(err)
	}
	fmt.Println("Deleted deployment.")
}

完整版本可参考: https://github.com/kubernetes/client-go/tree/v0.20.2/examples/create-update-delete-deployment

客户端构造

静态客户端构造过程比较简单

clientset, err := kubernetes.NewForConfig(config)
deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)

因为k8s内置了非常多的资源,所以静态客户端是一个集合的概念,在这个集合的基础上可以继续选择自己所需资源的客户端。

代码的调用链还是比较简单的。

func NewForConfig(c *rest.Config) (*Clientset, error) {
	var cs Clientset
	var err error
	cs.appsV1, err = appsv1.NewForConfig(&configShallowCopy)
    // 省略其他客户端的配置代码
}

// appsv1.NewForConfig
func NewForConfig(c *rest.Config) (*AppsV1Client, error) {
	config := *c
	if err := setConfigDefaults(&config); err != nil {
		return nil, err
	}
	client, err := rest.RESTClientFor(&config)
	if err != nil {
		return nil, err
	}
	return &AppsV1Client{client}, nil
}

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

func newDeployments(c *AppsV1Client, namespace string) *deployments {
	return &deployments{
		client: c.RESTClient(),
		ns:     namespace,
	}
}

还记得之前说的GVR么,这里的apps是G(roup), v1是V(ersion), Deployments是R(esource),通过GVR我们就能定位到我们要使用的客户端。因为apps是一个组(Group)的概念, 所以这个组下面还有DaemonSetsStatefulSets等资源。

总的来说,我们可以通过GVR定位到了我们所需资源的客户端,下面再看看客户端的常用接口的源代码。

获取 Get/List

两者的源代码如下:

func (c *deployments) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("deployments").
		Name(name).
		VersionedParams(&options, scheme.ParameterCodec).
		Do(ctx).
		Into(result)
	return
}

func (c *deployments) List(ctx context.Context, opts metav1.ListOptions) (result *v1.DeploymentList, err error) {
	var timeout time.Duration
	if opts.TimeoutSeconds != nil {
		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
	}
	result = &v1.DeploymentList{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("deployments").
		VersionedParams(&opts, scheme.ParameterCodec).
		Timeout(timeout).
		Do(ctx).
		Into(result)
	return
}

可以看到两者的区别不是太大,前者多了一个Name的参数,用于指定资源的名字,这样就能定位到唯一的资源个体了,另一个区别就是options对象不一样。

值得注意的是所有资源的同类型操作,公用同一个Options对象,比如Get操作对应metav1.GetOptions

如果使用了构造者的设计模式,对诸如NamespaceResource等方法不会太陌生,这些方法基本上就是在restClient上设置一个属性。

新增 Create

代码如下:

func (c *deployments) Create(ctx context.Context, deployment *v1.Deployment, opts metav1.CreateOptions) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Post().
		Namespace(c.ns).
		Resource("deployments").
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(deployment).
		Do(ctx).
		Into(result)
	return
}

更新 Upadate/Patch

代码如下:

func (c *deployments) Update(ctx context.Context, deployment *v1.Deployment, opts metav1.UpdateOptions) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Put().
		Namespace(c.ns).
		Resource("deployments").
		Name(deployment.Name).
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(deployment).
		Do(ctx).
		Into(result)
	return
}

func (c *deployments) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Patch(pt).
		Namespace(c.ns).
		Resource("deployments").
		Name(name).
		SubResource(subresources...).
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(data).
		Do(ctx).
		Into(result)
	return
}

值得注意的是,我们常使用的kubectl apply对于更新的操作其实用的是Patch接口。

除了更新对象本身,很多资源还有子资源的说法,比如deployment还可以了UpdateStatus, UpdateScale

删除 Delete

代码如下:

func (c *deployments) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
	return c.client.Delete().
		Namespace(c.ns).
		Resource("deployments").
		Name(name).
		Body(&opts).
		Do(ctx).
		Error()
}

当然了,你也可以一次性删除多个deployment对象, 可以使用DeleteCollection方法。

总结

总的来说静态客户端的代码如果不深入到restClient其实阅读起来没啥难度,比较千篇一律,而看起来千篇一律的一个重要的原因就是,这些静态客户端代码是通过代码生成的。

因为不会深入到请求构造的代码中,所以本文可能看起来比较水,这个也没啥办法,慢慢写,慢慢看吧。其实大多数资源还有一个重要的方法,那就是Watch方法,这个主要用于k8s的Informer, 等讲到Informer在说吧。

kubernetes client-go源代码阅读之静态客户端

client-go的客户端可以分为两类,一类称为静态客户端,另一类称为动态客户端,前者的好处在接口已经固定,在写代码的时候有很好的提示,坏处是无法访问非k8s内置资源,后者的好处是够灵活,但是需要使用者知道GVK/GVR等前置知识。两者各有好处,需要根据自己需要选择。

其实还有一个发现客户端DiscoveryClient,但是它是用于发现集群资源版本的,而不是操作资源的客户端。

虽然分为两类,但是,只是两者暴露的接口不同而已,两者底层都是使用的rest.RESTClient对象,所以最终代码的深入都会到同一个地方,为了不造成文章的冗余,静态客户端和动态客户端的源码阅读部分都不会深入到发送请求,构造请求的层面,所以静态/动态客户端的源代码解读可能不够

快速入门

在之前的文章已经贴过使用静态客户端的增删改查的代码,这里就不贴完整的代码了,还是以deployment对象/资源为例, 代码删减如下:

func main() {
	var kubeconfig *string
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	clientset, err := kubernetes.NewForConfig(config)
	deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)

    deployment := &appsv1.Deployment{}

	// Create Deployment
	fmt.Println("Creating deployment...")
	result, err := deploymentsClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
	fmt.Printf("Created deployment %q.\n", result.GetObjectMeta().GetName())
    
	fmt.Println("Updating deployment...")
	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		result, getErr := deploymentsClient.Get(context.TODO(), "demo-deployment", metav1.GetOptions{})
		result.Spec.Replicas = int32Ptr(1)            // reduce replica count
		result.Spec.Template.Spec.Containers[0].Image = "nginx:1.13" // change nginx version
		_, updateErr := deploymentsClient.Update(context.TODO(), result, metav1.UpdateOptions{})
		return updateErr
	})
	fmt.Println("Updated deployment...")
    
	fmt.Printf("Listing deployments in namespace %q:\n", apiv1.NamespaceDefault)
	list, err := deploymentsClient.List(context.TODO(), metav1.ListOptions{})
	for _, d := range list.Items {
		fmt.Printf(" * %s (%d replicas)\n", d.Name, *d.Spec.Replicas)
	}

	fmt.Println("Deleting deployment...")
	deletePolicy := metav1.DeletePropagationForeground
	if err := deploymentsClient.Delete(context.TODO(), "demo-deployment", metav1.DeleteOptions{
		PropagationPolicy: &deletePolicy,
	}); err != nil {
		panic(err)
	}
	fmt.Println("Deleted deployment.")
}

完整版本可参考: https://github.com/kubernetes/client-go/tree/v0.20.2/examples/create-update-delete-deployment

客户端构造

静态客户端构造过程比较简单

clientset, err := kubernetes.NewForConfig(config)
deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)

因为k8s内置了非常多的资源,所以静态客户端是一个集合的概念,在这个集合的基础上可以继续选择自己所需资源的客户端。

代码的调用链还是比较简单的。

func NewForConfig(c *rest.Config) (*Clientset, error) {
	var cs Clientset
	var err error
	cs.appsV1, err = appsv1.NewForConfig(&configShallowCopy)
    // 省略其他客户端的配置代码
}

// appsv1.NewForConfig
func NewForConfig(c *rest.Config) (*AppsV1Client, error) {
	config := *c
	if err := setConfigDefaults(&config); err != nil {
		return nil, err
	}
	client, err := rest.RESTClientFor(&config)
	if err != nil {
		return nil, err
	}
	return &AppsV1Client{client}, nil
}

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

func newDeployments(c *AppsV1Client, namespace string) *deployments {
	return &deployments{
		client: c.RESTClient(),
		ns:     namespace,
	}
}

还记得之前说的GVR么,这里的apps是G(roup), v1是V(ersion), Deployments是R(esource),通过GVR我们就能定位到我们要使用的客户端。因为apps是一个组(Group)的概念, 所以这个组下面还有DaemonSetsStatefulSets等资源。

总的来说,我们可以通过GVR定位到了我们所需资源的客户端,下面再看看客户端的常用接口的源代码。

获取 Get/List

两者的源代码如下:

func (c *deployments) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("deployments").
		Name(name).
		VersionedParams(&options, scheme.ParameterCodec).
		Do(ctx).
		Into(result)
	return
}

func (c *deployments) List(ctx context.Context, opts metav1.ListOptions) (result *v1.DeploymentList, err error) {
	var timeout time.Duration
	if opts.TimeoutSeconds != nil {
		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
	}
	result = &v1.DeploymentList{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("deployments").
		VersionedParams(&opts, scheme.ParameterCodec).
		Timeout(timeout).
		Do(ctx).
		Into(result)
	return
}

可以看到两者的区别不是太大,前者多了一个Name的参数,用于指定资源的名字,这样就能定位到唯一的资源个体了,另一个区别就是options对象不一样。

值得注意的是所有资源的同类型操作,公用同一个Options对象,比如Get操作对应metav1.GetOptions

如果使用了构造者的设计模式,对诸如NamespaceResource等方法不会太陌生,这些方法基本上就是在restClient上设置一个属性。

新增 Create

代码如下:

func (c *deployments) Create(ctx context.Context, deployment *v1.Deployment, opts metav1.CreateOptions) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Post().
		Namespace(c.ns).
		Resource("deployments").
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(deployment).
		Do(ctx).
		Into(result)
	return
}

更新 Upadate/Patch

代码如下:

func (c *deployments) Update(ctx context.Context, deployment *v1.Deployment, opts metav1.UpdateOptions) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Put().
		Namespace(c.ns).
		Resource("deployments").
		Name(deployment.Name).
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(deployment).
		Do(ctx).
		Into(result)
	return
}

func (c *deployments) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Deployment, err error) {
	result = &v1.Deployment{}
	err = c.client.Patch(pt).
		Namespace(c.ns).
		Resource("deployments").
		Name(name).
		SubResource(subresources...).
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(data).
		Do(ctx).
		Into(result)
	return
}

值得注意的是,我们常使用的kubectl apply对于更新的操作其实用的是Patch接口。

除了更新对象本身,很多资源还有子资源的说法,比如deployment还可以了UpdateStatus, UpdateScale

删除 Delete

代码如下:

func (c *deployments) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
	return c.client.Delete().
		Namespace(c.ns).
		Resource("deployments").
		Name(name).
		Body(&opts).
		Do(ctx).
		Error()
}

当然了,你也可以一次性删除多个deployment对象, 可以使用DeleteCollection方法。

总结

总的来说静态客户端的代码如果不深入到restClient其实阅读起来没啥难度,比较千篇一律,而看起来千篇一律的一个重要的原因就是,这些静态客户端代码是通过代码生成的。

因为不会深入到请求构造的代码中,所以本文可能看起来比较水,这个也没啥办法,慢慢写,慢慢看吧。其实大多数资源还有一个重要的方法,那就是Watch方法,这个主要用于k8s的Informer, 等讲到Informer在说吧。

参考链接