Kubernetes 中 apiserver 加载 schema 流程
在 apiserver 初始化、启动时,会加载所有能识别的 schema。加载的过程通过 import pkg 后,触发对应包下
init()
方法来加载。
入口
cmd/kube-apiserver/apiserver.go
: 初始化 apiserver 命令行,并运行。
1 | func main() { |
在 app.NewAPIServerCommand()
中,有一长串 import,其中这两条 import 语句完成了 schema 的加载。
1 | import ( |
初始化
导入 k8s.io/kubernetes/pkg/api/legacyscheme
后,此包下只有一个文件,名为 scheme.go
,该文件中会初始化 3 个变量。其中 Scheme
中会存储 k8s 默认能识别的 scheme。
1 | package legacyscheme |
导入 k8s.io/kubernetes/pkg/controlplane
后,在该包下的 import_known_versions.go
文件中,导入了所有 apiserver 支持的 API groups。
1 | package controlplane |
它们 import
后的行为是基本一致的,以 k8s.io/kubernetes/pkg/apis/apps/install
为例。该包下只有一个文件,名叫 install.go
,其中有一个 init()
方法,会在包导入时执行。
1 | // Package install installs the apps API group, making it available as |
这里可以这样简单理解(以 utilruntime.Must(apps.AddToScheme(scheme))
为例):
apps
这个gv
下所有的资源,通过apps
包中的AddToScheme
方法,都注册到了legacyscheme.Scheme
中,即前面初始化的Scheme
变量中。这样 k8s 就能识别该gv
下资源。
对于仅仅了解大致过程,上述的流程已经足够。但是如果点开 apps.AddToScheme(scheme)
方法,会有一种非常熟悉的感觉——在开发operator,通过k8s提供的脚本,来为 CRD 生成对应 yaml 时,会生成相应的代码。
那么,这一套模板到底是怎么将 Scheme 加载进 legacyscheme.Scheme
去的呢?
One more step forward
apps.AddToScheme()
方法在 pkg/apis/apps/register.go
文件中,它有 3 个变量加上一个方法,即表示这些资源(Kind
)同属于这个 GV
:
1 | var ( |
其中执行添加操作的方式是 scheme.AddKnownTypes()
,对每种资源,将会在 legacyscheme.Scheme
的 gvkToType
、typeToGVK
添加对应的值,如下:
1 | s.gvkToType[gvk] = t |
方法 addKnownTypes(scheme *runtime.Scheme)
是如何绕了一大圈才执行的?
变量
SchemeBuilder
的类型是[]func(*Scheme) error
,也就是说它本身是一个数组,元素类型为一个函数。即:1
2
3
4
5// SchemeBuilder collects functions that add things to a scheme. It's to allow
// code to compile without explicitly referencing generated types. You should
// declare one in each package that will have generated deep copy or conversion
// functions.
type SchemeBuilder []func(*Scheme) error变量
SchemeBuilder
通过runtime.NewSchemeBuilder(addKnownTypes)
初始化之后,它(数组)含有一个元素,即addKnownTypes()
方法。1
2
3
4
5
6
7
8
9
10
11
12
13// Register adds a scheme setup function to the list.
func (sb *SchemeBuilder) Register(funcs ...func(*Scheme) error) {
for _, f := range funcs {
*sb = append(*sb, f)
}
}
// NewSchemeBuilder calls Register for you.
func NewSchemeBuilder(funcs ...func(*Scheme) error) SchemeBuilder {
var sb SchemeBuilder
sb.Register(funcs...)
return sb
}当执行
utilruntime.Must(apps.AddToScheme(scheme))
时,会执行SchemeBuilder.AddToScheme
方法,这个方法中会依次取 SchemeBuilder 中的每个元素,然后执行该元素(方法),如下:1
2
3
4
5
6
7
8
9
10// AddToScheme applies all the stored functions to the scheme. A non-nil error
// indicates that one function failed and the attempt was abandoned.
func (sb *SchemeBuilder) AddToScheme(s *Scheme) error {
for _, f := range *sb {
if err := f(s); err != nil {
return err
}
}
return nil
}
为什么要这样设计 SchemeBuilder
- 核心思想是让执行加载 Scheme 的逻辑不用知道具体的类型是什么。
- 将
SchemeBuilder
设计成数组的形式,可以提高加载时的灵活性?虽然绝大多数场景下,SchemeBuilder
只有一个元素,但是有一些场景下会有两个以上的元素。 SchemeBuilder
的 3 个方法AddToScheme()
、Register()
、NewSchemeBuilder()
都是基于SchemeBuilder
是数组形式时的应有做法,分别对应执行元素(方法)、添加(数组append()
)、初始化(数组)。
Kubernetes 中 apiserver 加载 schema 流程