在 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 请求。这种情况下,你需要知道集群的地址,并且拥有访问的凭证,可以通过以下命知道集群地址及凭证:
下面演示了通过 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 { "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 集群中的对象,比如新建一个 Nginx
的 Pod
:
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 }
同样,我们也可以通过 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
,也就是Group
、Version
、Resource
。以下面的 DaemonSet
为例,声明了 apiVersion
是 apps/v1
,其实就是隐含了 Group
是 apps
,Version
是v1
,Kind
就是定义的 DaemonSet
,而 kubectl 接收到这个声明之后,就可以根据这个声明去调用 API Server 对应的 URL 去获取信息,例如这个就是 /api/apps/v1/daemonsets
。
1 2 3 4 apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter
等等,不是 GVR
吗,怎么这里变成了 GVK
呢?实质上 Reource
和 Kind
基本上都是一个概念,只是 Kind
表示一个种类,在实际中它是首字母大写的; Resource
表示资源,在实际中它是全部小写的,并且有单数和复数之分。我们可以把 Kind
和 Resource
的关系理解成面向对象编程中类与对象的关系,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 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"` } 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"` }
Job
和 JobList
分别对应了 k8s 中两种不同种类的 Kind
:
Objects
:代表了系统中的 persistent entity
,client可以通过 create
、update
、 delelte
、 get
等动作来操作Object,典型的代表有:Pods
、Service
、Namespace
、Node
、ReplicationController
等
Lists
:通常是某种类型的 resource 集合,一个list kind必须以 List
命名结尾,它们都有 items
字段来返回 objects 的数组,典型的代表有:PodLists
、ServiceLists
、NodeLists
等
除了 types.go
,另外一个关键的文件是 register.go
,下面是 batch/v1
的 register.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 const GroupName = "batch" var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1" }func Resource (resource string ) schema .GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) localSchemeBuilder = &SchemeBuilder AddToScheme = localSchemeBuilder.AddToScheme ) func addKnownTypes (scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Job{}, &JobList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil }
在这里定义了 batch/v1
这个 ApiGroup
,并且通过 scheme.AddKnownTypes
将 Job
和 JobList
两种类型注册到 runtime.Scheme
中(runtime.Scheme
是什么东西?这里暂时跳过,将在下一节详述)。在 API 的定义中,剩下的除了 doc.go
外都是自动生成的代码了,所以关键想了解每种类型的 API 的话,看 types.go
中的具体定义就好。
在 Job
定义中,我们看到了四个关键字段 TypeMeta
和 ObjectMeta
,以及 Spec
和 Status
,这些字段对于每一种 Object 都广泛存在,比如下面的 Pod
定义,下面我们会依次解析其含义。
1 2 3 4 5 6 7 8 9 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"` }
在此之前,我们先回顾一下 Pod
的 manifest
:
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
用于表明该 Object
的 Kind
和对应的 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.ObjectKind
的 interface
。
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 }func (obj *TypeMeta) SetGroupVersionKind (gvk schema.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() } 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 }
每一个 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.Object
的 interface
,其中定义了一系列的 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
描述了复合资源的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.ListInterface
的 interface
,其中定义了一系列的 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 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
转化成对应的 GVR
和 GVK
的呢?
每个 GVR 对应一个http 路径(kind 不会),用于标识 Kubernetes API的 REST 接口。runtime.Scheme
将 golang object 映射为可能的GVK。一个GVK 到一个GVR 的映射被称为 REST mapping
,RESTMapper interface/ RESTMapping struct 来完成转换。
Objects 在上一小节中我们提到了 metav1.TypeMeta
、metav1.ObjectMeta
和 metav1.ListMeta
,Pod
直接继承自 metav1.TypeMeta
和 metav1.ObjectMeta
,PodList
继承自 metav1.TypeMeta
和 metav1.ListMeta
。而 metav1.TypeMeta
继承自 schema.ObjectKind interface
,提供了对于 GroupVersionKind
的 setter/getter
方法,metav1.ObjectMeta
继承自 metav1.Object interface
,提供了对于 Namespace
、Label
、Annotation
等一系列的 setter/getter
方法。
为了通过一个 API 对象来访问 Scheme 中所有的 API 对象,k8s 设计实现了 runtime.Object interface
,作为 schema.ObjectKind
和 metav1.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资源的定义,其中也包括各种 Spec
和 Status
的定义。但是 TypeMeta
和 ObjectMeta
这种数据结构,却都存在于 k8s.io/apimachinery
仓库中。
与 k8s.io/api
相比,k8s.io/apimachinery
显得复杂得多,包含各种 conversion
、labels
、codec
和 这里的 runtime.Scheme
,这些到底又都是做何用处呢?我们将在后续的文章依次解析。
现在,我们首先来看其中最最基础的 runtime.Scheme
,runtime.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
实现从 GroupVersionKind
到 go type
的映射
typeToGVK
实现从go type
到 GroupVersionKind
的映射,允许通过 go object
找到其 metadata
unversionedTypes
are transformed without conversion in ConvertToVersion
unversionedKind
用于映射哪些能够在任意group和version情况下的类型,key是一个string,也就是kind
fieldLabelConversionFuncs
:用于解决数据对象的属性名称的兼容性转换和检验,比如讲需要兼容Pod的spec.Host属性改为spec.nodeName的情况
defaulterFuncs
包含了所有 defaulting
相关的函数
converter
包含了所有转换函数,负责不同版本的数据对象转换问题
前文提到, Job
和 JobList
在 types.go
中定义之后,通过在 register.go
中注册到 runtime.Scheme
,其中调用了 AddKnownTypes
这个函数:
k8s.io/api/batch/v1/register.go 1 2 3 4 5 6 7 8 9 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
中,实际上是将 Type
和 GVK
注册到 runtime.Scheme
的 gvkToType
和 typeToGVK
中。
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 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
结构传入了注册 Job
和 JobList
的方法,并且声明了一个 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.AddToSchemefunc init () { v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1" }) utilruntime.Must(AddToScheme(Scheme)) }
RESTMapper 前面提到,RESTMapper
实现了从 GVR
到 GVK
的映射,下面显示了其提供的方法,听可以通过 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
结构包含有 GVR
和 GVK
,还有一个 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
保存了 GVK
和 GVR
之间的映射关系。
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
访问集群的方法,引入了 GVK
、GVR
等概念,接下来基于 API Convention
介绍了每个 API 实现过程中对应的数据结构。最后介绍了 runtime.Scheme
和 RESTMapper
,了解了 API 对象从 HTTP Path
到 GVR
再到 GVK
之间的转换。
References