Kubernetes Operator 测试面面观

栏目: 服务器 · 发布时间: 6年前

软件测试是一门工程技术,更是一门艺术。维护良好,质量过硬的测试用例能够大大提高软件开发时的幸福感。这篇文章通过对目前一些比较成熟的 Kubernetes Operator 的测试方案与方法的介绍,分析目前对 Kubernetes Operator 进行测试的一些最佳实践。

单元测试

首先,让我们先把镜头对准单元测试。单元测试又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作,这一测试是软件质量的第一道保障。

Kubernetes 的做法

tf-operator 中,我们采取了跟 Kubernetes 内置的 controller 类似的测试方案(例子可见 job_controller_test.go)。

如下面的代码所示,我们通过 Fake KubeConfig,创建了 Fake Clienset 和 Informer。然后利用 Indexer 将测试的数据注入到 Informer 的 Cache 中,同时把 Informer 的 Sync 状态置为 AlwaysReady。最后,我们还需要使用 Fake PodControl 和 ServiceControl。这一操作使得我们不会真正地在 Kubernetes 中创建对象,而是只单纯地进行一个记录。

config := &rest.Config{
    Host: "",
    ContentConfig: rest.ContentConfig{
        GroupVersion: &tfv1.SchemeGroupVersion,
    },
}
option := options.ServerOption{}
tfJobClientSet := tfjobclientset.NewForConfigOrDie(config)
ctr, kubeInformerFactory, _ := newTFController(config, kubeClientSet, kubeBatchClientSet, tfJobClientSet, controller.NoResyncPeriodFunc, option)
ctr.tfJobInformerSynced = testutil.AlwaysReady
ctr.PodInformerSynced = testutil.AlwaysReady
ctr.ServiceInformerSynced = testutil.AlwaysReady
tfJobIndexer := ctr.tfJobInformer.GetIndexer()
fakePodControl := ctr.PodControl.(*controller.FakePodControl)
fakeServiceControl := ctr.ServiceControl.(*control.FakeServiceControl)

// Inject the test data into informer cache.
podIndexer := kubeInformerFactory.Core().V1().Pods().Informer().GetIndexer()
testutil.SetPodsStatuses(podIndexer, tfJob, testutil.LabelWorker, tc.pendingWorkerPods, tc.activeWorkerPods, tc.succeededWorkerPods, tc.failedWorkerPods, nil, t)
testutil.SetPodsStatuses(podIndexer, tfJob, testutil.LabelPS, tc.pendingPSPods, tc.activePSPods, tc.succeededPSPods, tc.failedPSPods, nil, t)

serviceIndexer := kubeInformerFactory.Core().V1().Services().Informer().GetIndexer()
testutil.SetServices(serviceIndexer, tfJob, testutil.LabelWorker, tc.activeWorkerServices, t)
testutil.SetServices(serviceIndexer, tfJob, testutil.LabelPS, tc.activePSServices, t)

随后,我们将状态更新的函数也 Fake 掉,将其赋值到内存的一个对象中,以便在后续的测试中进行状态的比对。通过手动地调用 SyncTFJob 的方式,利用之前我们自己手动构造的 Cache 进行状态的 Sync。最后,就是根据构造的对象和实际更新后的对象的比对,来判断 Operator 在 Sync 的过程中是否达到了期望的状态。

var actual *tfv1.TFJob
ctr.updateStatusHandler = func(tfJob *tfv1.TFJob) error {
    actual = tfJob
    return nil
}

// Run the test logic.
tfJob := testutil.NewTFJob(tc.worker, tc.ps)
unstructured, err := testutil.ConvertTFJobToUnstructured(tfJob)
if err != nil {
    t.Errorf("Failed to convert the TFJob to Unstructured: %v", err)
}

if err := tfJobIndexer.Add(unstructured); err != nil {
    t.Errorf("Failed to add tfjob to tfJobIndexer: %v", err)
}

forget, err := ctr.syncTFJob(testutil.GetKey(tfJob, t))
// We need requeue syncJob task if podController error
if tc.ControllerError != nil {
    if err == nil {
        t.Errorf("%s: Syncing jobs would return error when podController exception", name)
    }
}
// ...
if int32(len(fakePodControl.Templates)) != tc.expectedPodCreations {
    t.Errorf("%s: unexpected number of pod creates.  Expected %d, saw %d\n", name, tc.expectedPodCreations, len(fakePodControl.Templates))
}

这一方法利用了 Kubernetes 的一些机制,绕过了对 Kubernetes API Server 和其他组件的依赖,直接利用 Informer 针对 Operator 的代码逻辑进行测试,可以测试单次的 Sync 过程中,Operator 是否如期望一般工作。

etcd-operator 的做法

etcd-operator 据我所知,是问世最久的 Operator,而 Operator 这一模式也是由 CoreOS 提出的。因此了解 etcd-operator 能起到以史为镜的作用。目前 etcd-operator 在实现中仍然没有像目前主流的 Operator 一样采用 Work Queue 的方式来避免阻塞的问题,而是在 Informer 中直接去进行处理的。而它的处理的逻辑被统一写成了

type Event struct {
	Type   kwatch.EventType
	Object *api.EtcdCluster
}

// handleClusterEvent returns true if cluster is ignored (not managed) by this instance.
func (c *Controller) handleClusterEvent(event *Event) (bool, error) {
	// ...
	switch event.Type {
	case kwatch.Added:
        // Deal with add logic
        // ...
	case kwatch.Modified:
		// Deal with modify logic
        // ...
	case kwatch.Deleted:
		// Deal with delete logic
        // ...
	}
	return false, nil
}

因此,在做单元测试的时候就相对容易些,可以直接构造 Event 对象,来进行测试。

Kubebuilder-generated Operator 的做法

Kubebuilder 的测试很难说是不是传统意义上的单元测试,它背后依赖的是 "sigs.k8s.io/controller-runtime/pkg/envtest".Environment。在运行时,它会启动一个真正的 API Server 和 etcd,随后把 CRD 注册到 Scheme 中并且把 Operator 运行起来。但值得注意的是,它不会启动 Controller Manager,这也意味着来自 API Server 的关于 Kubernetes 资源的事件不会真正被处理。

它与其他的做法有一些比较大的不同。首先,它会需要运行一个真正的 API Server 和 etcd,用来做对象存储。这就意味着在测试时可以使用真正的 Clientset 对 API Server 进行各种请求。

func TestMain(m *testing.M) {
	t := &envtest.Environment{
		ControlPlaneStartTimeout: controlPlaneTimeout,
		ControlPlaneStopTimeout:  controlPlaneTimeout,
		CRDDirectoryPaths:        []string{filepath.Join("..", "..", "..", "config", "crds")},
	}
	apis.AddToScheme(scheme.Scheme)

	var err error
	if cfg, err = t.Start(); err != nil {
		stdlog.Fatal(err)
	}

	code := m.Run()
	t.Stop()
	os.Exit(code)
}

其次,它会运行一个真正的 Operator,而不只是通过手动调用 Sync 过程的方式进行测试。如下代码所示,inner 这一对象就是真正的 Operator 的逻辑,而在这一函数中对其进行了再次的封装。利用了一个没有缓冲的 Channel 对其进行了执行的控制。根据 Golang 的内存模型,不带缓冲的 Channel 的接收操作 happens-before 相应 Channel 的发送操作完成。利用这一特性,在同一个测试用例中,测试的对象可以被多次的 Sync,每次 Sync 的状态都可以被检查。如果需要检查其中的 reconcile.Result 的值,如 Requeue 等,也可以改动这部分的逻辑来扩展。

// SetupTestReconcile returns a reconcile.Reconcile implementation that delegates to inner and
// writes the request to requests after Reconcile is finished.
func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) {
	requests := make(chan reconcile.Request)
	fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) {
		result, err := inner.Reconcile(req)
		requests <- req
		return result, err
	})
	return fn, requests
}

这样做的优势是可以在单个测试中多次 Sync,并且依赖真实的 API Server,可以直接简单地利用 Clientset 进行操作。而劣势也是依赖真实的 API Server,而有些 CI 系统对多进程支持不好,真正在持续集成环境下运行时会有各种各样的问题。

端到端测试

端到端测试是利用真实的外部组件,将系统当做黑盒,站在终端用户的角度进行的测试。这里的“真实的组件”指的就是 Kubernetes 还有一些外部依赖。相比于单元测试,端到端测试需要依赖一个真实的 Kubernetes 集群,同时由于其黑盒属性,就有了更多不同的选型。

Kubernetes 的做法

首先介绍下 Kubernetes 自身的 Controller 的 e2e 测试是如何做的。

Kubernetes 的端到端测试依赖一个关键的抽象,也就是 Framework。Framework 会负责把需要的 Client 创建好。同时,Framework 会为测试创建一个 Namespace,当前的测试用例会在这个 Namespace 下运行,这样从设计上就避免了并行测试可能引起的冲突。是一个非常有价值的特性。这也使得 Kubernetes 的测试用例可以并行地去运行。

其实 Kubernetes 的端到端测试有许多值得学习的地方,包括其整体的原则和哲学,到设计与实现。限于篇幅原因,这里不过多介绍了。

Operator-SDK generated Operator 的做法

Operator-SDK 的做法和 etcd-operator 的做法类似,也跟 Kubernetes 的做法有些异曲同工之妙,相当于是基于 Kubernetes 社区的实现做了一个新的抽象和改写。首先它需要设置 Main Entry:

func TestMain(m *testing.M) {
    f.MainEntry(m)
}

这一函数会根据传入的诸如 Kubeconfig,ProjectRoot 等参数,创建出 CRD 和 Operator。Operator 可以运行在集群外,也可以以 Pod 的方式运行在集群内。

随后,集群就可以进行测试了。可以完全把它当做黑盒,把 Clientset 当做 kubectl,将端到端的黑盒测试自动化。不过这里值得一提的是,这一方式不能通过 go test 直接进行测试。因为在 MainEntry 中,会依赖一些参数,而这些参数会涉及一些预处理的逻辑。举个例子,Operator-SDK 会把 Operator 所有的 YAML 文件合并成一个 YAML,然后再把这一临时的 YAML 文件的路径作为参数传递给后续的命令。

因此在 Operator-SDK 中是利用 operator-sdk test local 这一命令先进行预处理,然后再把处理好的参数传递给 go test 命令的。这一方式对用户并不是那么友好,必须依赖 operator-sdk 才能发起测试。但它默认支持一个用例一个 Namespace,与 Kubernetes 测试时的行为类似。

Kubebuilder-generated Operator 的做法

Kubebuilder 生成的测试,因为原本就依赖一个真实的 API Server 和 etcd,所以其实只要再创建出其他 Kubernetes 的组件,就可以了。但如果已经有在运行的 Kubernetes 集群,则可以利用 UseExistingCluster 来通过已经在运行的集群进行测试。

这样的方式相比于之前,可以真正地检查所有资源的状态。而在之前只有 API Server 的运行中,是做不到对状态的检查的,因为事件不会被 Kubernetes Controller Manager 处理,因此状态更新无法进行。

tf-operator 的做法

因为端到端测试是黑盒测试,只要能够利用 Kubernetes 的 API 进行请求就可以完成,因此 tf-operator 的端到端测试是用 Python 实现的。其实不只是端到端测试,tf-operator 的构建,也不是用 Make 或者 Bazel 做的,也是用 Python 实现的,他们都是历史遗留问题。不过这也证明了,只要能够解决集群部署和 CRD 以及 Operator 的安装,用什么语言都可以做 Operator 的端到端测试。

总结

目前 Operator 的单元测试存在多种不同的实现方案。Kubernetes 和 tf-operator 采取的方式能够细粒度地构造测试用例,同时检查 sync 的过程可否满足期望。etcd-operator 的方式为单元测试提供了一种新的思路,通过对 Operator 更高层次的抽象,针对高层次的抽象进行单元测试,可以避免手动利用 Indexer 构造测试场景的体力劳动。kubebuilder 生成的 Operator 实现的测试,并不是传统意义上的单元测试。它利用了真实的 API Server,在测试时可以利用 Client 获取真实的资源。但由于没有 Controller 的支持,所以对于不少需要依赖一些 Kubernetes 自身资源的状态来更新自己的状态的 CRD 而言,没办法进行状态的检查。这一问题在前面的方法中就不存在。

而对于端到端测试而言,基本所有的方案都是利用 Client 来对已经创建好的集群进行端到端的黑盒测试。而它们的区别主要体现在运行集群的方式。Kubernetes 和 Operator-SDK 的做法是利用 Framework 这一抽象,来部署集群环境。而 Operator-SDK 由于需要部署 CRD 和 Operator,因此基于 Kubernetes 原本的理念做了一些修改,支持从本地或者利用 Deployment 的方式部署 Operator 以便测试。而 kubebuilde 则是与其提供的单元测试采取了相似的方法,利用了 controller-runtime 提供的抽象和能力,在运行时注册 CRD,在测试代码中运行 Operator 的逻辑,依赖已经部署好的标准的 Kubernetes 集群进行端到端测试。但它默认情况下,没有每个测试使用一个 Namespace 的支持,需要用户自行实现这样的逻辑。

不同的测试选型适合于不同的 Operator,在测试时,可以根据 Operator 的特点来确定具体的测试方案。目前来看,并没有一个 One for all 的方案。

License

  • This article is licensed under CC BY-NC-SA 3.0.
  • Please contact me for commercial use.

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Uberland

Uberland

Alex Rosenblat / University of California Press / 2018-11-19 / GBP 21.00

Silicon Valley technology is transforming the way we work, and Uber is leading the charge. An American startup that promised to deliver entrepreneurship for the masses through its technology, Uber ins......一起来看看 《Uberland》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试