diff --git a/command/plan.go b/command/plan.go index 5752a41..d1335f3 100644 --- a/command/plan.go +++ b/command/plan.go @@ -13,12 +13,14 @@ import ( // migration operations to a temporary state. type PlanCommand struct { Meta + out string } // Run runs the procedure of this command. func (c *PlanCommand) Run(args []string) int { cmdFlags := flag.NewFlagSet("plan", flag.ContinueOnError) cmdFlags.StringVar(&c.configFile, "config", defaultConfigFile, "A path to tfmigrate config file") + cmdFlags.StringVar(&c.out, "out", "", "Save a plan file after dry-run migration to the given path") if err := cmdFlags.Parse(args); err != nil { c.UI.Error(fmt.Sprintf("failed to parse arguments: %s", err)) @@ -33,6 +35,7 @@ func (c *PlanCommand) Run(args []string) int { log.Printf("[DEBUG] [command] config: %#v\n", c.config) c.Option = newOption() + c.Option.PlanOut = c.out // The option may contains sensitive values such as environment variables. // So logging the option set log level to DEBUG instead of INFO. log.Printf("[DEBUG] [command] option: %#v\n", c.Option) @@ -111,6 +114,9 @@ Arguments: Options: --config A path to tfmigrate config file + --out=path Save a plan file after dry-run migration to the given path. + Note that applying the plan file only affects a local state, + make sure to force push it to remote after terraform apply. ` return strings.TrimSpace(helpText) } diff --git a/go.mod b/go.mod index 1cb4b63..982b05b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/google/go-cmp v0.5.2 github.com/hashicorp/aws-sdk-go-base v0.6.0 + github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/hcl/v2 v2.6.0 github.com/hashicorp/logutils v1.0.0 github.com/mattn/go-shellwords v1.0.10 diff --git a/go.sum b/go.sum index 5324ab5..8b2fa4f 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6K github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o= github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= diff --git a/tfexec/terraform.go b/tfexec/terraform.go index 5fced15..841660d 100644 --- a/tfexec/terraform.go +++ b/tfexec/terraform.go @@ -320,3 +320,13 @@ func hasPrefixOptions(opts []string, prefix string) bool { } return false } + +// getOptionValue returns a value if any element in a list of options has a given prefix. +func getOptionValue(opts []string, prefix string) string { + for _, opt := range opts { + if strings.HasPrefix(opt, prefix) { + return opt[len(prefix):] + } + } + return "" +} diff --git a/tfexec/terraform_plan.go b/tfexec/terraform_plan.go index 1d074b8..7c88f6d 100644 --- a/tfexec/terraform_plan.go +++ b/tfexec/terraform_plan.go @@ -24,21 +24,24 @@ func (c *terraformCLI) Plan(ctx context.Context, state *State, dir string, opts args = append(args, "-state="+tmpState.Name()) } - // disallow -out option for writing a plan file to a temporary file and load it to memory + // To return a plan file as a return value, we always use an -out option and load it to memory. + // if the option exists just use it else create a temporary file. + planOut := "" if hasPrefixOptions(opts, "-out=") { - return nil, fmt.Errorf("failed to build options. The -out= option is not allowed. Read a return value: %v", opts) - } - - tmpPlan, err := ioutil.TempFile("", "tfplan") - if err != nil { - return nil, fmt.Errorf("failed to create temporary plan file: %s", err) - } - defer os.Remove(tmpPlan.Name()) + planOut = getOptionValue(opts, "-out=") + } else { + tmpPlan, err := ioutil.TempFile("", "tfplan") + if err != nil { + return nil, fmt.Errorf("failed to create temporary plan file: %s", err) + } + planOut = tmpPlan.Name() + defer os.Remove(planOut) - if err := tmpPlan.Close(); err != nil { - return nil, fmt.Errorf("failed to close temporary plan file: %s", err) + if err := tmpPlan.Close(); err != nil { + return nil, fmt.Errorf("failed to close temporary plan file: %s", err) + } + args = append(args, "-out="+planOut) } - args = append(args, "-out="+tmpPlan.Name()) args = append(args, opts...) @@ -46,11 +49,11 @@ func (c *terraformCLI) Plan(ctx context.Context, state *State, dir string, opts args = append(args, dir) } - _, _, err = c.Run(ctx, args...) + _, _, err := c.Run(ctx, args...) // terraform plan -detailed-exitcode returns 2 if there is a diff. // So we intentionally ignore an error of read the plan file and returns the // original error of terraform plan command. - plan, _ := ioutil.ReadFile(tmpPlan.Name()) + plan, _ := ioutil.ReadFile(planOut) return NewPlan(plan), err } diff --git a/tfexec/terraform_plan_test.go b/tfexec/terraform_plan_test.go index 1cbf0f6..08f507f 100644 --- a/tfexec/terraform_plan_test.go +++ b/tfexec/terraform_plan_test.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "io/ioutil" + "os" + "path/filepath" "reflect" "regexp" "strings" @@ -139,22 +141,6 @@ func TestTerraformCLIPlan(t *testing.T) { want: nil, ok: false, }, - { - desc: "with -out= (conflict error)", - mockCommands: []*mockCommand{ - { - args: []string{"terraform", "plan", "-state=/path/to/tempfile", "-out=/path/to/planfile", "-input=false", "-out=foo.tfplan", "foo"}, - argsRe: regexp.MustCompile(`^terraform plan -state=.+ -out=\S+ -input=false -no-color -out=foo.tfplan foo$`), - runFunc: runFunc, - exitCode: 0, - }, - }, - dir: "foo", - opts: []string{"-input=false", "-out=foo.tfplan"}, - state: state, - want: nil, - ok: false, - }, } for _, tc := range cases { @@ -196,3 +182,30 @@ func TestAccTerraformCLIPlan(t *testing.T) { t.Error("plan success but returns nil") } } + +func TestAccTerraformCLIPlanWithOut(t *testing.T) { + SkipUnlessAcceptanceTestEnabled(t) + + source := `resource "null_resource" "foo" {}` + e := SetupTestAcc(t, source) + terraformCLI := NewTerraformCLI(e) + + err := terraformCLI.Init(context.Background(), "", "-input=false", "-no-color") + if err != nil { + t.Fatalf("failed to run terraform init: %s", err) + } + + planOut := "foo.tfplan" + plan, err := terraformCLI.Plan(context.Background(), nil, "", "-input=false", "-no-color", "-out="+planOut) + if err != nil { + t.Fatalf("failed to run terraform plan: %s", err) + } + + if plan == nil { + t.Error("plan success but returns nil") + } + + if _, err := os.Stat(filepath.Join(e.Dir(), planOut)); os.IsNotExist(err) { + t.Errorf("failed to find a plan file: %s, err %s", planOut, err) + } +} diff --git a/tfexec/terraform_test.go b/tfexec/terraform_test.go index 2c78cbf..1d3d29c 100644 --- a/tfexec/terraform_test.go +++ b/tfexec/terraform_test.go @@ -196,3 +196,34 @@ func TestAccTerraformCLIPlanHasChange(t *testing.T) { t.Fatalf("expect to have changes") } } + +func TestGetOptionValue(t *testing.T) { + cases := []struct { + desc string + opts []string + prefix string + want string + }{ + { + desc: "found", + opts: []string{"-input=false", "-no-color", "-out=foo.tfplan", "-detailed-exitcode"}, + prefix: "-out=", + want: "foo.tfplan", + }, + { + desc: "not found", + opts: []string{"-input=false", "-no-color", "-detailed-exitcode"}, + prefix: "-out=", + want: "", + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + got := getOptionValue(tc.opts, tc.prefix) + if got != tc.want { + t.Errorf("got: %s, want: %s", got, tc.want) + } + }) + } +} diff --git a/tfexec/test_helper.go b/tfexec/test_helper.go index ba192e1..e783c95 100644 --- a/tfexec/test_helper.go +++ b/tfexec/test_helper.go @@ -11,6 +11,8 @@ import ( "runtime" "strings" "testing" + + "github.com/hashicorp/go-version" ) // mockExecutor impolements the Executor interface for testing. @@ -325,3 +327,20 @@ func UpdateTestAccSource(t *testing.T, tf TerraformCLI, source string) { t.Fatalf("failed to update source: %s", err) } } + +// MatchTerraformVersion returns true if terraform version matches a given constraints. +func MatchTerraformVersion(ctx context.Context, tf TerraformCLI, constraints string) (bool, error) { + tfVersionRaw, err := tf.Version(ctx) + if err != nil { + return false, fmt.Errorf("failed to get terraform version: %s", err) + } + v, err := version.NewVersion(tfVersionRaw) + if err != nil { + return false, fmt.Errorf("failed to parse terraform version: %s", err) + } + c, err := version.NewConstraint(constraints) + if err != nil { + return false, fmt.Errorf("failed to new version constraint: %s", err) + } + return c.Check(v), nil +} diff --git a/tfmigrate/config.go b/tfmigrate/config.go index 1d0389b..2ccb4d3 100644 --- a/tfmigrate/config.go +++ b/tfmigrate/config.go @@ -24,4 +24,7 @@ type MigratorOption struct { // It's intended to inject a wrapper command such as direnv. // e.g.) direnv exec . terraform ExecPath string + + // PlanOut is a path to plan file to be saved. + PlanOut string } diff --git a/tfmigrate/multi_state_migrator.go b/tfmigrate/multi_state_migrator.go index d6e0dd8..f22a1fd 100644 --- a/tfmigrate/multi_state_migrator.go +++ b/tfmigrate/multi_state_migrator.go @@ -71,6 +71,9 @@ type MultiStateMigrator struct { toWorkspace string // actions is a list of multi state migration operations. actions []MultiStateAction + // o is an option for migrator. + // It is used for shared settings across Migrator instances. + o *MigratorOption // force operation in case of unexpected diff force bool } @@ -92,6 +95,7 @@ func NewMultiStateMigrator(fromDir string, toDir string, fromWorkspace string, t fromWorkspace: fromWorkspace, toWorkspace: toWorkspace, actions: actions, + o: o, force: force, } } @@ -127,9 +131,16 @@ func (m *MultiStateMigrator) plan(ctx context.Context) (*tfexec.State, *tfexec.S fromCurrentState = tfexec.NewState(fromNewState.Bytes()) toCurrentState = tfexec.NewState(toNewState.Bytes()) } + + // build plan options + planOpts := []string{"-input=false", "-no-color", "-detailed-exitcode"} + if m.o.PlanOut != "" { + planOpts = append(planOpts, "-out="+m.o.PlanOut) + } + // check if a plan in fromDir has no changes. log.Printf("[INFO] [migrator@%s] check diffs\n", m.fromTf.Dir()) - _, err = m.fromTf.Plan(ctx, fromCurrentState, "", "-input=false", "-no-color", "-detailed-exitcode") + _, err = m.fromTf.Plan(ctx, fromCurrentState, "", planOpts...) if err != nil { if exitErr, ok := err.(tfexec.ExitError); ok && exitErr.ExitCode() == 2 { if m.force { @@ -144,7 +155,7 @@ func (m *MultiStateMigrator) plan(ctx context.Context) (*tfexec.State, *tfexec.S // check if a plan in toDir has no changes. log.Printf("[INFO] [migrator@%s] check diffs\n", m.toTf.Dir()) - _, err = m.toTf.Plan(ctx, toCurrentState, "", "-input=false", "-no-color", "-detailed-exitcode") + _, err = m.toTf.Plan(ctx, toCurrentState, "", planOpts...) if err != nil { if exitErr, ok := err.(tfexec.ExitError); ok && exitErr.ExitCode() == 2 { if m.force { diff --git a/tfmigrate/multi_state_migrator_test.go b/tfmigrate/multi_state_migrator_test.go index 20930f7..b9a7c68 100644 --- a/tfmigrate/multi_state_migrator_test.go +++ b/tfmigrate/multi_state_migrator_test.go @@ -2,6 +2,8 @@ package tfmigrate import ( "context" + "io/ioutil" + "path/filepath" "reflect" "sort" "testing" @@ -238,11 +240,13 @@ func TestAccMultiStateMigratorApply(t *testing.T) { for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { ctx := context.Background() + //setup the initial files and states fromBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/fromDir") fromTf := tfexec.SetupTestAccWithApply(t, tc.fromWorkspace, fromBackend+tc.fromSource) toBackend := tfexec.GetTestAccBackendS3Config(t.Name() + "/toDir") toTf := tfexec.SetupTestAccWithApply(t, tc.toWorkspace, toBackend+tc.toSource) + //update terraform resource files for migration tfexec.UpdateTestAccSource(t, fromTf, fromBackend+tc.fromUpdatedSource) tfexec.UpdateTestAccSource(t, toTf, toBackend+tc.toUpdatedSource) @@ -260,6 +264,7 @@ func TestAccMultiStateMigratorApply(t *testing.T) { if !changed { t.Fatalf("expect to have changes in toDir") } + //perform state migration actions := []MultiStateAction{} for _, cmdStr := range tc.actions { @@ -269,7 +274,13 @@ func TestAccMultiStateMigratorApply(t *testing.T) { } actions = append(actions, action) } - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), tc.fromWorkspace, tc.toWorkspace, actions, nil, tc.force) + + o := &MigratorOption{} + if tc.force { + o.PlanOut = "foo.tfplan" + } + + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), tc.fromWorkspace, tc.toWorkspace, actions, o, tc.force) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -279,6 +290,7 @@ func TestAccMultiStateMigratorApply(t *testing.T) { if err != nil { t.Fatalf("failed to run migrator apply: %s", err) } + //verify state migration results fromGot, err := fromTf.StateList(ctx, nil, nil) if err != nil { @@ -312,6 +324,79 @@ func TestAccMultiStateMigratorApply(t *testing.T) { if changed != tc.toStateExpectChange { t.Fatalf("expected change in toDir is %t but actual value is %t", tc.toStateExpectChange, changed) } + + if tc.force { + // apply the saved plan files + fromPlan, err := ioutil.ReadFile(filepath.Join(fromTf.Dir(), o.PlanOut)) + if err != nil { + t.Fatalf("failed to read a saved plan file in fromDir: %s", err) + } + err = fromTf.Apply(ctx, tfexec.NewPlan(fromPlan), "", "-input=false", "-no-color") + if err != nil { + t.Fatalf("failed to apply the saved plan file in fromDir: %s", err) + } + toPlan, err := ioutil.ReadFile(filepath.Join(toTf.Dir(), o.PlanOut)) + if err != nil { + t.Fatalf("failed to read a saved plan file in toDir: %s", err) + } + err = toTf.Apply(ctx, tfexec.NewPlan(toPlan), "", "-input=false", "-no-color") + if err != nil { + t.Fatalf("failed to apply the saved plan file in toDir: %s", err) + } + + // Terraform >= v0.12.25 and < v0.13 has a bug for state push -force + // https://github.com/hashicorp/terraform/issues/25761 + fromTfVersionMatched, err := tfexec.MatchTerraformVersion(ctx, fromTf, ">= 0.12.25, < 0.13") + if err != nil { + t.Fatalf("failed to check terraform version constraints in fromDir: %s", err) + } + if fromTfVersionMatched { + t.Skip("skip the following test due to a bug in Terraform v0.12") + } + toTfVersionMatched, err := tfexec.MatchTerraformVersion(ctx, toTf, ">= 0.12.25, < 0.13") + if err != nil { + t.Fatalf("failed to check terraform version constraints in toDir: %s", err) + } + if toTfVersionMatched { + t.Skip("skip the following test due to a bug in Terraform v0.12") + } + + // Note that applying the plan file only affects a local state, + // make sure to force push it to remote after terraform apply. + // The -force flag is required here because the lineage of the state was changed. + fromState, err := ioutil.ReadFile(filepath.Join(fromTf.Dir(), "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to read a local state file in fromDir: %s", err) + } + err = fromTf.StatePush(ctx, tfexec.NewState(fromState), "-force") + if err != nil { + t.Fatalf("failed to force push the local state in fromDir: %s", err) + } + toState, err := ioutil.ReadFile(filepath.Join(toTf.Dir(), "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to read a local state file in toDir: %s", err) + } + err = toTf.StatePush(ctx, tfexec.NewState(toState), "-force") + if err != nil { + t.Fatalf("failed to force push the local state in toDir: %s", err) + } + + // confirm no changes + changed, err := fromTf.PlanHasChange(ctx, nil, "") + if err != nil { + t.Fatalf("failed to run PlanHasChange in fromDir: %s", err) + } + if changed { + t.Fatalf("expect not to have changes in fromDir") + } + changed, err = toTf.PlanHasChange(ctx, nil, "") + if err != nil { + t.Fatalf("failed to run PlanHasChange in toDir: %s", err) + } + if changed { + t.Fatalf("expect not to have changes in toDir") + } + } }) } } diff --git a/tfmigrate/state_import_action_test.go b/tfmigrate/state_import_action_test.go index 0fc73b9..5af0b1e 100644 --- a/tfmigrate/state_import_action_test.go +++ b/tfmigrate/state_import_action_test.go @@ -44,7 +44,7 @@ resource "aws_iam_user" "baz" { NewStateImportAction("aws_iam_user.baz", "baz"), } - m := NewStateMigrator(tf.Dir(), actions, nil, false) + m := NewStateMigrator(tf.Dir(), actions, &MigratorOption{}, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) diff --git a/tfmigrate/state_migrator.go b/tfmigrate/state_migrator.go index a456584..88e185f 100644 --- a/tfmigrate/state_migrator.go +++ b/tfmigrate/state_migrator.go @@ -63,6 +63,9 @@ type StateMigrator struct { tf tfexec.TerraformCLI // actions is a list of state migration operations. actions []StateAction + // o is an option for migrator. + // It is used for shared settings across Migrator instances. + o *MigratorOption // force operation in case of unexpected diff force bool } @@ -80,6 +83,7 @@ func NewStateMigrator(dir string, actions []StateAction, o *MigratorOption, forc return &StateMigrator{ tf: tf, actions: actions, + o: o, force: force, } } @@ -108,8 +112,14 @@ func (m *StateMigrator) plan(ctx context.Context) (*tfexec.State, error) { currentState = tfexec.NewState(newState.Bytes()) } + // build plan options + planOpts := []string{"-input=false", "-no-color", "-detailed-exitcode"} + if m.o.PlanOut != "" { + planOpts = append(planOpts, "-out="+m.o.PlanOut) + } + log.Printf("[INFO] [migrator@%s] check diffs\n", m.tf.Dir()) - _, err = m.tf.Plan(ctx, currentState, "", "-input=false", "-no-color", "-detailed-exitcode") + _, err = m.tf.Plan(ctx, currentState, "", planOpts...) if err != nil { if exitErr, ok := err.(tfexec.ExitError); ok && exitErr.ExitCode() == 2 { if m.force { diff --git a/tfmigrate/state_migrator_test.go b/tfmigrate/state_migrator_test.go index 44aaad5..c856f74 100644 --- a/tfmigrate/state_migrator_test.go +++ b/tfmigrate/state_migrator_test.go @@ -2,6 +2,8 @@ package tfmigrate import ( "context" + "io/ioutil" + "path/filepath" "reflect" "sort" "testing" @@ -146,7 +148,7 @@ resource "aws_iam_user" "qux" { NewStateImportAction("aws_iam_user.qux", "qux"), } - m := NewStateMigrator(tf.Dir(), actions, nil, false) + m := NewStateMigrator(tf.Dir(), actions, &MigratorOption{}, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -214,7 +216,10 @@ resource "aws_security_group" "baz" {} NewStateMvAction("aws_security_group.foo", "aws_security_group.foo2"), } - m := NewStateMigrator(tf.Dir(), actions, nil, true) + o := &MigratorOption{} + o.PlanOut = "foo.tfplan" + + m := NewStateMigrator(tf.Dir(), actions, o, true) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -247,4 +252,45 @@ resource "aws_security_group" "baz" {} if !changed { t.Fatalf("expect to have changes") } + + // apply the saved plan files + plan, err := ioutil.ReadFile(filepath.Join(tf.Dir(), o.PlanOut)) + if err != nil { + t.Fatalf("failed to read a saved plan file: %s", err) + } + err = tf.Apply(ctx, tfexec.NewPlan(plan), "", "-input=false", "-no-color") + if err != nil { + t.Fatalf("failed to apply the saved plan file: %s", err) + } + + // Terraform >= v0.12.25 and < v0.13 has a bug for state push -force + // https://github.com/hashicorp/terraform/issues/25761 + tfVersionMatched, err := tfexec.MatchTerraformVersion(ctx, tf, ">= 0.12.25, < 0.13") + if err != nil { + t.Fatalf("failed to check terraform version constraints: %s", err) + } + if tfVersionMatched { + t.Skip("skip the following test due to a bug in Terraform v0.12") + } + + // Note that applying the plan file only affects a local state, + // make sure to force push it to remote after terraform apply. + // The -force flag is required here because the lineage of the state was changed. + state, err := ioutil.ReadFile(filepath.Join(tf.Dir(), "terraform.tfstate")) + if err != nil { + t.Fatalf("failed to read a local state file: %s", err) + } + err = tf.StatePush(ctx, tfexec.NewState(state), "-force") + if err != nil { + t.Fatalf("failed to force push the local state: %s", err) + } + + // confirm no changes + changed, err = tf.PlanHasChange(ctx, nil, "") + if err != nil { + t.Fatalf("failed to run PlanHasChange: %s", err) + } + if changed { + t.Fatalf("expect not to have changes") + } } diff --git a/tfmigrate/state_mv_action_test.go b/tfmigrate/state_mv_action_test.go index 3cae594..3aa5878 100644 --- a/tfmigrate/state_mv_action_test.go +++ b/tfmigrate/state_mv_action_test.go @@ -41,7 +41,7 @@ resource "aws_security_group" "baz" {} NewStateMvAction("aws_security_group.bar", "aws_security_group.bar2"), } - m := NewStateMigrator(tf.Dir(), actions, nil, false) + m := NewStateMigrator(tf.Dir(), actions, &MigratorOption{}, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) diff --git a/tfmigrate/state_rm_action_test.go b/tfmigrate/state_rm_action_test.go index 4b22d8d..dd7ef16 100644 --- a/tfmigrate/state_rm_action_test.go +++ b/tfmigrate/state_rm_action_test.go @@ -40,7 +40,7 @@ resource "aws_security_group" "baz" {} NewStateRmAction([]string{"aws_security_group.qux"}), } - m := NewStateMigrator(tf.Dir(), actions, nil, false) + m := NewStateMigrator(tf.Dir(), actions, &MigratorOption{}, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err)