From d3e7cc60a1d33ea60c34ecea4bb954eb7dee1a7f Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Thu, 9 Dec 2021 17:30:05 +1100 Subject: [PATCH] Add tests for CRDConversion webhook Signed-off-by: Adam Malcontenti-Wilson --- go.mod | 1 + pkg/rotator/rotator_test.go | 102 ++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 8b071a3..becb560 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/pkg/errors v0.9.1 go.uber.org/atomic v1.7.0 k8s.io/api v0.21.4 + k8s.io/apiextensions-apiserver v0.21.4 k8s.io/apimachinery v0.21.4 k8s.io/client-go v0.21.4 sigs.k8s.io/controller-runtime v0.9.7 diff --git a/pkg/rotator/rotator_test.go b/pkg/rotator/rotator_test.go index 3b3e98d..40550bb 100644 --- a/pkg/rotator/rotator_test.go +++ b/pkg/rotator/rotator_test.go @@ -3,14 +3,16 @@ package rotator import ( "context" "fmt" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "testing" "time" "github.com/onsi/gomega" admissionv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" @@ -166,7 +168,17 @@ func TestEmptyIsInvalid(t *testing.T) { func setupManager(g *gomega.GomegaWithT) manager.Manager { disabledMetrics := "0" + + scheme := runtime.NewScheme() + err := corev1.AddToScheme(scheme) + g.Expect(err).NotTo(gomega.HaveOccurred(), "building runtime schema") + err = admissionv1.AddToScheme(scheme) + g.Expect(err).NotTo(gomega.HaveOccurred(), "building runtime schema") + err = apiextensionsv1.AddToScheme(scheme) + g.Expect(err).NotTo(gomega.HaveOccurred(), "building runtime schema") + opts := manager.Options{ + Scheme: scheme, MetricsBindAddress: disabledMetrics, } mgr, err := manager.New(cfg, opts) @@ -174,7 +186,7 @@ func setupManager(g *gomega.GomegaWithT) manager.Manager { return mgr } -func testWebhook(t *testing.T, secretKey types.NamespacedName, rotator *CertRotator, wh client.Object) { +func testWebhook(t *testing.T, secretKey types.NamespacedName, rotator *CertRotator, wh client.Object, webhooksField, caBundleField []string) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() @@ -196,13 +208,13 @@ func testWebhook(t *testing.T, secretKey types.NamespacedName, rotator *CertRota ensureCertWasGenerated(ctx, g, c, secretKey) // Wait for certificates to populated in managed webhookConfigurations - ensureWebhookPopulated(ctx, g, c, wh) + ensureWebhookPopulated(ctx, g, c, wh, webhooksField, caBundleField) // Zero out the certificates, ensure they are repopulated - resetWebhook(ctx, g, c, wh) + resetWebhook(ctx, g, c, wh, webhooksField, caBundleField) // Verify certificates are regenerated - ensureWebhookPopulated(ctx, g, c, wh) + ensureWebhookPopulated(ctx, g, c, wh, webhooksField, caBundleField) cancelFunc() wg.Wait() @@ -213,9 +225,11 @@ func TestReconcileWebhook(t *testing.T) { testCases := []struct { name string webhookType WebhookType + webhooksField []string + caBundleField []string webhookConfig client.Object }{ - {"validating", Validating, &admissionv1.ValidatingWebhookConfiguration{ + {"validating", Validating, []string{"webhooks"}, []string{"clientConfig", "caBundle"}, &admissionv1.ValidatingWebhookConfiguration{ Webhooks: []admissionv1.ValidatingWebhook{ { Name: "testpolicy.kubernetes.io", @@ -227,7 +241,7 @@ func TestReconcileWebhook(t *testing.T) { }, }, }}, - {"mutating", Mutating, &admissionv1.MutatingWebhookConfiguration{ + {"mutating", Mutating, []string{"webhooks"}, []string{"clientConfig", "caBundle"}, &admissionv1.MutatingWebhookConfiguration{ Webhooks: []admissionv1.MutatingWebhook{ { Name: "testpolicy.kubernetes.io", @@ -239,6 +253,39 @@ func TestReconcileWebhook(t *testing.T) { }, }, }}, + {"crdconversion", CRDConversion, nil, []string{"spec", "conversion", "webhook", "clientConfig", "caBundle"}, &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "testcrds.example.com"}, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "example.com", + Scope: apiextensionsv1.NamespaceScoped, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Kind: "TestCRD", + ListKind: "TestCRDList", + Plural: "testcrds", + Singular: "testcrd", + }, + Conversion: &apiextensionsv1.CustomResourceConversion{ + Strategy: apiextensionsv1.WebhookConverter, + Webhook: &apiextensionsv1.WebhookConversion{ + ClientConfig: &apiextensionsv1.WebhookClientConfig{ + URL: strPtr("https://localhost/webhook"), + }, + ConversionReviewVersions: []string{"v1", "v1beta1"}, + }, + }, + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Storage: true, + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + }, + }, + }, + }, + }, + }}, } for _, tt := range testCases { @@ -249,6 +296,11 @@ func TestReconcileWebhook(t *testing.T) { whName = fmt.Sprintf("test-webhook-%s", tt.name) ) + // CRDConversion type requires exact name + if tt.webhookConfig.GetName() != "" { + whName = tt.webhookConfig.GetName() + } + key := types.NamespacedName{Namespace: nsName, Name: secretName} rotator := &CertRotator{ SecretKey: key, @@ -263,7 +315,7 @@ func TestReconcileWebhook(t *testing.T) { wh := tt.webhookConfig wh.SetName(whName) - testWebhook(t, key, rotator, wh) + testWebhook(t, key, rotator, wh, tt.webhooksField, tt.caBundleField) }) } } @@ -327,7 +379,20 @@ func ensureCertWasGenerated(ctx context.Context, g *gomega.WithT, c client.Reade }, timeout, interval).Should(gomega.BeTrue(), "waiting for certificate generation") } -func ensureWebhookPopulated(ctx context.Context, g *gomega.WithT, c client.Client, wh interface{}) { +func extractWebhooks(g *gomega.WithT, u *unstructured.Unstructured, webhooksField []string) []interface{} { + var webhooks []interface{} + var err error + + if webhooksField != nil { + webhooks, _, err = unstructured.NestedSlice(u.Object, webhooksField...) + g.Expect(err).NotTo(gomega.HaveOccurred(), "cannot extract webhooks from object") + } else { + webhooks = []interface{}{u.Object} + } + return webhooks +} + +func ensureWebhookPopulated(ctx context.Context, g *gomega.WithT, c client.Client, wh interface{}, webhooksField, caBundleField []string) { const timeout = 15 * time.Second const interval = 50 * time.Millisecond @@ -341,14 +406,11 @@ func ensureWebhookPopulated(ctx context.Context, g *gomega.WithT, c client.Clien if err := c.Get(ctx, key, whu); err != nil { return false } - webhooks, found, err := unstructured.NestedSlice(whu.Object, "webhooks") - if len(webhooks) == 0 || !found || err != nil { - return false - } + webhooks := extractWebhooks(g, whu, webhooksField) for _, w := range webhooks { - clientConfig, found, err := unstructured.NestedMap(w.(map[string]interface{}), "clientConfig") - if !found || err != nil || clientConfig["caBundle"] == nil || len(clientConfig["caBundle"].(string)) == 0 { + caBundle, found, err := unstructured.NestedFieldNoCopy(w.(map[string]interface{}), caBundleField...) + if !found || err != nil || caBundle == nil || len(caBundle.(string)) == 0 { return false } } @@ -356,7 +418,7 @@ func ensureWebhookPopulated(ctx context.Context, g *gomega.WithT, c client.Clien }, timeout, interval).Should(gomega.BeTrue(), "waiting for webhook reconciliation") } -func resetWebhook(ctx context.Context, g *gomega.WithT, c client.Client, wh interface{}) { +func resetWebhook(ctx context.Context, g *gomega.WithT, c client.Client, wh interface{}, webhooksField, caBundleField []string) { // convert to unstructured object to accept either ValidatingWebhookConfiguration or MutatingWebhookConfiguration whu := &unstructured.Unstructured{} err := c.Scheme().Convert(wh, whu, nil) @@ -368,13 +430,9 @@ func resetWebhook(ctx context.Context, g *gomega.WithT, c client.Client, wh inte return err } - webhooks, _, err := unstructured.NestedSlice(whu.Object, "webhooks") - if err != nil { - return err - } - + webhooks := extractWebhooks(g, whu, webhooksField) for _, w := range webhooks { - if err = unstructured.SetNestedField(w.(map[string]interface{}), nil, "clientConfig", "caBundle"); err != nil { + if err = unstructured.SetNestedField(w.(map[string]interface{}), nil, caBundleField...); err != nil { return err } }