接 Kubernetes:kube-apiserver 之 scheme(一)。
资源 convert
上篇说到资源版本之间通过内部版本 __internal 进行资源转换。这里进一步扩展介绍资源转换内容,以加深理解。
同样以例子开始,通过 kubectl 将 apps/v1beta1/Deployment 转换为 apps/v1/Deployment。
1apiVersion: apps/v1beta1
2kind: Deployment
3metadata:
4 name: myapp
5spec:
6 replicas: 1
7 template:
8 metadata:
9 labels:
10 app: myapp
11 spec:
12 containers:
13 - name: myapp
14 image: myapp:1.0.0
15 resources:
16 limits:
17 memory: "128Mi"
18 cpu: "500m"
19 ports:
20 - containerPort: 80
执行 kubectl convert -f v1beta1Deployment.yaml --output-version=apps/v1,输出 apps/v1/Deployment 资源配置:
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 labels:
5 app: myapp
6 name: myapp
7spec:
8 progressDeadlineSeconds: 600
9 replicas: 1
10 revisionHistoryLimit: 2
11 ...
12status: {}
这条命令背后,kubectl 将转换命令组成 client-go 识别的 Restful API 消息,通过 client-go 发给 kube-apiserver。kube-apiserver 根据事先注册的转换函数 convert 将 apps/v1beta1/Deployment 转换为 apps/__internal/Deployment,接着将 apps/__internal/Deployment 转换为 apps/v1/Deployment。
代码示例如下:
1v1beta1Deployment := &appsv1beta1.Deployment{
2 TypeMeta: metav1.TypeMeta{
3 Kind: "Deployment",
4 APIVersion: "apps/v1beta1",
5 },
6}
7
8// v1beta1 -> __internal
9targetVersion := schema.GroupVersion{Group: "apps", Version: "__internal"}
10objInternal, err := scheme.ConvertToVersion(v1beta1Deployment, targetVersion)
11if err != nil {
12 panic(err)
13}
14
15// __internal -> v1
16objV1, err := scheme.ConvertToVersion(objInternal, appsv1.SchemeGroupVersion)
17if err != nil {
18 panic(err)
19}
scheme.ConvertToVersion 函数转换对象到指定资源版本。
kube-apiserver 资源版本 convert
资源版本的转换首先需要注册版本转换函数到 scheme,只有注册过的版本才能进行资源版本转换。
kube-apiserver 通过导入包的方式注册转换函数。
1# kubernetes/cmd/kube-apiserver/app/server.go
2package app
3
4import (
5 "k8s.io/kubernetes/pkg/controlplane"
6 ...
7)
8
9# kubernetes/pkg/controlplane/import_known_versions.go
10package controlplane
11
12import (
13 _ "k8s.io/kubernetes/pkg/apis/apps/install"
14 ...
15)
kube-apiserver 启动的 app 包导入 controlplane 包,controlplane 继续导入资源组的安装包。以 apps 资源组为例,查看资源组下资源转换函数是怎么注册的。
资源组下的每个外部资源版本都有 zz_generated.conversion.go 文件,该文件由 conversion-go 自动生成,文件中定义了外部资源到内部资源的相互转换。
1# kubernetes/pkg/apis/apps/
2apps/
3 /v1
4 /zz_generated.conversion.go
5 /v1beta1
6 /zz_generated.conversion.go
7 /v1beta2
8 /zz_generated.conversion.go
以 v1 版本为例,zz_generated.conversion.go 中注册资源转换函数:
1package v1
2
3func init() {
4 localSchemeBuilder.Register(RegisterConversions)
5}
6
7func RegisterConversions(s *runtime.Scheme) error {
8 if err := s.AddGeneratedConversionFunc((*v1.DeploymentSpec)(nil), (*apps.DeploymentSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
9 return Convert_v1_DeploymentSpec_To_apps_DeploymentSpec(a.(*v1.DeploymentSpec), b.(*apps.DeploymentSpec), scope)
10 }); err != nil {
11 return err
12 }
13 if err := s.AddConversionFunc((*apps.DeploymentSpec)(nil), (*v1.DeploymentSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
14 return Convert_apps_DeploymentSpec_To_v1_DeploymentSpec(a.(*apps.DeploymentSpec), b.(*v1.DeploymentSpec), scope)
15 }); err != nil {
16 return err
17 }
18 ...
19}
可以看到,函数 Convert_v1_DeploymentSpec_To_apps_DeploymentSpec 注册了 v1/DeploymentSpec 到 __internal/DeploymentSpec 的资源转换,Convert_apps_DeploymentSpec_To_v1_DeploymentSpec 注册了 __internal/DeploymentSpec 到 v1/DeploymentSpec。
关于资源版本转换基本告一段落。下面进一步介绍,在 kube-apiserver 是怎么使用 scheme 资源注册表的。
使用资源注册表 scheme
这一节会进入 kube-apiserver 看 scheme 是如何使用的。需要说明的是,kube-apiserver 非常复杂,这里并不介绍启动流程,建立 Restful API 等内容,仅从 scheme 的视角看 kube-apiserver。
1# kubernetes/cmd/kube-apiserver/app/server.go
2
3func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) {
4 notFoundHandler := notfoundhandler.New(config.ControlPlane.GenericConfig.Serializer, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
5 apiExtensionsServer, err := config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
6 if err != nil {
7 return nil, err
8 }
9 ...
10}
在 CreateServerChain 函数中创建 API 扩展服务(APIExtensionServer),API 核心服务(KubeAPIServer) 和 API 聚合服务(AggregatorServer)。这里以 API 扩展服务(APIExtensionServer) 看 scheme 是如何使用的。
进入 config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler)) 方法。
1# kubernetes/vendor/k8s.io/apiserver/pkg/server/genericapiserver.go
2// APIGroupInfo 中保存资源注册表 Scheme
3func NewDefaultAPIGroupInfo(group string, scheme *runtime.Scheme, parameterCodec runtime.ParameterCodec, codecs serializer.CodecFactory) APIGroupInfo {
4 return APIGroupInfo{
5 PrioritizedVersions: scheme.PrioritizedVersionsForGroup(group),
6 VersionedResourcesStorageMap: map[string]map[string]rest.Storage{},
7 OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
8 Scheme: scheme,
9 ParameterCodec: parameterCodec,
10 NegotiatedSerializer: codecs,
11 }
12}
13
14# kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go
15// 将 apiserver.Scheme 传入 NewDefaultAPIGroupInfo
16func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
17 ...
18 apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
19 ...
20 if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
21 return nil, err
22 }
23}
24
25# kubernetes/vendor/k8s.io/apiserver/pkg/server/genericapiserver.go
26func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
27 return s.InstallAPIGroups(apiGroupInfo)
28}
29
30func (s *GenericAPIServer) InstallAPIGroups(apiGroupInfos ...*APIGroupInfo) error {
31 ...
32 for _, apiGroupInfo := range apiGroupInfos {
33 if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo, openAPIModels); err != nil {
34 return fmt.Errorf("unable to install api resources: %v", err)
35 }
36 }
37}
38
39// 在 GenericAPIServer.getAPIGroupVersion 方法中将 apiGroupInfo 转换解析为 apiGroupVersion
40// apiGroupVersion 中保存资源注册表 scheme
41func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, typeConverter managedfields.TypeConverter) error {
42 for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
43 apiGroupVersion, err := s.getAPIGroupVersion(apiGroupInfo, groupVersion, apiPrefix)
44 if err != nil {
45 return err
46 }
47
48 ...
49 discoveryAPIResources, r, err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer)
50 }
51}
52
53# kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/groupversion.go
54func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]apidiscoveryv2beta1.APIResourceDiscovery, []*storageversion.ResourceInfo, error) {
55 installer := &APIInstaller{
56 group: g,
57 prefix: prefix,
58 minRequestTimeout: g.MinRequestTimeout,
59 }
60
61 apiResources, resourceInfos, ws, registrationErrors := installer.Install()
62 ...
63}
64
65# kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/installer.go
66func (a *APIInstaller) Install() ([]metav1.APIResource, []*storageversion.ResourceInfo, *restful.WebService, []error) {
67 for _, path := range paths {
68 apiResource, resourceInfo, err := a.registerResourceHandlers(path, a.group.Storage[path], ws)
69 ...
70 }
71 ...
72}
73
74func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, *storageversion.ResourceInfo, error) {
75 fqKindToRegister, err := GetResourceKind(a.group.GroupVersion, storage, a.group.Typer)
76 if err != nil {
77 return nil, nil, err
78 }
79}
80
81func GetResourceKind(groupVersion schema.GroupVersion, storage rest.Storage, typer runtime.ObjectTyper) (schema.GroupVersionKind, error) {
82 object := storage.New()
83 fqKinds, _, err := typer.ObjectKinds(object)
84 if err != nil {
85 return schema.GroupVersionKind{}, err
86 }
87 ...
88}
函数链很长,最后在函数 GetResourceKind 中调用 typer.ObjectKinds(object) 获取对象 object 的类型。其中,typer 是一个获取对象类型的接口,对应的实例是 scheme,实际是调用 scheme.ObjectKinds 方法获取对象类型。
注意,能获取对象类型是因为该对象提前注册在资源注册表 scheme 中,否则将会报 no kind is registered for the type xxx 错误。
示例代码如下。
1package main
2
3import (
4 "fmt"
5
6 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7 "k8s.io/apimachinery/pkg/runtime"
8 "k8s.io/apimachinery/pkg/runtime/schema"
9 "k8s.io/kubernetes/pkg/apis/core"
10)
11
12func main() {
13 pod := &core.Pod{
14 TypeMeta: metav1.TypeMeta{
15 Kind: "Pod",
16 },
17 ObjectMeta: metav1.ObjectMeta{
18 Labels: map[string]string{"name": "foo"},
19 },
20 }
21
22 coreGV := schema.GroupVersion{Group: "", Version: "v1"}
23 schema := runtime.NewScheme()
24 schema.AddKnownTypes(coreGV, &core.Pod{})
25
26 gvk, _, err := schema.ObjectKinds(pod)
27 if err != nil {
28 fmt.Println(err)
29 }
30
31 fmt.Println(gvk)
32}
资源对象 runtime.Object
上面说到资源对象,这里有必要在扩展下资源对象 runtime.Object。runtime.Object 是一个接口,资源需要实现该接口,通过接口方法可以实现资源和接口的相互转换。示意图如下。

示意代码如下:
1package main
2
3import (
4 "reflect"
5
6 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7 "k8s.io/apimachinery/pkg/runtime"
8 "k8s.io/kubernetes/pkg/apis/core"
9)
10
11func main() {
12 pod := &core.Pod{
13 TypeMeta: metav1.TypeMeta{
14 Kind: "Pod",
15 },
16 ObjectMeta: metav1.ObjectMeta{
17 Labels: map[string]string{"name": "foo"},
18 },
19 }
20
21 obj := runtime.Object(pod)
22
23 pod2, ok := obj.(*core.Pod)
24 if !ok {
25 panic("unexpected runtime object")
26 }
27
28 if !reflect.DeepEqual(pod, pod2) {
29 panic("unexpected")
30 }
31}
介绍完资源注册表 scheme,后续将继续介绍 kube-apiserver 是怎么启动,注册 RESTful API,如何实现认证,鉴权等操作,未完待续…