Skip to content

Commit

Permalink
Allow use of OpenTofu by setting TFMIGRATE_EXEC_PATH to tofu
Browse files Browse the repository at this point in the history
To support OpenTofu, I've relaxed a regular expression so that the
output of the tofu version command can be parsed at least. Also, to use
the tofu command for testing, read environment variables at the time of
initializing the TerraformCLI instance.

This fix optimistically assumes that Terraform and OpenTofu features and
version numbers are compatible, but the implementations will diverge and
eventually become incorrect as time goes on. I think some abstractions,
such as capability, will be needed before adding new features
depending on Terraform 1.6+.
  • Loading branch information
minamijoyo committed Oct 26, 2023
1 parent 8b44751 commit c4a506d
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ Options:
You can customize the behavior by setting environment variables.

- `TFMIGRATE_LOG`: A log level. Valid values are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default to `INFO`.
- `TFMIGRATE_EXEC_PATH`: A string how terraform command is executed. Default to `terraform`. It's intended to inject a wrapper command such as direnv. e.g.) `direnv exec . terraform`.
- `TFMIGRATE_EXEC_PATH`: A string how terraform command is executed. Default to `terraform`. It's intended to inject a wrapper command such as direnv. e.g.) `direnv exec . terraform`. To use OpenTofu, set this to `tofu`.

Some history storage implementations may read additional cloud provider-specific environment variables. For details, refer to a configuration file section for storage block described below.

Expand Down
16 changes: 12 additions & 4 deletions tfexec/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,25 +152,33 @@ type terraformCLI struct {
Executor

// execPath is a string which executes the terraform command.
// If empty, default to terraform.
// Default to terraform. To use OpenTofu, set this to `tofu`.
execPath string
}

var _ TerraformCLI = (*terraformCLI)(nil)

// NewTerraformCLI returns an implementation of the TerraformCLI interface.
// This function reads the environment variable TFMIGRATE_EXEC_PATH and sets it
// to execPath.
func NewTerraformCLI(e Executor) TerraformCLI {
execPath := os.Getenv("TFMIGRATE_EXEC_PATH")
if len(execPath) == 0 {
// The default binary path is `terraform`.
execPath = "terraform"
}

return &terraformCLI{
Executor: e,
execPath: execPath,
}
}

// Run is a low-level generic method for running an arbitrary terraform command.
func (c *terraformCLI) Run(ctx context.Context, args ...string) (string, string, error) {
// The default binary path is `terraform`.
name := "terraform"
name := c.execPath
// If execPath is customized
if len(c.execPath) > 0 {
if name != "terraform" {
// execPath may contain spaces and environment variables, so we parse it.
// e.g.) "direnv exec . terraform" => ["direnv", "exec", ".", "terraform"]
parts, err := shellwords.Parse(c.execPath)
Expand Down
28 changes: 22 additions & 6 deletions tfexec/terraform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ func TestTerraformCLIRun(t *testing.T) {
exitCode: 0,
},
},
args: []string{"version"},
want: "Terraform v0.12.28\n",
ok: true,
args: []string{"version"},
execPath: "terraform",
want: "Terraform v0.12.28\n",
ok: true,
},
{
desc: "failed to run terraform version",
Expand All @@ -38,9 +39,10 @@ func TestTerraformCLIRun(t *testing.T) {
exitCode: 1,
},
},
args: []string{"version"},
want: "",
ok: false,
args: []string{"version"},
execPath: "terraform",
want: "",
ok: false,
},
{
desc: "with execPath (no space)",
Expand Down Expand Up @@ -70,6 +72,20 @@ func TestTerraformCLIRun(t *testing.T) {
want: "Terraform v0.12.28\n",
ok: true,
},
{
desc: "with execPath (tofu)",
mockCommands: []*mockCommand{
{
args: []string{"tofu", "version"},
stdout: "OpenTofu v1.6.0-alpha3\n",
exitCode: 0,
},
},
args: []string{"version"},
execPath: "tofu",
want: "OpenTofu v1.6.0-alpha3\n",
ok: true,
},
}

for _, tc := range cases {
Expand Down
6 changes: 3 additions & 3 deletions tfexec/terraform_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// tfVersionRe is a pattern to parse outputs from terraform version.
var tfVersionRe = regexp.MustCompile(`^Terraform v(.+)\s*\n`)
var tfVersionRe = regexp.MustCompile(`^(Terraform|OpenTofu) v(.+)\s*\n`)

// Version returns a version number of Terraform.
func (c *terraformCLI) Version(ctx context.Context) (*version.Version, error) {
Expand All @@ -20,10 +20,10 @@ func (c *terraformCLI) Version(ctx context.Context) (*version.Version, error) {
}

matched := tfVersionRe.FindStringSubmatch(stdout)
if len(matched) != 2 {
if len(matched) != 3 {
return nil, fmt.Errorf("failed to parse terraform version: %s", stdout)
}
version, err := version.NewVersion(matched[1])
version, err := version.NewVersion(matched[2])
if err != nil {
return nil, err
}
Expand Down
34 changes: 26 additions & 8 deletions tfexec/terraform_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,35 @@ func TestTerraformCLIVersion(t *testing.T) {
cases := []struct {
desc string
mockCommands []*mockCommand
execPath string
want string
ok bool
}{
{
desc: "parse outputs of terraform version",
desc: "terraform version",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "version"},
stdout: "Terraform v0.12.28\n",
stdout: "Terraform v1.6.2\n",
exitCode: 0,
},
},
want: "0.12.28",
ok: true,
execPath: "terraform",
want: "1.6.2",
ok: true,
},
{
desc: "tofu version",
mockCommands: []*mockCommand{
{
args: []string{"tofu", "version"},
stdout: "OpenTofu v1.6.0-alpha3\n",
exitCode: 0,
},
},
execPath: "tofu",
want: "1.6.0-alpha3",
ok: true,
},
{
desc: "failed to run terraform version",
Expand All @@ -36,8 +51,9 @@ func TestTerraformCLIVersion(t *testing.T) {
exitCode: 1,
},
},
want: "",
ok: false,
execPath: "terraform",
want: "",
ok: false,
},
{
desc: "with check point warning",
Expand All @@ -52,15 +68,17 @@ is 0.12.29. You can update by downloading from https://www.terraform.io/download
exitCode: 0,
},
},
want: "0.12.28",
ok: true,
execPath: "terraform",
want: "0.12.28",
ok: true,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
e := NewMockExecutor(tc.mockCommands)
terraformCLI := NewTerraformCLI(e)
terraformCLI.SetExecPath(tc.execPath)
got, err := terraformCLI.Version(context.Background())
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
Expand Down
1 change: 1 addition & 0 deletions tfmigrate/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type MigratorOption struct {
// ExecPath is a string how terraform command is executed. Default to terraform.
// It's intended to inject a wrapper command such as direnv.
// e.g.) direnv exec . terraform
// To use OpenTofu, set this to `tofu`.
ExecPath string

// PlanOut is a path to plan file to be saved.
Expand Down
2 changes: 2 additions & 0 deletions tfmigrate/multi_state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ func NewMultiStateMigrator(fromDir string, toDir string, fromWorkspace string, t
fromTf := tfexec.NewTerraformCLI(tfexec.NewExecutor(fromDir, os.Environ()))
toTf := tfexec.NewTerraformCLI(tfexec.NewExecutor(toDir, os.Environ()))
if o != nil && len(o.ExecPath) > 0 {
// While NewTerraformCLI reads the environment variable TFMIGRATE_EXEC_PATH
// at initialization, the MigratorOption takes precedence over it.
fromTf.SetExecPath(o.ExecPath)
toTf.SetExecPath(o.ExecPath)
}
Expand Down
2 changes: 2 additions & 0 deletions tfmigrate/state_migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func NewStateMigrator(dir string, workspace string, actions []StateAction,
e := tfexec.NewExecutor(dir, os.Environ())
tf := tfexec.NewTerraformCLI(e)
if o != nil && len(o.ExecPath) > 0 {
// While NewTerraformCLI reads the environment variable TFMIGRATE_EXEC_PATH
// at initialization, the MigratorOption takes precedence over it.
tf.SetExecPath(o.ExecPath)
}

Expand Down

0 comments on commit c4a506d

Please sign in to comment.