コントローラのテスト

controller-runtimeではenvtestパッケージを提供しており、コントローラやWebhookの簡易的なテストを実施することが可能です。

envtestはetcdとkube-apiserverを立ち上げてテスト用の環境を構築します。 環境変数USE_EXISTING_CLUSTERを指定すれば、既存のKubernetesクラスタを利用したテストをおこなうことも可能です。

なおcontroller-genが生成するテストコードでは、Ginkgoというテストフレームワークを利用しています。 このフレームワークの利用方法についてはGinkgonoのドキュメントを御覧ください。

テスト環境のセットアップ

controller-genによって自動生成されたcontrollers/suite_test.goを見てみましょう。

controllers/suite_test.go

package controllers

import (
    "path/filepath"
    "testing"

    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    multitenancyv1 "github.com/zoetrope/kubebuilder-training/codes/api/v1"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/rest"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/envtest"
    "sigs.k8s.io/controller-runtime/pkg/envtest/printer"
    logf "sigs.k8s.io/controller-runtime/pkg/log"
    "sigs.k8s.io/controller-runtime/pkg/log/zap"
    // +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment

func TestAPIs(t *testing.T) {
    RegisterFailHandler(Fail)

    RunSpecsWithDefaultAndCustomReporters(t,
        "Controller Suite",
        []Reporter{printer.NewlineReporter{}})
}

var _ = BeforeSuite(func(done Done) {
    logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))

    By("bootstrapping test environment")
    testEnv = &envtest.Environment{
        CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
    }

    var err error
    cfg, err = testEnv.Start()
    Expect(err).ToNot(HaveOccurred())
    Expect(cfg).ToNot(BeNil())

    err = multitenancyv1.AddToScheme(scheme.Scheme)
    Expect(err).NotTo(HaveOccurred())

    // +kubebuilder:scaffold:scheme

    mgr, err := ctrl.NewManager(cfg, ctrl.Options{
        Scheme: scheme.Scheme,
    })
    Expect(err).ToNot(HaveOccurred())

    err = (&TenantReconciler{
        Client:   mgr.GetClient(),
        Log:      ctrl.Log.WithName("controllers").WithName("Tenant"),
        Scheme:   mgr.GetScheme(),
        Recorder: mgr.GetEventRecorderFor("tenant-controller"),
    }).SetupWithManager(mgr)
    Expect(err).ToNot(HaveOccurred())

    go func() {
        err = mgr.Start(ctrl.SetupSignalHandler())
        Expect(err).ToNot(HaveOccurred())
    }()

    k8sClient = mgr.GetClient()
    Expect(k8sClient).ToNot(BeNil())

    close(done)
}, 60)

var _ = AfterSuite(func() {
    By("tearing down the test environment")
    err := testEnv.Stop()
    Expect(err).ToNot(HaveOccurred())
})

まずenvtest.Environmentでテスト用の環境設定をおこないます。 ここでは、CRDDirectoryPathsで適用するCRDのマニフェストのパスを指定しています。

testEnv.Start()を呼び出すとetcdとkube-apiserverが起動します。 あとはコントローラのメイン関数と同様に初期化処理をおこなうだけです。

テスト終了時にはetcdとkube-apiserverを終了するようにtestEnv.Stop()を呼び出します。

コントローラのテスト

それでは実際のテストを書いていきましょう。

controllers/tenant_controller_test.go

package controllers

import (
    "context"
    "errors"

    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    multitenancyv1 "github.com/zoetrope/kubebuilder-training/codes/api/v1"
    corev1 "k8s.io/api/core/v1"
    rbacv1 "k8s.io/api/rbac/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

var _ = Describe("Tenant controller", func() {

    const (
        tenantName = "sample"
    )

    Context("when creating Tenant resource", func() {
        It("Should create namespaces", func() {
            ctx := context.Background()
            tenant := &multitenancyv1.Tenant{
                TypeMeta: metav1.TypeMeta{
                    APIVersion: multitenancyv1.GroupVersion.String(),
                    Kind:       "Tenant",
                },
                ObjectMeta: metav1.ObjectMeta{
                    Name: tenantName,
                },
                Spec: multitenancyv1.TenantSpec{
                    Namespaces: []string{
                        "test1",
                        "test2",
                    },
                    NamespacePrefix: "",
                    Admin: rbacv1.Subject{
                        Kind:      "ServiceAccount",
                        Name:      "default",
                        Namespace: "default",
                    },
                },
            }
            err := k8sClient.Create(ctx, tenant)
            Expect(err).Should(Succeed())

            createdTenant := &multitenancyv1.Tenant{}
            Eventually(func() error {
                err := k8sClient.Get(ctx, client.ObjectKey{Name: tenantName}, createdTenant)
                if err != nil {
                    return err
                }
                cond := findCondition(createdTenant.Status.Conditions, multitenancyv1.ConditionReady)
                if cond == nil {
                    return errors.New("condition not found")
                }
                if cond.Status != corev1.ConditionTrue {
                    return errors.New(cond.Reason)
                }
                return nil
            }).Should(Succeed())

            nsList := &corev1.NamespaceList{}
            err = k8sClient.List(ctx, nsList, client.MatchingFields(map[string]string{ownerControllerField: tenant.Name}))
            Expect(err).Should(Succeed())

            var namespaces []string
            for _, ns := range nsList.Items {
                namespaces = append(namespaces, ns.Name)
            }
            Expect(namespaces).Should(ContainElement("test1"))
            Expect(namespaces).Should(ContainElement("test2"))

        })
    })
})

このテストではまずテナントのオブジェクトを用意して、k8sClientを利用してKubernetesクラスタにリソースの作成をおこなっています。 次にテナントリソースを取得して、ステータスがReadyになったことを確認しています。 最後に指定した通りのnamespaceリソースが作成されていることを確認しています。

テストの実行

make testgo testでテストを実行してみましょう。

$ go test -v ./controllers/
=== RUN   TestAPIs
Running Suite: Controller Suite
===============================
Random Seed: 1595578997
Will run 1 of 1 specs

Ran 1 of 1 Specs in 5.511 seconds
SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestAPIs (5.51s)
PASS
ok      github.com/zoetrope/kubebuilder-training/codes/controllers      5.528s

results matching ""

    No results matching ""