Skip to content

Commit

Permalink
chore: refactor admission webhook tests (#409)
Browse files Browse the repository at this point in the history
Signed-off-by: odubajDT <ondrej.dubaj@dynatrace.com>
  • Loading branch information
odubajDT committed Mar 23, 2023
1 parent 3212eba commit 29c7c28
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 133 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/zapr v1.2.3 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
Expand Down
48 changes: 28 additions & 20 deletions webhooks/featureflagconfiguration_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,12 @@ type FeatureFlagConfigurationValidator struct {
// Handle validates a FeatureFlagConfiguration
func (m *FeatureFlagConfigurationValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
config := corev1alpha1.FeatureFlagConfiguration{}
err := m.decoder.Decode(req, &config)
if err != nil {
if err := m.decoder.Decode(req, &config); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if config.Spec.FeatureFlagSpec != "" {
if !m.isJSON(config.Spec.FeatureFlagSpec) {
return admission.Denied(fmt.Sprintf("FeatureFlagSpec is not valid JSON: %s", config.Spec.FeatureFlagSpec))
}
err = validateJSONSchema(schemas.FlagdDefinitions, config.Spec.FeatureFlagSpec)
if err != nil {
return admission.Denied(fmt.Sprintf("FeatureFlagSpec is not valid JSON: %s", err.Error()))
}
}

if config.Spec.ServiceProvider != nil && config.Spec.ServiceProvider.Credentials != nil {
// Check the provider and whether it has an existing secret
providerKeySecret := corev1.Secret{}
if err := m.Client.Get(ctx, client.ObjectKey{
Name: config.Spec.ServiceProvider.Credentials.Name,
Namespace: config.Spec.ServiceProvider.Credentials.Namespace,
}, &providerKeySecret); errors.IsNotFound(err) {
return admission.Denied("credentials secret not found")
}
if err := m.validateFlagSourceConfiguration(ctx, config); err != nil {
return admission.Denied(err.Error())
}

return admission.Allowed("")
Expand Down Expand Up @@ -92,3 +75,28 @@ func validateJSONSchema(schemaJSON string, inputJSON string) error {
}
return nil
}

func (m *FeatureFlagConfigurationValidator) validateFlagSourceConfiguration(ctx context.Context, config corev1alpha1.FeatureFlagConfiguration) error {
if config.Spec.FeatureFlagSpec != "" {
if !m.isJSON(config.Spec.FeatureFlagSpec) {
return fmt.Errorf("FeatureFlagSpec is not valid JSON: %s", config.Spec.FeatureFlagSpec)
}
err := validateJSONSchema(schemas.FlagdDefinitions, config.Spec.FeatureFlagSpec)
if err != nil {
return fmt.Errorf("FeatureFlagSpec is not valid JSON: %s", err.Error())
}
}

if config.Spec.ServiceProvider != nil && config.Spec.ServiceProvider.Credentials != nil {
// Check the provider and whether it has an existing secret
providerKeySecret := corev1.Secret{}
if err := m.Client.Get(ctx, client.ObjectKey{
Name: config.Spec.ServiceProvider.Credentials.Name,
Namespace: config.Spec.ServiceProvider.Credentials.Namespace,
}, &providerKeySecret); errors.IsNotFound(err) {
return fmt.Errorf("credentials secret not found")
}
}

return nil
}
238 changes: 125 additions & 113 deletions webhooks/featureflagconfiguration_webhook_test.go
Original file line number Diff line number Diff line change
@@ -1,127 +1,139 @@
package webhooks

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"context"
"fmt"
"testing"

"github.com/open-feature/open-feature-operator/apis/core/v1alpha1"
corev1alpha1 "github.com/open-feature/open-feature-operator/apis/core/v1alpha1"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
featureFlagConfigurationNamespace = "test-validate-featureflagconfiguration"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

var featureFlagSpec = `
{
"flags": {
"new-welcome-message": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "on"
}
}
}
`
func TestFeatureFlagConfigurationWebhook_validateFlagSourceConfiguration(t *testing.T) {
const credentialsName = "credentials-name"
const featureFlagConfigurationName = "test-feature-flag-configuration"

func setupValidateFeatureFlagConfigurationResources() {
ns := &corev1.Namespace{}
ns.Name = featureFlagConfigurationNamespace
err := k8sClient.Create(testCtx, ns)
Expect(err).ShouldNot(HaveOccurred())
}

func featureflagconfigurationCleanup() {
ffConfig := &corev1alpha1.FeatureFlagConfiguration{}
ffConfig.Namespace = featureFlagConfigurationNamespace
ffConfig.Name = featureFlagConfigurationName
err := k8sClient.Delete(testCtx, ffConfig, client.GracePeriodSeconds(0))
Expect(err).ShouldNot(HaveOccurred())
}

var _ = Describe("featureflagconfiguration validation webhook", func() {
It("should pass when featureflagspec contains valid json", func() {
ffConfig := &corev1alpha1.FeatureFlagConfiguration{}
ffConfig.Namespace = featureFlagConfigurationNamespace
ffConfig.Name = featureFlagConfigurationName
ffConfig.Spec.FeatureFlagSpec = featureFlagSpec
err := k8sClient.Create(testCtx, ffConfig)
Expect(err).ShouldNot(HaveOccurred())

featureflagconfigurationCleanup()
})
tests := []struct {
name string
obj corev1alpha1.FeatureFlagConfiguration
secret *corev1.Secret
out error
}{
{
name: "valid without ServiceProvider",
obj: corev1alpha1.FeatureFlagConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: featureFlagConfigurationName,
Namespace: featureFlagConfigurationNamespace,
},
Spec: corev1alpha1.FeatureFlagConfigurationSpec{
FeatureFlagSpec: featureFlagSpec,
},
},
out: nil,
},
{
name: "invalid json",
obj: corev1alpha1.FeatureFlagConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: featureFlagConfigurationName,
Namespace: featureFlagConfigurationNamespace,
},
Spec: corev1alpha1.FeatureFlagConfigurationSpec{
FeatureFlagSpec: `{"invalid":json}`,
},
},
out: fmt.Errorf("FeatureFlagSpec is not valid JSON: {\"invalid\":json}"),
},
{
name: "invalid schema",
obj: corev1alpha1.FeatureFlagConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: featureFlagConfigurationName,
Namespace: featureFlagConfigurationNamespace,
},
Spec: corev1alpha1.FeatureFlagConfigurationSpec{
FeatureFlagSpec: `{
"flags":{
"foo": {}
}
}`,
},
},
out: fmt.Errorf("FeatureFlagSpec is not valid JSON: - flags.foo: Must validate one and only one schema (oneOf)\n- flags.foo: state is required\n- flags.foo: defaultVariant is required\n- flags.foo: Must validate all the schemas (allOf)\n"),
},
{
name: "valid with ServiceProvider",
obj: corev1alpha1.FeatureFlagConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: featureFlagConfigurationName,
Namespace: featureFlagConfigurationNamespace,
},
Spec: corev1alpha1.FeatureFlagConfigurationSpec{
FeatureFlagSpec: featureFlagSpec,
ServiceProvider: &corev1alpha1.FeatureFlagServiceProvider{
Name: "flagd",
Credentials: &corev1.ObjectReference{
Name: credentialsName,
Namespace: featureFlagConfigurationNamespace,
},
},
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: credentialsName,
Namespace: featureFlagConfigurationNamespace,
},
},
out: nil,
},
{
name: "non-existing secret in ServiceProvider",
obj: corev1alpha1.FeatureFlagConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: featureFlagConfigurationName,
Namespace: featureFlagConfigurationNamespace,
},
Spec: corev1alpha1.FeatureFlagConfigurationSpec{
FeatureFlagSpec: featureFlagSpec,
ServiceProvider: &corev1alpha1.FeatureFlagServiceProvider{
Name: "flagd",
Credentials: &corev1.ObjectReference{
Name: credentialsName,
Namespace: featureFlagConfigurationNamespace,
},
},
},
},
out: fmt.Errorf("credentials secret not found"),
},
}

It("should fail when featureflagspec contains invalid json", func() {
ffConfig := &corev1alpha1.FeatureFlagConfiguration{}
ffConfig.Namespace = featureFlagConfigurationNamespace
ffConfig.Name = featureFlagConfigurationName
ffConfig.Spec.FeatureFlagSpec = `{"invalid":json}`
err := k8sClient.Create(testCtx, ffConfig)
Expect(err).Should(HaveOccurred())
})
err := v1alpha1.AddToScheme(scheme.Scheme)
require.Nil(t, err)

It("should fail when featureflagspec doesn't conform to the schema", func() {
ffConfig := &corev1alpha1.FeatureFlagConfiguration{}
ffConfig.Namespace = featureFlagConfigurationNamespace
ffConfig.Name = featureFlagConfigurationName
ffConfig.Spec.FeatureFlagSpec = `
{
"flags":{
"foo": {}
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validator := FeatureFlagConfigurationValidator{
Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build(),
Log: ctrl.Log.WithName("webhook"),
}
`
err := k8sClient.Create(testCtx, ffConfig)
Expect(err).Should(HaveOccurred())
})

It("should check for existence of provider secret when service provider is given", func() {
const credentialsName = "credentials-name"
providerKeySecret := &corev1.Secret{}
providerKeySecret.Name = credentialsName
providerKeySecret.Namespace = featureFlagConfigurationNamespace
err := k8sClient.Create(testCtx, providerKeySecret)
Expect(err).ShouldNot(HaveOccurred())

ffConfig := &corev1alpha1.FeatureFlagConfiguration{}
ffConfig.Namespace = featureFlagConfigurationNamespace
ffConfig.Name = featureFlagConfigurationName
ffConfig.Spec.FeatureFlagSpec = featureFlagSpec
ffConfig.Spec.ServiceProvider = &corev1alpha1.FeatureFlagServiceProvider{
Name: "flagd",
Credentials: &corev1.ObjectReference{
Name: credentialsName,
Namespace: featureFlagConfigurationNamespace,
},
}
err = k8sClient.Create(testCtx, ffConfig)
Expect(err).ShouldNot(HaveOccurred())

featureflagconfigurationCleanup()

// cleanup secret
err = k8sClient.Delete(testCtx, providerKeySecret)
Expect(err).ShouldNot(HaveOccurred())
})
if tt.secret != nil {
err := validator.Client.Create(context.TODO(), tt.secret)
require.Nil(t, err)
}

It("should fail if provider secret doesn't exist when service provider is given", func() {
const credentialsName = "credentials-name"
out := validator.validateFlagSourceConfiguration(context.TODO(), tt.obj)
require.Equal(t, tt.out, out)
})

ffConfig := &corev1alpha1.FeatureFlagConfiguration{}
ffConfig.Namespace = featureFlagConfigurationNamespace
ffConfig.Name = featureFlagConfigurationName
ffConfig.Spec.FeatureFlagSpec = featureFlagSpec
ffConfig.Spec.ServiceProvider = &corev1alpha1.FeatureFlagServiceProvider{
Name: "flagd",
Credentials: &corev1.ObjectReference{
Name: credentialsName,
Namespace: featureFlagConfigurationNamespace,
},
}
err := k8sClient.Create(testCtx, ffConfig)
Expect(err).Should(HaveOccurred())
})
})
}
}
21 changes: 21 additions & 0 deletions webhooks/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ var (
const (
podMutatingWebhookPath = "/mutate-v1-pod"
validatingFeatureFlagConfigurationWebhookPath = "/validate-v1alpha1-featureflagconfiguration"
featureFlagConfigurationNamespace = "test-validate-featureflagconfiguration"
featureFlagSpec = `
{
"flags": {
"new-welcome-message": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "on"
}
}
}`
)

func strPtr(s string) *string { return &s }
Expand Down Expand Up @@ -249,6 +263,13 @@ var _ = BeforeSuite(func() {

})

func setupValidateFeatureFlagConfigurationResources() {
ns := &corev1.Namespace{}
ns.Name = featureFlagConfigurationNamespace
err := k8sClient.Create(testCtx, ns)
Expect(err).ShouldNot(HaveOccurred())
}

var _ = AfterSuite(func() {
By("tearing down the test environment")
testCancel()
Expand Down

0 comments on commit 29c7c28

Please sign in to comment.