Xinzhao's Blog

controller-runtime 介绍

GoKubernetescontroller-runtime

2020-08-20

概述

controller-runtimeKubebuilder 的子项目,提供了一系列用于构建 controller 的库;Kubebuilder 本身也是生成了大量的使用 controller-runtime 的模板代码。controller-runtime 中的几个基本概念:

使用

下面以官方的一个简单例子来分步介绍怎么使用 controller-runtime 构建一个 controller:首先定义 Reconciler,其中包含 controller 的主要逻辑,然后使用 Builder 生成 Controller 并加入到 Manager 中,最后启动 Manager

注:以 v0.5.0 版本为例,最新版 Reconcile 函数定义有变化

每一步的详细介绍如下:

Reconciler

介绍

Reconciler 的定义如下,仅包含一个 Reconcile 函数:

type Reconciler interface {
// Reconciler performs a full reconciliation for the object referred to by the Request.
// The Controller will requeue the Request to be processed again if an error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
Reconcile(Request) (Result, error)
}

RequestResult 定义如下:

// Request contains the information necessary to reconcile a Kubernetes object.  This includes the
// information to uniquely identify the object - its Name and Namespace. It does NOT contain information about
// any specific Event or the object contents itself.
type Request struct {
// NamespacedName is the name and namespace of the object to reconcile.
types.NamespacedName
}

// Result contains the result of a Reconciler invocation.
type Result struct {
// Requeue tells the Controller to requeue the reconcile key. Defaults to false.
Requeue bool

// RequeueAfter if greater than 0, tells the Controller to requeue the reconcile key after the Duration.
// Implies that Requeue is true, there is no need to set Requeue to true at the same time as RequeueAfter.
RequeueAfter time.Duration
}

Request 包含本次 reconcile object 的 namespace 和 name,object 类型是生成 controller 时配置的,一个 Reconciler 仅能处理一种类型的 object;Result 基本不用关心,如果 Reconcile 返回 error 会自动 requeue。

使用示例

定义一个 ReplicaSetReconciler,其中包含一个由 controller-runtime 提供的一个 generic client,功能同普通的 kubernetes client,能获取到集群的所有资源:

// ReplicaSetReconciler is a simple ControllerManagedBy example implementation.
type ReplicaSetReconciler struct {
client.Client
}

不过和普通 kubernetes client 不同的是,这个通用 client 是一个 client 就能 CRUD 所有类型的资源,非常方便和易于使用。

然后是实现业务逻辑:

// Implement the business logic:
// This function will be called when there is a change to a ReplicaSet or a Pod with an OwnerReference
// to a ReplicaSet.
//
// * Read the ReplicaSet
// * Read the Pods
// * Set a Label on the ReplicaSet with the Pod count
func (a *ReplicaSetReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
// Read the ReplicaSet
rs := &appsv1.ReplicaSet{}
err := a.Get(context.TODO(), req.NamespacedName, rs)
if err != nil {
return reconcile.Result{}, err
}

// List the Pods matching the PodTemplate Labels
pods := &corev1.PodList{}
err = a.List(context.TODO(), client.InNamespace(req.Namespace).MatchingLabels(rs.Spec.Template.Labels), pods)
if err != nil {
return reconcile.Result{}, err
}

// Update the ReplicaSet
rs.Labels["pod-count"] = fmt.Sprintf("%v", len(pods.Items))
err = a.Update(context.TODO(), rs)
if err != nil {
return reconcile.Result{}, err
}

return reconcile.Result{}, nil
}

InjectClient 将 manager 真实的 client 赋给 ReplicaSetReconciler

func (a *ReplicaSetReconciler) InjectClient(c client.Client) error {
a.Client = c
return nil
}

Builder 和 Manager

Builder 用来为 Reconciler 生成 ControllerManager 用来管理和启动 Controller,直接用例子来介绍,首先生成一个 Manager

mgr, err := manager.New(config, manager.Options{})
if err != nil {
log.Error(err, "could not create manager")
os.Exit(1)
}

其中 config 为 client-go 的 rest.Config

ReplicaSetReconciler 生成 Controller 并加入到 Manager 中:

_, err = builder.
ControllerManagedBy(mgr). // Create the ControllerManagedBy
For(&appsv1.ReplicaSet{}). // ReplicaSet is the Application API
Owns(&corev1.Pod{}). // ReplicaSet owns Pods created by it
Build(&ReplicaSetReconciler{})
if err != nil {
log.Error(err, "could not create controller")
os.Exit(1)
}

其中 For 函数用于指定我们要 reconcile 的 object 类型,Owns 用来 watch 其 owner 是 reconcile object 类型的 object(Owns 可以指定多种类型),这两种类型 object 的增/删/改事件均会触发 Reconcile 函数。

最后启动 Manager,整个组件启动:

if err := mgr.Start(stopCh); err != nil {
log.Error(err, "could not start manager")
os.Exit(1)
}

单元测试

为普通的 controller 写单元测试主要还是依赖 client-go 提供的 fake client,将测试需要的各种 object append 到一个 fake client 中,使用这个 fake client 来完成测试,有用到 lister 的话需要手动往对应的 informer indexer 中添加相应的 object,示例:

// 创建 fake client
f.client = fake.NewSimpleClientset(f.objects...)

// 创建基于 fake client 的 informer
informer := informers.NewSharedInformerFactory(f.client, 1)

// 往 informer indexer 中添加 object
for _, s := range f.storageClasses {
informer.Native().Storage().V1().StorageClasses().Informer().GetIndexer().Add(s)
}

controller-runtime 是使用了自己的 envtest 包在本地启动一个真正的 apiserver 和 etcd,然后连接这个 apiserver 进行测试,我们还是需要 fake objects,不过和 fake client 不同,这些 fake objects 是要创建到 controller-runtime 启动的 apiserver 中,如果是 CR 的话,还需要先往这个新的 apiserver 中注册 CRD。一个完整的示例:

func TestNodeLocalStorageReconciler(t *testing.T) {
// 配置 testEnv,其中包含该项单测需要的 CRD 等
testEnv := &envtest.Environment{
CRDs: []runtime.Object{
newNodeLocalStorageCRD(),
},
}

// 启动测试环境(etcd 和 apiserver),返回该环境的 rest config
config, err := testEnv.Start()
if err != nil {
t.Fatal(err)
}
defer testEnv.Stop()

// 生成 controller-runtime 需要的 client
c, err := client.New(config, client.Options{
Scheme: scheme,
})
if err != nil {
t.Fatal(err)
}

// 初始化 Reconciler
nlsReconciler := &NodeLocalStorageReconciler{
Client: c,
}

// 准备好测试数据
nls1 := test.GetNodeLocalStorage()
nls1.Name = "test"
if err = c.Create(context.TODO(), nls1); err != nil {
t.Fatal(err)
}

// test cases
testCases := []struct {
describe string
namespace string
name string
isErr bool
}{
{
describe: "normal test",
namespace: "",
name: "test",
isErr: false,
},
}

// 测试每个 case
for _, testCase := range testCases {
_, err := nlsReconciler.Reconcile(reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: testCase.namespace,
Name: testCase.name,
},
})
if testCase.isErr != (err != nil) {
t.Fatalf("%s unexpected error: %v", testCase.describe, err)
}
}
}

总结

controller-runtime 框架本身提供了很多库来帮助构建 controller,让整个流程变得简单,屏蔽了很多通用的细节,能够让构建 controller 整个过程变得更简单,感兴趣的同学可以在不同的场景和需求下都尝试一下;即便不使用 controller-runtime 我们也可以单独使用它的 generic clientenvtest 等通用库。

单独说下使用 envtest 需要运行环境(你本地和 CI 等环境)安装 Kubebuilder 提供的一系列 bin 文件(主要是 etcdkube-apiserver),本地的话自己下载就好了,如果是 CI 环境需要的话,可以在基础镜像里面增加这些文件,一个示例:

RUN mkdir -p /usr/local && \
wget https://go.kubebuilder.io/dl/2.3.1/linux/amd64 && \
tar xvf amd64 && \
mv kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder && \
rm amd64

Powered by ☕️, 🍟 and 🍦