【k8s开发篇】k8s client-go 之 四种client

参考

  1. client-go实战之一:准备工作
  2. client-go实战之二:RESTClient
  3. client-go实战之三:Clientset
  4. client-go实战之四:dynamicClient
  5. client-go实战之五:DiscoveryClient
  6. Kubernetes的Group、Version、Resource学习小记

感想

概括

  • 1 | RESTClient 只对一种GVK操作的 client
  • 2 | Clientset 对多种GVK操作的 client集合
  • 3 | dynamicClient 适合识别非结构体资源,如自定义资源CRD
  • 4 | DiscoveryClient 用于发现目前有多少种 GVK

0 | 开发接口文档

  • https://v1-19.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#-strong-write-operations-deployment-v1-apps-strong-
  • 数据结构初始化的烦恼
    看过下面代码后您可能在烦恼:创建资源时,数据结构的字段太多太复杂根本记不住,对应的代码不好写,这里分享一个我的做法,如下图,我在开发的时候一共有两个窗口,左侧是官方的yaml示例,右侧用了GoLand的分屏功能,分屏的左侧是我写代码的窗口,右侧是数据结构定义,此时内容不会搞错,数据结构也能对应上,写起来就舒服多了:

【k8s开发篇】k8s client-go 之 四种client_第1张图片

1 | RESTClient

  • client-go实战之二:RESTClient

  • 下面例子就是 模拟获取 pod

    • GET /api/v1/namespaces/{namespace}/pods/{name}
    • https://v1-19.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#-strong-read-operations-pod-v1-core-strong-
  • 可以理解 RESTClient 客户端为最笨的方法

    • 需要实现上面的请求

    • 定义组,版本配置信息 config.GroupVersion = &corev1.SchemeGroupVersion

    • 创建客户端实例 restClient, err := rest.RESTClientFor(config)

    • 进行请求获取结果(指定 ns,指定资源类型,要获取多少个,结果存到 result 结构体中)

      • 	// 保存pod结果的数据结构实例
        	result := &corev1.PodList{}
        
          //  ----------  此部分是最重要的 发送get请求 ---------
        	//  指定namespace
        	namespace := "kube-system"
        	// 设置请求参数,然后发起请求
        	// GET请求
        	err = restClient.Get().
        		//  指定namespace,参考path : /api/v1/namespaces/{namespace}/pods
        		Namespace(namespace).
        		// 查找多个pod,参考path : /api/v1/namespaces/{namespace}/pods
        		Resource("pods").
        		// 指定大小限制和序列化工具
        		VersionedParams(&metav1.ListOptions{Limit:100}, scheme.ParameterCodec).
        		// 请求
        		Do(context.TODO()).
        		// 结果存入result
        		Into(result)
        
  • 全部代码如下:

  • RESTClient是client-go最基础的客户端,主要是对HTTP Reqeust进行了封装,对外提供RESTful风格的API,并且提供丰富的API用于各种设置,相比其他几种客户端虽然更复杂,但是也更为灵活;
  • 使用RESTClient对kubernetes的资源进行增删改查的基本步骤如下:
  1. 确定要操作的资源类型(例如查找deployment列表),去官方API文档中找到对于的path、数据结构等信息,后面会用到;
  2. 加载配置kubernetes配置文件(和kubectl使用的那种kubeconfig完全相同);
  3. 根据配置文件生成配置对象,并且通过API对配置对象就行设置(例如请求的path、Group、Version、序列化反序列化工具等);
  4. 创建RESTClient实例,入参是配置对象;
  5. 调用RESTClient实例的方法向kubernetes的API Server发起请求,编码用fluent风格将各种参数传入(例如指定namespace、资源等),如果是查询类请求,还要传入数据结构实例的指针,改数据结构用于接受kubernetes返回的查询结果;
  • 接下来的编码实战也是按照上述流程进行的;
package main

import (
	"context"
	"flag"
	"fmt"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"path/filepath"
)

func main() {
	var kubeconfig *string

	// home是家目录,如果能取得家目录的值,就可以用来做默认值
	if home:=homedir.HomeDir(); home != "" {
		// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
		// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		// 如果取不到当前用户的家目录,就没办法设置kubeconfig的默认目录了,只能从入参中取
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}

	flag.Parse()

	// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

	// kubeconfig加载失败就直接退出了
	if err != nil {
		panic(err.Error())
	}

	// 参考path : /api/v1/namespaces/{namespace}/pods
	config.APIPath = "api"
	// pod的group是空字符串  指定要访问的组  core  v1
	config.GroupVersion = &corev1.SchemeGroupVersion
	// 指定序列化工具
	config.NegotiatedSerializer = scheme.Codecs

	// 根据配置信息构建restClient实例
	restClient, err := rest.RESTClientFor(config)

	if err!=nil {
		panic(err.Error())
	}

	// 保存pod结果的数据结构实例
	result := &corev1.PodList{}

  //  ----------  此部分是最重要的 发送get请求 ---------
	//  指定namespace
	namespace := "kube-system"
	// 设置请求参数,然后发起请求
	// GET请求
	err = restClient.Get().
		//  指定namespace,参考path : /api/v1/namespaces/{namespace}/pods
		Namespace(namespace).
		// 查找多个pod,参考path : /api/v1/namespaces/{namespace}/pods
		Resource("pods").
		// 指定大小限制和序列化工具
		VersionedParams(&metav1.ListOptions{Limit:100}, scheme.ParameterCodec).
		// 请求
		Do(context.TODO()).
		// 结果存入result
		Into(result)

	if err != nil {
		panic(err.Error())
	}

	// 表头
	fmt.Printf("namespace\t status\t\t name\n")

	// 每个pod都打印namespace、status.Phase、name三个字段
	for _, d := range result.Items {
		fmt.Printf("%v\t %v\t %v\n",
			d.Namespace,
			d.Status.Phase,
			d.Name)
	}
}

2 | Clientset

  • client-go实战之三:Clientset

  • Clientset源码阅读的**切入点就是其名字中的set,**这是个集合,里面有很多东西,看一下Clientset数据结构的源码(限于篇幅只展示了一部分):

    • 本质很简单:就是封装,上面说过 RESTClient,其只能指定一种(group/verison)的客户端(还需要手动配置),所以这里就进行了封装直接用就好
    • 比如 appsV1 *appsv1.AppsV1Client 就相当于下面两步的组合,节省了人力(不需手动创建)
      • config.GroupVersion = &corev1.SchemeGroupVersion
      • restClient, err := rest.RESTClientFor(config)
type Clientset struct {
	*discovery.DiscoveryClient
	admissionregistrationV1      *admissionregistrationv1.AdmissionregistrationV1Client
	admissionregistrationV1beta1 *admissionregistrationv1beta1.AdmissionregistrationV1beta1Client
	internalV1alpha1             *internalv1alpha1.InternalV1alpha1Client
	appsV1                       *appsv1.AppsV1Client
	appsV1beta1                  *appsv1beta1.AppsV1beta1Client
	appsV1beta2                  *appsv1beta2.AppsV1beta2Client
	authenticationV1             *authenticationv1.AuthenticationV1Client
    ...

  • 举例子

    • 看下面 func createDeployment(clientset *kubernetes.Clientset) 部分

      • 其实就是实现创建 deployment 的请求 POST /apis/apps/v1/namespaces/{namespace}/deployments
    • 可以看到我们不需要手动创建 RESTClient,直接从 clientset 中获取即可

      • // POST /apis/apps/v1/namespaces/{namespace}/deployments
        // 获取 deployment 操作客户端
        deploymentClient := clientset.
        		AppsV1().
        		Deployments(NAMESPACE)
        
    • 获取客户端后,构建请求内容,然后发起请求进行创建

      • // 实例化一个数据结构
        	deployment := &appsv1.Deployment{
        		....
        	}
        // 发起创建 deployment 请求
        // POST /apis/apps/v1/namespaces/{namespace}/deployments
        	result, err := deploymentClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
        
  • 全部代码:

  • 本次编码实战的需求如下:
  • 写一段代码,检查用户输入的operate参数,该参数默认是create,也可以接受clean;
  • 如果operate参数等于create,就执行以下操作:
  1. 新建名为test-clientset的namespace
  2. 新建一个deployment,namespace为test-clientset,镜像用tomcat,副本数为2
  3. 新建一个service,namespace为test-clientset,类型是NodePort
  • 如果operate参数等于clean,就删除create操作中创建的service、deployment、namespace等资源:
  • 以上需求使用Clientset客户端实现,完成后咱们用浏览器访问来验证tomcat是否正常;
package main

import (
	"context"
	"flag"
	"fmt"
	appsv1 "k8s.io/api/apps/v1"
	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"k8s.io/utils/pointer"
	"path/filepath"
)

const (
	NAMESPACE = "test-clientset"
	DEPLOYMENT_NAME = "client-test-deployment"
	SERVICE_NAME = "client-test-service"
)

func main() {

	var kubeconfig *string

	// home是家目录,如果能取得家目录的值,就可以用来做默认值
	if home:=homedir.HomeDir(); home != "" {
		// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
		// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		// 如果取不到当前用户的家目录,就没办法设置kubeconfig的默认目录了,只能从入参中取
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}

	// 获取用户输入的操作类型,默认是create,还可以输入clean,用于清理所有资源
	operate := flag.String("operate", "create", "operate type : create or clean")

	flag.Parse()

	// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

	// kubeconfig加载失败就直接退出了
	if err != nil {
		panic(err.Error())
	}

	// 实例化clientset对象
	clientset, err := kubernetes.NewForConfig(config)

	if err!= nil {
		panic(err.Error())
	}

	fmt.Printf("operation is %v\n", *operate)

	// 如果要执行清理操作
	if "clean"==*operate {
		clean(clientset)
	} else {
		// 创建namespace
		createNamespace(clientset)

		// 创建deployment
		createDeployment(clientset)

		// 创建service
		createService(clientset)
	}
}

// 清理本次实战创建的所有资源
func clean(clientset *kubernetes.Clientset) {
	emptyDeleteOptions := metav1.DeleteOptions{}

	// 删除service
	if err := clientset.CoreV1().Services(NAMESPACE).Delete(context.TODO(), SERVICE_NAME, emptyDeleteOptions) ; err != nil {
		panic(err.Error())
	}

	// 删除deployment
	if err := clientset.AppsV1().Deployments(NAMESPACE).Delete(context.TODO(), DEPLOYMENT_NAME, emptyDeleteOptions) ; err != nil {
		panic(err.Error())
	}

	// 删除namespace
	if err := clientset.CoreV1().Namespaces().Delete(context.TODO(), NAMESPACE, emptyDeleteOptions) ; err != nil {
		panic(err.Error())
	}
}

// 新建namespace
func createNamespace(clientset *kubernetes.Clientset) {
	namespaceClient := clientset.CoreV1().Namespaces()

	namespace := &apiv1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: NAMESPACE,
		},
	}

	result, err := namespaceClient.Create(context.TODO(), namespace, metav1.CreateOptions{})

	if err!=nil {
		panic(err.Error())
	}

	fmt.Printf("Create namespace %s \n", result.GetName())
}

// 新建service
func createService(clientset *kubernetes.Clientset) {
	// 得到service的客户端
	serviceClient := clientset.CoreV1().Services(NAMESPACE)

	// 实例化一个数据结构
	service := &apiv1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name: SERVICE_NAME,
		},
		Spec: apiv1.ServiceSpec{
			Ports: []apiv1.ServicePort{{
					Name: "http",
					Port: 8080,
					NodePort: 30080,
				},
			},
			Selector: map[string]string{
				"app" : "tomcat",
			},
			Type: apiv1.ServiceTypeNodePort,
		},
	}

	result, err := serviceClient.Create(context.TODO(), service, metav1.CreateOptions{})

	if err!=nil {
		panic(err.Error())
	}

	fmt.Printf("Create service %s \n", result.GetName())
}

// POST /apis/apps/v1/namespaces/{namespace}/deployments
// 新建deployment
func createDeployment(clientset *kubernetes.Clientset) {
	// 得到deployment的客户端
	deploymentClient := clientset.
		AppsV1().
		Deployments(NAMESPACE)

	// 实例化一个数据结构
	deployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name: DEPLOYMENT_NAME,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: pointer.Int32Ptr(2),
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app" : "tomcat",
				},
			},

			Template: apiv1.PodTemplateSpec{
				ObjectMeta:metav1.ObjectMeta{
					Labels: map[string]string{
						"app" : "tomcat",
					},
				},
				Spec: apiv1.PodSpec{
					Containers: []apiv1.Container{
						{
							Name: "tomcat",
							Image: "tomcat:8.0.18-jre8",
							ImagePullPolicy: "IfNotPresent",
							Ports: []apiv1.ContainerPort{
								{
									Name: "http",
									Protocol: apiv1.ProtocolSCTP,
									ContainerPort: 8080,
								},
							},
						},
					},
				},
			},
		},
	}

	result, err := deploymentClient.Create(context.TODO(), deployment, metav1.CreateOptions{})

	if err!=nil {
		panic(err.Error())
	}

	fmt.Printf("Create deployment %s \n", result.GetName())
}

3 | dynamicClient

3.1 | 前置知识1 —— Object.runtime (资源对象的顶级抽象)

  • 在kubernetes的代码世界中,资源对象对应着具体的数据结构,这些数据结构都实现了同一个接口,名为Object.runtime,源码位置是staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go,定义如下:
type Object interface {
	GetObjectKind() schema.ObjectKind
	DeepCopyObject() Object
}
  • DeepCopyObject方法顾名思义,就是深拷贝,也就是将内存中的对象克隆出一个新的对象;
  • 至于GetObjectKind方法的作用,相信聪明的您也猜到了:处理Object.runtime类型的变量时,只要调用其GetObjectKind方法就知道它的具体身份 kind了(如deployment,service等);
  • 最后再次强调:**资源对象都是Object.runtime的实现;(记住Object.runtime是资源对象的最顶级抽象) **
  • 有人可能迷茫 资源资源对象 这两个概念:
    • 资源:一般指 kind,如 pod,deployment,daemonset 等
    • 资源对象:指的是资源的实例化,比如创建一个具体的pod(设置 name namespace image 等)
    • 可以理解为资源为一种类型,资源对象 为该类型实例化的一个实例

3.2 | 前置知识2 —— Unstructured(承接非结构化数据)

  • Unstructured 就是非结构化数据,就是没有一个固定的数据结构,与此相对的是结构化数据

  • 结构化数据如下:

    • 先看一个简单的JSON字符串:

    • {
      	"id": 101,
      	"name": "Tom"
      }
      
    • 上述JSON的字段名称和字段值类型都是固定的,因此可以针对性编写一个数据结构来处理它:

    • type Person struct {
      	ID int
      	Name String
      }
      
  • 为什么我们需要考虑 Unstructured 数据 ?

    • 在实际的kubernetes环境中,可能会遇到一些无法预知结构的数据,例如前面的JSON字符串中还有第三个字段,字段值的具体内容和类型在编码时并不知晓,而是在真正运行的时候才知道,那么在编码时如何处理呢?相信您会想到用interface{}来表示,实际上client-go也是这么做的,来看Unstructured数据结构的源码,路径是staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go

    • type Unstructured struct {
      	// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
      	// map[string]interface{}
      	// children.
      	Object map[string]interface{}
      }
      
    • 显然,上述数据结构定义并不能发挥什么作用,真正重要的是关联的方法,如下图,可见client-go已经为Unstructured准备了丰富的方法,借助这些方法可以灵活的处理非结构化数据:

    • 【k8s开发篇】k8s client-go 之 四种client_第2张图片

3.3 | 前置知识点3 —— Unstructured与资源对象的相互转换

  • 一个非常重要的知识点:可以用Unstructured实例生成资源对象,也可以用资源对象生成Unstructured实例,这个神奇的能力是unstructuredConverterFromUnstructuredToUnstructured方法分别实现的,下面的代码片段展示了如何将Unstructured实例转为PodList实例:

    // 实例化一个PodList数据结构,用于接收从unstructObj转换后的结果
    podList := &apiv1.PodList{}
    
    // unstructObj
    err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructObj.UnstructuredContent(), podList)
    
    
  • 您可能会好奇上述FromUnstructured方法究竟是如何实现转换的,咱们去看下此方法的内部实现,如下图所示,其实也没啥悬念了,通过反射可以得到podList的字段信息

    【k8s开发篇】k8s client-go 之 四种client_第3张图片

  • 至此,Unstructured的分析就结束了吗?没有,强烈推荐您进入上图红框2中的fromUnstructured方法去看细节,这里面是非常精彩的,以podList为例,这是个数据结构,而fromUnstructured只处理原始类型,对于数据结构会调用structFromUnstructured方法处理,在structFromUnstructured方法中处理数据结构的每个字段,又会调用fromUnstructured,这是相互迭代的过程,最终,不论podList中有多少数据结构的嵌套都会被处理掉,篇幅所限就不展开相信分析了,下图是一部分关键代码:

    【k8s开发篇】k8s client-go 之 四种client_第4张图片

  • 小结:Unstructured转为资源对象的套路并不神秘:

    • 无非是用反射取得资源对象的字段类型
    • 然后按照字段名去Unstructured的map中取得原始数据
    • 再用反射设置到资源对象的字段中即可;

3.4 dynamicClient 正式介绍

  • 为什么需要这种动态客户端?

    • deployment、pod这些资源,其数据结构是明确的固定的,可以精确对应到Clientset中的数据结构和方法
    • 但是对于CRD(用户自定义资源),Clientset客户端就无能为力了,此时需要有一种数据结构来承载资源对象的数据,也要有对应的方法来处理这些数据;
      • 此刻,前面提到的Unstructured可以登场了,没错,把Clientset不支持的资源对象交给Unstructured来承载,接下来看看dynamicClient和Unstructured的关系:
  • 先看数据结构定义,和clientset没啥区别,只有个restClient字段:

    type dynamicClient struct {
    	client *rest.RESTClient
    }
    
  • 这个数据结构只有一个关联方法Resource,入参为GVR,返回的是另一个数据结构dynamicResourceClient:

    func (c *dynamicClient) Resource(resource schema.GroupVersionResource) NamespaceableResourceInterface {
    	return &dynamicResourceClient{client: c, resource: resource}
    }
    
  • 通过上述代码可知,dynamicClient的关键是数据结构dynamicResourceClient及其关联方法,来看看这个dynamicResourceClient,如下图,果然,dynamicClient所有和资源相关的操作都是dynamicResourceClient在做(代理模式?),选了create方法细看,序列化和反序列化都交给unstructured的UnstructuredJSONScheme,与kubernetes的交互交给Restclient
    【k8s开发篇】k8s client-go 之 四种client_第5张图片

  • 小结:

    • 与Clientset不同,dynamicClient为各种类型的资源都提供统一的操作API,资源需要包装为Unstructured数据结构
    • 内部使用了Restclient与kubernetes交互;

需求确认

  • 本次编码实战的需求很简单:查询指定namespace下的所有pod,然后在控制台打印出来,要求用dynamicClient实现;
  • 您可能会问:pod是kubernetes的内置资源,更适合Clientset来操作,而dynamicClient更适合处理CRD不是么?—您说得没错,这里用pod是因为折腾CRD太麻烦了,定义好了还要在kubernetes上发布,于是干脆用pod来代替CRD,反正dynamicClient都能处理,咱们通过实战掌握dynamicClient的用法就行了,以后遇到各种资源都能处理之;
package main

import (
	"context"
	"flag"
	"fmt"
	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"path/filepath"
)

func main() {

	var kubeconfig *string

	// home是家目录,如果能取得家目录的值,就可以用来做默认值
	if home:=homedir.HomeDir(); home != "" {
		// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
		// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		// 如果取不到当前用户的家目录,就没办法设置kubeconfig的默认目录了,只能从入参中取
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}

	flag.Parse()

	// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

	// kubeconfig加载失败就直接退出了
	if err != nil {
		panic(err.Error())
	}

	dynamicClient, err := dynamic.NewForConfig(config)

	if err != nil {
		panic(err.Error())
	}

	// dynamicClient的唯一关联方法所需的入参
	gvr := schema.GroupVersionResource{Version: "v1", Resource: "pods"}

	// 使用dynamicClient的查询列表方法,查询指定namespace下的所有pod,
	// 注意此方法返回的数据结构类型是UnstructuredList
	unstructObj, err := dynamicClient.
		Resource(gvr).
		Namespace("kube-system").
		List(context.TODO(), metav1.ListOptions{Limit: 100})

	if err != nil {
		panic(err.Error())
	}

	// 实例化一个PodList数据结构,用于接收从unstructObj转换后的结果
	podList := &apiv1.PodList{}

	// 转换
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructObj.UnstructuredContent(), podList)

	if err != nil {
		panic(err.Error())
	}

	// 表头
	fmt.Printf("namespace\t status\t\t name\n")

	// 每个pod都打印namespace、status.Phase、name三个字段
	for _, d := range podList.Items {
		fmt.Printf("%v\t %v\t %v\n",
			d.Namespace,
			d.Status.Phase,
			d.Name)
	}
}
  • 上述代码中有三处重点需要注意:
  1. Resource方法指定了本次操作的资源类型;
  2. List方法向kubernetes发起请求;
  3. FromUnstructured将Unstructured数据结构转成PodList,其原理前面已经分析过;
  • 执行go run main.go,如下,可以从kubernetes取得数据,并且转换成PodList也正常:
zhaoqin@zhaoqindeMBP-2 dynamicclientdemo % go run main.go
namespace        status          name
kube-system      Running         coredns-7f89b7bc75-5pdwc
kube-system      Running         coredns-7f89b7bc75-nvbvm
kube-system      Running         etcd-hedy
kube-system      Running         kube-apiserver-hedy
kube-system      Running         kube-controller-manager-hedy
kube-system      Running         kube-flannel-ds-v84vc
kube-system      Running         kube-proxy-hlppx
kube-system      Running         kube-scheduler-hedy

4 | DiscoveryClient

  • 咱们之前学习的Clientset和dynamicClient都是面向资源对象的(例如创建deployment实例、查看pod实例)

    • 理解为:创建实例,创建资源对象
  • DiscoveryClient则不同,它聚焦的是资源,例如查看当前kubernetes有哪些Group、Version、Resource

    • 理解为:获取资源,就是 GVK

下面是DiscoveryClient数据结构的字段和关联方法,再次看到了熟悉的restClient字段,还有一众方法皆是与Group、Version、Resource有关:

【k8s开发篇】k8s client-go 之 四种client_第6张图片

  • 从上图可见,DiscoveryClient数据结构有两个字段:restClient和LegacyPrefix

    • 这个LegacyPrefix是啥呢?去看看新建DiscoveryClient实例的方法,如下图红框,原来是个固定字符串/api,看起来像是url中的一部分:

    【k8s开发篇】k8s client-go 之 四种client_第7张图片

  • 挑一个DiscoveryClient的关联方法看看,如下图红框,果然,LegacyPrefix就是url中的一部分:

    【k8s开发篇】k8s client-go 之 四种client_第8张图片

需求确认

  • 本次实战的需求很简单:从kubernetes查询所有的Group、Version、Resource信息,在控制台打印出来;
package main

import (
	"flag"
	"fmt"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"path/filepath"
)

func main() {

	var kubeconfig *string

	// home是家目录,如果能取得家目录的值,就可以用来做默认值
	if home:=homedir.HomeDir(); home != "" {
		// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
		// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		// 如果取不到当前用户的家目录,就没办法设置kubeconfig的默认目录了,只能从入参中取
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}

	flag.Parse()

	// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

	// kubeconfig加载失败就直接退出了
	if err != nil {
		panic(err.Error())
	}

	// 新建discoveryClient实例
	discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)

	if err != nil {
		panic(err.Error())
	}

	// 获取所有分组和资源数据
	APIGroup, APIResourceListSlice, err := discoveryClient.ServerGroupsAndResources()

	if err != nil {
		panic(err.Error())
	}

	// 先看Group信息
	fmt.Printf("APIGroup :\n\n %v\n\n\n\n",APIGroup)

	// APIResourceListSlice是个切片,里面的每个元素代表一个GroupVersion及其资源
	for _, singleAPIResourceList := range APIResourceListSlice {

		// GroupVersion是个字符串,例如"apps/v1"
		groupVerionStr := singleAPIResourceList.GroupVersion

		// ParseGroupVersion方法将字符串转成数据结构
		gv, err := schema.ParseGroupVersion(groupVerionStr)

		if err != nil {
			panic(err.Error())
		}

		fmt.Println("*****************************************************************")
		fmt.Printf("GV string [%v]\nGV struct [%#v]\nresources :\n\n", groupVerionStr, gv)

		// APIResources字段是个切片,里面是当前GroupVersion下的所有资源
		for _, singleAPIResource := range singleAPIResourceList.APIResources {
			fmt.Printf("%v\n", singleAPIResource.Name)
		}
	}
}
  • 执行go run main.go,截取部分执行结果如下,所有资源都被打印出来了:
...
*****************************************************************
GV string [discovery.k8s.io/v1beta1]
GV struct [schema.GroupVersion{Group:"discovery.k8s.io", Version:"v1beta1"}]
resources :

endpointslices
*****************************************************************
GV string [flowcontrol.apiserver.k8s.io/v1beta1]
GV struct [schema.GroupVersion{Group:"flowcontrol.apiserver.k8s.io", Version:"v1beta1"}]
resources :

flowschemas
flowschemas/status
prioritylevelconfigurations
prioritylevelconfigurations/status
  • 以上就是DiscoveryClient的基本用法,您是否觉得这样的实战太easy了,那咱们就来个延伸阅读,看看DiscoveryClient的周边场景;

4.1 | kubectl中如何使用DiscoveryClient

  • kubectl api-versions命令 —— 获取 所有 api
    • 大家应该不陌生吧,可以返回当前kubernetes环境的所有Group+Version的组合,如下:
zhaoqin@zhaoqindeMBP-2 discoveryclientdemo % kubectl api-versions
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1
apiextensions.k8s.io/v1
apiextensions.k8s.io/v1beta1
apiregistration.k8s.io/v1
apiregistration.k8s.io/v1beta1
apps/v1
authentication.k8s.io/v1
...

  • 通过查看kubectl源码可见,上述命令的背后就是使用了DiscoveryClient来实现的,如下图红框所示:

【k8s开发篇】k8s client-go 之 四种client_第9张图片

  • 还有一处没有明确:上图红框2中的o.discoveryClient究竟是不是DiscoveryClient呢?虽然名字很像,但还是瞅一眼才放心,结果这一瞅有了新发现,如下所示,discoveryClient的数据结构是CachedDiscoveryInterface:
type APIVersionsOptions struct {
	discoveryClient discovery.CachedDiscoveryInterface

	genericclioptions.IOStreams
}
  • 从名称CachedDiscoveryInterface来看,kubectl对GVR数据是做了本地缓存的,想想也是,GVR不经常变化,没必要每次都去API Server拉取,关于缓存的细节请参考:staging/src/k8s.io/client-go/discovery/cached/disk/cached\_discovery.go,这里就不展开了;
  • 至此,client-go的四种客户端工具实战以及相关源码的浅层次分析就全部完成了,在您做client-go开发的时候,希望这些内容能给您提供一些参考;

你可能感兴趣的:(Kubernetes学习笔记,golang,kubernetes,开发语言)