Skip to content

Commit

Permalink
Add tests for CRDConversion webhook
Browse files Browse the repository at this point in the history
Signed-off-by: Adam Malcontenti-Wilson <amalcontenti-wilson@zendesk.com>
  • Loading branch information
adammw committed Jan 20, 2022
1 parent 4e3926d commit d3e7cc6
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 22 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 80 additions & 22 deletions pkg/rotator/rotator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -166,15 +168,25 @@ 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)
g.Expect(err).NotTo(gomega.HaveOccurred(), "creating 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()

Expand All @@ -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()
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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)
})
}
}
Expand Down Expand Up @@ -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

Expand All @@ -341,22 +406,19 @@ 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
}
}
return true
}, 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)
Expand All @@ -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
}
}
Expand Down

0 comments on commit d3e7cc6

Please sign in to comment.