使用client-go的模拟客户端来测试创建Kubernetes对象
在本文中,我们将使用client-go的kubernetes/fake包来演示如何实现测试函数。
测试函数将检查被测试函数内的处理是否创建了Kubernetes对象。
假客户是指
通过使用kubernetes/fake包提供的Clientset,可以在执行机器的内存上模拟使用clientset进行对象获取和创建等操作,而无需实际向Kubernetes API发送请求。
Clientset是由client-go提供的Kubernetes包中的一个客户端,它包含了与Kubernetes API请求各种Kubernetes默认资源的功能,并在使用Go语言操作默认资源如Pod和Deployment时被使用。
Kubernetes的Clientset软件包符合Kubernetes软件包的Interface接口要求。同时,kubernetes/fake软件包提供的Clientset也满足Kubernetes软件包的Interface接口要求。
如果在测试对象函数的参数中使用`kubernetes`包的`Interface`接口来接收客户端,那么在传递类型时可以使用`kubernetes`包的`Clientset`和`kubernetes/fake`包的`Clientset`。
使用虚拟客户端的测试实现示例
被测试的函数
作为使用kubernetes/fake包的Clientset进行测试的示例,我们将创建以下程序的测试。
被测试的函数是replaceNameAndCreateNewPod()函数。
在这个函数中,根据传递的Pod对象,使用podName参数的值覆盖Name字段的值,创建一个新的Kubernetes对象。
客户端集参数的类型是kubernetes.Interface,这是关键之处。
package main
import (
"context"
"flag"
"fmt"
"os"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
var (
kubeconfig string
podName string
)
func main() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "kubeconfig path")
flag.StringVar(&podName, "name", "", "Pod Name")
flag.Parse()
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
clientset, _ := kubernetes.NewForConfig(config)
base := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "<REPLACE_THIS_FIELD>",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "sleep-container",
Image: "alpine",
Command: []string{"sleep", "3600"},
},
},
},
}
_, err := replaceNameAndCreateNewPod(clientset, base, podName)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func replaceNameAndCreateNewPod(clientset kubernetes.Interface, base *corev1.Pod, podName string) (*corev1.Pod, error) {
newPod := base.DeepCopy()
newPod.ObjectMeta.Name = podName
_, err := clientset.CoreV1().Pods("default").Create(context.TODO(), newPod, metav1.CreateOptions{})
if err != nil {
return nil, err
}
return newPod, nil
}
考试函数
要测试replaceNameAndCreateNewPod()函数的行为,我们有以下三个观点。
-
- 在执行时没有发生错误
-
- 新的Pod对象的Name字段的值已经被修改
- 通过clientset,新的Pod对象被创建了
测试函数的实现如下所示。
package main
import (
"context"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"github.com/stretchr/testify/assert"
)
func TestReplaceNameAndCreateNewPod(t *testing.T) {
clientset := fake.NewSimpleClientset()
original := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "<REPLACE_THIS_FIELD>",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "sleep-container",
Image: "alpine",
Command: []string{"sleep", "3600"},
},
},
},
}
tests := []struct {
name string
input string
expected string
}{
{
name: "check equal to expected name",
input: "sample-pod",
expected: "sample-pod",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
newPod, err := replaceNameAndCreateNewPod(clientset, original, tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.expected, newPod.ObjectMeta.Name)
_, err = clientset.CoreV1().Pods("default").Get(context.TODO(), tt.expected, metav1.GetOptions{})
assert.NoError(t, err)
})
}
}
在TestReplaceNameAndCreateNewPod()函数的开头,调用了kubernetes/fake包中的NewSimpleClientset()函数,以初始化客户端。
在调用replaceNameAndCreateNewPod()时,可以看到我们传递了在这里初始化的客户端。
在测试中,首先要检查replaceNameAndCreateNewPod()函数的返回值和错误对象是否返回了预期的值。
newPod, err := replaceNameAndCreateNewPod(clientset, original, tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.expected, newPod.ObjectMeta.Name)
下一步我们在以下部分进行检查,检查是否创建了预期的Pod对象。
fake.Clientset在这一部分中发挥了作用。
_, err = clientset.CoreV1().Pods("default").Get(context.TODO(), tt.expected, metav1.GetOptions{})
assert.NoError(t, err)
执行以上代码时,测试将会成功。
go test .
ok main/create-pod 0.638s
對「假客戶」功能進行實驗。
当调用kubernetes/fake包的Clientset的Create()方法时,我们将尝试在内存中真实地创建对象。
create_pod.goのreplaceNameAndCreateNewPod()関数の一部実装をコメントアウトしてテストを実行します。(contextパッケージの参照もなくなるためimport文もコメントアウトしてください)
由于这个更改,将不再调用clientset的Create()方法,因此假设上述的第三个测试位置将失败。
func replaceNameAndCreateNewPod(clientset kubernetes.Interface, base *corev1.Pod, podName string) (*corev1.Pod, error) {
newPod := base.DeepCopy()
newPod.ObjectMeta.Name = podName
// _, err := clientset.CoreV1().Pods("default").Create(context.TODO(), newPod, metav1.CreateOptions{})
// if err != nil {
// return nil, err
// }
return newPod, nil
}
変更後にテストを実行すると以下の出力を得ます。
出力から、3つ目の観点のチェック箇所でnot foundなエラーが返され、テストに失敗していることがわかります。
go test . 19:22:34
--- FAIL: TestReplaceNameAndCreateNewPod (0.00s)
--- FAIL: TestReplaceNameAndCreateNewPod/check_equal_to_expected_name (0.00s)
create_pod_test.go:51:
Error Trace: /Users/yutookamoto/work/create_k8s_custom_resource/create-pod/create_pod_test.go:51
Error: Received unexpected error:
pods "sample-pod" not found
Test: TestReplaceNameAndCreateNewPod/check_equal_to_expected_name
FAIL
FAIL main/create-pod 0.549s
FAIL
このことから、kubernetes/fakeパッケージのClientsetはCreate()メソッドによるKubernetes APIへの操作をメモリ上で記録できており、テスト内でkubernetesパッケージのClientsetの動きを模倣できていることがわかりました。
Custom Resourceを扱う場合のfake client
今回は例としてPodオブジェクトの操作を題材にしため、kubernetes/fakeパッケージのClientsetを利用しました。
一方で、Custom Resourceのオブジェクトを扱う際にはclient-goが提供するdynamic/fakeパッケージのFakeDynamicClientを利用可能です。
こちらを利用することでdynamicパッケージのDynamicClientを模倣できます。
关于DynamicClient,请参考另一篇名为client-go的文章,其中详细解释了如何使用GET方法获取自定义资源对象。
总结
本記事では、client-goのkubernetes/fakeパッケージを使ってKubernetesオブジェクトの作成をテストする方法を紹介しました。
通过使用kubernetes/fake包中的Clientset对象,我们能够模拟kubernetes包中的Clientset对象的行为并进行测试。
需要注意的是,当使用kubernetes/fake包中的Clientset时,对于资源的写入到etcd以及Admission Webhook中的验证和变异将不会对目标资源起作用。请注意这一点。
请参考
- Kubernetes Programming with Go