Skip to content

Commit

Permalink
Merge pull request #152 from mdb/mdb/skip-plan
Browse files Browse the repository at this point in the history
add skip_plan option to state migrator
  • Loading branch information
minamijoyo authored Sep 21, 2023
2 parents 18eb9c4 + b47619e commit fe82d85
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 26 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ The `state` migration updates the state in a single directory. It has the follow
- `"import <address> <id>"`
- `"replace-provider <address> <address>"`
- `force` (optional): Apply migrations even if plan show changes
- `skip_plan` (optional): If true, `tfmigrate` will not perform and analyze a `terraform plan`.

Note that `dir` is relative path to the current working directory where `tfmigrate` command is invoked.

Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/state_import_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ resource "time_static" "baz" { triggers = {} }
NewStateImportAction("time_static.baz", "2006-01-02T15:04:05Z"),
}

m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down
37 changes: 23 additions & 14 deletions tfmigrate/state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type StateMigratorConfig struct {
// Force option controls behaviour in case of unexpected diff in plan.
// When set forces applying even if plan shows diff.
Force bool `hcl:"force,optional"`
// SkipPlan controls whether or not to run and analyze Terraform plan.
SkipPlan bool `hcl:"to_skip_plan,optional"`
// Workspace is the state workspace which the migration works with.
Workspace string `hcl:"workspace,optional"`
}
Expand Down Expand Up @@ -62,7 +64,7 @@ func (c *StateMigratorConfig) NewMigrator(o *MigratorOption) (Migrator, error) {
c.Workspace = "default"
}

return NewStateMigrator(dir, c.Workspace, actions, o, c.Force), nil
return NewStateMigrator(dir, c.Workspace, actions, o, c.Force, c.SkipPlan), nil
}

// StateMigrator implements the Migrator interface.
Expand All @@ -74,6 +76,8 @@ type StateMigrator struct {
// o is an option for migrator.
// It is used for shared settings across Migrator instances.
o *MigratorOption
// skipPlan controls whether or not to run and analyze Terraform plan.
skipPlan bool
// force operation in case of unexpected diff
force bool
// workspace is the state workspace which the migration works with.
Expand All @@ -84,7 +88,7 @@ var _ Migrator = (*StateMigrator)(nil)

// NewStateMigrator returns a new StateMigrator instance.
func NewStateMigrator(dir string, workspace string, actions []StateAction,
o *MigratorOption, force bool) *StateMigrator {
o *MigratorOption, force bool, skipPlan bool) *StateMigrator {
e := tfexec.NewExecutor(dir, os.Environ())
tf := tfexec.NewTerraformCLI(e)
if o != nil && len(o.ExecPath) > 0 {
Expand All @@ -96,6 +100,7 @@ func NewStateMigrator(dir string, workspace string, actions []StateAction,
actions: actions,
o: o,
force: force,
skipPlan: skipPlan,
workspace: workspace,
}
}
Expand Down Expand Up @@ -145,19 +150,23 @@ func (m *StateMigrator) plan(ctx context.Context) (currentState *tfexec.State, e
planOpts = append(planOpts, "-out="+m.o.PlanOut)
}

log.Printf("[INFO] [migrator@%s] check diffs\n", m.tf.Dir())
_, err = m.tf.Plan(ctx, currentState, planOpts...)
if err != nil {
if exitErr, ok := err.(tfexec.ExitError); ok && exitErr.ExitCode() == 2 {
if !m.force {
log.Printf("[ERROR] [migrator@%s] unexpected diffs\n", m.tf.Dir())
return nil, fmt.Errorf("terraform plan command returns unexpected diffs: %s", err)
if m.skipPlan {
log.Printf("[INFO] [migrator@%s] skipping check diffs\n", m.tf.Dir())
} else {
log.Printf("[INFO] [migrator@%s] check diffs\n", m.tf.Dir())
_, err = m.tf.Plan(ctx, currentState, planOpts...)
if err != nil {
if exitErr, ok := err.(tfexec.ExitError); ok && exitErr.ExitCode() == 2 {
if !m.force {
log.Printf("[ERROR] [migrator@%s] unexpected diffs\n", m.tf.Dir())
return nil, fmt.Errorf("terraform plan command returns unexpected diffs: %s", err)
}
log.Printf("[INFO] [migrator@%s] unexpected diffs, ignoring as force option is true: %s", m.tf.Dir(), err)
// reset err to nil to intentionally ignore unexpected diffs.
err = nil
} else {
return nil, err
}
log.Printf("[INFO] [migrator@%s] unexpected diffs, ignoring as force option is true: %s", m.tf.Dir(), err)
// reset err to nil to intentionally ignore unexpected diffs.
err = nil
} else {
return nil, err
}
}

Expand Down
94 changes: 88 additions & 6 deletions tfmigrate/state_migrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ func TestStateMigratorConfigNewMigrator(t *testing.T) {
o: nil,
ok: true,
},
{
desc: "with skip_plan true",
config: &StateMigratorConfig{
Dir: "dir1",
Actions: []string{
"mv null_resource.foo null_resource.foo2",
"mv null_resource.bar null_resource.bar2",
"rm time_static.baz",
"import time_static.qux 2006-01-02T15:04:05Z",
},
SkipPlan: true,
},
o: nil,
ok: true,
},
}

for _, tc := range cases {
Expand Down Expand Up @@ -166,7 +181,7 @@ resource "time_static" "qux" { triggers = {} }
}

force := false
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down Expand Up @@ -236,7 +251,7 @@ resource "null_resource" "bar" {}
}

force := false
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down Expand Up @@ -308,7 +323,7 @@ resource "null_resource" "baz" {}
o := &MigratorOption{}
o.PlanOut = "foo.tfplan"
force := true
m := NewStateMigrator(tf.Dir(), workspace, actions, o, force)
m := NewStateMigrator(tf.Dir(), workspace, actions, o, force, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down Expand Up @@ -408,6 +423,73 @@ resource "null_resource" "baz" {}
}
}

func TestAccStateMigratorApplyWithSkipPlan(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 := source

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

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")
}

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

force := false
skipPlan := true
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, force, skipPlan)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
}

err = m.Apply(ctx)
if err != nil {
t.Fatalf("failed to run migrator apply: %s", err)
}

got, err := tf.StateList(ctx, nil, nil)
if err != nil {
t.Fatalf("failed to run terraform state list: %s", err)
}

want := []string{
"null_resource.foo2",
"null_resource.bar",
}
sort.Strings(got)
sort.Strings(want)
if !reflect.DeepEqual(got, want) {
t.Errorf("got state: %v, want state: %v", got, want)
}

changed, err = tf.PlanHasChange(ctx, nil)
if err != nil {
t.Fatalf("failed to run PlanHasChange: %s", err)
}
if !changed {
t.Fatalf("expect to have changes")
}
}

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

Expand Down Expand Up @@ -440,7 +522,7 @@ resource "null_resource" "bar" {}
}

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

err := m.Plan(ctx)
if err == nil {
Expand Down Expand Up @@ -479,7 +561,7 @@ resource "null_resource" "bar" {}
}

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

err := m.Plan(ctx)
if err == nil {
Expand Down Expand Up @@ -524,7 +606,7 @@ resource "null_resource" "bar" {}
}

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

err := m.Plan(ctx)
if err == nil {
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/state_mv_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ resource "null_resource" "baz" {}
NewStateMvAction("null_resource.bar", "null_resource.bar2"),
}

m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down
4 changes: 2 additions & 2 deletions tfmigrate/state_replace_provider_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ resource "null_resource" "foo" {}
}

expected := "replace-provider action requires Terraform version >= 0.13.0"
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false, false)
err := m.Plan(ctx)
if err == nil || strings.Contains(err.Error(), expected) {
t.Fatalf("expected to receive '%s' error using legacy Terraform; got: %s", expected, err)
Expand Down Expand Up @@ -96,7 +96,7 @@ func TestAccStateReplaceProviderAction(t *testing.T) {
NewStateReplaceProviderAction("registry.terraform.io/-/null", "registry.terraform.io/hashicorp/null"),
}

m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/state_rm_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ resource "null_resource" "baz" {}
NewStateRmAction([]string{"null_resource.qux"}),
}

m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/state_xmv_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ resource "null_resource" "bar2" {}
NewStateXmvAction("null_resource.*", "null_resource.${1}2"),
}

m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false)
m := NewStateMigrator(tf.Dir(), workspace, actions, &MigratorOption{}, false, false)
err = m.Plan(ctx)
if err != nil {
t.Fatalf("failed to run migrator plan: %s", err)
Expand Down

0 comments on commit fe82d85

Please sign in to comment.