K8s 核心数据结构(2)#
参考书籍:《Kubernetes源码剖析-郑旭东著》
Kubernetes 内置资源概览#
资源组 | 资源种类 | 说明 |
---|
apiextensions.k8s.io | CustomResourceDefinition | 自定义资源类型 , 由 APIExtensions Server 负责管理该资源类型 |
apiregistration.k8s.io | APIService | 聚合资源类型,由 AggregatorServer 负责管理该资源类 |
admissionregistration.k8s.io | MutatingWebhookConfiguration | 变更准入控制器资源类型( Webhook) |
| ValidatingWebhookConfiguration | 验证准入控制器资源类型 ( Webhook) |
apps | ControllerRevision | 记录资源对象所有的历史版本的资源类型 |
| DaemonSet | 在 Pod 资源对象的基础上提供守护进程的资源类型 |
| Deployment | 在 Pod资源对象的基础上提供支持无状态服务的资源类型 |
| ReplicaSet | 在 Pod 资源对象的基础上提供一组 Pod 副本的资源类型 |
| StatefulSet | 在 Pod资源对象的基础上提供支持有状态服务的资源类型 |
auditregistration.k8s.io | AuditSink | 审计资源类型 |
authentication.k8s.io | TokenReview | 认证资源类型 |
authorization.k8s.io | LocalSubjectAccessReview | 授权检查用户是否可以在指定的命名空间中执行操作 |
| SelfSubjectAccessReview | 授权检查用户是否可以执行操作(若不指定 spec.namespace,则在所有的命名空间中执行操作) |
| SelfSubjectRulesReview | 授权枚举用户可以在指定的命名空间中执行一组操作 |
| SubjectAccessReview | 授 权检查用户是否可以执行操作 |
autoscaling | HorizontalPodAutoscaler | 在 Pod 资源对象的基础上提供水平自动伸缩资源类型 |
batch | Job | 提供一次性任务的资源类型 |
| CronJob | 提供定时任务的资源类型 |
certificates.k8s.io | CertificateSigningRequest | 提供证书管理的资源类型 |
coordination.k8s.io | Leases | 提供领导者选举机制的资源类型 |
core | ComponentStatus | 该资源类型已被奔用,其用于提供获取 Kuberetes 组件运行状况的资源类型 |
| ConfigMap | 提供容器内应用程序配置管理的资源类型 |
| Endpoints | 提供将外部服务器映射为内部服务的资源类型 |
| Event | 提供 Kubernetes 集群事件管理的资源类型 |
| LimitRange | 为命名空间中的每种资源对象设置资源(硬件资源)使 用限制 |
| Namespace | 提供资源对象所在的命名空间的资源类型 |
| Node | 提供 Kubernetes 集群中管理工作节点的资源类型。每个节点都有一个唯一标识符 |
| PersistentVolume | 提供 PV 存储的资源类型 |
| PersistentVolumeClaim | 提供 PVC 存储的资源类型 |
| Pod | 提供容器集合管理的资源类型 |
| PodTemplate | 提供用于描述预定义 Pod 资源对象副本数模板的资源类型 |
| ReplicationController | 在 Pod资源对象的基础上提供副本数保持不变的资源类型 |
| ResourceQuota | 提供每个命名空间配额限制的资源类型 |
| Secret | 提供存储密码 、Token 、密钥等敏感数据的资源类型 |
| Service | 提供负载均衡器为 Pod 资源对象的代理服务的资源类型 |
| ServiceAccount | 提供 ServiceAccount 认证的资源类型 |
events.k8s.io | Event | 提供Kuberetes集群事件管理的资源类型 |
networking.k8s.io | RuntimeClass | 提供容器运行时功能的资源类型 |
| Ingress | 提供 从Kubernetes 集群外部访问集群内部服务管理的资源类型 |
node.k8s.io | RuntimeClass | 提供容器运行时功能的资源类型 |
policy | Evictions | 在 Pod 资源对象的基础上提供驱逐策略的资源类型 |
| PodDisruptionBudget | 提供限制同时中断 Pod 的数量 ,以保证集群的高可用性 |
| PodSecurityPolicy | 提供控制 Pod 资源安全相关策略的资源类型 |
rbac.authorization.k8s.io | ClusterRole | 提供 RBAC 集群角色的资源类型 |
| ClusterRoleBinding | 提供 RBAC 集群角色鄉定的资源类型 |
| Role | 提供 RBAC 角色的资源类型 |
| RoleBinding | 提供 RBAC 角色绑定的资源类型 |
scheduling.k8s.io | PriorityClass | 提供 Pod 资源对象优先级管理的资源类型 |
settings.k8s.10 | PodPreset | 在创建 Pod 资源对象时,可以将特定信息注入 Pod 资源对象中 |
storage.k8s.io | StorageClass | 提供动态设置PV存储参数的资源类 |
| VolumeAttachment | 供触发 CSI ControllerPublish 和 ControllerUnpublish 操作的资源类型 |
runtime.Object
类型基石#
runtime.Object
是Kubernetes 类型系统的基石。Kubernetes 上的所有资源对象 ( Resource 0bject ) 实际上就是一种 Go 语言的 Struct 类型 , 相当于一种数据结构 ,它们都有一个共同的结构叫runtime.Object
。runtime.Object
被设计为 Interface 接口类型, 作为资源对象的通用资源对象,runtime.Obejct
类型基石如图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 对象接口必须被在 Scheme 注册的所有 API 类型支持。由于方案中的对象是
// 预期被序列化到线路,对象必须提供给 Scheme 的接口允许
// 用于设置对象表示的种类、版本和组的序列化程序。对象可以选择
// 在不希望序列化的情况下返回无操作 ObjectKindAccessor。
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
type ObjectKind interface {
// SetGroupVersionKind sets or clears the intended serialized kind of an object. Passing kind nil
// should clear the current setting.
SetGroupVersionKind(kind GroupVersionKind)
// GroupVersionKind returns the stored group, version, and kind of an object, or an empty struct
// if the object does not expose or provide these fields.
GroupVersionKind() GroupVersionKind
}
|
runtime.Object 提供了两个方法 ,分别是 GetObjectKind 和 DeepCopyObject。
GetObjectKind : 用于设置并返回 GroupVersionKind。
DeepCopyObject: 用于深复制当前资源对象并返回 。
深复制相当于将数据结构克隆一份,因此它不与原始对象共享任何内容。它使代码在不修改原始对象的情况下可以改变克隆对象的任何属性。
那么,如何确认一个资源对象是否可以转换成runtime.Object 通用资源对象呢?
这时需要确认该资源对象是否拥有 GetobjectKind 和 DeepCopyobjeot 方法。Kubernetes 的每一个资源对象都嵌入了metav1.TypeMeta 类型,metav1.TypeMeta 类型实现了GetObjectkind 方法,所以资源对象拥有该方法。 另外,Kubernetes 的每一 个资源对象都实现了 DeepCopyobject 方法,该方法一般被定义在 zz_generated.deepcopy.go
文件中。因此,可以认为该资源对象能够转换成 runtime.Object 通用资源对象。
所以,Kubernetes 的任意资源对象都可以通过 runtime.Object 存储它的类型并允许深复制操作。通过 runtime.Object Example 代码示例,可以将资源对象转换成通用资源对象并再次转换回资源对象。runtime.Object Example 代码示例如下
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
| package main
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/apis/core"
"reflect"
)
func main() {
pod := &core.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"name": "foo"},
},
}
obj := runtime.Object(pod)
pod2, ok := obj.(*core.Pod)
if !ok {
panic("unexpected")
}
if !reflect.DeepEqual(pod, pod2) {
panic("unexpected")
}
}
|
在以上代码示例中,首先实例化 Pod 资源, 得到 Pod 资源对象, 通过 runtime.object
将 Pod 资源对象转换成通用资源对象(得到 obj )。然后通过断言的方式,将obj 通用资源对象转换成 Pod 资源对象(得到 pod2 )。最终通过 reflect (反射,来验证转换之前和转换之后的资源对象是否相等。
Unstructured数据#
数据可以分为结构化数据 (StructuredData )和非结构化数据 ( Unstructured Data ) 。 Kubernetes 内部会经常处理这两种数据。
1. 结构化数据#
预先知道数据结构的数据类型是结构化数据。例如,JSON 数据:
1
2
3
4
| {
"id": 1
"name": "Derek"
}
|
要使用这种数据,需要创建一个struct 数据结构,其具有id 和name 属性:
1
2
3
4
| type Student struct{
ID int
Name string
}
|
2. 非结构化数据#
无法预知数据结构的数据类型或属性名称不确定的数据类型是非结构化数据, 其无法通过构建预定的struct 数据结构来序列化或反序列化数据。例如
1
2
3
4
5
| {
"id": 1,
"name": "Derek",
"description": ...
}
|
我们无法事先得知description 的数据类型,它可能是字符串,也可能是数组嵌套等。原因在于Go 语言是强类型语言,它需要预先知道数据类型,Go 语言在处理JSON 数据时 不如动态语言那样便捷。 当无法预知数据结构的数据类型或属性名称不确定时,通过如 下结构来解决问题
1
| var result map[string]interface{}
|
每个字符串对应一 JSON 属性,其映射 interface{} 类型对应值 ,可以是任何类型。使用 interface 字段时,通过 Go 语言断言的方式进行类型转换:
1
2
3
| if description, ok := result["description"].(string);ok{
fmt.Println(description)
}
|
3. Kubernetes 非结构化数据处理#
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
| // staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go
// Unstructured objects store values as map[string]interface{}, with only values that can be serialized
// to JSON allowed.
type Unstructured interface {
Object
// NewEmptyInstance returns a new instance of the concrete type containing only kind/apiVersion and no other data.
// This should be called instead of reflect.New() for unstructured types because the go type alone does not preserve kind/apiVersion info.
NewEmptyInstance() Unstructured
// UnstructuredContent returns a non-nil map with this object's contents. Values may be
// []interface{}, map[string]interface{}, or any primitive type. Contents are typically serialized to
// and from JSON. SetUnstructuredContent should be used to mutate the contents.
UnstructuredContent() map[string]interface{}
// SetUnstructuredContent updates the object content to match the provided map.
SetUnstructuredContent(map[string]interface{})
// IsList returns true if this type is a list or matches the list convention - has an array called "items".
IsList() bool
// EachListItem should pass a single item out of the list as an Object to the provided function. Any
// error should terminate the iteration. If IsList() returns false, this method should return an error
// instead of calling the provided function.
EachListItem(func(Object) error) error
}
// 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{}
}
|
在上述代码中,Kubernetes 非结构化数据通过 map[string]interface{} 表达,并提供接口。在 client-go 编程式交互的 DynamicClient 内部,实现了 Unstructured 类型, 用于处理非结构化数据。
Scheme 资源注册表#
大家在使用Windows 操作系统时都应该听说过, 当在操作系统上安装应用程序 时, 该程序 的一些信息会注册到注册表中; 当从操作系统 上卸载应用程序时, 会从 注册表中删除该程序的相关信息。而 KubernetesScheme 资源注册表类似于Windows 操作系统上的注册表,只不过注册的是资源类型。
Kuberetes 系统拥有众多资源,每一种资源就是一个资源类型,这些资源类型需要有统一的注册 、 存储 、 查询 、 管理等机制 。目前 Kuberetes 系统中的所有资源类型都已注册到 Scheme 资源注册表中,其是 一个内存型的资源注册表,拥有如下特点。
- 支持注册多种资源类型,包括内部版本和外部版本。
- 支持多种版 本转换机制。
- 支持不同资源的序列化/反序列化机制。
Scheme 资源注册表支持两种资源类型( Type )的注册,分别是 UnversionedType 和KnownType 资源类型,分别介绍如下
- UnversionedType: 无版本资源类型,这是一个早期 Kubernetes 系统中的概 念, 它主要应用于某些没有版本的资源类型,该类型的资源对象并不需要进行转换。在目前的 Kubernetes 发行版本中,无版本类型已被弱化,几乎所有的资源对象都拥有版本, 但在 metav1 元数据中还有部分类型 ,它们既属于 meta.k8s.io/v1 又属于 UnversionedType 无版本资源类型,例如 metav1.Status, metav1.APIVersions、metav1.APIGroupList、metavI.APIGroup、metav1.APIResourceList.
- KnownType: 是目前Kubernetes 最常用的资源类型,也可称其为“ 拥有版本的资源类型”
Schema 资源注册表数据结构#
Scheme资源注册表数据结构主要由4 个map结构组成,它们分别是. gvkToType、typeToGVK、unversionedTypes、unversionedKinds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| eme struct {
// gvkToType allows one to figure out the go type of an object with
// the given version and name.
gvkToType map[schema.GroupVersionKind]reflect.Type
// typeToGVK allows one to find metadata for a given go object.
// The reflect.Type we index by should *not* be a pointer.
typeToGVK map[reflect.Type][]schema.GroupVersionKind
// unversionedTypes are transformed without conversion in ConvertToVersion.
unversionedTypes map[reflect.Type]schema.GroupVersionKind
// unversionedKinds are the names of kinds that can be created in the context of any group
// or version
// TODO: resolve the status of unversioned types.
unversionedKinds map[string]reflect.Type
...
}
|
Scheme 资源注册表结构字段说明如下。
gVkToType: 存储 GVK 与 Type 的映射关系。
typeToGVK: 存储 Type 与 GVK 的映射关系,一个 Type 会对应一个或多个 GVK。
unversionedTypes: 存储 UnversionedType 与 GVK 的映射关系。
unversionedKinds: 存储 Kind (资源种类)名称与UnversionedType 的映射关系。
Scheme 资源注册表通过Go 语言的map结构实现映射关系,这些映射关系可以 实现高效的正向和反向检索,从 Scheme 资源注册表中检索某个 GVK 的Type, 它的时间复杂度为O(1)。
Scheme 资源注册表在 Kubernetes 系统体系中属于非常核心的数据结构。示例代码:
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
| package main
import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func main() {
// KnownType external
coreGV := schema.GroupVersion{Group: "", Version: "v1"}
extensionsGV := schema.GroupVersion{Group: "extensions", Version: "v1beta1"}
// KnownType internal
coreInternalGV := schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}
// UnversionedType
Unversioned := schema.GroupVersion{Group: "", Version: "v1"}
scheme := runtime.NewScheme()
scheme.AddKnownTypes(coreGV, &corev1.Pod{})
scheme.AddKnownTypes(extensionsGV, &appsv1.DaemonSet{})
scheme.AddKnownTypes(coreInternalGV, &corev1.Pod{})
scheme.AddUnversionedTypes(Unversioned, &metav1.Status{})
}
|
在上述代码中,首先定义了两种类型的GV( 资源组、资源版本),KnownType 类型有 coreGV、extensionsGV、coreInternalGV 对象,其中 corelnternalGV 对象属于内部版本(即runtime.APIVersioninternal ),而 UnversionedType 类型有 Unversioned 对象。
通过 runtime.NewScheme 实例化一个新的Scheme资源注册表。注册资源类型到 Scheme 资源注册表有两种方式,第一种通过scheme.AddKnownTypes 方法注册 KnownType 类型的对象,第二种通过 scheme. AddUnversionedTypes 方法注册 UnversionedType 类型的对象。
在 Scheme Example 代码示例中,我们往 Scheme 资源注册表中分别注册了Pod、 DaemonSet、Pod ( 内部版本)及 Status (无版本资源类型)类型对象,那么这些资源的映射关系,如图
GVK(资源组、资源版本、资源种类)在Scheme资源注册表中以<group>/<version>, Kind =<kind>
的形式存在,其中对于 Kind ( 资源种类)字段,在注册时如果不指定该 字段的名称,那么默认使用类型的名称,例如corev1.Pod 类型,通过reflect 机制获 取资源类型的名称,那么它的资源种类 Kind-Pod。
资源类型在Scheme 资源注册表中以 Go Type (通过 reflect 机制获取)形式存在。
另外, 需要注意的是,UnversionedType 类型的对象在通过 scheme.AddUnversionedTypes 方法注册时,会同时存在于4个map 结构中,代码示例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| /*
AddUnversionedTypes 将提供的类型注册为“未版本化”,这意味着它们遵循特殊规则。
每当序列化此类型的对象时,它都会使用提供的组版本进行序列化,而不是
转换。因此,未版本化的对象应该永远保持向后兼容,就好像它们在一个永远不会更新的 API 组和版本。
*/
func (s *Scheme) AddUnversionedTypes(version schema.GroupVersion, types ...Object) {
s.addObservedVersion(version)
s.AddKnownTypes(version, types...)
for _, obj := range types {
t := reflect.TypeOf(obj).Elem()
gvk := version.WithKind(t.Name())
s.unversionedTypes[t] = gvk
if old, ok := s.unversionedKinds[gvk.Kind]; ok && t != old {
panic(fmt.Sprintf("%v.%v has already been registered as unversioned kind %q - kind name must be unique in scheme %q", old.PkgPath(), old.Name(), gvk, s.schemeName))
}
s.unversionedKinds[gvk.Kind] = t
}
}
|
资源注册表注册方法#
在Scheme 资源 注册表中,不同的资源类型使用的注册方法不同,分别介绍如下。
- scheme. AddUnversionedTypes: 注册 UnversionedType 资源类型。
- scheme. AddKnownTypes: 注册 KnownType 资源类型。
- scheme. AddKnownTypewithName: 注册KnownType 资源类型,须指定资源的 Kind 资源种类名
以 scheme. AddKnownTypes 方法为例,在注册资源类型时,无须指定 Kind 名称, 而是通过 reflect 机制获取资源类型的名称作为资源种类名称,代码示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| /*
AddKnownTypes 将“types”中传递的所有类型注册为版本“version”的成员。
传递给类型的所有对象都应该是指向结构的指针。去报道的名字
该结构在编码时成为“种类”字段。版本不能为空 - 使用
APIVersionInternal 常数,如果你有一个没有正式版本的类型
*/
func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
s.addObservedVersion(gv)
for _, obj := range types {
t := reflect.TypeOf(obj)
if t.Kind() != reflect.Pointer {
panic("All types must be pointers to structs.")
}
t = t.Elem()
s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
}
}
|
资源注册表查询方法#
在运行过程中,kube-apiserver 组件常对 Scheme 资源注册表进行查询,它提供了如下方法。
- scheme.KnownTypes: 查询注册表中指定GV 下的资源类型。
- scheme. AlKnownTypes: 查询注册表中所有GVK 下的资源类型。
- scheme.ObjectKinds: 查询资源对象所对应的GVK, 一个资源对象可能存在多个GVK。
- scheme.New: 查询GVK 所对应的资源对象。
- scheme.IsGroupRegistered: 判断指定的资源组是否已经注册。
- scheme.IsVersionRegistered : 判断指定的 G V 是否己经注册。
- scheme.Recognizes: 判断指定的GVK 是否己经注册。
- scheme.IsUnversioned: 判断指定的资源对象是否属于 UnversionedType 类型。
Codec 编解码器#
在详解 Codec 编解码器之前,先认识下 Codec 编解码器与 Serializer 序列化器之间的差异。
Serializer: 序列化器,包含序列化操作与反序列化操作。序列化操作是将数据 (例如数组 、对象或结构体等 ) 转换为宇符串的过程 , 反序列化操作是将字符串转换为数据的过程,因此可以轻松地维护数据结构 并存储或传输数据
Codee: 编解码器,包含编码器与解码器。编解码器是一个通用术语,指的是可以表示数据的任何格式,或者将数据转换为特定格式的过程。所以,可以将 Serializer 序列化器也理解为 Codec 编解码器的一种 。
Codec 编解码器通用接又定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type Decoder interface {
Decode(data []byte, defaults *schema.GroupVersionKind, into Object) (Object, *schema.GroupVersionKind, error)
}
type Encoder interface {
Encode(obj Object, w io.Writer) error
Identifier() Identifier
}
type Serializer interface {
Encoder
Decoder
}
// Codec is a Serializer that deals with the details of versioning objects. It offers the same
// interface as Serializer, so this is a marker to consumers that care about the version of the objects
// they receive.
type Codec Serializer
|
从Codec 编解码器通用接又的定义可以看出,Serializer 序列化器属于Codec 编解码器的一种,这是因为每种序列化器都实现了Encoder 与 Decoder 方法。我们可以认为,只要实现了Encoder 与Decoder 方法的数据结构,就是序列化器。Kubernetes 目前支持 3 种主要的序列化器。Codec 编解码器如图
Codec 编解码器包含了种序列化器 , 在进行编解码操作时 ,每一种序列化器都对资源对象的 metav1.TypeMeta (即 APIVersion 和Kind 字段)进行验证,如果资源对象未提供这些字段,就会返回错误。每种序列化器分别实现了 Encode 序列化方法与 Decode 反序列化方法 ,分别介绍如下
- jsonSerializer: JSON 格式序列化/反序列化器。它使用
application/json
的 ContentType 作为标识 - yamlSerializer: YAML 格式化格式序列化/反序列化器。它使用
application/yaml
的 ContentType 作为标识 - protobufSerializer: Protobuf 格式化格式序列化/反序列化器。它使用
application/vnd.kubernetes.protobuf
的 ContentType 作为标识
Codec 编解码器将 Eted 集群中的数据进行编解码操作。
Codec 编解码实例化#
Codec 编码器通过 NewCodecFactory 函数实例化,在实例化的过程中会将 jsonSerializer、yamlSerializer、protobufSerializer 序列化器全部实例化,NewCodecFactory -> newSerializersForScheme ,示例代码如下
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
| func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, options CodecFactoryOptions) []serializerType {
jsonSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: false, Strict: options.Strict},
)
jsonSerializerType := serializerType{
AcceptContentTypes: []string{runtime.ContentTypeJSON},
ContentType: runtime.ContentTypeJSON,
FileExtensions: []string{"json"},
EncodesAsText: true,
Serializer: jsonSerializer,
Framer: json.Framer,
StreamSerializer: jsonSerializer,
}
if options.Pretty {
jsonSerializerType.PrettySerializer = json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: true, Strict: options.Strict},
)
}
strictJSONSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: false, Pretty: false, Strict: true},
)
jsonSerializerType.StrictSerializer = strictJSONSerializer
yamlSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict},
)
strictYAMLSerializer := json.NewSerializerWithOptions(
mf, scheme, scheme,
json.SerializerOptions{Yaml: true, Pretty: false, Strict: true},
)
protoSerializer := protobuf.NewSerializer(scheme, scheme)
protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme)
serializers := []serializerType{
jsonSerializerType,
{
AcceptContentTypes: []string{runtime.ContentTypeYAML},
ContentType: runtime.ContentTypeYAML,
FileExtensions: []string{"yaml"},
EncodesAsText: true,
Serializer: yamlSerializer,
StrictSerializer: strictYAMLSerializer,
},
{
AcceptContentTypes: []string{runtime.ContentTypeProtobuf},
ContentType: runtime.ContentTypeProtobuf,
FileExtensions: []string{"pb"},
Serializer: protoSerializer,
// note, strict decoding is unsupported for protobuf,
// fall back to regular serializing
StrictSerializer: protoSerializer,
Framer: protobuf.LengthDelimitedFramer,
StreamSerializer: protoRawSerializer,
},
}
for _, fn := range serializerExtensions {
if serializer, ok := fn(scheme); ok {
serializers = append(serializers, serializer)
}
}
return serializers
}
|
jsonSerializer 与 yamlSerializer 分别通过json.NewSerializer 和 json.NewYAMLSerializer 函数进行实例化,jsonSerializer通过 application/json 的ContentType 标识, 文件扩展名为 json,而yamlSerializer 通过application/yaml 的 ContentType 标识,文件扩展名 为 yaml。protobufserializer 通过 protobuf.NewSerializer 函数进行实例化,它通过 application/ vnd.kubernetes.protobuf 的ContentType 标识,文件扩展名为pb
jsonSerializer 与 yamlSerializer 序列化器#
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
| // staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go
func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
data := originalData
if s.options.Yaml {
altered, err := yaml.YAMLToJSON(data)
if err != nil {
return nil, nil, err
}
data = altered
}
actual, err := s.meta.Interpret(data)
if err != nil {
return nil, nil, err
}
if gvk != nil {
*actual = gvkWithDefaults(*actual, *gvk)
}
if unk, ok := into.(*runtime.Unknown); ok && unk != nil {
unk.Raw = originalData
unk.ContentType = runtime.ContentTypeJSON
unk.GetObjectKind().SetGroupVersionKind(*actual)
return unk, actual, nil
}
if into != nil {
_, isUnstructured := into.(runtime.Unstructured)
types, _, err := s.typer.ObjectKinds(into)
switch {
case runtime.IsNotRegisteredError(err), isUnstructured:
strictErrs, err := s.unmarshal(into, data, originalData)
if err != nil {
return nil, actual, err
}
// when decoding directly into a provided unstructured object,
// extract the actual gvk decoded from the provided data,
// and ensure it is non-empty.
if isUnstructured {
*actual = into.GetObjectKind().GroupVersionKind()
if len(actual.Kind) == 0 {
return nil, actual, runtime.NewMissingKindErr(string(originalData))
}
// TODO(109023): require apiVersion here as well once unstructuredJSONScheme#Decode does
}
if len(strictErrs) > 0 {
return into, actual, runtime.NewStrictDecodingError(strictErrs)
}
return into, actual, nil
case err != nil:
return nil, actual, err
default:
*actual = gvkWithDefaults(*actual, types[0])
}
}
if len(actual.Kind) == 0 {
return nil, actual, runtime.NewMissingKindErr(string(originalData))
}
if len(actual.Version) == 0 {
return nil, actual, runtime.NewMissingVersionErr(string(originalData))
}
// use the target if necessary
obj, err := runtime.UseOrCreateObject(s.typer, s.creater, *actual, into)
if err != nil {
return nil, actual, err
}
strictErrs, err := s.unmarshal(obj, data, originalData)
if err != nil {
return nil, actual, err
} else if len(strictErrs) > 0 {
return obj, actual, runtime.NewStrictDecodingError(strictErrs)
}
return obj, actual, nil
}
|
Decode 两数支持两种格式的反序列化操作 ,分别是 YAML 格式和 JSON格式。
如果是 YAML 格式,则通过 yaml.YAMLTOJSON 两数将 JSON 格式数据转换为资源对象并填充到data 字段中。此时,无论反序列化操作的是YAML格式还是JSON 格式,data 字段中都是JSON格式数据。按着通过s.meta.Interpret 函数从JSON格式 数据中提取出资源对象的metav1.TypeMeta (即 APIVersion 和Kind 字段)。最后通过 caseSensitiveJsonlterator.Unmarshal 函数 (即json-iterator )将JSON 数据反序列化并返回。
protobufSerializer 序列化器#
Protobuf (Google Protocol Buffer )是Google 公司内部的混合语言数据标准, Protocol Buffers是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列 化。它很适合做数据存储或成为 RPC 数据交换格式。它可用于通信协议、数据存储等领域,与语言无关、与平台无关、可扩展的序列化结构数据格式。
1
2
3
4
5
6
7
8
| package lm;
message helloworld
{
required int32 id = 1;
required string str = 2;
optional int32 opt = 3;
}
|
Protobuf 序列化器使用 proto 库来实现序列化和反序列操作
1. 序列化操作#
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
| func (s *Serializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error {
if memAlloc == nil {
klog.Error("a mandatory memory allocator wasn't provided, this might have a negative impact on performance, check invocations of EncodeWithAllocator method, falling back on runtime.SimpleAllocator")
memAlloc = &runtime.SimpleAllocator{}
}
prefixSize := uint64(len(s.prefix))
var unk runtime.Unknown
switch t := obj.(type) {
case *runtime.Unknown:
estimatedSize := prefixSize + uint64(t.Size())
data := memAlloc.Allocate(estimatedSize)
i, err := t.MarshalTo(data[prefixSize:])
if err != nil {
return err
}
copy(data, s.prefix)
_, err = w.Write(data[:prefixSize+uint64(i)])
return err
default:
kind := obj.GetObjectKind().GroupVersionKind()
unk = runtime.Unknown{
TypeMeta: runtime.TypeMeta{
Kind: kind.Kind,
APIVersion: kind.GroupVersion().String(),
},
}
}
switch t := obj.(type) {
case bufferedMarshaller:
// this path performs a single allocation during write only when the Allocator wasn't provided
// it also requires the caller to implement the more efficient Size and MarshalToSizedBuffer methods
encodedSize := uint64(t.Size())
estimatedSize := prefixSize + estimateUnknownSize(&unk, encodedSize)
data := memAlloc.Allocate(estimatedSize)
i, err := unk.NestedMarshalTo(data[prefixSize:], t, encodedSize)
if err != nil {
return err
}
copy(data, s.prefix)
_, err = w.Write(data[:prefixSize+uint64(i)])
return err
case proto.Marshaler:
// this path performs extra allocations
data, err := t.Marshal()
if err != nil {
return err
}
unk.Raw = data
estimatedSize := prefixSize + uint64(unk.Size())
data = memAlloc.Allocate(estimatedSize)
i, err := unk.MarshalTo(data[prefixSize:])
if err != nil {
return err
}
copy(data, s.prefix)
_, err = w.Write(data[:prefixSize+uint64(i)])
return err
default:
// TODO: marshal with a different content type and serializer (JSON for third party objects)
return errNotMarshalable{reflect.TypeOf(obj)}
}
}
|
Encode 函数首先验证资源对象 proto.Marshaler类型,proto.Marshaler是一个interface接又类型,该接又专门留给对象自定义实现的序列化操作。如果资源对象为proto.Marshaler类型,则通过t.Marshal序列化函数进行编码。
而且,通过unk.MarshalTo两数在编码后的数据前加上protoEncodingPrefix前缀,前缀为magic-number特殊标识,其用于标识一个包的完整性。所有通过protobufSerializer序列化器编码的数据都会有前缀。前缀数据共4字节,分别是0x6b、0x38、0x73、0x00,其中第4个字节是为编码样式保留的。
2. 反序列化操作#
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
| func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
prefixLen := len(s.prefix)
switch {
case len(originalData) == 0:
// TODO: treat like decoding {} from JSON with defaulting
return nil, nil, fmt.Errorf("empty data")
case len(originalData) < prefixLen || !bytes.Equal(s.prefix, originalData[:prefixLen]):
return nil, nil, fmt.Errorf("provided data does not appear to be a protobuf message, expected prefix %v", s.prefix)
case len(originalData) == prefixLen:
// TODO: treat like decoding {} from JSON with defaulting
return nil, nil, fmt.Errorf("empty body")
}
data := originalData[prefixLen:]
unk := runtime.Unknown{}
if err := unk.Unmarshal(data); err != nil {
return nil, nil, err
}
actual := unk.GroupVersionKind()
copyKindDefaults(&actual, gvk)
if intoUnknown, ok := into.(*runtime.Unknown); ok && intoUnknown != nil {
*intoUnknown = unk
if ok, _, _ := s.RecognizesData(unk.Raw); ok {
intoUnknown.ContentType = runtime.ContentTypeProtobuf
}
return intoUnknown, &actual, nil
}
if into != nil {
types, _, err := s.typer.ObjectKinds(into)
switch {
case runtime.IsNotRegisteredError(err):
pb, ok := into.(proto.Message)
if !ok {
return nil, &actual, errNotMarshalable{reflect.TypeOf(into)}
}
if err := proto.Unmarshal(unk.Raw, pb); err != nil {
return nil, &actual, err
}
return into, &actual, nil
case err != nil:
return nil, &actual, err
default:
copyKindDefaults(&actual, &types[0])
// if the result of defaulting did not set a version or group, ensure that at least group is set
// (copyKindDefaults will not assign Group if version is already set). This guarantees that the group
// of into is set if there is no better information from the caller or object.
if len(actual.Version) == 0 && len(actual.Group) == 0 {
actual.Group = types[0].Group
}
}
}
if len(actual.Kind) == 0 {
return nil, &actual, runtime.NewMissingKindErr(fmt.Sprintf("%#v", unk.TypeMeta))
}
if len(actual.Version) == 0 {
return nil, &actual, runtime.NewMissingVersionErr(fmt.Sprintf("%#v", unk.TypeMeta))
}
return unmarshalToObject(s.typer, s.creater, &actual, into, unk.Raw)
}
|
Decode两数首先验证protoEncodingPrefix前缀,前级为magic-number特殊标识,其用于标识一个包的完整性,然后验证资源对象是否为proto.Mesage类型,最后通过 proto.Unmarshal 反序列化函数进行解码。
Converter 资源版本转换器#
在Kubernetes 系统中,同一资源拥有多个资源版本,Kubernetes 系统允许同一资 源的不同资源版本进行转换,例如Deployment 资源对象, 当前运行的是v1beta1 资 源版本,但vlbetal 资源版本的某些功能或字段不如v1资源版本完善,则可以将 Deployment 资源对象的v1beta1 资源版本转换为 v1 版本。可通过kubectl convert 命 令进行资源版本转换,执行命令如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| apiVersion: apps/v1beta1
kind: Deployment
metadata:
name:nginx-deployment
spec:
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort:80
|
1
| kubectl convert -f vlbeta1Deployment.yami --output-version=apps/v1
|
首先,定义一个 YAML Manifest File 资源描述文件,该文件中定义Deployment 资源版本为v1beta1。通过执行kubect convert 命令,–output -ver sion 将 资源版本转换 为指定的资源版本v1。如果指定的资源版本不在Scheme 资源注册表中,则会报错。 如果不指定资源版本,则默认转换为资源的首选版本。
Converter 资源版本转换器主要用于解决多资源版本转换问题,Kubernetes 系统 中的一 个资源支持多个资源版本,如果要在每个资源版本之间转换,最直接的方式是,每个资源版本都支持其他资源版本的转换,但这样处理起来非常麻烦。例如,某个资源对象支持 3 个资源版本,那么就需要提前定义一个资源版本转换到其他两个资源版本(vI– vlalphal,vI- vlbetal)、(vlalphal-v1,vlalphal-v1beta1)及( v1beta1-v1, vlbetal–vlalphal),随着资源版本的增加,资源版本转换的定义会越来越多。
为了解决这个问题,Kubernetes通过内部版本(InternalVersion)机制实现资源版本转换,Converter 资源版本转换过程如图
当需要在两个资源版本之间转换时,例如 vlalphal–vlbetal 或 vlalphal–v1。 Converter 资源版本转换器先将第 一个资源版本转换为_ internal 内部版本,再转换为相应的资源版本。每个资源只要能支持内部版本,就能与其他任何资源版本进行间接的资源版本转换
Converter 转换器数据结构#
Converter转换器数据结构主要存放转换函数(即Conversion Funcs)。Converter 转换器数据结构代码示例如下
1
2
3
4
5
6
7
8
9
10
| // Converter knows how to convert one type to another.
type Converter struct {
// Map from the conversion pair to a function which can
// do the conversion.
conversionFuncs ConversionFuncs
generatedConversionFuncs ConversionFuncs
// Set of conversions that should be treated as a no-op
ignoredUntypedConversions map[typePair]struct{}
}
|
- conversionFuncs: 默认转换两数。这些转换西数 一般定义在资源目录下的 conversion.go 代码文件中。
- generatedConversionFuncs: 自动生成的转换两数。这些转换函数 一般定 义在资源目录下的 zz_generated.conversion.go 代码文件中,是由代码生成器自动生成的转换函数。
- ignoredConversions: 若资源对象注册到此字段,则忽略此资源对象的转换操作。
Converter转换器数据结构中存放的转换两数(即ConversionFuncs)可以分为两类,分别为默认的转换两数(即conversionFuncs字段)和自动生成的转换函数(即generatedConversionFuncs字段)。它们都通过ConversionFuncs来管理转换函数,代码示例如下
1
2
3
4
5
6
7
8
9
10
11
| type ConversionFuncs struct {
untyped map[typePair]ConversionFunc
}
type typePair struct {
source reflect.Type
dest reflect.Type
}
type ConversionFunc func(a, b interface{}, scope Scope) error
|
ConversionFunc 类型函数(即Type Function )定义了转换函数实现的结构,将资 源对象a转换为资源对象b。a 参数定义了转换源(即source)的资源类型,b 参数定义 了转换目标(即dest)的资源类型。scope 定义了多次转换机制(即递归调用转换函数)。
ConversionFunc 类型函数的资源对象传参必须是指针,否则无法进 行转换并抛出异常
Converter 注册转换函数#
Converter 转换函数需要通过注册才能在Kubernetes 内部使用,目前 Kubernetes 支持 5 个注册转换西数,分别介绍如下。
- scheme. AddlgnoredConversionType :注册忽略的资源类型,不会执行转换操作,忽略资源对象的转换操作。
- scheme. AddConversionFuncs: 注册多个Conversion Func 转换函数
- scheme.AddConversionFunc: 注册单个Conversion Func转换函数
- scheme. AddGeneratedConversionFunc: 注册自动生成的转换函数
- scheme.AddFieldLabelConversionFune: 注册字段标签 ( FieldLabel )的转换函数
以apps/v1资源组、资源版本为例,通过scheme.AddConversionFuncs函数注册所有资源的转换函数,代码示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // v1版本好像已经弃用了
func addConversionFuncs(scheme *runtime.Scheme) error {
// Add field label conversions for kinds having selectable nothing but ObjectMeta fields.
if err := scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("StatefulSet"),
func(label, value string) (string, string, error) {
switch label {
case "metadata.name", "metadata.namespace", "status.successful":
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported for appsv1beta2.StatefulSet: %s", label)
}
}); err != nil {
return err
}
return nil
}
|
Converter 资源版本转换原理#
Converter 转换器在 Kubernetes 源码中实际应用非常广泛,例如 Deployment 资源对象,起初使用 v1beta1 资源版本,而 v1 资源版本更稳定,则会将 v1beta1 资源版本转换为v1资源版本 。Converter 资源版本转换过程如图
代码示例
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
| // ! 版本转换在1.7中废弃,此代码在最新版本中转换失败
package main
import (
"fmt"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/apis/apps"
)
func main() {
// KnownType external
scheme := runtime.NewScheme()
scheme.AddKnownTypes(appsv1beta1.SchemeGroupVersion, &appsv1beta1.Deployment{})
scheme.AddKnownTypes(appsv1.SchemeGroupVersion, &appsv1.Deployment{})
scheme.AddKnownTypes(apps.SchemeGroupVersion, &appsv1.Deployment{})
metav1.AddToGroupVersion(scheme, appsv1beta1.SchemeGroupVersion)
metav1.AddToGroupVersion(scheme, appsv1.SchemeGroupVersion)
v1betalDeployment := &appsv1beta1.Deployment{TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1beta1"}}
// v1beta1 - internal
objInternal, err := scheme.ConvertToVersion(v1betalDeployment, apps.SchemeGroupVersion)
if err != nil {
panic(err)
}
fmt.Println("GVK:", objInternal.GetObjectKind().GroupVersionKind().String())
// internal - v1
objV1, err := scheme.ConvertToVersion(objInternal, appsv1.SchemeGroupVersion)
if err != nil {
panic(err)
}
v1Deployment, ok := objV1.(*appsv1.Deployment)
if !ok {
panic("not a Deployment")
}
fmt.Println("GVK:", v1Deployment.GetObjectKind().GroupVersionKind().String())
}
|
代码分析:
第 1 部分 : 实例化一个空的 scheme 资源注册表 , 将 v1beta1 资源版本 、 v1 资源版本及内部版本 ( internal ) 的 Deployment 资源注册到 scheme 资源注册表中 。
第 2 部分 : 实例化 v1beta1 Deployment 资源对象 , 通过 scheme.ConvefiToVersion 将其转换为目标资源版本( 即 internale 版本 ),得到 objlnternal资源对象 , objlnternal 资源对象的 GVK 输出为 “ / , Kind= ”
第 3 部分 : 将 obj lnternal 资源对象通过 scheme.ConvertToVersion 转换为目标资源版本 ( 即 v1 资源版本 ) , 得到 objV1资源对象 , 并通过断言的方式来验证是否转换成功 , objV1 资源对象的 GVK 输出为 “ apps/v 1 , Kind=Deployment " 。
- 在 converter Example 代码示例的第 2 部分中 , 将 vlbetal 资源版本转换为内部版本 ( 即 internal 版本 ) , 得到转换后资源对象的 GVK 为 “ / , Kind= ” 。 在这里 , 读者肯定会产生疑问 , 为什么 vlbetal 资源版本转换为内部版本以后得到的 GVK 为 “ / ,Kind= ” 而不是 “ apps/__internal , Kmd=Deployment ” 。 下面带着疑问来看看 Kubernetes源码实现 。
- scheme 资源注册表可以通过两种方式进行版本转换 , 分别介绍如下 。
- scheme.ConvertToVersion : 将传入的 (in) 资源对象转换成目标 ( target )资源版本 ,在版本转换之前 ,会将资源对象深复制一份后再执行转换操作 ,相当于安全的内存对象转换操作 。与 scheme.ConvertToVersion 功能相同 ,
- scheme.UnsafeConvcrtToVersion :但在转换过程中不会深复制资源对象 , 而是直接对原资源对象进行转换操作 , 尽可能高效地实现转换 。但该操作是非安全的内存对象转换操作 。
scheme.ConvertToVersion 与 scheme.UnsafeConveftToVersion 资源版本转换功能都依赖于 s.convertToVersion 函数来实现 , Converter 转换器流程图如图
1.获取传入的资源对象的反射类型#
资源版本转换的类型可以是 runtime.ObJect 或 runtime.Unstructured , 它们都属于Go 语言里的 struct 数据结构 , 通过 Go 语言标准库 reflect 机制获取该资源类型的反射类型 , 因为在 scheme 资源注册表中是以反射类型注册资源的 。 获取传入的资源对象的反射类型 。
2.从资源注册表中查找到传入的资源对象的 GVK#
从 scheme 资源注册表中查找到传入的资源对象的所有 GVK , 验证传入的资源对象是否己经注册 , 如果未曾注册 , 则返回错误.
3.从个 GVK 中选出与目标资源对象相匹配的 GVK#
target.KindForGroupVersionKinds 函数从多个可转换的 GVK 中选出与目标资源对象相匹配的 GVK 。 这里有一个优化点 , 转换过程是相对耗时的 , 大量的相同资源之间进行版本转换的耗时会比较长 。 在 Kubemetes 源码中判断 , 如果目标资源对象的 GVK 在可转换的 G VK 列表中 , 则直接将传入的资源对象的 G VK 设置为目标资源对象的 GVK , 而无须执行转换操作 , 缩短部分耗时 。
4.判断传入的资源对象是否属于 Unversioned 类型#
对于 Unversioned 类型 , 前面曾介绍过 , 即无版本类型 ( UnversionedType) 。 属于该类型的资源对象并不需要进行转换操作 , 而是直接将传入的资源对象的 GVK 设置为目标资源对象的 GVK 。
5.执行转换操作#
在执行转换操作之前 , 先判断是否需要对传入的资源对象执行深复制操作 , 然后通过 s.converter.Convert 转换函数执行转换操作.
实际的转换函数是通过 doconversion 函数执行的 , 执行过程如下 。
- 从默认转换函数列表 ( 即 c.conversionFuncs ) 中查找出 pair 对应的转换函数 , 如果存在则执行该转换函数 ( 即fn ) 并返回 。
- 从自动生成的转换函数列表 ( 即 generatedConversionFuncs ) 中查找出 pair 对应的转换函数 , 如果存在则执行该转换函数 ( 即fn ) 并返回 。
- 如果默认转换函数列表和自动生成的转换函数列表中都不存在当前资源对象的转换函数 , 则使用 doconversion 函数传入的转换函数 ( 即 f) 。 调用 f
之前 , 需要将 s rc 与 dest 资源对象通过 EnforcePtr 函数取指针的值 , 因为函数传入的转换函数接收的是非指针资源对象 。
6.设置转换后资源对象的 GVK#
在代码示例的第 2 部分中 , 将 v1beta1 资源版本转换为内部版本 ( 即 internal 版本 ),得到转换后资源对象的 GVK 为 “ / , Kind= ” 。 原因在于 setTargetKind 函数 , 转换操作执行完成以后 , 通过 setTargetKind 函数设置转换后资源对象的 GVK , 判断当前资源对象是否为内部版本 ( 即 APIVersion1nternal) , 是内部版本则设置 GVK 为 schema.GroupVersionKind{}