Skip to content

Commit

Permalink
Add terraform validate support
Browse files Browse the repository at this point in the history
  • Loading branch information
paultyng committed Sep 1, 2020
1 parent 7b8e759 commit 8880293
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/hashicorp/go-getter v1.4.0
github.com/hashicorp/go-version v1.2.1
github.com/hashicorp/terraform-json v0.5.0
github.com/stretchr/testify v1.6.1
github.com/zclconf/go-cty v1.2.1
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1U
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
Expand Down Expand Up @@ -63,8 +64,10 @@ github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8j
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
Expand All @@ -76,8 +79,11 @@ github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdI
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand Down Expand Up @@ -166,8 +172,12 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
13 changes: 13 additions & 0 deletions tfexec/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ var (
workspaceDoesNotExistRegexp = regexp.MustCompile(`Workspace "(.+)" doesn't exist.`)

workspaceAlreadyExistsRegexp = regexp.MustCompile(`Workspace "(.+)" already exists`)

configInvalidErrRegexp = regexp.MustCompile(`There are some problems with the configuration, described below.`)
)

func parseError(err error, stderr string) error {
Expand Down Expand Up @@ -58,10 +60,21 @@ func parseError(err error, stderr string) error {
if len(submatches) == 2 {
return &ErrWorkspaceExists{submatches[1]}
}
case configInvalidErrRegexp.MatchString(stderr):
return &ErrConfigInvalid{stderr: stderr}
}

return errors.New(stderr)
}

type ErrConfigInvalid struct {
stderr string
}

func (e *ErrConfigInvalid) Error() string {
return "configuration is invalid"
}

type ErrNoSuitableBinary struct {
err error
}
Expand Down
6 changes: 6 additions & 0 deletions tfexec/internal/e2etest/testdata/invalid/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
bad_block {
}

terraform {
bad_attribute = "string"
}
108 changes: 108 additions & 0 deletions tfexec/internal/e2etest/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package e2etest

import (
"context"
"errors"
"testing"

"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"

"github.com/hashicorp/terraform-exec/tfexec"
)

var (
validateMinVersion = version.Must(version.NewVersion("0.12.0"))
)

func TestValidate(t *testing.T) {
runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(validateMinVersion) {
t.Skip("terraform validate -json was added in Terraform 0.12, so test is not valid")
}

err := tf.Init(context.Background())
if err != nil {
t.Fatal(err)
}

validation, err := tf.Validate(context.Background())
if err != nil {
t.Fatal(err)
}

if !validation.Valid {
t.Fatalf("expected valid, got %#v", validation)
}
})

runTest(t, "invalid", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(validateMinVersion) {
t.Skip("terraform validate -json was added in Terraform 0.12, so test is not valid")
}

err := tf.Init(context.Background())
if err != nil {
t.Logf("error initializing: %s", err)

// allow for invalid config errors only here
// 0.13 will return this, 0.12 will not
// unsure why 0.12 terraform init does not have a non-zero exit code for syntax problems
var confErr *tfexec.ErrConfigInvalid
if !errors.As(err, &confErr) {
t.Fatalf("expected err ErrConfigInvalid, got %T: %s", err, err)
}
}

actual, err := tf.Validate(context.Background())
if err != nil {
t.Fatal(err)
}

// reset byte locations in actual as CRLF issues render them off between operating systems
for _, diag := range actual.Diagnostics {
diag.Range.Start.Byte = 0
diag.Range.End.Byte = 0
}

assert.Equal(t, &tfexec.Validation{
Valid: false,
ErrorCount: 2,
WarningCount: 0,
Diagnostics: []tfexec.Diagnostic{
{
Severity: "error",
Summary: "Unsupported block type",
Detail: "Blocks of type \"bad_block\" are not expected here.",
Range: tfexec.Range{
Filename: "main.tf",
Start: tfexec.Pos{
Line: 1,
Column: 1,
},
End: tfexec.Pos{
Line: 1,
Column: 10,
},
},
},
{
Severity: "error",
Summary: "Unsupported argument",
Detail: "An argument named \"bad_attribute\" is not expected here.",
Range: tfexec.Range{
Filename: "main.tf",
Start: tfexec.Pos{
Line: 5,
Column: 5,
},
End: tfexec.Pos{
Line: 5,
Column: 18,
},
},
},
},
}, actual)
})
}
41 changes: 41 additions & 0 deletions tfexec/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tfexec

import (
"bytes"
"context"
"encoding/json"
"fmt"
)

// Validate represents the validate subcommand to the Terraform CLI. The -json
// flag support was added in 0.12.0, so this will not work on earlier versions.
func (tf *Terraform) Validate(ctx context.Context) (*Validation, error) {
err := tf.compatible(ctx, tf0_12_0, nil)
if err != nil {
return nil, fmt.Errorf("terraform validate -json was added in 0.12.0: %w", err)
}

cmd := tf.buildTerraformCmd(ctx, "validate", "-no-color", "-json")

var outbuf = bytes.Buffer{}
cmd.Stdout = &outbuf

err = tf.runTerraformCmd(cmd)
// TODO: this command should not exit 1 if you pass -json as its hard to differentiate other errors
if err != nil && cmd.ProcessState.ExitCode() != 1 {
return nil, err
}

var out Validation
jsonErr := json.Unmarshal(outbuf.Bytes(), &out)
if jsonErr != nil {
// the original call was possibly bad, if it has an error, actually just return that
if err != nil {
return nil, err
}

return nil, jsonErr
}

return &out, nil
}
30 changes: 30 additions & 0 deletions tfexec/validate_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tfexec

// TODO: move these types to terraform-json

type Validation struct {
Valid bool `json:"valid"`
ErrorCount int `json:"error_count"`
WarningCount int `json:"warning_count"`

Diagnostics []Diagnostic `json:"diagnostics"`
}

type Diagnostic struct {
Severity string `json:"severity"`
Summary string `json:"summary"`
Detail string `json:"detail"`
Range Range `json:"range"`
}

type Range struct {
Filename string `json:"filename"`
Start Pos `json:"start"`
End Pos `json:"end"`
}

type Pos struct {
Line int `json:"line"`
Column int `json:"column"`
Byte int `json:"byte"`
}

0 comments on commit 8880293

Please sign in to comment.