DynamicClient 是一种动态客户端,它可以操作任意的 Kubernetes 资源,即不仅可以操作 Kubernetes 自身内置的资源,还可操作 CRD.
DynamicClient 简介
技巧
在几乎所有情况下,除了 Server Side Apply (SSA)
之外,我们都应优先使用 ClientSet
而不是 DynamicClient
。
前文咱们学习了 ClientSet
客户端,发现 ClientSet
在 deployment
、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-go
为 Unstructured
准备了丰富的方法
,借助这些方法可以灵活的处理非结构化数据。
小结:
与 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=
注释定义了 SSA
的 Key
。
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 客户端介绍完毕。