github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/webhooks/appconfig_defaulter_test.go (about) 1 // Copyright (c) 2020, 2022, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package webhooks 5 6 import ( 7 "context" 8 "fmt" 9 "net/http" 10 "os" 11 "path/filepath" 12 "testing" 13 14 "github.com/crossplane/oam-kubernetes-runtime/apis/core" 15 oamv1 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" 16 "github.com/golang/mock/gomock" 17 "github.com/prometheus/client_golang/prometheus/testutil" 18 "github.com/stretchr/testify/assert" 19 "github.com/verrazzano/verrazzano/application-operator/metricsexporter" 20 "github.com/verrazzano/verrazzano/application-operator/mocks" 21 "go.uber.org/zap" 22 istiofake "istio.io/client-go/pkg/clientset/versioned/fake" 23 admissionv1 "k8s.io/api/admission/v1" 24 "k8s.io/apimachinery/pkg/runtime" 25 "k8s.io/client-go/kubernetes/fake" 26 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 27 "sigs.k8s.io/yaml" 28 ) 29 30 func decoder() *admission.Decoder { 31 scheme := runtime.NewScheme() 32 _ = core.AddToScheme(scheme) 33 decoder, err := admission.NewDecoder(scheme) 34 if err != nil { 35 zap.S().Errorf("Failed creating new decoder: %v", err) 36 } 37 return decoder 38 } 39 40 // TestAppConfigDefaulterHandleError tests handling an invalid appconfig admission.Request 41 // GIVEN a AppConfigDefaulter and an appconfig admission.Request 42 // WHEN Handle is called with an invalid admission.Request containing no content 43 // THEN Handle should return an error with http.StatusBadRequest 44 func TestAppConfigDefaulterHandleError(t *testing.T) { 45 46 decoder := decoder() 47 defaulter := &AppConfigWebhook{} 48 _ = defaulter.InjectDecoder(decoder) 49 req := admission.Request{} 50 res := defaulter.Handle(context.TODO(), req) 51 assert.False(t, res.Allowed) 52 assert.Equal(t, int32(http.StatusBadRequest), res.Result.Code) 53 } 54 55 // TestAppConfigDefaulterHandle tests handling an appconfig admission.Request 56 // GIVEN a AppConfigDefaulter and an appconfig admission.Request 57 // WHEN Handle is called with an admission.Request containing appconfig 58 // THEN Handle should return a patch response 59 func TestAppConfigDefaulterHandle(t *testing.T) { 60 61 decoder := decoder() 62 defaulter := &AppConfigWebhook{} 63 _ = defaulter.InjectDecoder(decoder) 64 req := admission.Request{} 65 req.Object = runtime.RawExtension{Raw: readYaml2Json(t, "hello-conf.yaml")} 66 res := defaulter.Handle(context.TODO(), req) 67 assert.True(t, res.Allowed) 68 assert.NotEqual(t, 0, len(res.Patches)) 69 } 70 71 // TestAppConfigWebhookHandleDelete tests handling an appconfig Delete admission.Request 72 // GIVEN a AppConfigWebhook and an appconfig Delete admission.Request 73 // WHEN Handle is called with an admission.Request containing appconfig 74 // THEN Handle should return an Allowed response with no patch 75 func TestAppConfigWebhookHandleDelete(t *testing.T) { 76 77 testAppConfigWebhookHandleDelete(t, true, true, false) 78 } 79 80 // TestAppConfigWebhookHandleDeleteDryRun tests handling a dry run appconfig Delete admission.Request 81 // GIVEN a AppConfigWebhook and an appconfig Delete admission.Request 82 // WHEN Handle is called with an admission.Request containing appconfig and set for a dry run 83 // THEN Handle should return an Allowed response with no patch 84 func TestAppConfigWebhookHandleDeleteDryRun(t *testing.T) { 85 86 testAppConfigWebhookHandleDelete(t, true, true, true) 87 } 88 89 // TestAppConfigWebhookHandleDeleteCertNotFound tests handling an appconfig Delete admission.Request where the app config cert is not found 90 // GIVEN a AppConfigWebhook and an appconfig Delete admission.Request 91 // 92 // WHEN Handle is called with an admission.Request containing appconfig and the cert is not found 93 // THEN Handle should return an Allowed response with no patch 94 func TestAppConfigWebhookHandleDeleteCertNotFound(t *testing.T) { 95 96 testAppConfigWebhookHandleDelete(t, false, true, false) 97 } 98 99 // TestAppConfigWebhookHandleDeleteSecretNotFound tests handling an appconfig Delete admission.Request where the app config secret is not found 100 // GIVEN a AppConfigWebhook and an appconfig Delete admission.Request 101 // 102 // WHEN Handle is called with an admission.Request containing appconfig and the secret is not found 103 // THEN Handle should return an Allowed response with no patch 104 func TestAppConfigWebhookHandleDeleteSecretNotFound(t *testing.T) { 105 106 testAppConfigWebhookHandleDelete(t, true, false, false) 107 } 108 109 // TestAppConfigDefaulterHandleMarshalError tests handling an appconfig with json marshal error 110 // GIVEN a AppConfigDefaulter with mock appconfigMarshalFunc 111 // WHEN Handle is called with an admission.Request containing appconfig 112 // THEN Handle should return error with http.StatusInternalServerError 113 func TestAppConfigDefaulterHandleMarshalError(t *testing.T) { 114 115 decoder := decoder() 116 defaulter := &AppConfigWebhook{} 117 _ = defaulter.InjectDecoder(decoder) 118 req := admission.Request{} 119 req.Object = runtime.RawExtension{Raw: readYaml2Json(t, "hello-conf.yaml")} 120 appconfigMarshalFunc = func(v interface{}) ([]byte, error) { 121 return nil, fmt.Errorf("json marshal error") 122 } 123 res := defaulter.Handle(context.TODO(), req) 124 assert.False(t, res.Allowed) 125 assert.Equal(t, int32(http.StatusInternalServerError), res.Result.Code) 126 } 127 128 type mockErrorDefaulter struct { 129 } 130 131 func (*mockErrorDefaulter) Default(appConfig *oamv1.ApplicationConfiguration, dryRun bool, log *zap.SugaredLogger) error { 132 return fmt.Errorf("mockErrorDefaulter error") 133 } 134 135 func (*mockErrorDefaulter) Cleanup(appConfig *oamv1.ApplicationConfiguration, dryRun bool, log *zap.SugaredLogger) error { 136 return nil 137 } 138 139 // TestAppConfigDefaulterHandleDefaultError tests handling a defaulter error 140 // GIVEN a AppConfigDefaulter with mock defaulter 141 // WHEN Handle is called with an admission.Request containing appconfig 142 // THEN Handle should return error with http.StatusInternalServerError 143 func TestAppConfigDefaulterHandleDefaultError(t *testing.T) { 144 145 decoder := decoder() 146 defaulter := &AppConfigWebhook{Defaulters: []AppConfigDefaulter{&mockErrorDefaulter{}}} 147 _ = defaulter.InjectDecoder(decoder) 148 req := admission.Request{} 149 req.Object = runtime.RawExtension{Raw: readYaml2Json(t, "hello-conf.yaml")} 150 res := defaulter.Handle(context.TODO(), req) 151 assert.False(t, res.Allowed) 152 assert.Equal(t, int32(http.StatusInternalServerError), res.Result.Code) 153 } 154 155 // TestHandleFailed tests to make sure the failure metric is being exposed 156 func TestHandleFailed(t *testing.T) { 157 158 assert := assert.New(t) 159 // Create a request and decode(Handle) it 160 decoder := decoder() 161 defaulter := &AppConfigWebhook{} 162 _ = defaulter.InjectDecoder(decoder) 163 req := admission.Request{} 164 defaulter.Handle(context.TODO(), req) 165 reconcileerrorCounterObject, err := metricsexporter.GetSimpleCounterMetric(metricsexporter.AppconfigHandleError) 166 assert.NoError(err) 167 // Expect a call to fetch the error 168 reconcileFailedCounterBefore := testutil.ToFloat64(reconcileerrorCounterObject.Get()) 169 reconcileerrorCounterObject.Get().Inc() 170 reconcileFailedCounterAfter := testutil.ToFloat64(reconcileerrorCounterObject.Get()) 171 assert.Equal(reconcileFailedCounterBefore, reconcileFailedCounterAfter-1) 172 } 173 174 func testAppConfigWebhookHandleDelete(t *testing.T, certFound, secretFound, dryRun bool) { 175 mocker := gomock.NewController(t) 176 mockClient := mocks.NewMockClient(mocker) 177 178 if !dryRun { 179 // list projects 180 mockClient.EXPECT(). 181 List(gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()) 182 183 } 184 decoder := decoder() 185 186 webhook := &AppConfigWebhook{ 187 Client: mockClient, 188 KubeClient: fake.NewSimpleClientset(), 189 IstioClient: istiofake.NewSimpleClientset(), 190 } 191 _ = webhook.InjectDecoder(decoder) 192 req := admission.Request{ 193 AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Delete}, 194 } 195 req.OldObject = runtime.RawExtension{Raw: readYaml2Json(t, "hello-conf.yaml")} 196 if dryRun { 197 dryRun := true 198 req.DryRun = &dryRun 199 } 200 res := webhook.Handle(context.TODO(), req) 201 assert.True(t, res.Allowed) 202 assert.Equal(t, 0, len(res.Patches)) 203 } 204 205 func readYaml2Json(t *testing.T, path string) []byte { 206 filename, _ := filepath.Abs(fmt.Sprintf("testdata/%s", path)) 207 yamlBytes, err := os.ReadFile(filename) 208 if err != nil { 209 t.Fatalf("Error reading %v: %v", path, err) 210 } 211 jsonBytes, err := yaml.YAMLToJSON(yamlBytes) 212 if err != nil { 213 zap.S().Errorf("Failed json marshal: %v", err) 214 } 215 return jsonBytes 216 }