Skip to content

Commit

Permalink
Parse version -json output and fall back to plaintext (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko authored Mar 29, 2021
1 parent e0c9ea1 commit e842b4c
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 26 deletions.
53 changes: 49 additions & 4 deletions tfexec/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package tfexec
import (
"bytes"
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"

"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-json"
)

var (
Expand Down Expand Up @@ -37,9 +39,52 @@ func (tf *Terraform) Version(ctx context.Context, skipCache bool) (tfVersion *ve

// 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
Expand All @@ -50,7 +95,7 @@ func (tf *Terraform) version(ctx context.Context) (*version.Version, map[string]
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)
}
Expand All @@ -65,7 +110,7 @@ var (
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)
Expand Down
76 changes: 54 additions & 22 deletions tfexec/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ import (
"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
Expand All @@ -28,29 +29,29 @@ func TestParseVersionOutput(t *testing.T) {
}{
// 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
Expand All @@ -62,29 +63,29 @@ is 0.13.1. You can update by downloading from https://www.terraform.io/downloads

// 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
is 0.12.26. You can update by downloading from https://www.terraform.io/downloads.html
`,
},
{
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
Expand All @@ -95,7 +96,7 @@ is 0.12.26. You can update by downloading from https://www.terraform.io/download
},
} {
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)
}
Expand All @@ -117,6 +118,37 @@ is 0.12.26. You can update by downloading from https://www.terraform.io/download
}
}

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
Expand Down

0 comments on commit e842b4c

Please sign in to comment.