前言

在进入 kube-apiserver 源码分析前,有一个非常重要的概念需要了解甚至熟悉的:资源注册表(scheme)。

Kubernetes 中一切皆资源,管理的是资源,创建、更新、删除的是资源。如何对庞杂的资源进行管理就成了一件大事。Kubernetes 通过引入 scheme 资源注册表,将资源信息注册到资源注册表,各个组件通过索引资源注册表实现资源的管理。

介绍

直接开始阅读 kube-apiserver scheme 部分的源码是比较困难的,这里举代码示例如下:

 1package main
 2
 3import (
 4	"fmt"
 5
 6	appsv1 "k8s.io/api/apps/v1"
 7	corev1 "k8s.io/api/core/v1"
 8	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 9	"k8s.io/apimachinery/pkg/runtime"
10	"k8s.io/apimachinery/pkg/runtime/schema"
11)
12
13func main() {
14	// KnownType external
15	coreGV := schema.GroupVersion{Group: "", Version: "v1"}
16	extensionsGV := schema.GroupVersion{Group: "extensions", Version: "v1beta1"}
17
18	// KnownType internal
19	coreInternalGV := schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}
20
21	// UnversionedType
22	Unversioned := schema.GroupVersion{Group: "", Version: "v1"}
23
24	schema := runtime.NewScheme()
25	schema.AddKnownTypes(coreGV, &corev1.Pod{})
26	schema.AddKnownTypes(extensionsGV, &appsv1.DaemonSet{})
27	schema.AddKnownTypes(coreInternalGV, &corev1.Pod{})
28	schema.AddUnversionedTypes(Unversioned, &metav1.Status{})
29
30	fmt.Println(*schema)
31    fmt.Println(schema.KnownTypes(coreGV))
32}

示例中有几点需要关注。

  1. 通过 runtime.NewScheme 创建 scheme 实例:
 1func NewScheme() *Scheme {
 2	s := &Scheme{
 3		gvkToType:                 map[schema.GroupVersionKind]reflect.Type{},
 4		typeToGVK:                 map[reflect.Type][]schema.GroupVersionKind{},
 5		unversionedTypes:          map[reflect.Type]schema.GroupVersionKind{},
 6		unversionedKinds:          map[string]reflect.Type{},
 7		fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
 8		defaulterFuncs:            map[reflect.Type]func(interface{}){},
 9		versionPriority:           map[string][]string{},
10		schemeName:                naming.GetNameFromCallsite(internalPackages...),
11	}
12	s.converter = conversion.NewConverter(nil)
13
14	...
15	return s
16}

Scheme 的结构体有四个 field 需要重点关注:

  • gvkToType: 记录的是 schema.GroupVersionKind 和资源类型的映射关系。
  • typeToGVK: 记录的是资源类型和 schema.GroupVersionKind 的映射关系。
  • unversionedTypes: 记录的是无版本资源类型和 schema.GroupVersionKind 的映射关系。
  • unversionedKinds: 记录的是资源种类 Kind 和无版本资源类型的映射关系。

还有一个 Scheme.converter 也挺重要,这里先跳过不讲。

  1. 调用 SchemeAddKnownTypesAddUnversionedTypes 方法建立资源 Group/Version/Kind 和资源类型的相互映射关系。

AddKnownTypes 为例,函数通过反射 reflect.TypeOf(obj) 获得对象 runtime.Object 的资源类型。这里,对象是资源的接口,可实现资源和对象的相互转换。

 1func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
 2	s.addObservedVersion(gv)
 3	for _, obj := range types {
 4		t := reflect.TypeOf(obj)
 5		if t.Kind() != reflect.Pointer {
 6			panic("All types must be pointers to structs.")
 7		}
 8		t = t.Elem()
 9		s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
10	}
11}
12
13func (gv GroupVersion) WithKind(kind string) GroupVersionKind {
14	return GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind}
15}
16
17func (s *Scheme) AddKnownTypeWithName(gvk schema.GroupVersionKind, obj Object) {
18	...
19	t := reflect.TypeOf(obj)
20	s.gvkToType[gvk] = t
21}

然后调用 gv.WithKind(t.Name()) 转换为 GroupVersionKind,可以看出上面反射的资源类型作为 kind 赋给 GroupVersionKind

最后调用 AddKnownTypeWithNameschema.GroupVersionKind 和资源类型写入 Scheme.gvkToType

执行代码结果如下:

 1{
 2	map[
 3		{ __internal Pod}:0xb46180 
 4		{ v1 Pod}:0xb46180 
 5		{ v1 Status}:0xb56d40 
 6		{extensions v1beta1 DaemonSet}:0xb44e40
 7	] 
 8	map[
 9		0xb44e40:[{extensions v1beta1 DaemonSet}] 
10		0xb46180:[{ v1 Pod} { __internal Pod}] 
11		0xb56d40:[{ v1 Status}]
12	] 
13	map[
14		0xb56d40:{ v1 Status}
15	] 
16	map[
17		Status:0xb56d40
18	] 
19	map[] 
20	map[] 
21	0xc000124888 
22	map[] 
23	[{ v1} {extensions v1beta1}] 
24	pkg/runtime/scheme.go:100
25}
26map[Pod:v1.Pod Status:v1.Status]
  1. Kubernetes 资源分为外部资源和内部资源。外部资源是对外可访问的,其版本为 v1/v1beta1 等,内部资源是 Kubernetes 内部访问的资源,其版本为 __internal

为什么会有内部版本呢?
为了适配版本间的 convert

试想,如果 v1v1apha1v1beta1v1beta2 要相互转换需要建立十二种转换关系:

 1v1 		 ---->  v1alpha1  
 2v1 		 ---->  v1beta1 
 3v1 		 ---->  v1beta2  
 4v1alpha1 ---->  v1  
 5v1alpha1 ---->  v1beta1  
 6v1alpha1 ---->  v1beta2
 7v1beta1  ---->  v1  
 8v1beta1  ---->  v1alpha1  
 9v1beta1  ---->  v1beta2
10v1beta2  ---->  v1  
11v1beta2  ---->  v1alpha1  
12v1beta2  ---->  v1beta1

可以看到这种转换已经比较复杂了,随着资源增多,这种资源版本之间的转换会越来越复杂且难以维护。Kubernetes 通过引入内部版本 __internl 建立每种资源到 __internal 的转换,从而实现不同资源版本的相互转换。

1v1 			---->  __internal  
2__internal  ---->  v1 
3v1alpha1 	---->  __internal 
4__internal  ---->  v1alpha1 
5v1beta1 	---->  __internal  
6__internal  ---->  v1beta1
7v1beta2 	---->  __internal
8__internal  ---->  v1beta2

转换示意图如下:
scheme_convert

可以看到,相当干净,清爽。

kube-apiserver scheme

第一节介绍了 scheme 结构及部分特性。继续看 kube-apiserverscheme 的应用。

注册资源

kube-apiserver 中资源的注册通过导入包的形式实现。Kubernetes 将资源拆分为三类,并且由三种 HTTP Server 负责处理这三类资源,架构如下。

scheme 架构

关于这幅图,这里不多讲,后续会逐层介绍。 要知道的是,三类资源分别注册到 extensionsapiserver.Schemelegacyscheme.Schemeaggregatorscheme.Scheme 三种资源注册表中。

legacyscheme.Scheme 资源注册表为例。
kube-apiserver 的启动 app 包中导入 "k8s.io/kubernetes/pkg/api/legacyscheme""k8s.io/kubernetes/pkg/controlplane"包:

1package app
2
3import (
4	"k8s.io/kubernetes/pkg/api/legacyscheme"
5	"k8s.io/kubernetes/pkg/controlplane"
6)

legacyscheme 包中创建了 Scheme 资源注册表,Codecs 编码器和 ParameterCodec 参数编码器。

 1# kubernetes/pkg/api/legacyscheme/scheme.go
 2
 3package legacyscheme
 4
 5var (
 6	Scheme = runtime.NewScheme()
 7
 8	Codecs = serializer.NewCodecFactory(Scheme)
 9
10	ParameterCodec = runtime.NewParameterCodec(Scheme)
11)

controlplane 包中导入需要注册的资源组:

1# kubernetes/pkg/controlplane/import_known_versions.go
2
3package controlplane
4
5import (
6	_ "k8s.io/kubernetes/pkg/apis/apps/install"
7)

以注册 apps 资源组为例:

 1# kubernetes/pkg/apis/apps/install/install.go
 2
 3package install
 4
 5import (
 6	"k8s.io/apimachinery/pkg/runtime"
 7	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 8	"k8s.io/kubernetes/pkg/api/legacyscheme"
 9	"k8s.io/kubernetes/pkg/apis/apps"
10	"k8s.io/kubernetes/pkg/apis/apps/v1"
11	"k8s.io/kubernetes/pkg/apis/apps/v1beta1"
12	"k8s.io/kubernetes/pkg/apis/apps/v1beta2"
13)
14
15func init() {
16	Install(legacyscheme.Scheme)
17}
18
19func Install(scheme *runtime.Scheme) {
20	utilruntime.Must(apps.AddToScheme(scheme))
21	utilruntime.Must(v1beta1.AddToScheme(scheme))
22	utilruntime.Must(v1beta2.AddToScheme(scheme))
23	utilruntime.Must(v1.AddToScheme(scheme))
24	utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta2.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
25}

资源组中调用 apps.AddToSchemev1beta1.AddToScheme,v1beta2.AddToSchemev1.AddToScheme 函数将同一资源组不同版本的资源注册到 legacyscheme.Scheme 资源注册表中。

其中 apps.AddToScheme 注册的是内部版本的资源:

 1package apps
 2
 3var (
 4	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
 5	AddToScheme = SchemeBuilder.AddToScheme
 6)
 7
 8const GroupName = "apps"
 9
10var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
11
12func addKnownTypes(scheme *runtime.Scheme) error {
13	scheme.AddKnownTypes(SchemeGroupVersion,
14		&DaemonSet{},
15		&DaemonSetList{},
16		&Deployment{},
17		&DeploymentList{},
18		&DeploymentRollback{},
19		&autoscaling.Scale{},
20		&StatefulSet{},
21		&StatefulSetList{},
22		&ControllerRevision{},
23		&ControllerRevisionList{},
24		&ReplicaSet{},
25		&ReplicaSetList{},
26	)
27	return nil
28}

v1beta1.AddToScheme,v1beta2.AddToSchemev1.AddToScheme 注册的是外部版本的资源,以 v1.AddToScheme 为例:

 1package v1
 2
 3const GroupName = "apps"
 4
 5var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}
 6
 7var (
 8	SchemeBuilder      = runtime.NewSchemeBuilder(addKnownTypes)
 9	localSchemeBuilder = &SchemeBuilder
10	AddToScheme        = localSchemeBuilder.AddToScheme
11)
12
13func addKnownTypes(scheme *runtime.Scheme) error {
14	scheme.AddKnownTypes(SchemeGroupVersion,
15		&Deployment{},
16		&DeploymentList{},
17		&StatefulSet{},
18		&StatefulSetList{},
19		&DaemonSet{},
20		&DaemonSetList{},
21		&ReplicaSet{},
22		&ReplicaSetList{},
23		&ControllerRevision{},
24		&ControllerRevisionList{},
25	)
26	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
27	return nil
28}

需要注意的是,内部版本和外部版本注册的资源并不一样。比如 Deployment 资源,外部版本引用的是 kubernetes/vendor/k8s.io/api/apps/v1/types.go 中的资源定义:

1type Deployment struct {
2	metav1.TypeMeta `json:",inline"`
3	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
4	Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
5	Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
6}

资源声明了 jsonprotobuf tag,以便于外部访问的序列化,反序列化操作。

内部版本引用的是 kubernetes/pkg/apis/apps/types.go 中的资源定义:

1type Deployment struct {
2	metav1.TypeMeta
3	metav1.ObjectMeta
4	Spec DeploymentSpec
5	Status DeploymentStatus
6}

内部资源不需要被外部访问,没有 tag 声明。

到这里,kube-apiserver 关于资源的注册就差不多了。下一篇将进一步介绍 schemeconvertkube-apiserver 中如何使用 schemeschemeobject 相互转换等内容。