深入探究Kubernetes Operator的测试与开发
1. 测试Reconcile函数
1.1 测试工具介绍
为了测试Reconcile函数,我们将使用ginkgo(Go语言的测试框架)和controller - runtime库中的envtest包。envtest包通过启动一个简单的本地控制平面来提供Kubernetes测试环境。
1.2 安装envtest二进制文件
安装步骤如下:
1. 安装setup - envtest工具:
$ go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
- 安装特定Kubernetes版本的二进制文件:
$ setup-envtest use 1.23
输出示例:
Version: 1.23.5
OS/Arch: linux/amd64
Path: /path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64
- 使用默认目录或环境变量:
- 创建符号链接使用默认目录:
$ sudo mkdir /usr/local/kubebuilder
$ sudo ln -s /path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64 /usr/local/kubebuilder/bin
- 使用环境变量:
$ source <(setup-envtest use -i -p env 1.23.5)
$ echo $KUBEBUILDER_ASSETS
/path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64
1.3 使用envtest
控制平面仅运行API Server和etcd,没有控制器。这意味着测试的Operator创建Kubernetes资源时,没有控制器会做出反应。创建测试环境,首先要创建envtest.Environment结构的实例。示例代码如下:
import (
"path/filepath"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "crd"),
},
ErrorIfCRDPathMissing: true,
}
启动和停止环境:
cfg, err := testEnv.Start()
// ...
err := testEnv.Stop()
1.4 定义ginkgo套件
使用Go测试函数启动ginkgo规范:
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestMyReconciler_Reconcile(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t,
"Controller Suite",
)
}
BeforeSuite和AfterSuite函数示例:
import (
"context"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
mygroupv1alpha1 "github.com/feloy/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
var (
testEnv *envtest.Environment
ctx context.Context
cancel context.CancelFunc
k8sClient client.Client
)
var _ = BeforeSuite(func() {
log.SetLogger(zap.New(
zap.WriteTo(GinkgoWriter),
zap.UseDevMode(true),
))
ctx, cancel = context.WithCancel(
context.Background(),
)
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "crd"),
},
ErrorIfCRDPathMissing: true,
}
var err error
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
scheme := runtime.NewScheme()
err = clientgoscheme.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
err = mygroupv1alpha1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
mgr, err := manager.New(cfg, manager.Options{
Scheme: scheme,
})
Expect(err).ToNot(HaveOccurred())
k8sClient = mgr.GetClient()
err = builder.
ControllerManagedBy(mgr).
Named(Name).
For(&mygroupv1alpha1.MyResource{}).
Owns(&appsv1.Deployment{}).
Complete(&MyReconciler{})
go func() {
defer GinkgoRecover()
err = mgr.Start(ctx)
Expect(err).ToNot(
HaveOccurred(),
"failed to run manager",
)
}()
})
var _ = AfterSuite(func() {
cancel()
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
1.5 编写测试
测试计划如下:
1. 创建MyResource实例,验证Reconcile函数是否创建预期的“低级”资源。
2. 低级资源创建后,更新其状态,验证MyResource实例的状态是否相应更新。
测试代码示例:
import (
"fmt"
"math/rand"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
mygroupv1alpha1 "github.com/feloy/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
var _ = Describe("MyResource controller", func() {
When("When creating a MyResource instance", func() {
var (
myres mygroupv1alpha1.MyResource
ownerref *metav1.OwnerReference
name string
namespace = "default"
deployName string
image string
)
BeforeEach(func() {
image = fmt.Sprintf("myimage%d", rand.Intn(1000))
myres = mygroupv1alpha1.MyResource{
Spec: mygroupv1alpha1.MyResourceSpec{
Image: image,
},
}
name = fmt.Sprintf("myres%d", rand.Intn(1000))
myres.SetName(name)
myres.SetNamespace(namespace)
err := k8sClient.Create(ctx, &myres)
Expect(err).NotTo(HaveOccurred())
ownerref = metav1.NewControllerRef(
&myres,
mygroupv1alpha1.SchemeGroupVersion.
WithKind("MyResource"),
)
deployName = fmt.Sprintf("%s-deployment", name)
})
AfterEach(func() {
k8sClient.Delete(ctx, &myres)
})
It("should create a deployment", func() {
var dep appsv1.Deployment
Eventually(
deploymentExists(deployName, namespace, &dep),
10, 1
).Should(BeTrue())
})
When("deployment is found", func() {
var dep appsv1.Deployment
BeforeEach(func() {
Eventually(
deploymentExists(deployName, namespace, &dep),
10, 1,
).Should(BeTrue())
})
It("should be owned by the MyResource instance", func() {
Expect(dep.GetOwnerReferences()).
To(ContainElement(*ownerref))
})
It("should use the image specified in MyResource instance", func() {
Expect(
dep.Spec.Template.Spec.Containers[0].Image,
).To(Equal(image))
})
When("deployment ReadyReplicas is 1", func() {
BeforeEach(func() {
dep.Status.Replicas = 1
dep.Status.ReadyReplicas = 1
err := k8sClient.Status().Update(ctx, &dep)
Expect(err).NotTo(HaveOccurred())
})
It("should set status ready for MyResource instance", func() {
Eventually(
getMyResourceState(name, namespace), 10, 1,
).Should(Equal("Ready"))
})
})
})
})
})
func deploymentExists(
name, namespace string, dep *appsv1.Deployment,
) func() bool {
return func() bool {
err := k8sClient.Get(ctx, client.ObjectKey{
Namespace: namespace,
Name: name,
}, dep)
return err == nil
}
}
func getMyResourceState(
name, namespace string,
) func() (string, error) {
return func() (string, error) {
myres := mygroupv1alpha1.MyResource{}
err := k8sClient.Get(ctx, types.NamespacedName{
Namespace: namespace,
Name: name,
}, &myres)
if err != nil {
return "", err
}
return myres.Status.State, nil
}
}
1.6 测试执行流程
| 测试编号 | 测试条件 | 前置操作 | 测试内容 | 后置操作 |
|---|---|---|---|---|
| 1 | 创建MyResource实例 | 创建MyResource实例 | 检查是否创建Deployment | 删除MyResource实例 |
| 2 | 创建MyResource实例,Deployment已找到 | 创建MyResource实例,等待Deployment创建 | 检查Deployment是否由MyResource实例拥有 | 删除MyResource实例 |
| 3 | 创建MyResource实例,Deployment已找到 | 创建MyResource实例,等待Deployment创建 | 检查Deployment是否使用MyResource实例指定的镜像 | 删除MyResource实例 |
| 4 | 创建MyResource实例,Deployment已找到,Deployment ReadyReplicas为1 | 创建MyResource实例,等待Deployment创建,更新Deployment状态 | 检查MyResource实例状态是否变为Ready | 删除MyResource实例 |
1.7 测试流程mermaid图
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B(创建MyResource实例):::process
B --> C{Deployment是否创建}:::decision
C -- 是 --> D(检查Deployment所有者):::process
D --> E(检查Deployment镜像):::process
E --> F{Deployment ReadyReplicas是否为1}:::decision
F -- 是 --> G(检查MyResource状态):::process
C -- 否 --> H(等待Deployment创建):::process
H --> C
F -- 否 --> I(更新Deployment状态):::process
I --> F
G --> J(删除MyResource实例):::process
J --> K([结束]):::startend
2. 使用Kubebuilder创建Operator
2.1 Kubebuilder简介
Kubebuilder SDK可帮助创建新资源及其相关的Operator。它提供命令来初始化项目、添加资源和控制器,还能构建和部署自定义资源定义(CRD)和Manager到集群。
2.2 安装Kubebuilder
可从 此处 获取相关安装资源。
2.3 创建项目
创建项目的步骤如下:
1. 创建一个空目录并进入:
$ mkdir myresource-kb
$ cd myresource-kb
- 执行
kubebuilder init命令:
$ kubebuilder init --domain myid.dev --repo github.com/myid/myresource
- `--domain`:域名,用作GVK组名的后缀。
- `--repo`:生成的Go代码的模块名。
创建项目后,可查看Makefile的可用命令:
$ make help
构建Manager二进制文件:
$ make build
本地运行Manager:
$ make run
或者直接执行二进制文件:
$ ./bin/manager
建议此时初始化版本控制项目(如Git)并提交初始版本:
$ git init
$ git commit -am 'kubebuilder init --domain myid.dev --repo github.com/myid/myresource'
2.4 向项目添加自定义资源
当前Manager没有管理任何控制器,需要执行 kubebuilder create api 命令添加自定义资源和相关控制器:
$ kubebuilder create api --group mygroup --version v1alpha1 --kind MyResource
执行命令后会询问是否创建资源和控制器,都回复 y 。
Create Resource [y/n]
y
Create Controller [y/n]
y
执行后可通过 git status 查看文件变化:
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: PROJECT
modified: go.mod
modified: go.sum
modified: main.go
Untracked files:
(use "git add <file>..." to include in what will be committed)
api/
config/crd/
config/rbac/myresource_editor_role.yaml
config/rbac/myresource_viewer_role.yaml
config/samples/
controllers/
no changes added to commit (use "git add" and/or "git commit -a")
此命令的影响如下:
- PROJECT 文件:包含项目定义,新增了第一个资源的定义。
- main.go 和 controllers 目录:定义了自定义资源的控制器。
- api/v1alpha1 目录:使用Go结构定义自定义资源,包含 deepcopy - gen 生成的代码和 AddToScheme 函数。
- config/samples 目录:包含一个新的YAML文件,定义了自定义资源的实例。
- config/rbac 目录:包含两个新的 ClusterRole 资源文件,分别用于查看和编辑 MyResource 实例。
- config/crd 目录:包含用于构建CRD的kustomize文件。
2.5 项目结构变化表格
| 目录/文件 | 变化说明 |
|---|---|
| PROJECT | 新增资源定义 |
| go.mod, go.sum | 文件被修改 |
| main.go | 文件被修改 |
| api/v1alpha1 | 新增自定义资源定义及相关代码 |
| config/samples | 新增自定义资源实例YAML文件 |
| config/rbac | 新增两个ClusterRole资源文件 |
| config/crd | 包含构建CRD的kustomize文件 |
| controllers | 新增控制器相关代码 |
2.6 Kubebuilder项目创建流程mermaid图
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B(创建空目录):::process
B --> C(进入目录):::process
C --> D(执行kubebuilder init):::process
D --> E(查看Makefile命令):::process
E --> F(构建Manager二进制文件):::process
F --> G(本地运行Manager):::process
G --> H(初始化Git项目):::process
H --> I(提交初始版本):::process
I --> J(执行kubebuilder create api):::process
J --> K{是否创建资源和控制器}:::decision
K -- 是 --> L(完成资源和控制器添加):::process
K -- 否 --> M(结束操作):::process
L --> N([结束]):::startend
M --> N
综上所述,我们介绍了Kubernetes Operator的测试方法,包括使用ginkgo和envtest进行测试,以及如何使用Kubebuilder SDK创建Operator项目和添加自定义资源。这些方法和工具能帮助开发者更高效地开发和测试Kubernetes Operator。
超级会员免费看
1061

被折叠的 条评论
为什么被折叠?



