配置 envtest 以进行集成测试

controller-runtime/pkg/envtest Go 库帮助您通过设置和启动 etcd 和 Kubernetes API 服务器的实例来编写控制器的集成测试,而无需 kubelet、controller-manager 或其他组件。

安装

安装二进制文件的过程非常简单,只需运行 make envtest。默认情况下,envtest 会将 Kubernetes API 服务器的二进制文件下载到您项目中的 bin/ 文件夹。make test 是一个一条龙的解决方案,用于下载二进制文件、设置测试环境和运行测试。

您可以参考 Kubebuilder 脚手架的 Makefile,观察到 envtest 的设置在所有 controller-runtime 版本中始终保持一致。从 release-0.19 开始,它被配置为自动从正确的位置下载工件,确保 kubebuilder 用户不受影响。

在隔离/断网环境中的安装

如果您想下载包含二进制文件的 tarball,以便在不联网的环境中使用,可以使用 setup-envtest 来本地下载所需的二进制文件。有很多方法可以配置 setup-envtest 以避免连接互联网,您可以在 这里 阅读相关内容。以下示例将展示如何使用 setup-envtest 设置的大多数默认值来安装 Kubernetes API 二进制文件。

下载二进制文件

make envtest 将会把 setup-envtest 二进制文件下载到 ./bin/ 目录。

make envtest

使用 setup-envtest 安装二进制文件时,会将其存储在特定于操作系统的位置,您可以在 这里 阅读更多相关信息。

./bin/setup-envtest use 1.31.0

更新测试目标

一旦这些二进制文件安装完成,修改 test make 目标,加入 -i,如下所示。-i 只会检查本地安装的二进制文件,而不会访问远程资源。你也可以设置 ENVTEST_INSTALLED_ONLY 环境变量。

test: manifests generate fmt vet
    KUBEBUILDER_ASSETS="$(shell $(ENVTEST) 使用 $(ENVTEST_K8S_VERSION) -i --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out

注意:ENVTEST_K8S_VERSION 需要与您上面下载的 setup-envtest 匹配。否则,您将看到如下错误。

no such version (1.24.5) exists on disk for this architecture (darwin/amd64) -- try running `list -i` to see what"在磁盘上"

编写测试

在集成测试中使用 envtest 的一般流程如下:

import sigs.k8s.io/controller-runtime/pkg/envtest

//指定 testEnv 配置
testEnv = &envtest.Environment{
	CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
}

//开始测试环境
cfg, err = testEnv.Start()

//编写测试逻辑

//停止测试环境
err = testEnv.Stop()

kubebuilder 在它生成的 /controllers 目录下的 ginkgo 测试套件中,为您处理测试环境的样板代码的设置和拆卸。

测试运行的日志以 test-env 为前缀。

配置您的测试控制平面

Controller-runtime 的 envtest 框架需要本地有 kubectlkube-apiserveretcd 可执行文件,以模拟真实集群的 API 部分。

make test 命令将把这些二进制文件安装到 bin/ 目录,并在运行使用 envtest 的测试时使用它们。即,

./bin/k8s/
└── 1.25.0-darwin-amd64
    ├── etcd
    ├── kube-apiserver
    └── kubectl

您可以使用环境变量和/或标志来指定在集成测试中 kubectlapi-serveretcd 的设置。

环境变量

变量名Type何时使用
使用现有集群布尔值与其建立一个本地控制平面,不如指向现有集群的控制平面。
KUBEBUILDER_ASSETS目录路径将集成测试指向一个包含所有二进制文件的目录(api-server、etcd 和 kubectl)。
TEST_ASSET_KUBE_APISERVERTEST_ASSET_ETCDTEST_ASSET_KUBECTL分别指向 api-server、etcd 和 kubectl 二进制文件的路径类似于 KUBEBUILDER_ASSETS,但更为细化。指向集成测试使用除了默认二进制文件以外的其他二进制文件。这些环境变量还可以用来确保特定测试使用期望版本的这些二进制文件运行。
KUBEBUILDER_CONTROLPLANE_START_TIMEOUTKUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT支持 time.ParseDuration 的格式的持续时间。为测试控制平面指定不同于默认值的超时时间,以便(分别)启动和停止;任何超出这些时间的测试运行将失败。
KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT布尔值将其设置为 true 以将控制平面的 stdout 和 stderr 附加到 os.Stdout 和 os.Stderr。当调试测试失败时,这可能很有用,因为输出将包括来自控制平面的输出。

请注意,test 这个 Makefile 目标将确保在使用它时所有设置都正确。不过,如果您想在不使用 Makefile 目标的情况下运行测试,例如通过 IDE,则可以直接在您的 suite_test.go 代码中设置环境变量:

var _ = BeforeSuite(func(done Done) {
	Expect(os.Setenv("TEST_ASSET_KUBE_APISERVER", "../bin/k8s/1.25.0-darwin-amd64/kube-apiserver")).To(Succeed())
	Expect(os.Setenv("TEST_ASSET_ETCD", "../bin/k8s/1.25.0-darwin-amd64/etcd")).To(Succeed())
	Expect(os.Setenv("TEST_ASSET_KUBECTL", "../bin/k8s/1.25.0-darwin-amd64/kubectl")).To(Succeed())
	// OR
	Expect(os.Setenv("KUBEBUILDER_ASSETS", "../bin/k8s/1.25.0-darwin-amd64")).To(Succeed())

	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
	testenv = &envtest.Environment{}

	_, err := testenv.Start()
	Expect(err).NotTo(HaveOccurred())

	close(done)
}, 60)

var _ = AfterSuite(func() {
	Expect(testenv.Stop()).To(Succeed())

	Expect(os.Unsetenv("TEST_ASSET_KUBE_APISERVER")).To(Succeed())
	Expect(os.Unsetenv("TEST_ASSET_ETCD")).To(Succeed())
	Expect(os.Unsetenv("TEST_ASSET_KUBECTL")).To(Succeed())

})

旗帜

这是一个示例,展示了在你的集成测试中修改启动 API 服务器的标志,与 envtest.DefaultKubeAPIServerFlags 中的默认值进行对比。

customApiServerFlags := []string{
	"--secure-port=6884",
	"--admission-control=MutatingAdmissionWebhook",
}

apiServerFlags := append([]string(nil), envtest.DefaultKubeAPIServerFlags...)
apiServerFlags = append(apiServerFlags, customApiServerFlags...)

testEnv = &envtest.Environment{
	CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
	KubeAPIServerFlags: apiServerFlags,
}

测试考虑事项

除非您正在使用现有的集群,否则请记住,在测试上下文中没有运行内置控制器。在某些方面,测试控制平面将与“真实“集群表现不同,这可能会影响您编写测试的方式。一个常见的例子是垃圾收集;因为没有控制器监视内置资源,尽管设置了 OwnerReference,对象也不会被删除。

为了测试删除生命周期是否正常工作,请测试所有权,而不是仅仅断言存在性。例如:

expectedOwnerReference := v1.OwnerReference{
	Kind:       "MyCoolCustomResource",
	APIVersion: "my.api.example.com/v1beta1",
	UID:        "d9607e19-f88f-11e6-a518-42010a800195",
	Name:       "userSpecifiedResourceName",
}
Expect(deployment.ObjectMeta.OwnerReferences).To(ContainElement(expectedOwnerReference))

Cert-Manager 和 Prometheus 选项

使用 Kubebuilder 创建的项目可以启用 metricscert-manager 选项。请注意,当我们使用 ENV TEST 时,我们旨在测试控制器及其调整过程。这被视为集成测试,因为 ENV TEST API 会在集群中进行测试,因此会下载并使用二进制文件来配置其前置要求。然而,其主要目的是对控制器进行 单元 测试。

因此,在常见情况下,测试对账时您不需要关注这些选项。但是,如果您希望在安装了 Prometheus 和 Cert-manager 的情况下进行测试,可以在运行测试之前添加安装它们所需的步骤。以下是一个示例。

    // 在测试之前添加安装 Prometheus operator 和 cert-manager 的操作。    BeforeEach(func() {
        By("installing prometheus operator")
        Expect(utils.InstallPrometheusOperator()).To(Succeed())

        By("installing the cert-manager")
        Expect(utils.InstallCertManager()).To(Succeed())
    })

    // 您也可以在测试后将它们移除::
    AfterEach(func() {
        By("uninstalling the Prometheus manager bundle")
        utils.UninstallPrometheusOperManager()

        By("uninstalling the cert-manager bundle")
        utils.UninstallCertManager()
    })

请检查以下示例,了解如何实现上述操作:

const (
	prometheusOperatorVersion = "0.51"
	prometheusOperatorURL     = "https://raw.githubusercontent.com/prometheus-operator/" + "prometheus-operator/release-%s/bundle.yaml"
	certmanagerVersion = "v1.5.3"
	certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
)

func warnError(err error) {
	_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
}

// InstallPrometheusOperator 安装 prometheus 操作器,用于导出启用的指标。func InstallPrometheusOperator() error {
	url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
	cmd := exec.Command("kubectl", "apply", "-f", url)
	_, err := Run(cmd)
	return err
}

// UninstallPrometheusOperator 卸载 Prometheusfunc UninstallPrometheusOperator() {
	url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
	cmd := exec.Command("kubectl", "delete", "-f", url)
	if _, err := Run(cmd); err != nil {
		warnError(err)
	}
}

// UninstallCertManager 卸载证书管理器func UninstallCertManager() {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "delete", "-f", url)
	if _, err := Run(cmd); err != nil {
		warnError(err)
	}
}

// InstallCertManager 安装证书管理器包。func InstallCertManager() error {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "apply", "-f", url)
	if _, err := Run(cmd); err != nil {
		return err
	}
	// 等待 cert-manager-webhook 准备就绪,如果在集群上卸载后重新安装 cert-manager,可能需要一些时间。	cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
		"--for", "condition=Available",
		"--namespace", "cert-manager",
		"--timeout", "5m",
		)

	_, err := Run(cmd)
	return err
}

// LoadImageToKindClusterWithName 将本地 Docker 镜像加载到 Kind 集群中func LoadImageToKindClusterWithName(name string) error {
	cluster := "kind"
	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
		cluster = v
	}

	kindOptions := []string{"load", "docker-image", name, "--name", cluster}
	cmd := exec.Command("kind", kindOptions...)
	_, err := Run(cmd)
	return err
}

然而,请注意,对于指标和cert-manager的测试可能更适合作为端到端(e2e)测试,而不是在使用ENV TEST进行控制器测试时进行的测试。您可以查看在Operator-SDK 仓库中实施的示例示例,了解如何编写e2e测试以确保您项目的基本工作流程。此外,请注意,您可以在已配置集群上运行测试,使用现有集群进行测试的选项。

testEnv = &envtest.Environment{
	UseExistingCluster: true,
}