From e2f9a6923792cf0c74e0da5c39984d14262c282d Mon Sep 17 00:00:00 2001 From: Gabriel Saratura Date: Wed, 1 Jun 2022 16:38:07 +0200 Subject: [PATCH] Add tests --- Makefile | 5 + operator/operatortest/envtest.go | 4 +- operator/standalone/create_it_test.go | 16 +- operator/standalone/create_test.go | 8 +- operator/standalone/delete.go | 30 ++- operator/standalone/delete_it_test.go | 301 ++++++++++++++++++++++++++ 6 files changed, 333 insertions(+), 31 deletions(-) create mode 100644 operator/standalone/delete_it_test.go diff --git a/Makefile b/Makefile index d10b2ab..b5056db 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,11 @@ install-samples: export KUBECONFIG = $(KIND_KUBECONFIG) install-samples: generate-go install-crd ## Install samples into cluster yq package/samples/*.yaml | kubectl apply -f - +.PHONY: delete-instance +delete-instance: export KUBECONFIG = $(KIND_KUBECONFIG) +delete-instance: ## Deletes sample instance if it exists + kubectl delete -f package/samples/postgresql.appcat.vshn.io_postgresqlstandalone.yaml --ignore-not-found=true + .PHONY: run-operator run-operator: ## Run in Operator mode against your current kube context go run . -v 1 operator diff --git a/operator/operatortest/envtest.go b/operator/operatortest/envtest.go index a85ecb2..f07bece 100644 --- a/operator/operatortest/envtest.go +++ b/operator/operatortest/envtest.go @@ -60,8 +60,8 @@ func (ts *Suite) SetupSuite() { info, err := os.Stat(envtestAssets) absEnvtestAssets, _ := filepath.Abs(envtestAssets) - ts.Require().NoErrorf(err, "'%s' does not seem to exist. Check KUBEBUILDER_ASSETS and make sure you run `make integration-test` before you run this test in your IDE.", absEnvtestAssets) - ts.Require().Truef(info.IsDir(), "'%s' does not seem to be a directory. Check KUBEBUILDER_ASSETS and make sure you run `make integration-test` before you run this test in your IDE.", absEnvtestAssets) + ts.Require().NoErrorf(err, "'%s' does not seem to exist. Check KUBEBUILDER_ASSETS and make sure you run `make test-integration` before you run this test in your IDE.", absEnvtestAssets) + ts.Require().Truef(info.IsDir(), "'%s' does not seem to be a directory. Check KUBEBUILDER_ASSETS and make sure you run `make test-integration` before you run this test in your IDE.", absEnvtestAssets) absCrds, _ := filepath.Abs(crdDir) info, err = os.Stat(crdDir) diff --git a/operator/standalone/create_it_test.go b/operator/standalone/create_it_test.go index 59a1a3e..ee848af 100644 --- a/operator/standalone/create_it_test.go +++ b/operator/standalone/create_it_test.go @@ -76,7 +76,7 @@ func (ts *CreateStandalonePipelineSuite) Test_FetchOperatorConfig() { p := &CreateStandalonePipeline{ operatorNamespace: tc.givenNamespace, client: ts.Client, - instance: newInstance("instance"), + instance: newInstance("instance", "my-app"), } tc.prepare() err := p.fetchOperatorConfig(ts.Context) @@ -93,7 +93,7 @@ func (ts *CreateStandalonePipelineSuite) Test_FetchOperatorConfig() { func (ts *CreateStandalonePipelineSuite) Test_EnsureDeploymentNamespace() { // Arrange p := &CreateStandalonePipeline{ - instance: newInstance("test-ensure-namespace"), + instance: newInstance("test-ensure-namespace", "my-app"), client: ts.Client, } currentRand := namegeneratorRNG @@ -117,7 +117,7 @@ func (ts *CreateStandalonePipelineSuite) Test_EnsureCredentialSecret() { ns := ServiceNamespacePrefix + "my-app-instance" ts.EnsureNS(ns) p := &CreateStandalonePipeline{ - instance: newInstance("instance"), + instance: newInstance("instance", "my-app"), client: ts.Client, deploymentNamespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}, } @@ -137,7 +137,7 @@ func (ts *CreateStandalonePipelineSuite) Test_EnsureCredentialSecret() { func (ts *CreateStandalonePipelineSuite) Test_EnsureHelmRelease() { // Arrange p := &CreateStandalonePipeline{ - instance: newInstance("instance"), + instance: newInstance("instance", "my-app"), client: ts.Client, helmChart: &v1alpha1.ChartMeta{Repository: "https://host/path", Version: "version", Name: "postgres"}, helmValues: helmvalues.V{"key": "value"}, @@ -160,7 +160,7 @@ func (ts *CreateStandalonePipelineSuite) Test_EnsureHelmRelease() { func (ts *CreateStandalonePipelineSuite) Test_EnrichStatus() { // Arrange p := &CreateStandalonePipeline{ - instance: newInstance("enrich-status"), + instance: newInstance("enrich-status", "my-app"), client: ts.Client, helmChart: &v1alpha1.ChartMeta{Repository: "https://host/path", Version: "version", Name: "postgres"}, deploymentNamespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: generateClusterScopedNameForInstance()}}, @@ -186,7 +186,7 @@ func (ts *CreateStandalonePipelineSuite) Test_EnrichStatus() { func (ts *CreateStandalonePipelineSuite) Test_FetchHelmRelease() { // Arrange p := &CreateStandalonePipeline{ - instance: newInstance("fetch-release"), + instance: newInstance("fetch-release", "my-app"), client: ts.Client, } p.instance.Status.HelmChart = &v1alpha1.ChartMetaStatus{ @@ -208,7 +208,7 @@ func (ts *CreateStandalonePipelineSuite) Test_FetchHelmRelease() { func (ts *CreateStandalonePipelineSuite) Test_FetchCredentialSecret() { // Arrange p := CreateStandalonePipeline{ - instance: newInstance("fetch-credentials"), + instance: newInstance("fetch-credentials", "my-app"), } credentialSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -241,7 +241,7 @@ func (ts *CreateStandalonePipelineSuite) Test_FetchCredentialSecret() { func (ts *CreateStandalonePipelineSuite) Test_FetchService() { // Arrange p := CreateStandalonePipeline{ - instance: newInstance("fetch-service"), + instance: newInstance("fetch-service", "my-app"), } service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ diff --git a/operator/standalone/create_test.go b/operator/standalone/create_test.go index 491f6c9..16942d9 100644 --- a/operator/standalone/create_test.go +++ b/operator/standalone/create_test.go @@ -115,7 +115,7 @@ func TestCreateStandalonePipeline_OverrideTemplateValues(t *testing.T) { func TestCreateStandalonePipeline_ApplyValuesFromInstance(t *testing.T) { p := CreateStandalonePipeline{ config: newPostgresqlStandaloneOperatorConfig("cfg", "postgresql-system"), - instance: newInstance("instance"), + instance: newInstance("instance", "my-app"), } err := p.applyValuesFromInstance(nil) require.NoError(t, err) @@ -153,7 +153,7 @@ func TestCreateStandalonePipeline_ApplyValuesFromInstance(t *testing.T) { func TestCreateStandalonePipeline_IsHelmReleaseReady(t *testing.T) { p := CreateStandalonePipeline{ - instance: newInstance("release-ready"), + instance: newInstance("release-ready", "my-app"), } p.instance.Status.HelmChart = &v1alpha1.ChartMetaStatus{} @@ -207,9 +207,9 @@ func newPostgresqlStandaloneOperatorConfig(name string, namespace string) *v1alp }, } } -func newInstance(name string) *v1alpha1.PostgresqlStandalone { +func newInstance(name string, namespace string) *v1alpha1.PostgresqlStandalone { return &v1alpha1.PostgresqlStandalone{ - ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "my-app"}, + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: v1alpha1.PostgresqlStandaloneSpec{ Parameters: v1alpha1.PostgresqlStandaloneParameters{ MajorVersion: v1alpha1.PostgresqlVersion14, diff --git a/operator/standalone/delete.go b/operator/standalone/delete.go index a9ec898..41b5045 100644 --- a/operator/standalone/delete.go +++ b/operator/standalone/delete.go @@ -50,15 +50,14 @@ func (d *DeleteStandalonePipeline) RunPipeline(ctx context.Context) error { func (d *DeleteStandalonePipeline) deleteHelmRelease(ctx context.Context) error { helmRelease := &helmv1beta1.Release{ ObjectMeta: metav1.ObjectMeta{ - Name: d.instance.Status.HelmChart.DeploymentNamespace, - Namespace: d.instance.Status.HelmChart.DeploymentNamespace, + Name: d.instance.Status.HelmChart.DeploymentNamespace, }, } err := d.client.Delete(ctx, helmRelease) if err != nil && apierrors.IsNotFound(err) { d.helmReleaseDeleted = true } - return skipOnNotFound(err) + return client.IgnoreNotFound(err) } // deleteNamespace removes the namespace of the PostgreSQL instance @@ -66,11 +65,14 @@ func (d *DeleteStandalonePipeline) deleteHelmRelease(ctx context.Context) error func (d *DeleteStandalonePipeline) deleteNamespace(ctx context.Context) error { deploymentNamespace := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: d.instance.Status.HelmChart.DeploymentNamespace, - Namespace: d.instance.Status.HelmChart.DeploymentNamespace, + Name: d.instance.Status.HelmChart.DeploymentNamespace, }, } - return skipOnNotFound(d.client.Delete(ctx, deploymentNamespace)) + propagation := metav1.DeletePropagationBackground + deleteOptions := &client.DeleteOptions{ + PropagationPolicy: &propagation, + } + return client.IgnoreNotFound(d.client.Delete(ctx, deploymentNamespace, deleteOptions)) } // deleteConnectionSecret removes the connection secret of the PostgreSQL instance @@ -81,24 +83,18 @@ func (d *DeleteStandalonePipeline) deleteConnectionSecret(ctx context.Context) e Namespace: d.instance.Namespace, }, } - return skipOnNotFound(d.client.Delete(ctx, connectionSecret)) + return client.IgnoreNotFound(d.client.Delete(ctx, connectionSecret)) } // removeFinalizer removes the finalizer from the PostgreSQL CRD func (d *DeleteStandalonePipeline) removeFinalizer(ctx context.Context) error { - controllerutil.RemoveFinalizer(d.instance, finalizer) - return d.client.Update(ctx, d.instance) + if controllerutil.RemoveFinalizer(d.instance, finalizer) { + return d.client.Update(ctx, d.instance) + } + return nil } // isHelmReleaseDeleted checks whether the Release was completely deleted func (d *DeleteStandalonePipeline) isHelmReleaseDeleted(_ context.Context) bool { return d.helmReleaseDeleted } - -// skipOnNotFound treats not found error as nil -func skipOnNotFound(err error) error { - if err != nil && !apierrors.IsNotFound(err) { - return err - } - return nil -} diff --git a/operator/standalone/delete_it_test.go b/operator/standalone/delete_it_test.go new file mode 100644 index 0000000..54302ff --- /dev/null +++ b/operator/standalone/delete_it_test.go @@ -0,0 +1,301 @@ +//go:build integration + +package standalone + +import ( + "context" + "github.com/vshn/appcat-service-postgresql/apis/postgresql/v1alpha1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" + + pipeline "github.com/ccremer/go-command-pipeline" + helmv1beta1 "github.com/crossplane-contrib/provider-helm/apis/release/v1beta1" + "github.com/stretchr/testify/suite" + "github.com/vshn/appcat-service-postgresql/operator/operatortest" +) + +type DeleteStandalonePipelineSuite struct { + operatortest.Suite +} + +func TestDeleteStandalonePipeline(t *testing.T) { + suite.Run(t, new(DeleteStandalonePipelineSuite)) +} + +func (ts *DeleteStandalonePipelineSuite) BeforeTest(suiteName, testName string) { + ts.Context = pipeline.MutableContext(context.Background()) + setClientInContext(ts.Context, ts.Client) + ts.RegisterScheme(helmv1beta1.SchemeBuilder.AddToScheme) +} + +func (ts *DeleteStandalonePipelineSuite) Test_DeleteHelmRelease() { + tests := map[string]struct { + prepare func(releaseNameString string) + givenReleaseName string + expectedHelmReleaseDeleted bool + expectedError string + }{ + "GivenNonExistingHelmRelease_WhenDeleting_ThenExpectNoFurtherAction": { + prepare: func(releaseName string) {}, + givenReleaseName: "postgresql-release", + expectedHelmReleaseDeleted: true, + expectedError: "", + }, + "GivenAnExistingHelmRelease_WhenHelmReleaseStillExists_ThenExpectReconciliation": { + prepare: func(releaseName string) { + release := newPostgresqlHelmRelease(releaseName) + ts.EnsureResources(release) + }, + givenReleaseName: "postgresql-release", + expectedHelmReleaseDeleted: false, + expectedError: "", + }, + "GivenAnExistingHelmRelease_WhenDeletingGeneratesAnError_ThenReturnError": { + prepare: func(releaseName string) {}, + // we purposefully set namespace to empty so that we generate an error so that we avoid mocking. + givenReleaseName: "", + expectedHelmReleaseDeleted: false, + expectedError: "resource name may not be empty", + }, + } + for name, tc := range tests { + ts.Run(name, func() { + d := &DeleteStandalonePipeline{ + client: ts.Client, + instance: newBuilderInstance("instance", "namespace"). + setDeploymentNamespace(tc.givenReleaseName). + get(), + helmReleaseDeleted: false, + } + tc.prepare(tc.givenReleaseName) + err := d.deleteHelmRelease(ts.Context) + + // Assert + ts.Assert().Equal(tc.expectedHelmReleaseDeleted, d.helmReleaseDeleted) + if tc.expectedError != "" { + ts.Assert().EqualError(err, tc.expectedError) + return + } + ts.Assert().NoError(err) + resultRelease := &helmv1beta1.Release{} + err = ts.Client.Get( + ts.Context, + client.ObjectKey{Name: tc.givenReleaseName}, + resultRelease, + ) + ts.AssertResourceNotExists(resultRelease.GetDeletionTimestamp(), err) + }) + } +} + +func (ts *DeleteStandalonePipelineSuite) Test_DeleteNamespace() { + tests := map[string]struct { + prepare func(namespace string) + givenNamespace string + expectedError string + }{ + "GivenNonExistingNamespace_WhenDeleting_ThenExpectNoFurtherAction": { + prepare: func(namespace string) {}, + givenNamespace: "non-existing-namespace", + expectedError: "", + }, + "GivenExistingNamespace_WhenDeleting_ThenExpectNoFurtherAction": { + prepare: func(namespace string) { ts.EnsureNS(namespace) }, + givenNamespace: "existing-namespace", + expectedError: "", + }, + "GivenExistingNamespace_WhenDeletingGeneratesAnError_ThenReturnError": { + prepare: func(namespace string) { ts.EnsureNS("an-existing-namespace") }, + // we purposefully set namespace to empty so that we generate an error so that we avoid mocking. + givenNamespace: "", + expectedError: "resource name may not be empty", + }, + } + for name, tc := range tests { + ts.Run(name, func() { + d := &DeleteStandalonePipeline{ + client: ts.Client, + instance: newBuilderInstance("instance", "namespace"). + setDeploymentNamespace(tc.givenNamespace). + get(), + helmReleaseDeleted: false, + } + tc.prepare(tc.givenNamespace) + err := d.deleteNamespace(ts.Context) + + // Assert + if tc.expectedError != "" { + ts.Assert().EqualError(err, tc.expectedError) + return + } + ts.Assert().NoError(err) + resultNs := &corev1.Namespace{} + err = ts.Client.Get( + ts.Context, + types.NamespacedName{Name: tc.givenNamespace}, + resultNs, + ) + ts.AssertResourceNotExists(resultNs.GetDeletionTimestamp(), err) + }) + } +} + +func (ts *DeleteStandalonePipelineSuite) Test_DeleteConnectionSecret() { + tests := map[string]struct { + prepare func(name, namespace string) + givenNamespace string + givenSecret string + expectedError string + }{ + "GivenNonExistingSecret_WhenDeleting_ThenExpectNoFurtherAction": { + prepare: func(name, namespace string) {}, + givenNamespace: "test-namespace", + givenSecret: "non-existing-secret", + expectedError: "", + }, + "GivenExistingSecret_WhenDeleting_ThenExpectNoFurtherAction": { + prepare: func(name, namespace string) { + ts.EnsureNS(namespace) + ts.EnsureResources(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}) + }, + givenNamespace: "test-namespace", + givenSecret: "existing-secret", + expectedError: "", + }, + "GivenExistingSecret_WhenDeletingGeneratesAnError_ThenReturnError": { + prepare: func(name, namespace string) { + ts.EnsureNS(namespace) + ts.EnsureResources(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "secret", Namespace: namespace}}) + }, + givenNamespace: "test-namespace", + // we purposefully set namespace to empty so that we generate an error so that we avoid mocking. + givenSecret: "", + expectedError: "resource name may not be empty", + }, + } + for name, tc := range tests { + ts.Run(name, func() { + d := &DeleteStandalonePipeline{ + client: ts.Client, + instance: newBuilderInstance("instance", tc.givenNamespace). + setConnectionSecret(tc.givenSecret). + get(), + helmReleaseDeleted: false, + } + tc.prepare(tc.givenSecret, tc.givenNamespace) + err := d.deleteConnectionSecret(ts.Context) + + // Assert + if tc.expectedError != "" { + ts.Assert().EqualError(err, tc.expectedError) + return + } + ts.Assert().NoError(err) + resultSecret := &corev1.Secret{} + err = ts.Client.Get( + ts.Context, + types.NamespacedName{Name: tc.givenSecret, Namespace: tc.givenNamespace}, + resultSecret, + ) + ts.AssertResourceNotExists(resultSecret.GetDeletionTimestamp(), err) + }) + } +} + +func (ts *DeleteStandalonePipelineSuite) Test_RemoveFinalizer() { + tests := map[string]struct { + prepare func(instance *v1alpha1.PostgresqlStandalone) + givenInstance string + givenNamespace string + }{ + "GivenAnInstance_WhenDeletingFinalizer_ThenExpectInstanceWithNoFinalizer": { + prepare: func(instance *v1alpha1.PostgresqlStandalone) { + ts.EnsureNS("remove-finalizer") + ts.EnsureResources(instance) + }, + + givenInstance: "instance", + givenNamespace: "remove-finalizer", + }, + } + for name, tc := range tests { + ts.Run(name, func() { + instance := newBuilderInstance(tc.givenInstance, tc.givenNamespace).setFinalizers(finalizer).get() + d := &DeleteStandalonePipeline{ + client: ts.Client, + instance: instance, + helmReleaseDeleted: false, + } + tc.prepare(instance) + err := d.removeFinalizer(ts.Context) + // Assert + ts.Require().NoError(err) + releaseResult := &helmv1beta1.Release{} + err = ts.Client.Get( + ts.Context, + types.NamespacedName{Name: tc.givenInstance, Namespace: tc.givenNamespace}, + releaseResult, + ) + ts.Assert().Empty(releaseResult.Finalizers) + }) + } +} + +func newPostgresqlHelmRelease(name string) *helmv1beta1.Release { + return &helmv1beta1.Release{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } +} + +// AssertResourceNotExists checks if the given resource is not existing or is existing with a deletion timestamp. +// Test fails if the resource exists or there's another error. +func (ts *DeleteStandalonePipelineSuite) AssertResourceNotExists(deletionTime *metav1.Time, err error) { + if err != nil { + ts.Require().True(apierrors.IsNotFound(err)) + } else { + ts.Require().False(deletionTime.IsZero()) + } +} + +type PostgresqlStandaloneBuilder struct { + *v1alpha1.PostgresqlStandalone +} + +func newBuilderInstance(name, namespace string) *PostgresqlStandaloneBuilder { + return &PostgresqlStandaloneBuilder{newInstance(name, namespace)} +} + +func (b *PostgresqlStandaloneBuilder) setDeploymentNamespace(namespace string) *PostgresqlStandaloneBuilder { + b.Status = v1alpha1.PostgresqlStandaloneStatus{ + PostgresqlStandaloneObservation: v1alpha1.PostgresqlStandaloneObservation{ + HelmChart: &v1alpha1.ChartMetaStatus{ + DeploymentNamespace: namespace, + }, + }, + } + return b +} + +func (b *PostgresqlStandaloneBuilder) setConnectionSecret(secret string) *PostgresqlStandaloneBuilder { + b.Spec = v1alpha1.PostgresqlStandaloneSpec{ + ConnectableInstance: v1alpha1.ConnectableInstance{ + WriteConnectionSecretToRef: v1alpha1.ConnectionSecretRef{ + Name: secret, + }, + }, + } + return b +} + +func (b *PostgresqlStandaloneBuilder) setFinalizers(finalizers ...string) *PostgresqlStandaloneBuilder { + b.Finalizers = finalizers + return b +} + +func (b *PostgresqlStandaloneBuilder) get() *v1alpha1.PostgresqlStandalone { + return b.PostgresqlStandalone +}