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

文章目录

与client-go静态客户端相对应的自然就是动态客户端了,动态客户端的价值在于灵活,不用重新生成客户端代码就能访问k8s集群中的所有资源,即使是非内置资源。

快速入门

下面是官方的一个例子,完整版可参考: https://github.com/kubernetes/client-go/blob/v0.20.2/examples/dynamic-create-update-delete-deployment/main.go

func main() {
	var kubeconfig *string
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	client, err := dynamic.NewForConfig(config)

	deploymentRes := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}

	deployment := &unstructured.Unstructured{/*具体代码被省略了*/}

	// Create Deployment
	fmt.Println("Creating deployment...")
	result, err := client.Resource(deploymentRes).Namespace(namespace).Create(context.TODO(), deployment, metav1.CreateOptions{})
	fmt.Printf("Created deployment %q.\n", result.GetName())

	fmt.Println("Updating deployment...")
	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		// Retrieve the latest version of Deployment before attempting update
		// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
		result, getErr := client.Resource(deploymentRes).Namespace(namespace).Get(context.TODO(), "demo-deployment", metav1.GetOptions{})
		// update replicas to 1
		if err := unstructured.SetNestedField(result.Object, int64(1), "spec", "replicas"); err != nil {
			panic(fmt.Errorf("failed to set replica value: %v", err))
		}

		// extract spec containers
		containers, found, err := unstructured.NestedSlice(result.Object, "spec", "template", "spec", "containers")
		if err != nil || !found || containers == nil {
			panic(fmt.Errorf("deployment containers not found or error in spec: %v", err))
		}

		// update container[0] image
		if err := unstructured.SetNestedField(containers[0].(map[string]interface{}), "nginx:1.13", "image"); err != nil {
			panic(err)
		}
		if err := unstructured.SetNestedField(result.Object, containers, "spec", "template", "spec", "containers"); err != nil {
			panic(err)
		}

		_, updateErr := client.Resource(deploymentRes).Namespace(namespace).Update(context.TODO(), result, metav1.UpdateOptions{})
		return updateErr
	})
	fmt.Println("Updated deployment...")

	fmt.Printf("Listing deployments in namespace %q:\n", apiv1.NamespaceDefault)
	list, err := client.Resource(deploymentRes).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
	for _, d := range list.Items {
		replicas, found, err := unstructured.NestedInt64(d.Object, "spec", "replicas")
		if err != nil || !found {
			fmt.Printf("Replicas not found for deployment %s: error=%s", d.GetName(), err)
			continue
		}
		fmt.Printf(" * %s (%d replicas)\n", d.GetName(), replicas)
	}

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

动态客户端的与静态客户端的区别主要在于是否手动传GVR。

客户端构造

具体定位到deployment客户端,可以这么写

client, err := dynamic.NewForConfig(config)
deploymentRes := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
deploymentsClient := client.Resource(deploymentRes).Namespace(namespace)

为了减少文章篇幅,动态客户端就只分析Create的代码了, 其他接口代码可阅读client-go/dynamic/simple.go,大致逻辑都是差不多的,不同点主要在于验证逻辑和最终的构造方法。

Create 代码解析

代码如下

func (c *dynamicResourceClient) Create(ctx context.Context, obj *unstructured.Unstructured, opts metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
    // 1. 
	outBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
    
    // 2.
	name := ""
	if len(subresources) > 0 {
		accessor, err := meta.Accessor(obj)
		name = accessor.GetName()
		if len(name) == 0 {
			return nil, fmt.Errorf("name is required")
		}
	}
	// 3.
	result := c.client.client.
		Post().
		AbsPath(append(c.makeURLSegments(name), subresources...)...).
		Body(outBytes).
		SpecificallyVersionedParams(&opts, dynamicParameterCodec, versionV1).
		Do(ctx)
	// 4. 
	retBytes, err := result.Raw()
	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, retBytes)
	return uncastObj.(*unstructured.Unstructured), nil
}

代码主要分为四个部分。

  1. 将对象序列化成[]byte对象,用于后续上传
  2. 如果是子资源,就进一步获取子资源的名字,比如status, scale等
  3. 构造请求,具体就是使用Post方法,构造请求路径,设置请求体参数, 指定编解码参数等,最后请求
  4. 将结果解码并返回

k8s里面的编解码是个很复杂的过程,这个以后再说,可以单独写一篇或者两篇文章呢。

总结

在不使用反射的情况下,静态客户端的接口调用比较机械化,不够灵活,所以存在动态客户端,GVR的指定可以动态的指定,相较于静态客户端要灵活很多,两者各有优缺点。

参考链接