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  }