Skip to content

Commit

Permalink
build: use is_backend_terraform_cloud in code, pre testing and docume…
Browse files Browse the repository at this point in the history
…ntation
  • Loading branch information
GoodmanBen committed Mar 10, 2022
1 parent 55953bc commit ebe6d77
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 65 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## master (Unreleased)

ENHANCEMENTS:

Support Terraform Cloud as a remote backend ([#72](https://github.com/minamijoyo/tfmigrate/pull/72)).

## 0.3.1 (2022/01/26)

ENHANCEMENTS:
Expand Down
5 changes: 2 additions & 3 deletions command/file_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ func NewFileRunner(filename string, config *config.TfmigrateConfig, option *tfmi
return nil, err
}

// TODO: New state migrator object is created here, need to pass in config here
// TODO: (config will contain details on whether or not the backend is terraform cloud
m, err := mc.Migrator.NewMigrator(option)
m, err := mc.Migrator.NewMigrator(option, config.IsBackendTerraformCloud)

if err != nil {
return nil, err
}
Expand Down
14 changes: 11 additions & 3 deletions config/tfmigrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@ type TfmigrateBlock struct {
// MigrationDir is a path to directory where migration files are stored.
// Default to `.` (current directory).
MigrationDir string `hcl:"migration_dir,optional"`
// IsBackendTerraformCloud is a boolean indicating whether a backend is
// stored remotely in Terraform Cloud. Defaults to false.
IsBackendTerraformCloud bool `hcl:"is_backend_terraform_cloud,optional"`
// History is a block for migration history management.
History *HistoryBlock `hcl:"history,block"`
}

// TfmigrateConfig is a config for top-level CLI settings.
// TfmigrateBlock is just used for parsing HCL and
// TfmigrateConfig is used for building application logic.
// TODO: Here is where config is defined/used
type TfmigrateConfig struct {
// MigrationDir is a path to directory where migration files are stored.
// Default to `.` (current directory).
MigrationDir string
// IsBackendTerraformCloud is a boolean representing whether the remote
// backend is TerraformCloud. Defaults to a value of false.
IsBackendTerraformCloud bool
// History is a config for migration history management.
History *history.Config
}
Expand All @@ -50,7 +55,6 @@ func LoadConfigurationFile(filename string) (*TfmigrateConfig, error) {
// returns a TfmigrateConfig.
// Note that this method does not read a file and you should pass source of config in bytes.
// The filename is used for error message and selecting HCL syntax (.hcl and .json).
// TODO: Here is where the actual configuration is loaded.
func ParseConfigurationFile(filename string, source []byte) (*TfmigrateConfig, error) {
// Decode tfmigrate block.
var f ConfigurationFile
Expand All @@ -63,6 +67,9 @@ func ParseConfigurationFile(filename string, source []byte) (*TfmigrateConfig, e
if len(f.Tfmigrate.MigrationDir) > 0 {
config.MigrationDir = f.Tfmigrate.MigrationDir
}
if f.Tfmigrate.IsBackendTerraformCloud {
config.IsBackendTerraformCloud = f.Tfmigrate.IsBackendTerraformCloud
}

if f.Tfmigrate.History != nil {
history, err := parseHistoryBlock(*f.Tfmigrate.History)
Expand All @@ -78,6 +85,7 @@ func ParseConfigurationFile(filename string, source []byte) (*TfmigrateConfig, e
// NewDefaultConfig returns a new instance of TfmigrateConfig.
func NewDefaultConfig() *TfmigrateConfig {
return &TfmigrateConfig{
MigrationDir: ".",
MigrationDir: ".",
IsBackendTerraformCloud: false,
}
}
32 changes: 11 additions & 21 deletions tfexec/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ type TerraformCLI interface {
// so we need to switch the backend to local for temporary state operations.
// The filename argument must meet constraints for override file.
// (e.g.) _tfexec_override.tf
OverrideBackendToLocal(ctx context.Context, filename string, workspace string) (func(), error)
OverrideBackendToLocal(ctx context.Context, filename string, workspace string, isBackendTerraformCloud bool) (func(), error)

// PlanHasChange is a helper method which runs plan and return true if the plan has change.
PlanHasChange(ctx context.Context, state *State, opts ...string) (bool, error)
Expand Down Expand Up @@ -197,7 +197,8 @@ func (c *terraformCLI) SetExecPath(execPath string) {
// so we need to switch the backend to local for temporary state operations.
// The filename argument must meet constraints in order to override the file.
// (e.g.) _tfexec_override.tf
func (c *terraformCLI) OverrideBackendToLocal(ctx context.Context, filename string, workspace string) (func(), error) {
func (c *terraformCLI) OverrideBackendToLocal(ctx context.Context, filename string,
workspace string, isBackendTerraformCloud bool) (func(), error) {
// create local backend override file.
path := filepath.Join(c.Dir(), filename)
contents := `
Expand Down Expand Up @@ -229,6 +230,7 @@ terraform {
return nil, fmt.Errorf("failed to switch backend to local: %s", err)
}

// TODO
switchBackToRemoteFunc := func() {
log.Printf("[INFO] [executor@%s] remove the override file\n", c.Dir())
err := os.Remove(path)
Expand All @@ -253,25 +255,13 @@ terraform {
}
log.Printf("[INFO] [executor@%s] switch back to remote\n", c.Dir())

// TODO: First just see if removing the -reconfigure statement will work? Yes it does, which is good.
// TODO: Possible solutions:
// TODO: 1) Pass in a command line argument into tfmigrate
// This option (might) be the cleanest, but not necessarily to implement. Will be helpful
// to figure out how command line arguments are used with this tool regardless.
// TODO: 2) New argument in configuration?
// This would be likely easier to reference within the code,
// but it would be not the best user experience to have to add the extra configuration. It feels
// like it is a step behind the optimal situation which would be just to infer it directly from
// the terraform itself.
// TODO: 3) Have tfmigrate read the terraform block's configuration for a cloud {} block.
// Likely will be tricky to implement, but this is a "public" tool and so should implement the
// smoothest/most automated solution.

err = c.Init(ctx, "-input=false", "-no-color", "-reconfigure")

//if (err != nil) && (stdOut == "\nInitializing Terraform Cloud...") {
// _, err = c.Init(ctx, "-input=false", "-no-color")
//} else
// Run the correct init command depending on whether the remote backend is Terraform Cloud
if !isBackendTerraformCloud {
err = c.Init(ctx, "-input=false", "-no-color", "-reconfigure")
} else {
err = c.Init(ctx, "-input=false", "-no-color")
}

if err != nil {
// we cannot return error here.
log.Printf("[ERROR] [executor@%s] failed to switch back to remote: %s\n", c.Dir(), err)
Expand Down
2 changes: 1 addition & 1 deletion tfexec/test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func SetupTestAccWithApply(t *testing.T, workspace string, source string) Terraf
tf := NewTerraformCLI(e)
ctx := context.Background()

_, err := tf.Init(ctx, "-input=false", "-no-color")
err := tf.Init(ctx, "-input=false", "-no-color")
if err != nil {
t.Fatalf("failed to run terraform init: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type MigrationConfig struct {
// MigratorConfig is an interface of factory method for Migrator.
type MigratorConfig interface {
// NewMigrator returns a new instance of Migrator.
NewMigrator(o *MigratorOption) (Migrator, error)
NewMigrator(o *MigratorOption, isBackendTerraformCloud bool) (Migrator, error)
}

// MigratorOption customizes a behavior of Migrator.
Expand Down
8 changes: 3 additions & 5 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.
// TODO: Needs to except a string
func setupWorkDir(ctx context.Context, tf tfexec.TerraformCLI, workspace string) (*tfexec.State, func(), error) {
func setupWorkDir(ctx context.Context, tf tfexec.TerraformCLI, workspace string, isBackendTerraformCloud bool) (*tfexec.State, func(), error) {
// check if terraform command is available.
version, err := tf.Version(ctx)
if err != nil {
Expand All @@ -33,7 +33,7 @@ func setupWorkDir(ctx context.Context, tf tfexec.TerraformCLI, workspace string)

// init folder
log.Printf("[INFO] [migrator@%s] initialize work dir\n", tf.Dir())
_, err = tf.Init(ctx, "-input=false", "-no-color")
err = tf.Init(ctx, "-input=false", "-no-color")
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -61,9 +61,7 @@ func setupWorkDir(ctx context.Context, tf tfexec.TerraformCLI, workspace string)
}
// override backend to local
log.Printf("[INFO] [migrator@%s] override backend to local\n", tf.Dir())
// TODO: The switchBackToRemoteFunc is generated here. Some kind of extra information needs to be passed
// TODO: into the function at this point, I suspect.
switchBackToRemoteFunc, err := tf.OverrideBackendToLocal(ctx, "_tfmigrate_override.tf", workspace)
switchBackToRemoteFunc, err := tf.OverrideBackendToLocal(ctx, "_tfmigrate_override.tf", workspace, isBackendTerraformCloud)
if err != nil {
return nil, nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion tfmigrate/mock_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type MockMigratorConfig struct {
var _ MigratorConfig = (*MockMigratorConfig)(nil)

// NewMigrator returns a new instance of MockMigrator.
func (c *MockMigratorConfig) NewMigrator(o *MigratorOption) (Migrator, error) {
func (c *MockMigratorConfig) NewMigrator(o *MigratorOption, isBackendTerraformCloud bool) (Migrator, error) {
return NewMockMigrator(c.PlanError, c.ApplyError), nil
}

Expand Down
30 changes: 18 additions & 12 deletions tfmigrate/multi_state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/minamijoyo/tfmigrate/tfexec"
)

// TODO: Changes needed here that match the state_migrator.go changes
// MultiStateMigratorConfig is a config for MultiStateMigrator.
type MultiStateMigratorConfig struct {
// FromDir is a working directory where states of resources move from.
Expand All @@ -33,7 +34,7 @@ type MultiStateMigratorConfig struct {
var _ MigratorConfig = (*MultiStateMigratorConfig)(nil)

// NewMigrator returns a new instance of MultiStateMigrator.
func (c *MultiStateMigratorConfig) NewMigrator(o *MigratorOption) (Migrator, error) {
func (c *MultiStateMigratorConfig) NewMigrator(o *MigratorOption, isBackendTerraformCloud bool) (Migrator, error) {
if len(c.Actions) == 0 {
return nil, fmt.Errorf("faild to NewMigrator with no actions")
}
Expand All @@ -56,7 +57,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), nil
return NewMultiStateMigrator(c.FromDir, c.ToDir, c.FromWorkspace, c.ToWorkspace, actions, o, c.Force, isBackendTerraformCloud), nil
}

// MultiStateMigrator implements the Migrator interface.
Expand All @@ -76,12 +77,16 @@ type MultiStateMigrator struct {
o *MigratorOption
// force operation in case of unexpected diff
force bool
// isBackendTerraformCloud is a boolean representing whether they remote backend
// is Terraform Cloud
isBackendTerraformCloud bool
}

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) *MultiStateMigrator {
func NewMultiStateMigrator(fromDir string, toDir string, fromWorkspace string, toWorkspace string,
actions []MultiStateAction, o *MigratorOption, force bool, isBackendTerraformCloud 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 @@ -90,13 +95,14 @@ func NewMultiStateMigrator(fromDir string, toDir string, fromWorkspace string, t
}

return &MultiStateMigrator{
fromTf: fromTf,
toTf: toTf,
fromWorkspace: fromWorkspace,
toWorkspace: toWorkspace,
actions: actions,
o: o,
force: force,
fromTf: fromTf,
toTf: toTf,
fromWorkspace: fromWorkspace,
toWorkspace: toWorkspace,
actions: actions,
o: o,
force: force,
isBackendTerraformCloud: isBackendTerraformCloud,
}
}

Expand All @@ -106,14 +112,14 @@ 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) (*tfexec.State, *tfexec.State, error) {
// setup fromDir.
fromCurrentState, fromSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.fromTf, m.fromWorkspace)
fromCurrentState, fromSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.fromTf, m.fromWorkspace, m.isBackendTerraformCloud)
if err != nil {
return nil, nil, err
}
// switch back it to remote on exit.
defer fromSwitchBackToRemoteFunc()
// setup toDir.
toCurrentState, toSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.toTf, m.toWorkspace)
toCurrentState, toSwitchBackToRemoteFunc, err := setupWorkDir(ctx, m.toTf, m.toWorkspace, m.isBackendTerraformCloud)
if err != nil {
return nil, nil, err
}
Expand Down
33 changes: 15 additions & 18 deletions tfmigrate/state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ type StateMigratorConfig struct {
var _ MigratorConfig = (*StateMigratorConfig)(nil)

// NewMigrator returns a new instance of StateMigrator.
// TODO: Get this done
// TODO
func (c *StateMigratorConfig) NewMigrator(o *MigratorOption) (Migrator, error) {
func (c *StateMigratorConfig) NewMigrator(o *MigratorOption, isBackendTerraformCloud bool) (Migrator, error) {
// default working directory
dir := "."
if len(c.Dir) > 0 {
Expand All @@ -62,7 +60,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, isBackendTerraformCloud), nil
}

// StateMigrator implements the Migrator interface.
Expand All @@ -78,36 +76,38 @@ type StateMigrator struct {
force bool
// workspace is the state workspace which the migration works with.
workspace string
// IsBackendTerraformCloud is whether the remote backed is TerraformCloud
isBackendTerraformCloud bool
}

var _ Migrator = (*StateMigrator)(nil)

// NewStateMigrator returns a new StateMigrator instance.
func NewStateMigrator(dir string, workspace string, actions []StateAction, o *MigratorOption, force bool) *StateMigrator {
func NewStateMigrator(dir string, workspace string, actions []StateAction,
o *MigratorOption, force bool, isBackendTerraformCloud bool) *StateMigrator {
e := tfexec.NewExecutor(dir, os.Environ())
tf := tfexec.NewTerraformCLI(e)
if o != nil && len(o.ExecPath) > 0 {
tf.SetExecPath(o.ExecPath)
}

return &StateMigrator{
tf: tf,
actions: actions,
o: o,
force: force,
workspace: workspace,
tf: tf,
actions: actions,
o: o,
force: force,
workspace: workspace,
isBackendTerraformCloud: isBackendTerraformCloud,
}
}

// plan computes a new state by applying state migration operations to a temporary state.
// It will fail if terraform plan detects any diffs with the new state.
// We intentionally keep this method private as to not expose internal states and unify
// the Migrator interface between a single and multi state migrator.
// TODO: What is available within the StateMigrator object? Within the "ctx" etc?
// TODO: Update to except and use configuration file
func (m *StateMigrator) plan(ctx context.Context) (*tfexec.State, error) {
// setup work dir.
currentState, switchBackToRemoteFunc, err := setupWorkDir(ctx, m.tf, m.workspace) // TODO
currentState, switchBackToRemoteFunc, err := setupWorkDir(ctx, m.tf, m.workspace, m.isBackendTerraformCloud)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -148,21 +148,18 @@ func (m *StateMigrator) plan(ctx context.Context) (*tfexec.State, error) {
return currentState, nil
}

// TODO: How will these updates affect multi-state migration? TBD.
// TODO: Needs to be updated
// Plan computes a new state by applying state migration operations to a temporary state.
// It will fail if terraform plan detects any diffs with the new state.
func (m *StateMigrator) Plan(ctx context.Context) error {
log.Printf("[INFO] [migrator] start state migrator plan\n")
_, err := m.plan(ctx) // TODO
_, err := m.plan(ctx)
if err != nil {
return err
}
log.Printf("[INFO] [migrator] state migrator plan success!\n")
return nil
}

// TODO: Needs to be updated
// Apply computes a new state and pushes it to remote state.
// It will fail if terraform plan detects any diffs with the new state.
// We are intended to this is used for state refactoring.
Expand All @@ -171,7 +168,7 @@ func (m *StateMigrator) Apply(ctx context.Context) error {
// Check if a new state does not have any diffs compared to real resources
// before push a new state to remote.
log.Printf("[INFO] [migrator] start state migrator plan phase for apply\n")
state, err := m.plan(ctx) // TODO
state, err := m.plan(ctx)
if err != nil {
return err
}
Expand Down

0 comments on commit ebe6d77

Please sign in to comment.