diff --git a/tfexec/terraform.go b/tfexec/terraform.go index 0d09fc4..f623fdf 100644 --- a/tfexec/terraform.go +++ b/tfexec/terraform.go @@ -111,6 +111,9 @@ type TerraformCLI interface { // StatePush pushes a given State to remote. StatePush(ctx context.Context, state *State, opts ...string) error + // StateWriteLocal saves the new state to a local state file. + StateWriteLocal(ctx context.Context, state *State) error + // WorkspaceNew creates a new workspace with name "workspace". WorkspaceNew(ctx context.Context, workspace string, opts ...string) error diff --git a/tfexec/terraform_state_local.go b/tfexec/terraform_state_local.go new file mode 100644 index 0000000..2e1ed3b --- /dev/null +++ b/tfexec/terraform_state_local.go @@ -0,0 +1,24 @@ +package tfexec + +import ( + "context" + "fmt" + "os" + "path/filepath" +) + +// StateWriteLocal saves the new state to a local state file. +func (c *terraformCLI) StateWriteLocal(_ context.Context, state *State) error { + localStateFile := "terraform.tfstate" + path := filepath.Join(c.Dir(), localStateFile) + if err := os.WriteFile(path, []byte(state.Bytes()), 0600); err != nil { + return fmt.Errorf("failed to write %s: %s", localStateFile, err) + } + + tmpState, err := writeTempFile(state.Bytes()) + defer os.Remove(tmpState.Name()) + if err != nil { + return err + } + return nil +} diff --git a/tfmigrate/migrator.go b/tfmigrate/migrator.go index bda1741..a23aee6 100644 --- a/tfmigrate/migrator.go +++ b/tfmigrate/migrator.go @@ -23,7 +23,7 @@ type Migrator interface { // setupWorkDir is a common helper function to set up work dir and returns the // current state and a switch back function. -func setupWorkDir(ctx context.Context, tf tfexec.TerraformCLI, workspace string, isBackendTerraformCloud bool, backendConfig []string, ignoreLegacyStateInitErr bool) (*tfexec.State, func() error, error) { +func setupWorkDir(ctx context.Context, tf tfexec.TerraformCLI, workspace string, isBackendTerraformCloud bool, backendConfig []string, ignoreLegacyStateInitErr bool, isLocal bool) (*tfexec.State, func() error, error) { // check if terraform command is available. version, err := tf.Version(ctx) if err != nil { @@ -68,11 +68,18 @@ func setupWorkDir(ctx context.Context, tf tfexec.TerraformCLI, workspace string, if err != nil { return nil, nil, err } - // override backend to local - log.Printf("[INFO] [migrator@%s] override backend to local\n", tf.Dir()) - switchBackToRemoteFunc, err := tf.OverrideBackendToLocal(ctx, "_tfmigrate_override.tf", workspace, isBackendTerraformCloud, backendConfig, ignoreLegacyStateInitErr) - if err != nil { - return nil, nil, err + if !isLocal { + // override backend to local + log.Printf("[INFO] [migrator@%s] override backend to local\n", tf.Dir()) + switchBackToRemoteFunc, err := tf.OverrideBackendToLocal(ctx, "_tfmigrate_override.tf", workspace, isBackendTerraformCloud, backendConfig, ignoreLegacyStateInitErr) + if err != nil { + return nil, nil, err + } + return currentState, switchBackToRemoteFunc, nil + } + switchBackToRemoteFunc := func() error { + log.Printf("[INFO] [executor@%s] nothing to override\n", tf.Dir()) + return nil } return currentState, switchBackToRemoteFunc, nil } diff --git a/tfmigrate/multi_state_migrator.go b/tfmigrate/multi_state_migrator.go index 6e05126..36010bc 100644 --- a/tfmigrate/multi_state_migrator.go +++ b/tfmigrate/multi_state_migrator.go @@ -17,11 +17,15 @@ type MultiStateMigratorConfig struct { // FromSkipPlan controls whether or not to run and analyze Terraform plan // within the from_dir. FromSkipPlan bool `hcl:"from_skip_plan,optional"` + // FromIsLocal controls whether or not fromDir is a local state. + FromIsLocal bool `hcl:"from_is_local,optional"` // ToDir is a working directory where states of resources move to. ToDir string `hcl:"to_dir"` // ToSkipPlan controls whether or not to run and analyze Terraform plan // within the to_dir. ToSkipPlan bool `hcl:"to_skip_plan,optional"` + // ToIsLocal controls whether or not toDir is a local state. + ToIsLocal bool `hcl:"to_is_local,optional"` // FromWorkspace is a workspace within FromDir FromWorkspace string `hcl:"from_workspace,optional"` // ToWorkspace is a workspace within ToDir @@ -63,7 +67,7 @@ func (c *MultiStateMigratorConfig) NewMigrator(o *MigratorOption) (Migrator, err c.ToWorkspace = "default" } - return NewMultiStateMigrator(c.FromDir, c.ToDir, c.FromWorkspace, c.ToWorkspace, actions, o, c.Force, c.FromSkipPlan, c.ToSkipPlan), nil + return NewMultiStateMigrator(c.FromDir, c.ToDir, c.FromWorkspace, c.ToWorkspace, actions, o, c.Force, c.FromSkipPlan, c.ToSkipPlan, c.FromIsLocal, c.ToIsLocal), nil } // MultiStateMigrator implements the Migrator interface. @@ -72,10 +76,14 @@ type MultiStateMigrator struct { fromTf tfexec.TerraformCLI // fromSkipPlan disables the running of Terraform plan in fromDir. fromSkipPlan bool + // fromIsLocal is true if fromDir is a local state. + fromIsLocal bool // fromTf is an instance of TerraformCLI which executes terraform command in a toDir. toTf tfexec.TerraformCLI // toSkipPlan disables the running of Terraform plan in toDir. toSkipPlan bool + // toIsLocal is true if toDir is a local state. + toIsLocal bool //fromWorkspace is the workspace from which the resource will be migrated fromWorkspace string //toWorkspace is the workspace to which the resource will be migrated @@ -93,7 +101,7 @@ var _ Migrator = (*MultiStateMigrator)(nil) // NewMultiStateMigrator returns a new MultiStateMigrator instance. func NewMultiStateMigrator(fromDir string, toDir string, fromWorkspace string, toWorkspace string, - actions []MultiStateAction, o *MigratorOption, force bool, fromSkipPlan bool, toSkipPlan bool) *MultiStateMigrator { + actions []MultiStateAction, o *MigratorOption, force bool, fromSkipPlan bool, toSkipPlan bool, fromIsLocal bool, toIsLocal bool) *MultiStateMigrator { fromTf := tfexec.NewTerraformCLI(tfexec.NewExecutor(fromDir, os.Environ())) toTf := tfexec.NewTerraformCLI(tfexec.NewExecutor(toDir, os.Environ())) if o != nil && len(o.ExecPath) > 0 { @@ -104,8 +112,10 @@ func NewMultiStateMigrator(fromDir string, toDir string, fromWorkspace string, t return &MultiStateMigrator{ fromTf: fromTf, fromSkipPlan: fromSkipPlan, + fromIsLocal: fromIsLocal, toTf: toTf, toSkipPlan: toSkipPlan, + toIsLocal: toIsLocal, fromWorkspace: fromWorkspace, toWorkspace: toWorkspace, actions: actions, @@ -120,7 +130,7 @@ func NewMultiStateMigrator(fromDir string, toDir string, fromWorkspace string, t // the Migrator interface between a single and multi state migrator. func (m *MultiStateMigrator) plan(ctx context.Context) (fromCurrentState *tfexec.State, toCurrentState *tfexec.State, err error) { // setup fromDir. - fromCurrentState, fromSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.fromTf, m.fromWorkspace, m.o.IsBackendTerraformCloud, m.o.BackendConfig, false) + fromCurrentState, fromSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.fromTf, m.fromWorkspace, m.o.IsBackendTerraformCloud, m.o.BackendConfig, false, m.fromIsLocal) if err != nil { return nil, nil, err } @@ -130,7 +140,7 @@ func (m *MultiStateMigrator) plan(ctx context.Context) (fromCurrentState *tfexec }() // setup toDir. - toCurrentState, toSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.toTf, m.toWorkspace, m.o.IsBackendTerraformCloud, m.o.BackendConfig, false) + toCurrentState, toSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.toTf, m.toWorkspace, m.o.IsBackendTerraformCloud, m.o.BackendConfig, false, m.toIsLocal) if err != nil { return nil, nil, err } @@ -231,15 +241,31 @@ func (m *MultiStateMigrator) Apply(ctx context.Context) error { // We push toState before fromState, because when moving resources across // states, write them to new state first and then remove them from old one. log.Printf("[INFO] [migrator] start multi state migrator apply phase\n") - log.Printf("[INFO] [migrator@%s] push the new state to remote\n", m.toTf.Dir()) - err = m.toTf.StatePush(ctx, toState) - if err != nil { - return err + if m.toIsLocal { + log.Printf("[INFO] [migrator@%s] save the new local state \n", m.toTf.Dir()) + err = m.toTf.StateWriteLocal(ctx, toState) + if err != nil { + return err + } + } else { + log.Printf("[INFO] [migrator@%s] push the new state to remote\n", m.toTf.Dir()) + err = m.toTf.StatePush(ctx, toState) + if err != nil { + return err + } } - log.Printf("[INFO] [migrator@%s] push the new state to remote\n", m.fromTf.Dir()) - err = m.fromTf.StatePush(ctx, fromState) - if err != nil { - return err + if m.fromIsLocal { + log.Printf("[INFO] [migrator@%s] save the new local state \n", m.fromTf.Dir()) + err = m.fromTf.StateWriteLocal(ctx, fromState) + if err != nil { + return err + } + } else { + log.Printf("[INFO] [migrator@%s] push the new state to remote\n", m.fromTf.Dir()) + err = m.fromTf.StatePush(ctx, fromState) + if err != nil { + return err + } } log.Printf("[INFO] [migrator] multi state migrator apply success!\n") return nil diff --git a/tfmigrate/multi_state_migrator_test.go b/tfmigrate/multi_state_migrator_test.go index 745e6c2..8f0b9fe 100644 --- a/tfmigrate/multi_state_migrator_test.go +++ b/tfmigrate/multi_state_migrator_test.go @@ -218,7 +218,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -331,7 +331,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, true, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, true, false, false, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -442,7 +442,7 @@ resource "null_resource" "baz" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, true) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, true, false, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -552,7 +552,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, true, true) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, true, true, false, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -666,7 +666,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -785,7 +785,7 @@ resource "null_resource" "qux2" {} o := &MigratorOption{} o.PlanOut = "foo.tfplan" force := true - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) @@ -1003,7 +1003,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, false) err := m.Plan(ctx) if err == nil { @@ -1058,7 +1058,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, false) err := m.Plan(ctx) if err == nil { @@ -1118,7 +1118,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, false) err := m.Plan(ctx) if err == nil { diff --git a/tfmigrate/multi_state_mv_action_test.go b/tfmigrate/multi_state_mv_action_test.go index 214e961..b580cf2 100644 --- a/tfmigrate/multi_state_mv_action_test.go +++ b/tfmigrate/multi_state_mv_action_test.go @@ -64,7 +64,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, false) err = m.Plan(ctx) if err != nil { t.Fatalf("failed to run migrator plan: %s", err) diff --git a/tfmigrate/multi_state_xmv_action_test.go b/tfmigrate/multi_state_xmv_action_test.go index 26429b9..71eb7cb 100644 --- a/tfmigrate/multi_state_xmv_action_test.go +++ b/tfmigrate/multi_state_xmv_action_test.go @@ -63,7 +63,7 @@ resource "null_resource" "qux" {} } o := &MigratorOption{} force := false - m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false) + m := NewMultiStateMigrator(fromTf.Dir(), toTf.Dir(), fromWorkspace, toWorkspace, actions, o, force, false, false, false, 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 bf8466b..9498564 100644 --- a/tfmigrate/state_migrator.go +++ b/tfmigrate/state_migrator.go @@ -123,7 +123,8 @@ func (m *StateMigrator) plan(ctx context.Context) (currentState *tfexec.State, e } // setup work dir. - currentState, switchBackToRemoteFunc, err := setupWorkDir(ctx, m.tf, m.workspace, m.o.IsBackendTerraformCloud, m.o.BackendConfig, ignoreLegacyStateInitErr) + // TODO: implement isLocal support + currentState, switchBackToRemoteFunc, err := setupWorkDir(ctx, m.tf, m.workspace, m.o.IsBackendTerraformCloud, m.o.BackendConfig, ignoreLegacyStateInitErr, false) if err != nil { return nil, err }