目录

Client-go简单教程(五)-DynamicClient

DynamicClient 是一种动态客户端,它可以操作任意的 Kubernetes 资源,即不仅可以操作 Kubernetes 自身内置的资源,还可操作 CRD.

DynamicClient 简介

技巧
在几乎所有情况下,除了 Server Side Apply (SSA) 之外,我们都应优先使用 ClientSet 而不是 DynamicClient

前文咱们学习了 ClientSet 客户端,发现 ClientSetdeployment 、service 这些 kubernetes 内置资源的时候是很方便的,每个资源都有其专属的方法,配合官方 API 文档和数据结构定义,开发起来比 Restclient 高效。 但如果要处理的不是 kubernetes 的内置资源,比如 CRD ,ClientSet 的代码中可没有用户自定义的东西,显然就用不上 Clientset 了,此时就需要使用 DynamicClient 了。

DynamicClient 它没有使用 k8s.io/api 中定义的 的各种 API 资源的 Go 结构体,而是使用 Unstructured 表示所有资源对象。

Unstructured 类型使用一个嵌套的 map[string]inferface{} 值来表示 API 资源的内部结构,该结构和服务端的 REST 负载非常相似。

Unstructured

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

1
2
3
4
{
	"id": 101,
	"name": "Tom"
}

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

1
2
3
4
type Person struct {
	ID int
	Name String
}

对于上面的 JSON 字符串就是结构化数据(Structured Data)

与结构化数据相对的就是非结构化数据了(Unstructured Data),在实际的kubernetes环境中,可能会遇到一些无法预知结构的数据,例如前面的 JSON 字符串中还有第三个字段,字段值的具体内容和类型在编码时并不知晓,而是在真正运行的时候才知道,来看 Unstructured 数据结构的源码,路径是 staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go

1
2
3
4
5
6
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-goUnstructured 准备了丰富的方法,借助这些方法可以灵活的处理非结构化数据。

小结:

Clientset 不同,DynamicClient 为各种类型的资源都提供统一的操作 API,资源需要包装为 Unstructured 数据结构

DynamicClient 编码

rollouts 资源并不是 kubernetes 内置的资源对象,我们通过 DynamicClient 来演示一下,如何获取 rollouts 资源的。

新建main.go,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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{Group: "argoproj.io", Version: "v1alpha1", Resource: "rollouts"}

	// 使用dynamicClient的查询列表方法,查询指定namespace下的所有pod,
	// 注意此方法返回的数据结构类型是UnstructuredList
	unstructObj, err := dynamicClient.
		Resource(gvr).
		Namespace("default").
		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)
	}
}

编码完成,执行go run main.go,即可获取指定namespace下所有pod的信息,控制台输出如下:

1
2
3
$ go run main.go
namespace        status          name
default  Paused  rollouts-demo

DynamicClient SSA

服务器端应用(SSA)是在 Kubernetes API 服务器中创建或更新资源的新方法,该服务器作为 Beta 功能添加到 Kubernetes 1.16 。 SSA 的优点之一是,它引入了比 Strategic Merge Patch 更好的补丁策略。

例如,如果一个服务有两个不同协议的端口,且监听的端口号相同的情况下,Strategic Merge Patch 无法确定应该更新哪个端口,因为它使用 port 作为 key 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: mydns
spec:
  selector:
    app: mydns
  ports:
    - protocol: TCP
      port: 53
    - protocol: UDP
      port: 53

SSA 可以识别两个端口,因为它可以将字段定义为一组切片(数组)。 以下示例中 ServiceSpec 中的两个+listMapKey=注释定义了 SSAKey

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type ServiceSpec struct {
    // The list of ports that are exposed by this service.
    // More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
    // +patchMergeKey=port
    // +patchStrategy=merge
    // +listType=map
    // +listMapKey=port
    // +listMapKey=protocol
    Ports []ServicePort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"port" protobuf:"bytes,1,rep,name=ports"`
}

使用 SSA 创建 deployment 资源:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package main

import (
	"context"
	"flag"
	"path/filepath"
	"encoding/json"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"k8s.io/client-go/discovery"
    "k8s.io/client-go/discovery/cached/memory"
    "k8s.io/client-go/rest"
	"k8s.io/client-go/restmapper"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/types"
)

const deploymentYAML = `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: default
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
`

var decUnstructured = yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)

func doSSA(ctx context.Context, cfg *rest.Config) error {

    // 1. Prepare a RESTMapper to find GVR
    dc, err := discovery.NewDiscoveryClientForConfig(cfg)
    if err != nil {
        return err
    }
    mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc))

    // 2. Prepare the dynamic client
    dyn, err := dynamic.NewForConfig(cfg)
    if err != nil {
        return err
    }

    // 3. Decode YAML manifest into unstructured.Unstructured
    obj := &unstructured.Unstructured{}
    _, gvk, err := decUnstructured.Decode([]byte(deploymentYAML), nil, obj)
    if err != nil {
        return err
    }

    // 4. Find GVR
    mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
    if err != nil {
        return err
    }

    // 5. Obtain REST interface for the GVR
    var dr dynamic.ResourceInterface
    if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
        // namespaced resources should specify the namespace
        dr = dyn.Resource(mapping.Resource).Namespace(obj.GetNamespace())
    } else {
        // for cluster-wide resources
        dr = dyn.Resource(mapping.Resource)
    }

    // 6. Marshal object into JSON
    data, err := json.Marshal(obj)
    if err != nil {
        return err
    }

    // 7. Create or Update the object with SSA
    //     types.ApplyPatchType indicates SSA.
    //     FieldManager specifies the field owner ID.
    _, err = dr.Patch(ctx, obj.GetName(), types.ApplyPatchType, data, metav1.PatchOptions{
        FieldManager: "sample-controller",
    })

    return err
}

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())
	}

	// 执行 SSA
	err = doSSA(context.TODO(), config)
	if err != nil {
		panic(err.Error())
	}
}

至此,DynamicClient 客户端介绍完毕。