3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2023

Day 4

client-goのfake clientでKubernetesオブジェクトの作成をテストする

Last updated at Posted at 2023-12-03

これは ZOZO Advent Calendar 2023 カレンダー Vol.5 の 4日目の記事です。

本記事では、client-gokubernetes/fakeパッケージを使って、テスト関数を実装する例を紹介します。
テスト関数では、テスト対象の関数内の処理によりKubernetesオブジェクトが作成されているかどうかの確認を行います。

fake clientとは

kubernetes/fakeパッケージが提供するClientsetを利用することで、実際にKubernetes APIへリクエストを送ることなく実行マシンのメモリ上でclientsetによるオブジェクト取得や作成といった動きを模倣できます。

client-goが提供するkubernetesパッケージのClientsetには様々なKubernetesのデフォルトリソースに関してKubernetes APIにリクエストするためのクライアントが含まれており、PodやDeploymentといったデフォルトリソースをGo言語で操作する際に利用されます。。

kubernetesパッケージのClientsetは、同じくkubernetesパッケージのInterfaceインターフェースを満たしています。
一方で、kubernetes/fakeパッケージが提供するClientsetkubernetesパッケージのInterfaceインターフェースを満たしています。

そのため、テスト対象の関数の引数にkubernetesパッケージのInterfaceインターフェースでクライアントを受け取るように実装していれば、kubernetesパッケージのClientsetkubernetes/fakeパッケージのClientsetのどちらの型でも渡すことが可能です。

fake clientを使ったテストの実装例

テスト対象の関数

kubernetes/fakeパッケージのClientsetを使ったテストの例として、次のプログラムのテストを作成します。
テスト対象の関数はreplaceNameAndCreateNewPod()関数です。

この関数では引数で渡されたPodオブジェクトを元に、Nameフィールドの値をpodName引数の値で上書きした新しいKubernetesオブジェクトを作成します。

clientset引数の型がkubernetes.Interafaceになっている点がポイントです。

create_pod.go
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()関数の振る舞いとしてテストしたい観点は次の3点になります。

  1. 実行時にエラーが発生しないこと
  2. 新しいPodオブジェクトのNameフィールドの値が変更されていること
  3. clientsetにより、新しいPodオブジェクトが作成されていること

テスト関数の実装は以下になります。

create_pod_test.go
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

fake clientの機能を実験

kubernetes/fakeパッケージのClientsetCreate()メソッドが呼ばれた際に、本当にメモリ上でオブジェクトが作成されているのか実験してみます。

create_pod.goreplaceNameAndCreateNewPod()関数の一部実装をコメントアウトしてテストを実行します。(contextパッケージの参照もなくなるためimport文もコメントアウトしてください)

この変更によりclientsetCreate()メソッドは呼ばれなくなるため、上記の3つ目のテスト箇所は失敗する想定です。

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パッケージのClientsetCreate()メソッドによるKubernetes APIへの操作をメモリ上で記録できており、テスト内でkubernetesパッケージのClientsetの動きを模倣できていることがわかりました。

Custom Resourceを扱う場合のfake client

今回は例としてPodオブジェクトの操作を題材にしため、kubernetes/fakeパッケージのClientsetを利用しました。
一方で、Custom Resourceのオブジェクトを扱う際にはclient-goが提供するdynamic/fakeパッケージのFakeDynamicClientを利用可能です。
こちらを利用することでdynamicパッケージのDynamicClientを模倣できます。

DynamicClientについては、別記事のclient-goでCustom ResourceのオブジェクトをGETするで説明しているため、気になる方は参考にしてみてください。

まとめ

本記事では、client-gokubernetes/fakeパッケージを使ってKubernetesオブジェクトの作成をテストする方法を紹介しました。

kubernetes/fakeパッケージのClientsetオブジェクトを利用することでkubernetesパッケージのClientsetオブジェクトの動きを模倣してテストを行うことができました。

注意点として、kubernetes/fakeパッケージのClientsetを利用した場合、etcdへのリソースの書き込みやAdmision Webhookでのvalidationmutationは対象のリソースに対して機能しないという点があります。こちらの点には注意してください。

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?