本文发布已超过一年。较旧的文章可能包含过时内容。请检查页面信息自发布以来是否已失效。

面向所有人的 Kubernetes 端到端测试

越来越多曾经作为 Kubernetes 一部分的组件现在正在 Kubernetes 外部开发。例如,存储驱动过去编译到 Kubernetes 二进制文件中,后来转移到主机上的独立的 FlexVolume 二进制文件中,现在则作为容器存储接口 (CSI) 驱动交付,部署在 Kubernetes 集群本身的 Pod 中。

这给开发此类组件的开发者带来了挑战:如何对这些外部组件在 Kubernetes 集群上进行端到端 (E2E) 测试?用于测试 Kubernetes 本身的 E2E 框架具备所有必要功能。然而,尝试在 Kubernetes 外部使用它很困难,只有通过仔细选择大量依赖项的正确版本才可能实现。E2E 测试在 Kubernetes 1.13 中变得更加简单。

这篇博文总结了 Kubernetes 1.13 中所做的更改。对于 CSI 驱动开发者,它将涵盖一项正在进行中的工作,即使存储测试也适用于测试第三方 CSI 驱动。如何使用它们将基于两个 Intel CSI 驱动进行展示:

测试这些驱动是推动大多数这些增强功能的主要动力。

E2E 概述

E2E 测试包含几个阶段:

  • 实现测试套件。这是本博文的主要焦点。Kubernetes E2E 框架是用 Go 语言编写的。它依赖 Ginkgo 来管理测试,依赖 Gomega 来进行断言。这些工具支持“行为驱动开发 (behavior driven development)”,它在“spec”中描述预期行为。在本博文中,“测试”用来指代单个 Ginkgo.It spec。测试使用 client-go 与 Kubernetes 集群交互。
  • 启动测试集群。像 kubetest 这样的工具可以在此提供帮助。
  • 针对该集群运行 E2E 测试套件。Ginkgo 测试套件可以使用 ginkgo 工具运行,也可以作为普通的 Go 测试使用 go test 运行。不带任何参数时,Kubernetes E2E 测试套件将根据 KUBECONFIG 等环境变量连接到默认集群,完全就像 kubectl 一样。Kubetest 也知道如何运行 Kubernetes E2E 套件。

Kubernetes 1.13 中的 E2E 框架增强功能

所有以下增强功能遵循相同基本模式:它们使得 E2E 框架在 Kubernetes 外部更加有用且易于使用,同时不改变原始 Kubernetes e2e.test 二进制文件的行为。

拆分 Provider 支持

在 Kubernetes <= 1.12 版本中使用 E2E 框架困难的主要原因是依赖于 provider 特定 SDK,这引入了大量软件包。仅仅是编译它就很不容易。

其中许多软件包仅用于某些测试。例如,测试预置卷的挂载必须首先像管理员那样预置此类卷,通过某种非 Kubernetes API 直接与特定的存储后端通信。

有一项工作致力于从核心 Kubernetes 中移除云 provider 特定测试PR #68483 中采取的方法可以看作是实现该目标的一个渐进步骤:而不是立即移除代码并破坏所有依赖它的测试,所有云 provider 特定代码都被移到了 test/e2e/framework/providers 下的可选软件包中。E2E 框架然后通过一个接口访问它,该接口由每个 vendor 软件包独立实现。

E2E 测试套件的作者决定将哪些软件包导入到测试套件中。vendor 支持随后通过 --provider 命令行标志激活。1.13 和 1.14 中的 Kubernetes e2e.test 二进制文件仍然包含与 1.12 相同的 provider 支持。也可以不包含任何软件包,这意味着只有通用 provider 可用:

  • “skeleton”:集群通过 Kubernetes API 访问,仅此而已
  • “local”:类似于“skeleton”,但此外 kubernetes/kubernetes/cluster 中的脚本可以在测试套件运行后通过 ssh 检索日志

外部文件

测试可能需要在运行时读取额外文件,例如 .yaml manifest。但是 Kubernetes e2e.test 二进制文件应该可直接使用且完全独立,因为这样可以简化分发和运行。Kubernetes 构建系统中的解决方案是使用 go-bindatatest/e2e/testing-manifests 下的所有文件链接到二进制文件中。E2E 框架过去对 go-bindata 的输出有硬依赖,现在bindata 支持是可选的。通过 testfiles 包访问文件时,文件将从不同源检索:

  • 相对于通过 --repo-root 参数指定的目录
  • 零个或多个 bindata chunk

测试参数

e2e.test 二进制文件接受控制测试执行的附加参数。2016 年,一项工作开始尝试用 Viper 配置文件替换所有 E2E 命令行参数。但该工作停滞了,这使得开发者对于如何处理测试特定参数没有明确指导。

v1.12 中的方法是将所有标志添加到中央 test/e2e/framework/test_context.go 文件中,这对于独立于框架开发的测试不起作用。自 PR #69105 以来,建议是使用标准的 flag 包来定义其参数,在其自己的源代码中。标志名称必须是分层的,使用点分隔不同级别,例如 my.test.parameter,并且必须是唯一的。flag 包强制执行唯一性,当第二次注册同一标志时会 panic。新的 config 包简化了多个选项的定义,这些选项存储在一个单一的 struct 中。

总结一下,现在参数是这样处理的:

  • 测试包中的 init 代码定义了测试和参数。实际参数值尚不可用,因此测试定义无法使用它们。
  • 测试套件的 init 代码解析参数和(可选)配置文件。
  • 测试运行,现在可以使用参数值。

然而,最近有人指出,理想的做法是将测试设置不暴露为命令行标志,而只通过配置文件设置,这是可能且期望的。关于这一点有一个未解决的 bug 和一个待处理的 PR

Viper 支持已得到增强。就像 provider 支持一样,它是完全可选的。通过导入 viperconfig 包并在解析普通命令行标志后调用它,它被引入到 e2e.test 二进制文件中。这样实现是为了确保所有可以通过命令行标志设置的变量,在标志出现在 Viper 配置文件中时也能被设置。例如,Kubernetes v1.13 的 e2e.test 二进制文件接受 --viper-config=/tmp/my-config.yaml 参数,当该文件内容如下时,会设置 my.test.parameter 的值为 value

在旧版本的 Kubernetes 中,该选项只能从当前目录加载文件,必须省略后缀,并且只有少数参数能通过这种方式设置。注意,Viper 仍然存在一个限制:它通过将配置文件条目与已知标志匹配来工作,不会警告未知配置文件条目,因此会遗漏拼写错误。Kubernetes 的一个更好的配置文件解析器仍在进行中。

从 .yaml manifest 创建条目

在 Kubernetes 1.12 中,对从 .yaml 文件加载单个条目有一些支持,但创建该条目需要手动编写代码完成。现在框架有了新方法,可以加载包含多个条目的 .yaml 文件,修补这些条目(例如,设置当前测试创建的命名空间),并创建它们。这目前被用于针对每个测试从与通过 kubectl 部署所用的完全相同的 .yaml 文件重新部署 CSI 驱动。如果 CSI 驱动支持在不同名称下运行,那么测试是完全独立的,可以并行运行。

然而,重新部署驱动会减慢测试执行速度,并且它不涵盖针对驱动的并发操作。更真实的测试场景是在启动测试集群时部署一次驱动,然后针对该部署运行所有测试。最终 Kubernetes E2E 测试将转向这种模式,一旦明确了如何扩展测试集群的启动过程使其也能包含安装 CSI 驱动等附加实体。

Kubernetes 1.14 中即将推出的增强功能

重用存储测试

能够在 Kubernetes 外部使用该框架使得构建自定义测试套件成为可能。但是一个没有测试的测试套件仍然无用。现有的一些测试,特别是存储相关的测试,也可以应用于 out-of-tree 组件。感谢 Masaki Kimura 所做的工作,Kubernetes 1.13 中的存储测试定义为可以针对不同的驱动多次实例化。

但历史往往会重演。与 providers 一样,定义这些测试的软件包也引入了所有 in-tree 存储后端的驱动定义,这反过来引入了比所需更多的额外软件包。这个问题已在即将发布的 Kubernetes 1.14 中得到解决

跳过不受支持的测试

一些存储测试依赖于集群的特定功能(例如在支持 XFS 的主机上运行)或驱动的功能(例如支持块卷)。这些条件在测试运行时进行检查,当条件不满足时,导致测试被跳过。好处是它记录了测试未运行的原因解释。

启动测试很慢,特别是当它必须首先部署 CSI 驱动程序时,但在其他场景下也是如此。在快速集群上,为测试创建命名空间测量需要 5 秒,并且会产生大量嘈杂的测试输出。本来可以通过跳过不支持的测试定义来解决这个问题,但这使得报告为什么某个测试甚至不是测试套件的一部分变得棘手。这种方法已被放弃,取而代之的是重新组织存储测试套件,使其首先检查条件,然后再执行更昂贵的测试设置步骤。

更易读的测试定义

同一个 PR 还重写了测试,使其像传统的 Ginkgo 测试一样运行,测试用例及其局部变量都在一个函数中

测试外部驱动程序

构建自定义的 E2E 测试套件仍然需要相当多的工作。将在Kubernetes 1.14 测试存档中分发的 e2e.test 二进制文件将具备测试已安装的存储驱动程序的能力,而无需重建测试套件。有关更多说明,请参阅这份README 文件

E2E 测试套件 HOWTO

测试套件初始化

第一步是设置定义测试套件所需的样板代码。在 Kubernetes E2E 中,这在 e2e.goe2e_test.go 文件中完成。也可以在单个 e2e_test.go 文件中完成。Kubernetes 在 e2e_test.go 中导入了各种提供程序、树内测试、Viper 配置支持以及 bindata 文件查找。e2e.go 控制实际执行,包括一些集群准备和指标收集。

一个更简单的起点是PMEM-CSI中的 e2e_[test].go 文件。它不使用任何提供程序、Viper、bindata,只导入存储测试。

与 PMEM-CSI 一样,OIM 也放弃了所有额外的功能,但由于它将自定义集群启动直接集成到了测试套件中,所以稍微复杂一些。在这种情况下,这样做很有用,因为某些附加组件必须在主机侧运行。将它们直接在 E2E 二进制文件中运行,使得使用 dlv 进行交互式调试变得更容易。

这两个 CSI 驱动程序都遵循 Kubernetes 的例子,使用 test/e2e 目录存放其测试套件,但任何其他目录和文件名也可以。

添加 E2E 存储测试

测试由导入到测试套件中的包定义。E2E 测试唯一特殊的地方在于它们使用 framework.NewDefaultFramework 实例化了一个 framework.Framework 指针(通常称为 f)。这个变量在每次测试的 BeforeEach 中都会重新初始化,并在 AfterEach 中释放。它在运行时(且仅在运行时!)具有 f.ClientSetf.Namespace,可供测试使用。

PMEM-CSI 存储测试导入了 Kubernetes 存储测试套件,并为必须已安装在测试集群中的 PMEM-CSI 驱动程序设置了一个 Provisioning 测试实例。存储测试套件会更改 StorageClass 以运行具有不同文件系统类型的测试。因此,StorageClass 是从 .yaml 文件创建的。

解释框架中可用的各种实用方法超出了本文的范围。阅读现有测试和框架的源代码是入门的好方法。

Vendoring

Vendoring Kubernetes 代码仍然不简单,即使消除了许多不必要的依赖项。k8s.io/kubernetes 不打算包含在其他项目中,并且其依赖项的定义方式不被 dep 等工具理解。其他 k8s.io 包旨在包含,但尚未遵循语义版本控制或未标记任何发布版本(k8s.io/kube-openapi, k8s.io/utils)。

PMEM-CSI 使用dep。其Gopkg.toml 文件是一个很好的起点。它启用了裁剪(dep 默认不启用)并将某些项目锁定到与所使用的 Kubernetes 版本兼容的版本。当 dep 没有选择兼容版本时,检查 Kubernetes 的编译并运行测试套件

go test ./test/e2e -args -help 是测试测试套件是否编译的最快方法。

一旦编译成功并且集群已经设置好,命令 go test -timeout=0 -v ./test/e2e -ginkgo.v 将运行所有测试。为了并行运行测试,请改用 ginkgo -p ./test/e2e 命令。

参与其中

Kubernetes E2E 框架由 SIG-testing 中的 testing-commons 子项目负责。请参阅该页面获取联系信息。

有各种任务可以开展工作,包括但不限于:

  • 将 test/e2e/framework 移至暂存仓库并重构其结构使其更模块化(#74352)。
  • 通过将更多代码移至 test/e2e/framework 来简化 e2e.go#74353)。
  • 从 Kubernetes E2E 测试套件中移除特定于提供程序的代码(#70194)。

特别感谢本文的审阅者: