Skip to content

Commit

Permalink
feat: Add support for local terraform states (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentgna authored Oct 3, 2023
1 parent 7b8cfb3 commit 8af3509
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 30 deletions.
3 changes: 3 additions & 0 deletions tfexec/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions tfexec/terraform_state_local.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 13 additions & 6 deletions tfmigrate/migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
50 changes: 38 additions & 12 deletions tfmigrate/multi_state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions tfmigrate/multi_state_migrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/multi_state_mv_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/multi_state_xmv_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion tfmigrate/state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 8af3509

Please sign in to comment.