Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add skip_plan option to state migrator #152

Merged
merged 1 commit into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}
mdb marked this conversation as resolved.
Show resolved Hide resolved
}

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