0%

【Kubernetes】API Centric

Kubernetes 系列的开篇 中我们提到 Kubernetes 是 REST API 为中心的系统,系统内的所有操作都通过向 ApiServer提交 API Requests 来实现。ApiServer 作为 Kubernetes 控制面的核心组件,提供给用户、集群内部组件、集群外组件了 REST API 接口,使得他们可以通过标准的 HTTP API 来创建、更新、删除、查询集群中的 Object。作为介绍 ApiServer 的第一篇,本文将从 API 入手,介绍 ApiServer 的设计与实现。

Access API

我们最常访问 Kubernetes API 的方式是通过 kubectl,如下所示:

1
2
3
$ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 2/2 2 2 57m

kubectl 会将我们的操作转换成 REST API 访问,提供一种更加方便的访问集群的方式。实际上,我们也可以直接访问 REST API,向 ApiServer 发出 HTTP 请求。这种情况下,你需要知道集群的地址,并且拥有访问的凭证,可以通过以下命知道集群地址及凭证:

1
$ kubectl config view

下面演示了通过 curl 访问 ApiServer的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
$ TOKEN=$(kubectl get secret $(kubectl get serviceaccount default -o jsonpath='{.secrets[0].name}') -o jsonpath='{.data.token}' | base64 --decode )
$ curl $APISERVER/api --header "Authorization: Bearer $TOKEN" --insecure
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "cls-edhzbzbn.ccs.tencent-cloud.com:60002"
}
]
}

每次需要指定 TOKEN 很麻烦,可以通过使用 kubectl 代理的方式访问 ApiServer,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ kubectl proxy --port=8080 &
$ curl http://localhost:8080/api/
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "cls-edhzbzbn.ccs.tencent-cloud.com:60002"
}
]
}

我们可以通过这种 GET 方式访问 Pods

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
$ curl http://localhost:8080/api/v1/namespaces/default/pods # 访问default namespace的Pod
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/namespaces/default/pods",
"resourceVersion": "1155810077"
},
"items": [
{
"metadata": {
"name": "nginx-deployment-6b474476c4-6gwr2",
"generateName": "nginx-deployment-6b474476c4-",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/pods/nginx-deployment-6b474476c4-6gwr2",
"uid": "0e6cfbe8-3e5c-4023-93ab-024a325fdbbe",
"resourceVersion": "1153961309",
...
},
"spec": {
"volumes": [
{
"name": "default-token-kjmg9",
"secret": {
"secretName": "default-token-kjmg9",
"defaultMode": 420
}
}
],
...
},
"status": {
"phase": "Running",
...
}
},
...
]
}

通常情况下,我们可以通过 yaml 格式的 manifest 创建 Kubernetes 集群中的对象,比如新建一个 NginxPod

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: ngnix-pod
spec:
containers:
- name: ngnix
image: nginx:1.14.2
ports:
- containerPort: 80

对于 ApiServer而言,他们接收的API 请求数据和返回的数据一般是 JSON 格式,比如上面的Pod资源可以描述为以下:

pod.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"apiVersion":"v1",
"kind":"Pod",
"metadata":{
"name":"nginx-pod"
},
"spec":{
"containers":[
{
"name":"ngnix",
"image":"nginx:1.14.2",
"ports":[
{
"containerPort": 80
}
]
}
]
}
}

使用 curl 发出 POST 请求如下,可以看到 Pod 成功创建:

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
$ curl -vvv -X POST \
http://localhost:8080/api/v1/namespaces/default/pods \
-H 'Content-Type: application/json' \
-d @pod.json
> POST /api/v1/namespaces/default/pods HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> Content-Type: application/json
> Content-Length: 351
>
* upload completely sent off: 351 out of 351 bytes
< HTTP/1.1 201 Created
< Cache-Control: no-cache, private
< Content-Length: 2526
< Content-Type: application/json
< Date: Fri, 27 Nov 2020 10:54:11 GMT
<
{
"kind": "Pod",
"apiVersion": "v1",
"metadata": {
"name": "nginx-pod",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/pods/nginx-pod",
"uid": "bf20eefc-f98f-4ed2-9ccb-c707698851ea",
"resourceVersion": "1157202534",
"creationTimestamp": "2020-11-27T10:54:11Z",
},
"spec": {
"containers": [
{
"name": "ngnix",
"image": "nginx:1.14.2",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
...
"imagePullPolicy": "IfNotPresent"
}
],
...
},
"status": {
"phase": "Pending",
"qosClass": "BestEffort"
}
* Connection #0 to host localhost left intact
}

同样,我们也可以通过 DELETE 删除 Pod

1
2
$ curl -vvv -X DELETE \
http://localhost:8080/api/v1/namespaces/default/pods/nginx-pod

通过上面的示例,我们可以看到,通过向 ApiServer 直接发送 REST API 请求是可以查询和操作集群中的资源对象的。对于不同类型的资源对象,Kubernetes 定义了一系列的 API Path,可以方便资源对象的管理。下图是 HTTP API 空间的一部分:

可以看到,其 API 可以分为三种类型:

  • core group:路径为 /api/v1
  • named groups:路径为 /apis/$ApiGroup/$Version
  • 暴露系统状态的API:比如 /metrics/healthz

在 Kubernetes 中,要定位一个资源对象,需要指定 GVR,也就是GroupVersionResource。以下面的 DaemonSet 为例,声明了 apiVersionapps/v1,其实就是隐含了 GroupappsVersionv1Kind 就是定义的 DaemonSet,而 kubectl 接收到这个声明之后,就可以根据这个声明去调用 API Server 对应的 URL 去获取信息,例如这个就是 /api/apps/v1/daemonsets

1
2
3
4
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter

等等,不是 GVR吗,怎么这里变成了 GVK 呢?实质上 ReourceKind 基本上都是一个概念,只是 Kind 表示一个种类,在实际中它是首字母大写的; Resource 表示资源,在实际中它是全部小写的,并且有单数和复数之分。我们可以把 KindResource的关系理解成面向对象编程中类与对象的关系,Kind其实就是一个类,用于描述对象的;而 Resource 就是具体的 Kind,可以理解成类已经实例化的对象。如果要定位某个 Namespace 下的 资源对象,则请求路径如下:

Kubernetes 的不同 API Group 都在按照自己的节奏在向前演进,那么如何知道当前 Kubernetes 版本支持哪些 API Group、哪些 Version和 哪些 Kind 呢,我们可以在Kubernetes API Reference v1.19 这里查阅到不同版本支持的 GVK

API Convention

Kubernetes is not just API-driven, but is API-centric.

如前面所说,ApiServer 是 k8s 控制面的中心,不论是 user client 还是 controllers 都直接和 ApiServer 通过 REST API 交互,系统的状态通过 etcd 来实现持久化。

前面提到,对于不同的资源类型,k8s 按照 ApiGroup 定义了不同类型的 API,如下图所示:

这些 API 的定义在 k8s.io/api 仓库中,你可以在 这里 找到它们,和前面所说的一样它们按照 ApiGroup 组织在一起,并且维护了不同的版本:

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

╭─ ~/go/src/k8s.io/kubernetes/staging/src/k8s.io/api > release-1.18 $
╰─ tree -L 2
.
├── BUILD
├── CONTRIBUTING.md
├── Godeps
│   ├── OWNERS
│   └── Readme
├── LICENSE
├── OWNERS
├── README.md
├── SECURITY_CONTACTS
├── ...
├── apps
│   ├── OWNERS
│   ├── v1
│   ├── v1beta1
│   └── v1beta2
├── ...
├── batch
│   ├── OWNERS
│   ├── v1
│   ├── v1beta1
│   └── v2alpha1
├── ...
├── core
│   └── v1
├── ...
├── rbac
│   ├── OWNERS
│   ├── v1
│   ├── v1alpha1
│   └── v1beta1
└── ...

Batch 为例,我们可以看看它具体是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
╭─ ~/go/src/k8s.io/kubernetes/staging/src/k8s.io/api/batch/v1 > release-1.18 $
╰─ tree
.
├── BUILD
├── doc.go
├── generated.pb.go
├── generated.proto
├── register.go
├── types.go
├── types_swagger_doc_generated.go
└── zz_generated.deepcopy.go

对于每个 ApiGroup,其中各个类型的 API 被定义在 types.go 中,下面是 Job 类型 和 JobList 的定义:

k8s.io/api/batch/v1/types.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Job represents the configuration of a single job.
type Job struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

Spec JobSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
Status JobStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

// JobList is a collection of jobs.
type JobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

Items []Job `json:"items" protobuf:"bytes,2,rep,name=items"`
}

JobJobList 分别对应了 k8s 中两种不同种类的 Kind

  • Objects:代表了系统中的 persistent entity,client可以通过 createupdatedelelteget 等动作来操作Object,典型的代表有:PodsServiceNamespaceNodeReplicationController
  • Lists:通常是某种类型的 resource 集合,一个list kind必须以 List 命名结尾,它们都有 items 字段来返回 objects 的数组,典型的代表有:PodListsServiceListsNodeLists

除了 types.go,另外一个关键的文件是 register.go,下面是 batch/v1register.go,内容比较简单:

k8s.io/api/batch/v1/register.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
// GroupName is the group name use in this package
const GroupName = "batch"

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)

// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Job{},
&JobList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

在这里定义了 batch/v1 这个 ApiGroup,并且通过 scheme.AddKnownTypesJobJobList 两种类型注册到 runtime.Scheme 中(runtime.Scheme是什么东西?这里暂时跳过,将在下一节详述)。在 API 的定义中,剩下的除了 doc.go 外都是自动生成的代码了,所以关键想了解每种类型的 API 的话,看 types.go 中的具体定义就好。

Job 定义中,我们看到了四个关键字段 TypeMetaObjectMeta,以及 SpecStatus,这些字段对于每一种 Object 都广泛存在,比如下面的 Pod 定义,下面我们会依次解析其含义。

1
2
3
4
5
6
7
8
9
// Pod is a collection of containers that can run on a host. This resource is created
// by clients and scheduled onto hosts.
type Pod struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

Spec PodSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
Status PodStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

在此之前,我们先回顾一下 Podmanifest

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: production
labels:
name: nginx
spec:
containers:
- image: nginx:latest

TypeMeta

TypeMeta 用于表明该 ObjectKind 和对应的 APIVersion,其定义如下:

k8s.io/apimachinery/pkg/apis/meta/v1/types.go
1
2
3
4
type TypeMeta struct {
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}

这正好对应于上面 manifest中的前两个字段:

1
2
apiVersion: v1
kind: Pod

metav1.TypeMeta 实现了 schema.ObjectKindinterface

k8s.io/apimachinery/pkg/apis/meta/v1/meta.go
1
2
3
4
5
6
7
8
9
10
11
func (obj *TypeMeta) GetObjectKind() schema.ObjectKind { return obj }

// SetGroupVersionKind satisfies the ObjectKind interface for all objects that embed TypeMeta
func (obj *TypeMeta) SetGroupVersionKind(gvk schema.GroupVersionKind) {
obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind()
}

// GroupVersionKind satisfies the ObjectKind interface for all objects that embed TypeMeta
func (obj *TypeMeta) GroupVersionKind() schema.GroupVersionKind {
return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
}

schema.ObjectKind又是干什么的呢?

k8s.io/apimachinery/pkg/runtime/schema/interfaces.go
1
2
3
4
type ObjectKind interface {
SetGroupVersionKind(kind GroupVersionKind)
GroupVersionKind() GroupVersionKind
}

GroupVersionKind 才是 k8s 的API 对象类型真身,包括Kind、Version和Group,schema.ObjectKind 提供了对应的 setter/getter 函数。

k8s.io/apimachinery/pkg/runtime/schema/group_version.go
1
2
3
4
5
type GroupVersionKind struct {
Group string
Version string
Kind string
}

ObjectMeta

每一个 Object 都必须有一个 metadata 字段, metadata 包含一些用于唯一识别对象的数据,包括 Object 所在的 namespace ,它在当前 namespace中能够唯一确定本对象的 name 字段,用于组织和分类的 labels 以及用于扩展功能的注解 annotations等。

k8s.io/apimachinery/pkg/apis/meta/v1/types.go
1
2
3
4
5
6
7
8
9
10
type ObjectMeta struct {
Name string
Namespace string
Labels map[string]string
Annotations map[string]string
OwnerReferences []OwnerReference
UID types.UID
SelfLink string
// ...
}

上述的结构体嵌入在 Kubernetes 的每一个对象中,为所有的对象提供类似命名、命名空间、标签和注解等最基本的支持,让开发者能够更好地管理 Kubernetes 集群。

ObjectMeta 的具体实现提供了 metav1.Objectinterface,其中定义了一系列的 Getter/Setter 接口:

k8s.io/apimachinery/pkg/apis/meta/v1/meta.go
1
2
3
4
5
6
7
8
9
10
11
12
13
type Object interface {
GetNamespace() string
SetNamespace(namespace string)
GetName() string
SetName(name string)
GetUID() types.UID
SetUID(uid types.UID)
GetLabels() map[string]string
SetLabels(labels map[string]string)
GetAnnotations() map[string]string
SetAnnotations(annotations map[string]string)
// ...
}

这些 Getter/Setter 接口获取的字段基本都是 ObjectMeta 结构体中定义的一些字段,这也是为什么 Kubernetes 对象都需要嵌入一个 ObjectMeta 结构体。

ListMeta

ListMeta 描述了复合资源的metadata,一个 Resource 要么只能有 ListMeta 要么只能有 ObjectMeta

k8s.io/apimachinery/pkg/apis/meta/v1/types.go
1
2
3
4
5
6
type ListMeta struct {
ResourceVersion string `json:"resourceVersion,omitempty" protobuf:"bytes,2,opt,name=resourceVersion"`
Continue string `json:"continue,omitempty" protobuf:"bytes,3,opt,name=continue"`
RemainingItemCount *int64 `json:"remainingItemCount,omitempty" protobuf:"bytes,4,opt,name=remainingItemCount"`
// ...
}

ListMeta 的具体实现提供了metav1.ListInterfaceinterface,其中定义了一系列的 Getter/Setter 接口:

k8s.io/apimachinery/pkg/apis/meta/v1/meta.go
1
2
3
4
5
6
7
8
9
10
type ListInterface interface {
GetResourceVersion() string
SetResourceVersion(version string)
GetSelfLink() string
SetSelfLink(selfLink string)
GetContinue() string
SetContinue(c string)
GetRemainingItemCount() *int64
SetRemainingItemCount(c *int64)
}

Spec

Spec 描述了我们期待 Object 在集群中的 desired state,集群中的 controllers 会一直运转,使得 object 的状态到达 Spec 所描述的那样。系统会根据 API Spec 的最新状态来驱动,也就是说,如果 Spec 中的一个值通过 PUT 操作从2更新到5,然后又通过另一个 PUT 操作从5更新到3,这个过程中 controller 并不要求 Status 一定需要经历 3 这个状态。也就是说,系统是 level-based 而不是 edge-based

1
2
3
4
5
6
7
// PodSpec is a description of a pod.
type PodSpec struct {
Volumes []Volume `json:"volumes,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name" protobuf:"bytes,1,rep,name=volumes"`
InitContainers []Container `json:"initContainers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,20,rep,name=initContainers"`
Containers []Container `json:"containers" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=containers"`
// ...
}

Status

如果说 Spec 对应了我们期待该对象在集群中的 desired state,那么 Status 就是对象在集群中的 observed state。下面是 PodStatus 的示例,描述了 Pod 当前的状态:

1
2
3
4
5
6
7
8
9
type PodStatus struct {
Phase PodPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase,casttype=PodPhase"`
Conditions []PodCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,rep,name=conditions"`
Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"`
Reason string `json:"reason,omitempty" protobuf:"bytes,4,opt,name=reason"`
InitContainerStatuses []ContainerStatus `json:"initContainerStatuses,omitempty" protobuf:"bytes,10,rep,name=initContainerStatuses"`
ContainerStatuses []ContainerStatus `json:"containerStatuses,omitempty" protobuf:"bytes,8,rep,name=containerStatuses"`
// ...
}

Resource/Kind/Type Mapping

前面提到,要定位一个API资源,需要指定其 GVR 或者 GVK,这两种本质相同,是同一个资源在不同场景的表现形式,在上一小节中我们也展示了每种API资源在 types.go 中数据结构的定义。那么问题来了,k8s 是如何从一个 REST HTTP Request Path 转化成对应的 GVRGVK 的呢?

每个 GVR 对应一个http 路径(kind 不会),用于标识 Kubernetes API的 REST 接口。runtime.Scheme 将 golang object 映射为可能的GVK。一个GVK 到一个GVR 的映射被称为 REST mapping,RESTMapper interface/ RESTMapping struct 来完成转换。

Objects

在上一小节中我们提到了 metav1.TypeMetametav1.ObjectMetametav1.ListMetaPod 直接继承自 metav1.TypeMetametav1.ObjectMetaPodList 继承自 metav1.TypeMetametav1.ListMeta。而 metav1.TypeMeta 继承自 schema.ObjectKind interface,提供了对于 GroupVersionKindsetter/getter 方法,metav1.ObjectMeta 继承自 metav1.Object interface ,提供了对于 NamespaceLabelAnnotation 等一系列的 setter/getter 方法。

为了通过一个 API 对象来访问 Scheme 中所有的 API 对象,k8s 设计实现了 runtime.Object interface,作为 schema.ObjectKindmetav1.Object 的统一基类。

Object interface must be supported by all API types registered with Scheme. Since objects in a scheme are expected to be serialized to the wire, the interface an Object must provide to the Scheme allows serializers to set the kind, version, and group the object is represented as.

k8s.io/apimachinery/pkg/runtime/interfaces.go
1
2
3
4
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}

Scheme

前面提到,对于不同 ApiGroup 中不同类型的API资源,我们在 k8s.io/api/<apiGroup>/<version>目录下:

  • types.go 中定义了不同类型的API的数据结构
  • register.go 负责将 types.go中定义的数据结构注册到 runtime.Scheme

这里的 runtime.Scheme 是什么东西?每个不同类型的API资源到底是如何注册的呢?在上面的介绍过程中我们看到,在 k8s.io/api 这个仓库中可以找到所有类型的API资源的定义,其中也包括各种 SpecStatus 的定义。但是 TypeMetaObjectMeta 这种数据结构,却都存在于 k8s.io/apimachinery 仓库中。

k8s.io/api 相比,k8s.io/apimachinery 显得复杂得多,包含各种 conversionlabelscodec 和 这里的 runtime.Scheme,这些到底又都是做何用处呢?我们将在后续的文章依次解析。

现在,我们首先来看其中最最基础的 runtime.Schemeruntime.Scheme 只是实现了一个序列化与类型转换的框架API,提供了注册资源数据类型与转换函数的功能,是实现多版本API和多版本配置的基础。

Scheme defines methods for serializing and deserializing API objects, a type registry for converting group, version, and kind information to and from Go schemas, and mappings between Go schemas of different versions.

Scheme 中,对应于 GVK 有以下定义:

  • a Type is a particular Go struct
  • a Version is a point-in-time identifier for a particular representation of that Type (typically backwards compatible)
  • a Kind is the unique name for that Type within the Version
  • a Group identifies a set of Versions, Kinds, and Types that evolve over time
  • An Unversioned Type is one that is not yet formally bound to a type and is promised to be backwards compatible
k8s.io/apimachinery/pkg/runtime/scheme.go
1
2
3
4
5
6
7
8
9
type Scheme struct {
gvkToType map[schema.GroupVersionKind]reflect.Type
typeToGVK map[reflect.Type][]schema.GroupVersionKind
unversionedTypes map[reflect.Type]schema.GroupVersionKind
unversionedKinds map[string]reflect.Type
fieldLabelConversionFuncs map[string]map[string]FieldLabelConversionFunc
defaulterFuncs map[reflect.Type]func(interface{})
converter *conversion.Converter
}

分析 Scheme 的各个字段:

  • gvkToType 实现从 GroupVersionKindgo type 的映射
  • typeToGVK 实现从go typeGroupVersionKind 的映射,允许通过 go object 找到其 metadata
  • unversionedTypesare transformed without conversion in ConvertToVersion
  • unversionedKind用于映射哪些能够在任意group和version情况下的类型,key是一个string,也就是kind
  • fieldLabelConversionFuncs:用于解决数据对象的属性名称的兼容性转换和检验,比如讲需要兼容Pod的spec.Host属性改为spec.nodeName的情况
  • defaulterFuncs 包含了所有 defaulting 相关的函数
  • converter 包含了所有转换函数,负责不同版本的数据对象转换问题

前文提到, JobJobListtypes.go 中定义之后,通过在 register.go 中注册到 runtime.Scheme,其中调用了 AddKnownTypes 这个函数:

k8s.io/api/batch/v1/register.go
1
2
3
4
5
6
7
8
9
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Job{},
&JobList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

我们看起具体实现,通过传入 GroupVersion 和 要注册的 Type,调用了 AddKnownTypeWithName

k8s.io/apimachinery/pkg/runtime/scheme.go
1
2
3
4
5
6
7
8
9
10
11
func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
s.addObservedVersion(gv)
for _, obj := range types {
t := reflect.TypeOf(obj)
if t.Kind() != reflect.Ptr {
panic("All types must be pointers to structs.")
}
t = t.Elem()
s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
}
}

AddKnownTypeWithName 中,实际上是将 TypeGVK 注册到 runtime.SchemegvkToTypetypeToGVK 中。

k8s.io/apimachinery/pkg/runtime/scheme.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
func (s *Scheme) AddKnownTypeWithName(gvk schema.GroupVersionKind, obj Object) {
s.addObservedVersion(gvk.GroupVersion())
t := reflect.TypeOf(obj)
if len(gvk.Version) == 0 {
panic(fmt.Sprintf("version is required on all types: %s %v", gvk, t))
}
if t.Kind() != reflect.Ptr {
panic("All types must be pointers to structs.")
}
t = t.Elem()
if t.Kind() != reflect.Struct {
panic("All types must be pointers to structs.")
}

if oldT, found := s.gvkToType[gvk]; found && oldT != t {
panic(fmt.Sprintf("Double registration of different types for %v: old=%v.%v, new=%v.%v in scheme %q", gvk, oldT.PkgPath(), oldT.Name(), t.PkgPath(), t.Name(), s.schemeName))
}

s.gvkToType[gvk] = t

for _, existingGvk := range s.typeToGVK[t] {
if existingGvk == gvk {
return
}
}
s.typeToGVK[t] = append(s.typeToGVK[t], gvk)
}

Kubernetes这个设计思路简单方便地建解决多版本的序列化和数据转换问题,下面是 runtime.Scheme 里序列化、反序列化的核心方法New()的代码:通过查找 gkvToType 里匹配的类型,以反射方法生成一个空的数据对象:

k8s.io/apimachinery/pkg/runtime/scheme.go
1
2
3
4
5
6
7
8
9
10
11
12
// New returns a new API object of the given version and name, or an error if it hasn't
// been registered. The version and kind fields must be specified.
func (s *Scheme) New(kind schema.GroupVersionKind) (Object, error) {
if t, exists := s.gvkToType[kind]; exists {
return reflect.New(t).Interface().(Object), nil
}

if t, exists := s.unversionedKinds[kind.Kind]; exists {
return reflect.New(t).Interface().(Object), nil
}
return nil, NewNotRegisteredErrForKind(kind)
}

我们继续看 k8s.io/api/batch/v1/register.go,它通过SchemeBuilder 结构传入了注册 JobJobList 的方法,并且声明了一个 AddToScheme 的全局变量:

k8s.io/api/batch/v1/register.go
1
2
3
4
5
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)

那么 SchemeBuilder 是什么呢?其本质上就是一个函数数组,通过 NewSchemeBuilder 将要执行的函数传入,并通过调用 AddToScheme 真正执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type SchemeBuilder []func(*Scheme) error

func NewSchemeBuilder(funcs ...func(*Scheme) error) SchemeBuilder {
var sb SchemeBuilder
sb.Register(funcs...)
return sb
}

func (sb *SchemeBuilder) Register(funcs ...func(*Scheme) error) {
for _, f := range funcs {
*sb = append(*sb, f)
}
}

func (sb *SchemeBuilder) AddToScheme(s *Scheme) error {
for _, f := range *sb {
if err := f(s); err != nil {
return err
}
}
return nil
}

可以看到在 register.go 中只是声明了 AddToScheme 的注册方法,并没有真正执行注册的过程。那么到底是在哪里执行注册的呢?答案是在 client-go 中,查看 k8s.io/client-go/kubernetes/scheme/register.go ,可以看到这里引用了所有 ApiGroup 各个版本的 AddToScheme ,创建了一个全局 AddToScheme,在 init 方法初始化的时候就会被调用。

k8s.io/client-go/kubernetes/scheme/register.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
// ...
batchv1.AddToScheme,
batchv1beta1.AddToScheme,
batchv2alpha1.AddToScheme,
// ...
corev1.AddToScheme,
//...
storagev1beta1.AddToScheme,
storagev1.AddToScheme,
storagev1alpha1.AddToScheme,
}

var AddToScheme = localSchemeBuilder.AddToScheme

func init() {
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
utilruntime.Must(AddToScheme(Scheme))
}

RESTMapper

前面提到,RESTMapper 实现了从 GVRGVK 的映射,下面显示了其提供的方法,听可以通过 GVK 生成一个 RESTMapping

k8s.io/apimachinery/pkg/api/meta/interface.go
1
2
3
4
5
6
7
8
9
10
11
type RESTMapper interface {
KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error)
KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error)
ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error)
ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error)

RESTMapping(gk schema.GroupKind, versions ...string) (*RESTMapping, error)
RESTMappings(gk schema.GroupKind, versions ...string) ([]*RESTMapping, error)

ResourceSingularizer(resource string) (singular string, err error)
}

RESTMapping 是什么呢?RSETMapping 结构包含有 GVRGVK,还有一个 Scope 标明资源是否为 root 或者 namespaced

k8s.io/apimachinery/pkg/api/meta/interface.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type RESTMapping struct {
Resource schema.GroupVersionResource
GroupVersionKind schema.GroupVersionKind
Scope RESTScope
}

type RESTScope interface {
Name() RESTScopeName
}

type RESTScopeName string

const (
RESTScopeNameNamespace RESTScopeName = "namespace"
RESTScopeNameRoot RESTScopeName = "root"
)

那么 RESTMapping 该如何使用呢?后续在 API 的安装介绍中将会看到,将会使用 RESTMapping 中的 Scope 来生成合适的 URL,这里暂时埋下一个种子。

DefaultRESTMapper 实现了 RESTMapper interface,可以看到这里通过几个 map 保存了 GVKGVR 之间的映射关系。

1
2
3
4
5
6
7
8
9
type DefaultRESTMapper struct {
defaultGroupVersions []schema.GroupVersion

resourceToKind map[schema.GroupVersionResource]schema.GroupVersionKind
kindToPluralResource map[schema.GroupVersionKind]schema.GroupVersionResource
kindToScope map[schema.GroupVersionKind]RESTScope
singularToPlural map[schema.GroupVersionResource]schema.GroupVersionResource
pluralToSingular map[schema.GroupVersionResource]schema.GroupVersionResource
}

Summarize

本文首先介绍了通过 REST API 访问集群的方法,引入了 GVKGVR 等概念,接下来基于 API Convention 介绍了每个 API 实现过程中对应的数据结构。最后介绍了 runtime.SchemeRESTMapper,了解了 API 对象从 HTTP PathGVR 再到 GVK 之间的转换。

References