diff --git a/.ci/pipeline_definitions b/.ci/pipeline_definitions index f03e0ea9..08363524 100644 --- a/.ci/pipeline_definitions +++ b/.ci/pipeline_definitions @@ -14,7 +14,7 @@ terraformer: target: terraformer steps: verify: - image: 'eu.gcr.io/gardener-project/3rd/golang:1.15.3' + image: 'eu.gcr.io/gardener-project/3rd/golang:1.15.5' jobs: head-update: traits: diff --git a/Dockerfile b/Dockerfile index 09926dcc..5503be5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 ############# golang-base ############# -FROM eu.gcr.io/gardener-project/3rd/golang:1.15.3 AS golang-base +FROM eu.gcr.io/gardener-project/3rd/golang:1.15.5 AS golang-base ############# base ############# FROM golang-base AS base diff --git a/pkg/terraformer/config_test.go b/pkg/terraformer/config_test.go index 119140f9..f1d22212 100644 --- a/pkg/terraformer/config_test.go +++ b/pkg/terraformer/config_test.go @@ -58,6 +58,7 @@ var _ = Describe("Terraformer Config", func() { }) AfterEach(func() { + testutils.CleanupTestObjects(ctx, testObjs) testutils.RunCleanupActions() }) diff --git a/pkg/terraformer/state_test.go b/pkg/terraformer/state_test.go index 1970e2bb..696923a4 100644 --- a/pkg/terraformer/state_test.go +++ b/pkg/terraformer/state_test.go @@ -86,6 +86,7 @@ var _ = Describe("Terraformer State", func() { AfterEach(func() { ctrl.Finish() + testutils.CleanupTestObjects(ctx, testObjs) testutils.RunCleanupActions() }) diff --git a/pkg/terraformer/terraformer.go b/pkg/terraformer/terraformer.go index 6838fbdf..773a4696 100644 --- a/pkg/terraformer/terraformer.go +++ b/pkg/terraformer/terraformer.go @@ -16,21 +16,28 @@ import ( "syscall" "time" + "github.com/gardener/terraformer/pkg/utils" + "github.com/go-logr/logr" + 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/util/clock" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" runtimelog "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/gardener/terraformer/pkg/utils" ) var ( // TerraformBinary is the name of the terraform binary, it allows to overwrite it for testing purposes TerraformBinary = "terraform" - // allow redirecting output in tests - Stdout, Stderr io.Writer = os.Stdout, os.Stderr + // Stdout alias to os.Stdout allowing output redirection in tests + Stdout io.Writer = os.Stdout + + // Stderr alias to os.Stderr allowing output redirection in tests + Stderr io.Writer = os.Stderr // SignalNotify allows mocking signal.Notify in tests SignalNotify = signal.Notify @@ -128,6 +135,10 @@ func (t *Terraformer) execute(command Command) (rErr error) { // stop file watcher and wait for it to be finished defer shutdownFileWatcher() + if err := t.addFinalizer(ctx); err != nil { + return err + } + // initialize terraform plugins if err := t.executeTerraform(ctx, Init); err != nil { return fmt.Errorf("error executing terraform %s: %w", Init, err) @@ -144,6 +155,11 @@ func (t *Terraformer) execute(command Command) (rErr error) { } } + // after a successful execution of destroy command, remove the finalizers from the resources + if command == Destroy { + return t.removeFinalizer(ctx) + } + return nil } @@ -205,3 +221,65 @@ func (t *Terraformer) executeTerraform(ctx context.Context, command Command) err log.Info("terraform process finished successfully", "command", command) return nil } + +func (t *Terraformer) addFinalizer(ctx context.Context) error { + logger := t.log.WithName("add-finalizer") + return t.updateObjectFinalizers(ctx, logger, controllerutil.AddFinalizer) + +} + +func (t *Terraformer) removeFinalizer(ctx context.Context) error { + logger := t.log.WithName("remove-finalizer") + return t.updateObjectFinalizers(ctx, logger, controllerutil.RemoveFinalizer) +} + +func (t *Terraformer) updateObjectFinalizers(ctx context.Context, log logr.Logger, patchObj func(obj controllerutil.Object, finalizerName string)) error { + objects := []controllerutil.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: t.config.Namespace, + Name: t.config.VariablesSecretName, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: t.config.Namespace, + Name: t.config.ConfigurationConfigMapName, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: t.config.Namespace, + Name: t.config.StateConfigMapName, + }, + }, + } + log.V(3).Info("updating finalizers for terraform resources", "namespace", t.config.Namespace) + + for _, obj := range objects { + key, err := client.ObjectKeyFromObject(obj) + if err != nil { + log.Error(err, "failed to construct key", "key", obj, "error") + return err + } + + err = t.client.Get(ctx, key, obj) + if err != nil { + if apierrors.IsNotFound(err) { + log.V(1).Info("skipping non-existing object", "key", obj) + continue + } + log.Error(err, "failed to get object", "key", obj, "error") + return err + } + + old := obj.DeepCopyObject() + patchObj(obj, TerraformerFinalizer) + if err := t.client.Patch(ctx, obj, client.MergeFrom(old)); client.IgnoreNotFound(err) != nil { + log.Error(err, "failed to update object in the store", "key", obj) + return err + } + } + log.V(3).Info("successfully updated finalizers for terraform resources", "namespace", t.config.Namespace) + return nil +} diff --git a/pkg/terraformer/terraformer_test.go b/pkg/terraformer/terraformer_test.go index b5b6ecf4..37f86b7d 100644 --- a/pkg/terraformer/terraformer_test.go +++ b/pkg/terraformer/terraformer_test.go @@ -13,16 +13,16 @@ import ( "sync" "syscall" + "github.com/gardener/terraformer/pkg/terraformer" + "github.com/gardener/terraformer/pkg/utils" + testutils "github.com/gardener/terraformer/test/utils" + "github.com/gardener/gardener/pkg/utils/test" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "k8s.io/apimachinery/pkg/util/clock" "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/gardener/terraformer/pkg/terraformer" - "github.com/gardener/terraformer/pkg/utils" - testutils "github.com/gardener/terraformer/test/utils" ) var _ = Describe("Terraformer", func() { @@ -83,6 +83,7 @@ var _ = Describe("Terraformer", func() { AfterEach(func() { resetVars() + testutils.CleanupTestObjects(ctx, testObjs) }) Context("basic tests without terraform binary", func() { @@ -125,11 +126,19 @@ var _ = Describe("Terraformer", func() { Expect(tf.Run(terraformer.Apply)).To(Succeed()) Eventually(logBuffer).Should(gbytes.Say("some terraform output")) Eventually(logBuffer).Should(gbytes.Say("terraform process finished successfully")) + testObjs.Refresh() + Expect(testObjs.ConfigurationConfigMap.Finalizers).To(ContainElement(terraformer.TerraformerFinalizer)) + Expect(testObjs.StateConfigMap.Finalizers).To(ContainElement(terraformer.TerraformerFinalizer)) + Expect(testObjs.VariablesSecret.Finalizers).To(ContainElement(terraformer.TerraformerFinalizer)) }) It("should run Destroy successfully", func() { Expect(tf.Run(terraformer.Destroy)).To(Succeed()) Eventually(logBuffer).Should(gbytes.Say("some terraform output")) Eventually(logBuffer).Should(gbytes.Say("terraform process finished successfully")) + testObjs.Refresh() + Expect(testObjs.ConfigurationConfigMap.Finalizers).ToNot(ContainElement(terraformer.TerraformerFinalizer)) + Expect(testObjs.StateConfigMap.Finalizers).ToNot(ContainElement(terraformer.TerraformerFinalizer)) + Expect(testObjs.VariablesSecret.Finalizers).ToNot(ContainElement(terraformer.TerraformerFinalizer)) }) It("should run Validate successfully", func() { Expect(tf.Run(terraformer.Validate)).To(Succeed()) diff --git a/pkg/terraformer/types.go b/pkg/terraformer/types.go index bf51911b..cd9c4833 100644 --- a/pkg/terraformer/types.go +++ b/pkg/terraformer/types.go @@ -29,6 +29,8 @@ const ( Validate Command = "validate" // Plan is the terraform `plan` command. Plan Command = "plan" + // TerraformerFinalizer is the finalizer used by the terraformer on the terraform configmaps and secrets + TerraformerFinalizer = "terraformer.gardener.cloud/state" ) // SupportedCommands contains the set of supported terraform commands, that can be run as `terraformer `. diff --git a/test/e2e/binary/binary_test.go b/test/e2e/binary/binary_test.go index f276c404..bf3aa079 100644 --- a/test/e2e/binary/binary_test.go +++ b/test/e2e/binary/binary_test.go @@ -106,6 +106,10 @@ var _ = Describe("terraformer", func() { ) }) + AfterEach(func() { + testutils.CleanupTestObjects(ctx, testObjs) + }) + Describe("flag validation", func() { It("should fail, if no command is given", func() { cmd := exec.Command(pathToTerraformer, args...) diff --git a/test/e2e/pod/pod_test.go b/test/e2e/pod/pod_test.go index aaa972f1..982c7071 100644 --- a/test/e2e/pod/pod_test.go +++ b/test/e2e/pod/pod_test.go @@ -60,6 +60,10 @@ var _ = Describe("Pod E2E test", func() { log.Info("using namespace") }) + AfterEach(func() { + testutils.CleanupTestObjects(ctx, testObjs) + }) + It("should apply and destroy config successfully", func() { var keyPairName string diff --git a/test/utils/objects.go b/test/utils/objects.go index 78f6f68e..642d16d3 100644 --- a/test/utils/objects.go +++ b/test/utils/objects.go @@ -7,10 +7,13 @@ package utils import ( "context" + "github.com/gardener/terraformer/pkg/terraformer" + . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) // TestObjects models a set of API objects used in tests @@ -85,6 +88,36 @@ func PrepareTestObjects(ctx context.Context, c client.Client, namespacePrefix st return o } +// CleanupTestObjects take care to remove the finalizers of the secret and configmaps +func CleanupTestObjects(ctx context.Context, o *TestObjects) { + configurationConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: o.ConfigurationConfigMap.Name, Namespace: o.Namespace}, + } + Expect(client.IgnoreNotFound(o.client.Get(ctx, ObjectKeyFromObject(configurationConfigMap), configurationConfigMap))).To(Succeed()) + copyConfigurationConfigMap := configurationConfigMap.DeepCopy() + controllerutil.RemoveFinalizer(copyConfigurationConfigMap, terraformer.TerraformerFinalizer) + Expect(client.IgnoreNotFound(o.client.Patch(ctx, copyConfigurationConfigMap, client.MergeFrom(configurationConfigMap)))).To(Succeed()) + Expect(client.IgnoreNotFound(o.client.Delete(ctx, copyConfigurationConfigMap))).To(Succeed()) + + stateConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: o.StateConfigMap.Name, Namespace: o.Namespace}, + } + Expect(client.IgnoreNotFound(o.client.Get(ctx, ObjectKeyFromObject(stateConfigMap), stateConfigMap))).To(Succeed()) + copyStateConfigMap := stateConfigMap.DeepCopy() + controllerutil.RemoveFinalizer(copyStateConfigMap, terraformer.TerraformerFinalizer) + Expect(client.IgnoreNotFound(o.client.Patch(ctx, copyStateConfigMap, client.MergeFrom(stateConfigMap)))).To(Succeed()) + Expect(client.IgnoreNotFound(o.client.Delete(ctx, copyStateConfigMap))).To(Succeed()) + + variablesSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: o.VariablesSecret.Name, Namespace: o.Namespace}, + } + Expect(client.IgnoreNotFound(o.client.Get(ctx, ObjectKeyFromObject(variablesSecret), variablesSecret))).To(Succeed()) + copyVariablesSecret := variablesSecret.DeepCopy() + controllerutil.RemoveFinalizer(copyVariablesSecret, terraformer.TerraformerFinalizer) + Expect(client.IgnoreNotFound(o.client.Patch(ctx, copyVariablesSecret, client.MergeFrom(variablesSecret)))).To(Succeed()) + Expect(client.IgnoreNotFound(o.client.Delete(ctx, copyVariablesSecret))).To(Succeed()) +} + // Refresh retrieves a fresh copy of the objects from the API server, so that tests can make assertions on them. func (o *TestObjects) Refresh() { Expect(o.client.Get(o.ctx, ObjectKeyFromObject(o.ConfigurationConfigMap), o.ConfigurationConfigMap)).To(Succeed())