Parse version -json output and fall back to plaintext (#138)
diff --git a/tfexec/version.go b/tfexec/version.go index 9e05b05..b6496ab 100644 --- a/tfexec/version.go +++ b/tfexec/version.go
@@ -3,12 +3,14 @@ import ( "bytes" "context" + "encoding/json" "fmt" "regexp" "strconv" "strings" "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-json" ) var ( @@ -37,9 +39,52 @@ // version does not use the locking on the Terraform instance and should probably not be used directly, prefer Version. func (tf *Terraform) version(ctx context.Context) (*version.Version, map[string]*version.Version, error) { - // TODO: 0.13.0-beta2? and above supports a `-json` on the version command, should add support - // for that here and fallback to string parsing + versionCmd := tf.buildTerraformCmd(ctx, nil, "version", "-json") + var outBuf bytes.Buffer + versionCmd.Stdout = &outBuf + + err := tf.runTerraformCmd(ctx, versionCmd) + if err != nil { + return nil, nil, err + } + + tfVersion, providerVersions, err := parseJsonVersionOutput(outBuf.Bytes()) + if err != nil { + if _, ok := err.(*json.SyntaxError); ok { + return tf.versionFromPlaintext(ctx) + } + } + + return tfVersion, providerVersions, err +} + +func parseJsonVersionOutput(stdout []byte) (*version.Version, map[string]*version.Version, error) { + var out tfjson.VersionOutput + err := json.Unmarshal(stdout, &out) + if err != nil { + return nil, nil, err + } + + tfVersion, err := version.NewVersion(out.Version) + if err != nil { + return nil, nil, fmt.Errorf("unable to parse version %q: %w", out.Version, err) + } + + providerVersions := make(map[string]*version.Version, 0) + for provider, versionStr := range out.ProviderSelections { + v, err := version.NewVersion(versionStr) + if err != nil { + return nil, nil, fmt.Errorf("unable to parse %q version %q: %w", + provider, versionStr, err) + } + providerVersions[provider] = v + } + + return tfVersion, providerVersions, nil +} + +func (tf *Terraform) versionFromPlaintext(ctx context.Context) (*version.Version, map[string]*version.Version, error) { versionCmd := tf.buildTerraformCmd(ctx, nil, "version") var outBuf bytes.Buffer @@ -50,7 +95,7 @@ return nil, nil, err } - tfVersion, providerVersions, err := parseVersionOutput(outBuf.String()) + tfVersion, providerVersions, err := parsePlaintextVersionOutput(outBuf.String()) if err != nil { return nil, nil, fmt.Errorf("unable to parse version: %w", err) } @@ -65,7 +110,7 @@ providerVersionOutputRe = regexp.MustCompile(`(\n\+ provider[\. ](?P<name>\S+) ` + simpleVersionRe + `)`) ) -func parseVersionOutput(stdout string) (*version.Version, map[string]*version.Version, error) { +func parsePlaintextVersionOutput(stdout string) (*version.Version, map[string]*version.Version, error) { stdout = strings.TrimSpace(stdout) submatches := versionOutputRe.FindStringSubmatch(stdout)
diff --git a/tfexec/version_test.go b/tfexec/version_test.go index 15405f0..17edb94 100644 --- a/tfexec/version_test.go +++ b/tfexec/version_test.go
@@ -7,19 +7,20 @@ "path/filepath" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-exec/tfinstall" ) -func TestParseVersionOutput(t *testing.T) { - var mustVer = func(s string) *version.Version { - v, err := version.NewVersion(s) - if err != nil { - t.Fatal(err) - } - return v +func mustVersion(t *testing.T, s string) *version.Version { + v, err := version.NewVersion(s) + if err != nil { + t.Fatal(err) } + return v +} +func TestParsePlaintextVersionOutput(t *testing.T) { for i, c := range []struct { expectedV *version.Version expectedProviders map[string]*version.Version @@ -28,29 +29,29 @@ }{ // 0.13 tests { - mustVer("0.13.0-dev"), nil, ` + mustVersion(t, "0.13.0-dev"), nil, ` Terraform v0.13.0-dev`, }, { - mustVer("0.13.0-dev"), map[string]*version.Version{ - "registry.terraform.io/hashicorp/null": mustVer("2.1.2"), - "registry.terraform.io/paultyng/null": mustVer("0.1.0"), + mustVersion(t, "0.13.0-dev"), map[string]*version.Version{ + "registry.terraform.io/hashicorp/null": mustVersion(t, "2.1.2"), + "registry.terraform.io/paultyng/null": mustVersion(t, "0.1.0"), }, ` Terraform v0.13.0-dev + provider registry.terraform.io/hashicorp/null v2.1.2 + provider registry.terraform.io/paultyng/null v0.1.0`, }, { - mustVer("0.13.0-dev"), nil, ` + mustVersion(t, "0.13.0-dev"), nil, ` Terraform v0.13.0-dev Your version of Terraform is out of date! The latest version is 0.13.1. You can update by downloading from https://www.terraform.io/downloads.html`, }, { - mustVer("0.13.0-dev"), map[string]*version.Version{ - "registry.terraform.io/hashicorp/null": mustVer("2.1.2"), - "registry.terraform.io/paultyng/null": mustVer("0.1.0"), + mustVersion(t, "0.13.0-dev"), map[string]*version.Version{ + "registry.terraform.io/hashicorp/null": mustVersion(t, "2.1.2"), + "registry.terraform.io/paultyng/null": mustVersion(t, "0.1.0"), }, ` Terraform v0.13.0-dev + provider registry.terraform.io/hashicorp/null v2.1.2 @@ -62,20 +63,20 @@ // 0.12 tests { - mustVer("0.12.26"), nil, ` + mustVersion(t, "0.12.26"), nil, ` Terraform v0.12.26 `, }, { - mustVer("0.12.26"), map[string]*version.Version{ - "null": mustVer("2.1.2"), + mustVersion(t, "0.12.26"), map[string]*version.Version{ + "null": mustVersion(t, "2.1.2"), }, ` Terraform v0.12.26 + provider.null v2.1.2 `, }, { - mustVer("0.12.18"), nil, ` + mustVersion(t, "0.12.18"), nil, ` Terraform v0.12.18 Your version of Terraform is out of date! The latest version @@ -83,8 +84,8 @@ `, }, { - mustVer("0.12.18"), map[string]*version.Version{ - "null": mustVer("2.1.2"), + mustVersion(t, "0.12.18"), map[string]*version.Version{ + "null": mustVersion(t, "2.1.2"), }, ` Terraform v0.12.18 + provider.null v2.1.2 @@ -95,7 +96,7 @@ }, } { t.Run(fmt.Sprintf("%d %s", i, c.expectedV), func(t *testing.T) { - actualV, actualProv, err := parseVersionOutput(c.stdout) + actualV, actualProv, err := parsePlaintextVersionOutput(c.stdout) if err != nil { t.Fatal(err) } @@ -117,6 +118,37 @@ } } +func TestParseJsonVersionOutput(t *testing.T) { + testStdout := []byte(`{ + "terraform_version": "0.15.0-beta1", + "platform": "darwin_amd64", + "provider_selections": { + "registry.terraform.io/hashicorp/aws": "3.31.0", + "registry.terraform.io/hashicorp/google": "3.58.0" + }, + "terraform_outdated": false +} +`) + tfVersion, pvs, err := parseJsonVersionOutput(testStdout) + if err != nil { + t.Fatal(err) + } + expectedTfVer := mustVersion(t, "0.15.0-beta1") + + if !expectedTfVer.Equal(tfVersion) { + t.Fatalf("version doesn't match (%q != %q)", + expectedTfVer.String(), tfVersion.String()) + } + + expectedPvs := map[string]*version.Version{ + "registry.terraform.io/hashicorp/aws": mustVersion(t, "3.31.0"), + "registry.terraform.io/hashicorp/google": mustVersion(t, "3.58.0"), + } + if diff := cmp.Diff(expectedPvs, pvs); diff != "" { + t.Fatalf("provider versions don't match: %s", diff) + } +} + func TestVersionInRange(t *testing.T) { for i, c := range []struct { expected bool