Skip to content

Commit

Permalink
Parse and detect Terraform CLI implementation type
Browse files Browse the repository at this point in the history
We are not using it to determine capability now, but we would like to
distinguish which implementation is being used and output it in the log
for future bug reports.

The execPath is inappropriate here because it could be replaced by a
wrapper such as direnv; it must be detected by parsing the output of the
version command.
  • Loading branch information
minamijoyo committed Nov 2, 2023
1 parent c4a506d commit ad4d2d6
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 25 deletions.
5 changes: 3 additions & 2 deletions tfexec/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ func NewPlan(b []byte) *Plan {
// As a result, the interface is opinionated and less flexible. For running arbitrary terraform commands
// you can use Run(), which is a low-level generic method.
type TerraformCLI interface {
// Version returns a Terraform version.
Version(ctx context.Context) (*version.Version, error)
// Version returns the Terraform execType and version number.
// The execType can be either terraform or opentofu.
Version(ctx context.Context) (string, *version.Version, error)

// Init initializes the current work directory.
Init(ctx context.Context, opts ...string) error
Expand Down
2 changes: 1 addition & 1 deletion tfexec/terraform_state_replace_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (c *terraformCLI) SupportsStateReplaceProvider(ctx context.Context) (bool,
return false, constraints, err
}

v, err := c.Version(ctx)
_, v, err := c.Version(ctx)
if err != nil {
return false, constraints, err
}
Expand Down
24 changes: 18 additions & 6 deletions tfexec/terraform_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,35 @@ import (
// tfVersionRe is a pattern to parse outputs from terraform version.
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) {
// Version returns the Terraform execType and version number.
// The execType can be either terraform or opentofu.
func (c *terraformCLI) Version(ctx context.Context) (string, *version.Version, error) {
stdout, _, err := c.Run(ctx, "version")
if err != nil {
return nil, err
return "", nil, err
}

matched := tfVersionRe.FindStringSubmatch(stdout)
if len(matched) != 3 {
return nil, fmt.Errorf("failed to parse terraform version: %s", stdout)
return "", nil, fmt.Errorf("failed to parse terraform version: %s", stdout)
}

execType := ""
switch matched[1] {
case "Terraform":
execType = "terraform"
case "OpenTofu":
execType = "opentofu"
default:
return "", nil, fmt.Errorf("unknown execType: %s", matched[1])
}

version, err := version.NewVersion(matched[2])
if err != nil {
return nil, err
return "", nil, err
}

return version, nil
return execType, version, nil
}

// truncatePreReleaseVersion is a helper function that removes
Expand Down
63 changes: 51 additions & 12 deletions tfexec/terraform_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ func TestTerraformCLIVersion(t *testing.T) {
desc string
mockCommands []*mockCommand
execPath string
want string
execType string
version string
ok bool
}{
{
Expand All @@ -27,7 +28,8 @@ func TestTerraformCLIVersion(t *testing.T) {
},
},
execPath: "terraform",
want: "1.6.2",
execType: "terraform",
version: "1.6.2",
ok: true,
},
{
Expand All @@ -40,9 +42,38 @@ func TestTerraformCLIVersion(t *testing.T) {
},
},
execPath: "tofu",
want: "1.6.0-alpha3",
execType: "opentofu",
version: "1.6.0-alpha3",
ok: true,
},
{
desc: "with wrapper",
mockCommands: []*mockCommand{
{
args: []string{"direnv", "exec", ".", "terraform", "version"},
stdout: "Terraform v1.6.2\n",
exitCode: 0,
},
},
execPath: "direnv exec . terraform",
execType: "terraform",
version: "1.6.2",
ok: true,
},
{
desc: "unknown execType",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "version"},
stdout: "MyTerraform v1.6.2\n",
exitCode: 0,
},
},
execPath: "terraform",
execType: "",
version: "",
ok: false,
},
{
desc: "failed to run terraform version",
mockCommands: []*mockCommand{
Expand All @@ -52,7 +83,8 @@ func TestTerraformCLIVersion(t *testing.T) {
},
},
execPath: "terraform",
want: "",
execType: "",
version: "",
ok: false,
},
{
Expand All @@ -69,7 +101,8 @@ is 0.12.29. You can update by downloading from https://www.terraform.io/download
},
},
execPath: "terraform",
want: "0.12.28",
execType: "terraform",
version: "0.12.28",
ok: true,
},
}
Expand All @@ -79,15 +112,18 @@ is 0.12.29. You can update by downloading from https://www.terraform.io/download
e := NewMockExecutor(tc.mockCommands)
terraformCLI := NewTerraformCLI(e)
terraformCLI.SetExecPath(tc.execPath)
got, err := terraformCLI.Version(context.Background())
execType, version, err := terraformCLI.Version(context.Background())
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
}
if !tc.ok && err == nil {
t.Fatalf("expected to return an error, but no error, got = %s", got)
t.Fatalf("expected to return an error, but no error, execType = %s, version = %s", execType, version.String())
}
if tc.ok && got.String() != tc.want {
t.Errorf("got: %s, want: %s", got, tc.want)
if tc.ok && execType != tc.execType {
t.Errorf("unexpected execType, got: %s, want: %s", execType, tc.execType)
}
if tc.ok && version.String() != tc.version {
t.Errorf("unexpected version, got: %s, want: %s", version.String(), tc.version)
}
})
}
Expand All @@ -98,14 +134,17 @@ func TestAccTerraformCLIVersion(t *testing.T) {

e := NewExecutor("", os.Environ())
terraformCLI := NewTerraformCLI(e)
got, err := terraformCLI.Version(context.Background())
execType, version, err := terraformCLI.Version(context.Background())
if err != nil {
t.Fatalf("failed to run terraform version: %s", err)
}
if got.String() == "" {
if execType == "" {
t.Error("failed to parse terraform execType")
}
if version.String() == "" {
t.Error("failed to parse terraform version")
}
fmt.Printf("got = %s\n", got)
fmt.Printf("got: execType = %s, version = %s\n", execType, version)
}

func TestTruncatePreReleaseVersion(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions tfexec/test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ func UpdateTestAccSource(t *testing.T, tf TerraformCLI, source string) {

// MatchTerraformVersion returns true if terraform version matches a given constraints.
func MatchTerraformVersion(ctx context.Context, tf TerraformCLI, constraints string) (bool, error) {
v, err := tf.Version(ctx)
_, v, err := tf.Version(ctx)
if err != nil {
return false, fmt.Errorf("failed to get terraform version: %s", err)
}
Expand All @@ -403,7 +403,7 @@ func MatchTerraformVersion(ctx context.Context, tf TerraformCLI, constraints str

// IsPreleaseTerraformVersion returns true if terraform version is a prelease.
func IsPreleaseTerraformVersion(ctx context.Context, tf TerraformCLI) (bool, error) {
v, err := tf.Version(ctx)
_, v, err := tf.Version(ctx)
if err != nil {
return false, fmt.Errorf("failed to get terraform version: %s", err)
}
Expand Down
4 changes: 2 additions & 2 deletions tfmigrate/migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ type Migrator interface {
// 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) {
// check if terraform command is available.
version, err := tf.Version(ctx)
execType, version, err := tf.Version(ctx)
if err != nil {
return nil, nil, err
}
log.Printf("[INFO] [migrator@%s] terraform version: %s\n", tf.Dir(), version)
log.Printf("[INFO] [migrator@%s] %s version: %s\n", tf.Dir(), execType, version)

supportsStateReplaceProvider, constraints, err := tf.SupportsStateReplaceProvider(ctx)
if err != nil {
Expand Down

0 comments on commit ad4d2d6

Please sign in to comment.