Skip to content

Commit

Permalink
acc test rollback failures in deferred funcs
Browse files Browse the repository at this point in the history
This adds acceptance tests surrounding the behavior outlined in issue #129 and
ensures `tfmigrate` exits nonzero if it encounters failures rolling back
to the targeted project(s)'s original remote state configuration(s).
  • Loading branch information
mdb committed Sep 9, 2023
1 parent 58ff50d commit cd51021
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 3 deletions.
17 changes: 14 additions & 3 deletions tfexec/test_helper.go
Expand Up @@ -331,14 +331,15 @@ func SetupTestAccForStateReplaceProvider(t *testing.T, workspace string, source

// SetupTestAccWithApply is an acceptance test helper for initializing a
// temporary work directory and applying a given source.
func SetupTestAccWithApply(t *testing.T, workspace string, source string) TerraformCLI {
func SetupTestAccWithApply(t *testing.T, workspace string, source string, opts ...string) TerraformCLI {
t.Helper()

e := SetupTestAcc(t, source)
tf := NewTerraformCLI(e)
ctx := context.Background()

err := tf.Init(ctx, "-input=false", "-no-color")
opts = append(opts, "-input=false", "-no-color")
err := tf.Init(ctx, opts...)
if err != nil {
t.Fatalf("failed to run terraform init: %s", err)
}
Expand All @@ -358,7 +359,17 @@ func SetupTestAccWithApply(t *testing.T, workspace string, source string) Terraf

// destroy resources after each test not to have any state.
t.Cleanup(func() {
err := tf.Destroy(ctx, "-input=false", "-no-color", "-auto-approve")
cleanupOpts := append(opts, "-reconfigure")

// Re-run terraform init to accommodate any tests that applied the original
// configuration with extra terraform init opts, such as -backend-config.
err := tf.Init(ctx, cleanupOpts...)
if err != nil {
// init errors in Terraform 0.12.31, yet the error does not impede terraform destroy.
t.Logf("failed to re-run terraform init in preparation for destroy; ignoring error: %s", err)
}

err = tf.Destroy(ctx, "-input=false", "-no-color", "-auto-approve")
if err != nil {
t.Fatalf("failed to run terraform destroy: %s", err)
}
Expand Down
182 changes: 182 additions & 0 deletions tfmigrate/multi_state_migrator_test.go
Expand Up @@ -2,10 +2,12 @@ package tfmigrate

import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"

"github.com/minamijoyo/tfmigrate/tfexec"
Expand Down Expand Up @@ -953,3 +955,183 @@ resource "null_resource" "qux2" {}
t.Error("expect not to have changes in toDir")
}
}

func TestAccMultiStateMigratorPlanWithSwitchBackToRemoteFuncError(t *testing.T) {
tfexec.SkipUnlessAcceptanceTestEnabled(t)
ctx := context.Background()

// setup the initial files and states
fromBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/fromDir")
// Intentionally remove the bucket key from Terraform backend configuration,
// to force an error when switching back to remote state.
fromBackend = strings.ReplaceAll(fromBackend, fmt.Sprintf("bucket = \"%s\"", tfexec.TestS3Bucket), "")

fromSource := `
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
resource "null_resource" "baz" {}
`
fromWorkspace := "default"
// Explicitly pass a -backend-config=bucket= option to terraform init, such
// that the initial init/apply works.
fromTf := tfexec.SetupTestAccWithApply(t, fromWorkspace, fromBackend+fromSource, fmt.Sprintf("-backend-config=bucket=%s", tfexec.TestS3Bucket))

toBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/toDir")
toSource := `
resource "null_resource" "qux" {}
`
toWorkspace := "default"
toTf := tfexec.SetupTestAccWithApply(t, toWorkspace, toBackend+toSource)

// update terraform resource files for migration
fromUpdatedSource := `
resource "null_resource" "baz" {}
`
tfexec.UpdateTestAccSource(t, fromTf, fromBackend+fromUpdatedSource)

toUpdatedSource := `
resource "null_resource" "foo" {}
resource "null_resource" "bar2" {}
resource "null_resource" "qux" {}
`
tfexec.UpdateTestAccSource(t, toTf, toBackend+toUpdatedSource)

// perform state migration
actions := []MultiStateAction{
NewMultiStateMvAction("null_resource.foo", "null_resource.foo"),
NewMultiStateMvAction("null_resource.bar", "null_resource.bar2"),
}
o := &MigratorOption{}
force := false
m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false)

err := m.Plan(ctx)
if err == nil {
t.Fatalf("expected migrator plan error")
}

expected := "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}
}

func TestAccMultiStateMigratorPlanWithInvalidMigration(t *testing.T) {
tfexec.SkipUnlessAcceptanceTestEnabled(t)
ctx := context.Background()

// setup the initial files and states
fromBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/fromDir")

fromSource := `
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
resource "null_resource" "baz" {}
`
fromWorkspace := "default"
fromTf := tfexec.SetupTestAccWithApply(t, fromWorkspace, fromBackend+fromSource)

toBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/toDir")
toSource := `
resource "null_resource" "qux" {}
`
toWorkspace := "default"
toTf := tfexec.SetupTestAccWithApply(t, toWorkspace, toBackend+toSource)

// update terraform resource files for migration
fromUpdatedSource := `
resource "null_resource" "baz" {}
`
tfexec.UpdateTestAccSource(t, fromTf, fromBackend+fromUpdatedSource)

toUpdatedSource := `
resource "null_resource" "foo" {}
resource "null_resource" "bar2" {}
resource "null_resource" "qux" {}
`
tfexec.UpdateTestAccSource(t, toTf, toBackend+toUpdatedSource)

// perform state migration
actions := []MultiStateAction{
NewMultiStateMvAction("null_resource.foo_bar", "null_resource.foo"),
NewMultiStateMvAction("null_resource.bar", "null_resource.bar2"),
}
o := &MigratorOption{}
force := false
m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false)

err := m.Plan(ctx)
if err == nil {
t.Fatalf("expected migrator plan error")
}

expected := "Invalid source address"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}
}

func TestAccMultiStateMigratorPlanWithInvalidMigrationAndSwitchBackToRemoteFuncError(t *testing.T) {
tfexec.SkipUnlessAcceptanceTestEnabled(t)
ctx := context.Background()

// setup the initial files and states
fromBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/fromDir")
// Intentionally remove the bucket key from Terraform backend configuration,
// to force an error when switching back to remote state.
fromBackend = strings.ReplaceAll(fromBackend, fmt.Sprintf("bucket = \"%s\"", tfexec.TestS3Bucket), "")

fromSource := `
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
resource "null_resource" "baz" {}
`
fromWorkspace := "default"
// Explicitly pass a -backend-config=bucket= option to terraform init, such
// that the initial init/apply works.
fromTf := tfexec.SetupTestAccWithApply(t, fromWorkspace, fromBackend+fromSource, fmt.Sprintf("-backend-config=bucket=%s", tfexec.TestS3Bucket))

toBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/toDir")
toSource := `
resource "null_resource" "qux" {}
`
toWorkspace := "default"
toTf := tfexec.SetupTestAccWithApply(t, toWorkspace, toBackend+toSource)

// update terraform resource files for migration
fromUpdatedSource := `
resource "null_resource" "baz" {}
`
tfexec.UpdateTestAccSource(t, fromTf, fromBackend+fromUpdatedSource)

toUpdatedSource := `
resource "null_resource" "foo" {}
resource "null_resource" "bar2" {}
resource "null_resource" "qux" {}
`
tfexec.UpdateTestAccSource(t, toTf, toBackend+toUpdatedSource)

// perform state migration
actions := []MultiStateAction{
NewMultiStateMvAction("null_resource.foo_bar", "null_resource.foo"),
NewMultiStateMvAction("null_resource.bar", "null_resource.bar2"),
}
o := &MigratorOption{}
force := false
m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false)

err := m.Plan(ctx)
if err == nil {
t.Fatalf("expected migrator plan error")
}

expected := "Invalid source address"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}

expected = "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}
}
136 changes: 136 additions & 0 deletions tfmigrate/state_migrator_test.go
Expand Up @@ -2,10 +2,12 @@ package tfmigrate

import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"

"github.com/minamijoyo/tfmigrate/tfexec"
Expand Down Expand Up @@ -405,3 +407,137 @@ resource "null_resource" "baz" {}
t.Fatalf("expect not to have changes")
}
}

func TestAccStateMigratorPlanWithSwitchBackToRemoteFuncError(t *testing.T) {
tfexec.SkipUnlessAcceptanceTestEnabled(t)

backend := tfexec.GetTestAccBackendS3Config(t.Name())

// Intentionally remove the bucket key from Terraform backend configuration,
// to force an error when switching back to remote state.
backend = strings.ReplaceAll(backend, fmt.Sprintf("bucket = \"%s\"", tfexec.TestS3Bucket), "")

source := `
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
`

workspace := "default"
// Explicitly pass a -backend-config=bucket= option to terraform init, such
// that the initial init/apply works.
tf := tfexec.SetupTestAccWithApply(t, workspace, backend+source, fmt.Sprintf("-backend-config=bucket=%s", tfexec.TestS3Bucket))
ctx := context.Background()

updatedSource := `
resource "null_resource" "foo2" {}
resource "null_resource" "bar" {}
`

tfexec.UpdateTestAccSource(t, tf, backend+updatedSource)

actions := []StateAction{
NewStateMvAction("null_resource.foo", "null_resource.foo2"),
}

force := false
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force)

err := m.Plan(ctx)
if err == nil {
t.Fatalf("expected migrator plan error")
}

expected := "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}
}

func TestAccStateMigratorPlanWithInvalidMigration(t *testing.T) {
tfexec.SkipUnlessAcceptanceTestEnabled(t)

backend := tfexec.GetTestAccBackendS3Config(t.Name())

source := `
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
`

workspace := "default"
tf := tfexec.SetupTestAccWithApply(t, workspace, backend+source)
ctx := context.Background()

updatedSource := `
resource "null_resource" "foo2" {}
resource "null_resource" "bar" {}
`

tfexec.UpdateTestAccSource(t, tf, backend+updatedSource)

actions := []StateAction{
NewStateMvAction("null_resource.doesnotexist", "null_resource.foo2"),
}

force := false
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force)

err := m.Plan(ctx)
if err == nil {
t.Fatalf("expected migrator plan error")
}

expected := "Invalid source address"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}
}

func TestAccStateMigratorPlanWithInvalidMigrationAndSwitchBackToRemoteFuncError(t *testing.T) {
tfexec.SkipUnlessAcceptanceTestEnabled(t)

backend := tfexec.GetTestAccBackendS3Config(t.Name())

// Intentionally remove the bucket key from Terraform backend configuration,
// to force an error when switching back to remote state.
backend = strings.ReplaceAll(backend, fmt.Sprintf("bucket = \"%s\"", tfexec.TestS3Bucket), "")

source := `
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
`

workspace := "default"
// Explicitly pass a -backend-config=bucket= option to terraform init, such
// that the initial init/apply works.
tf := tfexec.SetupTestAccWithApply(t, workspace, backend+source, fmt.Sprintf("-backend-config=bucket=%s", tfexec.TestS3Bucket))
ctx := context.Background()

updatedSource := `
resource "null_resource" "foo2" {}
resource "null_resource" "bar" {}
`

tfexec.UpdateTestAccSource(t, tf, backend+updatedSource)

actions := []StateAction{
NewStateMvAction("null_resource.doesnotexist", "null_resource.foo2"),
}

force := false
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force)

err := m.Plan(ctx)
if err == nil {
t.Fatalf("expected migrator plan error")
}

expected := "Invalid source address"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}

expected = "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}
}

0 comments on commit cd51021

Please sign in to comment.