From 62601ec2f119ad124d5ca27f0e6ccd9139bdbb9e Mon Sep 17 00:00:00 2001 From: Norwin Schnyder Date: Sun, 15 Aug 2021 09:36:47 +0200 Subject: [PATCH] Add tests for internal handler --- .github/dependabot.yml | 11 ++ .github/workflows/test.yaml | 33 ++++ go.mod | 2 + pkg/webhook/handler.go | 6 +- pkg/webhook/handler_test.go | 213 +++++++++++++++++++++++++ pkg/webhook/mutating_webhook_test.go | 28 ++++ pkg/webhook/validating_webhook_test.go | 32 ++++ pkg/webhook/webhook_suite_test.go | 13 ++ 8 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/test.yaml create mode 100644 pkg/webhook/handler_test.go create mode 100644 pkg/webhook/mutating_webhook_test.go create mode 100644 pkg/webhook/validating_webhook_test.go create mode 100644 pkg/webhook/webhook_suite_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..16f110d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..7dcbfdf --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,33 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + name: Test + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ^1.16 + + - name: Checkout + uses: actions/checkout@v2 + + - name: Test + run: go test ./... -coverprofile cover.out -timeout 30m + env: + CGO_ENABLED: 0 + GO111MODULE: on + GOOS: linux + GOARCH: amd64 + + - name: Send coverage + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: cover.out \ No newline at end of file diff --git a/go.mod b/go.mod index b2f1198..3085ff6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/snorwin/k8s-generic-webhook go 1.16 require ( + github.com/onsi/ginkgo v1.16.4 + github.com/onsi/gomega v1.14.0 k8s.io/api v0.21.3 k8s.io/apimachinery v0.21.3 sigs.k8s.io/controller-runtime v0.9.6 diff --git a/pkg/webhook/handler.go b/pkg/webhook/handler.go index 088fa46..e04fb15 100644 --- a/pkg/webhook/handler.go +++ b/pkg/webhook/handler.go @@ -31,10 +31,10 @@ type handler struct { func (h *handler) Handle(ctx context.Context, req admission.Request) admission.Response { // add metadata to context's logger logger := log.FromContext(ctx). - WithValues("Webhook", req.Kind). - WithValues("uid", req.UID). WithValues("name", req.Name). - WithValues("namespace", req.Namespace) + WithValues("namespace", req.Namespace). + WithValues("gvk", req.Kind.String()). + WithValues("uid", req.UID) ctx = log.IntoContext(ctx, logger) // decode object diff --git a/pkg/webhook/handler_test.go b/pkg/webhook/handler_test.go new file mode 100644 index 0000000..d37db48 --- /dev/null +++ b/pkg/webhook/handler_test.go @@ -0,0 +1,213 @@ +package webhook + +import ( + "context" + "encoding/json" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var _ = Describe("Handler", func() { + Context("Handle", func() { + var ( + decoder *admission.Decoder + ) + BeforeEach(func() { + scheme := runtime.NewScheme() + err := corev1.AddToScheme(scheme) + Ω(err).ShouldNot(HaveOccurred()) + decoder, err = admission.NewDecoder(scheme) + Ω(err).ShouldNot(HaveOccurred()) + + }) + It("should deny by default", func() { + result := (&handler{}).Handle(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeFalse()) + }) + It("should validate", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + } + raw, err := json.Marshal(pod) + Ω(err).ShouldNot(HaveOccurred()) + + h := handler{ + Handler: &ValidateFuncs{ + CreateFunc: func(ctx context.Context, request admission.Request) admission.Response { + return admission.Allowed("") + }, + UpdateFunc: func(ctx context.Context, request admission.Request) admission.Response { + return admission.Denied("") + }, + DeleteFunc: func(ctx context.Context, request admission.Request) admission.Response { + return admission.Denied("") + }, + }, + Object: &corev1.Pod{}, + } + err = h.InjectDecoder(decoder) + Ω(err).ShouldNot(HaveOccurred()) + + result := h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: raw, + }, + Operation: admissionv1.Create, + }, + }) + Ω(result.Allowed).Should(BeTrue()) + result = h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: raw, + }, + Operation: admissionv1.Update, + }, + }) + Ω(result.Allowed).Should(BeFalse()) + result = h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: raw, + }, + Operation: admissionv1.Delete, + }, + }) + Ω(result.Allowed).Should(BeFalse()) + + }) + It("should decode object", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + Spec: corev1.PodSpec{ + NodeName: "jin", + }, + } + raw, err := json.Marshal(pod) + Ω(err).ShouldNot(HaveOccurred()) + + h := handler{ + Handler: &MutateFunc{ + Func: func(ctx context.Context, request admission.Request) admission.Response { + if len(request.Object.Raw) > 0 { + Ω(request.Object.Object).Should(Equal(pod)) + } + if len(request.OldObject.Raw) > 0 { + Ω(request.OldObject.Object).Should(Equal(pod)) + } + return admission.Allowed("") + }, + }, + Object: &corev1.Pod{}, + } + err = h.InjectDecoder(decoder) + Ω(err).ShouldNot(HaveOccurred()) + + result := h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: raw, + }, + }, + }) + Ω(result.Allowed).Should(BeTrue()) + + result = h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + OldObject: runtime.RawExtension{ + Raw: raw, + }, + }, + }) + Ω(result.Allowed).Should(BeTrue()) + }) + It("should not decode invalid object", func() { + h := handler{ + Handler: &MutateFunc{ + Func: func(ctx context.Context, request admission.Request) admission.Response { + return admission.Allowed("") + }, + }, + Object: &corev1.Pod{}, + } + err := h.InjectDecoder(decoder) + Ω(err).ShouldNot(HaveOccurred()) + + result := h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: []byte{1, 2, 3, 4, 5}, + }, + }, + }) + Ω(result.Allowed).Should(BeFalse()) + + result = h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + OldObject: runtime.RawExtension{ + Raw: []byte{1, 2, 3, 4, 5}, + }, + }, + }) + Ω(result.Allowed).Should(BeFalse()) + }) + }) + Context("InjectDecoder", func() { + var ( + decoder *admission.Decoder + ) + BeforeEach(func() { + decoder = &admission.Decoder{} + }) + It("should pass decoder to validating webhook", func() { + webhook := ValidatingWebhook{} + Ω((&handler{Handler: &webhook}).InjectDecoder(decoder)).ShouldNot(HaveOccurred()) + Ω(webhook.Decoder).Should(Equal(decoder)) + }) + It("should pass decoder to mutating webhook", func() { + webhook := MutatingWebhook{} + Ω((&handler{Handler: &webhook}).InjectDecoder(decoder)).ShouldNot(HaveOccurred()) + Ω(webhook.Decoder).Should(Equal(decoder)) + }) + It("should never fail if handler not set", func() { + Ω((&handler{}).InjectDecoder(decoder)).ShouldNot(HaveOccurred()) + }) + }) + Context("InjectClient", func() { + var ( + client client.Client + ) + BeforeEach(func() { + client = fake.NewClientBuilder().Build() + }) + It("should pass client to validating webhook", func() { + webhook := ValidatingWebhook{} + Ω((&handler{Handler: &webhook}).InjectClient(client)).ShouldNot(HaveOccurred()) + Ω(webhook.Client).Should(Equal(client)) + }) + It("should pass client to mutating webhook", func() { + webhook := MutatingWebhook{} + Ω((&handler{Handler: &webhook}).InjectClient(client)).ShouldNot(HaveOccurred()) + Ω(webhook.Client).Should(Equal(client)) + }) + It("should never fail if handler not set", func() { + Ω((&handler{}).InjectClient(client)).ShouldNot(HaveOccurred()) + }) + }) +}) diff --git a/pkg/webhook/mutating_webhook_test.go b/pkg/webhook/mutating_webhook_test.go new file mode 100644 index 0000000..1d32896 --- /dev/null +++ b/pkg/webhook/mutating_webhook_test.go @@ -0,0 +1,28 @@ +package webhook_test + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/snorwin/k8s-generic-webhook/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var _ = Describe("Mutating Webhook", func() { + Context("MutateFunc", func() { + It("should by default allow all", func() { + result := (&webhook.MutateFunc{}).Mutate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + }) + It("should use defined functions", func() { + result := (&webhook.MutateFunc{ + Func: func(ctx context.Context, _ admission.Request) admission.Response { + return admission.Denied("") + }, + }).Mutate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeFalse()) + }) + }) +}) diff --git a/pkg/webhook/validating_webhook_test.go b/pkg/webhook/validating_webhook_test.go new file mode 100644 index 0000000..45bb331 --- /dev/null +++ b/pkg/webhook/validating_webhook_test.go @@ -0,0 +1,32 @@ +package webhook_test + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/snorwin/k8s-generic-webhook/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var _ = Describe("Validating Webhook", func() { + Context("ValidateFuncs", func() { + It("should by default allow all", func() { + result := (&webhook.ValidateFuncs{}).ValidateCreate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + result = (&webhook.ValidateFuncs{}).ValidateUpdate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + result = (&webhook.ValidateFuncs{}).ValidateDelete(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + }) + It("should use defined functions", func() { + result := (&webhook.ValidateFuncs{ + CreateFunc: func(ctx context.Context, _ admission.Request) admission.Response { + return admission.Denied("") + }, + }).ValidateCreate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeFalse()) + }) + }) +}) diff --git a/pkg/webhook/webhook_suite_test.go b/pkg/webhook/webhook_suite_test.go new file mode 100644 index 0000000..5ad73d8 --- /dev/null +++ b/pkg/webhook/webhook_suite_test.go @@ -0,0 +1,13 @@ +package webhook_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestWebhook(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Webhook Test Suite") +}