本文为 K8s API 和控制器 系列文章之一

sigs.k8s.io/apiserver-runtime 试图用 kubebuilder 构建控制器的理念提供一套快速构建 apiserver 框架,它做了如下事情

引入了 resource.Object interface resourcestrategy interfaces 等各种接口集合

  1. 尝试聚合存储层策略到 API 层:鼓励将 k8s.io/apiserver 各种 REST storage interfaces 直接放到 API structs 中实现
  2. 抛弃 Kubernetes 官方项目中较为复杂的 conversion 和 default funcs 代码生成,提供新协议 Defaulter interface 和 Converter interface,以及它们之上的 runtime.Scheme 注册实现
  3. 基于 k8s.io/apiserver RESTOptionsGetter interface 提供各种存储实现

有了以上三种抽象,sigs.k8s.io/apiserver-runtime 就支持一行代码创建 k8s apiserver

 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
package main

import (
	_ "k8s.io/client-go/plugin/pkg/client/auth" // register auth plugins
	"k8s.io/component-base/logs"
	"k8s.io/klog/v2"
	"sigs.k8s.io/apiserver-runtime/pkg/builder"
	"sigs.k8s.io/apiserver-runtime/sample/pkg/apis/sample/v1alpha1"
	"sigs.k8s.io/apiserver-runtime/sample/pkg/generated/openapi"
)

func main() {
	logs.InitLogs()
	defer logs.FlushLogs()

	err := builder.APIServer.
		WithOpenAPIDefinitions("sample", "v0.0.0", openapi.GetOpenAPIDefinitions).
		WithResource(&v1alpha1.Flunder{}). // namespaced resource
		WithResource(&v1alpha1.Fischer{}). // non-namespaced resource
		WithResource(&v1alpha1.Fortune{}). // resource with custom rest.Storage implementation
		WithLocalDebugExtension().
		Execute()
	if err != nil {
		klog.Fatal(err)
	}
}

框架内部会根据 API structs 所实现接口,自动完成 runtime.Scheme 注册,而且会照顾好 k8s.io/apiserver 各层

  1. 生成 APIGroupInfo struct,完成 HTTP API 层 install
  2. 完成 pkg/registry/generic/registry.Store 初始化,设置好存储层

一切看着很美好,但在使用时有各种各样的弊端。

首先是存储层策略到 API 层之后,有耦合过紧的问题

  • API package/module 必须依赖 sigs.k8s.io/apiserver-runtime 接口(显式/隐式均可)。如果使用者打算对外公开 API,那么会造成语义不清晰
  • 如果想在 apiserver 中引入其他项目的 Public API 呢?由于 Golang 只支持在 struct package 提供 interface 实现,而其他 API 不可能会实现框架接口。

x-kubernetes apiserver-using-runtime 只能通过 nested struct 方式,包装外部 API 实现 sigs.k8s.io/apiserver-runtime 接口们

var _ resource.Object = &Foo{}
var _ resource.MultiVersionObject = &Foo{}
var _ resource.ObjectList = &FooList{}

type Foo struct {
    hellov1.Foo
}

然后遇到了第二个问题,sigs.k8s.io/apiserver-runtime 社区不活跃,上次更新还是 2022 年底,只适配到 Kubernetes v1.26,且框架本身有一些 bug

  • 没有支持 shortname
  • 多版本 conversion, default 问题

所以变成需要这样就 bypass 框架

 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
53
54
55
56
57
58
59
60
61
62
63
64
func main() {
    defer logs.FlushLogs()

    logOpts := logs.NewOptions()

    err := builder.APIServer.
        WithAdditionalSchemeInstallers(func(s *runtime.Scheme) error {
            return hellov1.AddDefaultingFuncs(s)
        }).
        WithOpenAPIDefinitions("hello.zeng.dev-server", "v0.1.0", openapi.GetOpenAPIDefinitions).
        // customize backed storage (can be replace with any implemention instead of etcd
        // normally use WithResourceAndStorage is ok
        // we choose WithResourceAndHandler only because WithResourceAndStorage don't support shortNames
        WithResourceAndHandler(&resource.Foo{}, func(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (rest.Storage, error) {
            obj := &resource.Foo{}
            gvr := obj.GetGroupVersionResource()
            s := &restbuilder.DefaultStrategy{
                Object:         obj,
                ObjectTyper:    scheme,
                TableConvertor: rest.NewDefaultTableConvertor(gvr.GroupResource()),
            }
            store := &genericregistry.Store{
                NewFunc:                   obj.New,
                NewListFunc:               obj.NewList,
                PredicateFunc:             s.Match,
                DefaultQualifiedResource:  gvr.GroupResource(),
                CreateStrategy:            s,
                UpdateStrategy:            s,
                DeleteStrategy:            s,
                StorageVersioner:          gvr.GroupVersion(),
                SingularQualifiedResource: (resource.Foo{}).GetSingularQualifiedResource(),
                TableConvertor:            (resource.Foo{}),
            }

            options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: func(obj runtime.Object) (labels.Set, fields.Set, error) {
                accessor, ok := obj.(metav1.ObjectMetaAccessor)
                if !ok {
                    return nil, nil, fmt.Errorf("given object of type %T does implements metav1.ObjectMetaAccessor", obj)
                }
                om := accessor.GetObjectMeta()
                return om.GetLabels(), fields.Set{
                    "metadata.name":      om.GetName(),
                    "metadata.namespace": om.GetNamespace(),
                }, nil
            }}

            if err := store.CompleteWithOptions(options); err != nil {
                return nil, err
            }
            return &fooStorage{store}, nil
        }).
        WithOptionsFns(func(so *builder.ServerOptions) *builder.ServerOptions {
            // do log opts trick
            logs.InitLogs()
            logsapi.ValidateAndApply(logOpts, utilfeature.DefaultFeatureGate)
            return so
        }).
        WithFlagFns(func(ss *pflag.FlagSet) *pflag.FlagSet {
            logsapi.AddFlags(logOpts, ss)
            return ss
        }).
        Execute()
        ...
}

所以显而易见的问题来了:sigs.k8s.io/apiserver-runtime 本身也是基于 k8s.io/apiserver 提供增强。当项目需要灵活定制策略时,就不可避免需要直接使用底层库。结果是,开发者除了要熟悉 k8s.io/apiserver,还需要再学一套框架。

那为什么不从一开始直接使用 k8s.io/apiserver 呢?

而且随着 k8s.io/apiserver 提供的功能越来越多,框架更新便必然滞后。这也会增加维护成本。

使用框架时需要格外警惕,除非你确定项目的生命周期不长,只打算做一锤子买卖。 欢迎 clone x-kubernetes apiserver-using-runtime 代码查看崩溃过程。

最后 sigs.k8s.io/apiserver-runtime 的一些设计思路,比如降低 default conversion 复杂度,比如简化策略配置,非常适合实现为辅助 library,而非框架。