Webhookのテスト

controller-runtime v0.5.1以降では、envtestでwebhookのテストもサポートするようになりました。

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

コントローラ用のテスト環境を用意するコードはcontroller-genが自動生成してくれますが、Webhook用のコードはまだ生成してくれません(controller-gen 0.3.0の場合)。

そこでapi/v1/suite_test.goのようなコードを自分で用意する必要があります。

api/v1/suite_test.go

package v1

import (
    "crypto/tls"
    "fmt"
    "net"
    "path/filepath"
    "testing"
    "time"

    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    "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 TestWebhooks(t *testing.T) {
    RegisterFailHandler(Fail)

    RunSpecsWithDefaultAndCustomReporters(t,
        "Webhook 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")},
        WebhookInstallOptions: envtest.WebhookInstallOptions{
            DirectoryPaths: []string{filepath.Join("..", "..", "config", "webhook")},
        },
    }

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

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

    // +kubebuilder:scaffold:scheme

    wio := testEnv.WebhookInstallOptions
    mgr, err := ctrl.NewManager(cfg, ctrl.Options{
        Scheme:         scheme.Scheme,
        Host:           wio.LocalServingHost,
        Port:           wio.LocalServingPort,
        CertDir:        wio.LocalServingCertDir,
        LeaderElection: false,
    })
    Expect(err).ToNot(HaveOccurred())

    err = (&Tenant{}).SetupWebhookWithManager(mgr)
    Expect(err).ToNot(HaveOccurred())

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

    // wait for the webhook server to get ready
    dialer := &net.Dialer{Timeout: time.Second}
    addrPort := fmt.Sprintf("%s:%d", wio.LocalServingHost, wio.LocalServingPort)
    Eventually(func() error {
        conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
        if err != nil {
            return err
        }
        conn.Close()
        return nil
    }).Should(Succeed())

    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を作成する際に、Webhook用のマニフェストのパスを指定したり、ctrl.NewManagerを呼び出す際にHost,Port,CertDirのパラメータをtestEnvのパラメータで上書きする必要があります。

Webhookのテスト

コントローラと同様にWebhookのテストも書いてみましょう。

api/v1/tenant_webhook_test.go

package v1

import (
    "context"

    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
    rbacv1 "k8s.io/api/rbac/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

var _ = Describe("Tenant Webhook", func() {
    It("should create a valid tenant", func() {
        ctx := context.Background()
        tenant := &Tenant{
            TypeMeta: metav1.TypeMeta{
                APIVersion: GroupVersion.String(),
                Kind:       "Tenant",
            },
            ObjectMeta: metav1.ObjectMeta{
                Name: "test",
            },
            Spec: TenantSpec{
                Namespaces: []string{
                    "test1",
                    "test2",
                },
                NamespacePrefix: "",
                Admin: rbacv1.Subject{
                    Kind:      "ServiceAccount",
                    Name:      "default",
                    Namespace: "default",
                },
            },
        }
        err := k8sClient.Create(ctx, tenant)
        Expect(err).Should(Succeed())

        mutatedTenant := &Tenant{}
        err = k8sClient.Get(ctx, client.ObjectKey{Name: "test"}, mutatedTenant)
        Expect(err).Should(Succeed())
        Expect(mutatedTenant.Spec.NamespacePrefix).Should(Equal("test-"))
    })
})

このテストではまずテナントのオブジェクトを用意して、k8sClientを利用してKubernetesクラスタにリソースの作成をおこなっています。 その後、defaultingのWebhookにより、namespacePrefixの値が正しく設定されていることを確認しています。

テストの実行

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

$ go test -v ./api/v1/
=== RUN   TestWebhooks
Running Suite: Webhook Suite
============================
Random Seed: 1595580068
Will run 1 of 1 specs

Ran 1 of 1 Specs in 5.101 seconds
SUCCESS! -- 1 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestWebhooks (5.10s)
PASS
ok      github.com/zoetrope/kubebuilder-training/codes/api/v1   5.191s

results matching ""

    No results matching ""