diff --git a/go.mod b/go.mod index 12845c25..303a30f4 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 79ef9b39..9be0b4f3 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/tfexec/errors.go b/tfexec/errors.go index f4010985..0d16000b 100644 --- a/tfexec/errors.go +++ b/tfexec/errors.go @@ -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 { @@ -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 } diff --git a/tfexec/internal/e2etest/testdata/invalid/main.tf b/tfexec/internal/e2etest/testdata/invalid/main.tf new file mode 100644 index 00000000..2da3086e --- /dev/null +++ b/tfexec/internal/e2etest/testdata/invalid/main.tf @@ -0,0 +1,6 @@ +bad_block { +} + +terraform { + bad_attribute = "string" +} \ No newline at end of file diff --git a/tfexec/internal/e2etest/validate_test.go b/tfexec/internal/e2etest/validate_test.go new file mode 100644 index 00000000..f851eeb8 --- /dev/null +++ b/tfexec/internal/e2etest/validate_test.go @@ -0,0 +1,105 @@ +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 + cleanActual := []tfexec.Diagnostic{} + for _, diag := range actual.Diagnostics { + diag.Range.Start.Byte = 0 + diag.Range.End.Byte = 0 + cleanActual = append(cleanActual, diag) + } + + assert.Equal(t, []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, + }, + }, + }, + }, cleanActual) + }) +} diff --git a/tfexec/validate.go b/tfexec/validate.go new file mode 100644 index 00000000..1ee3042a --- /dev/null +++ b/tfexec/validate.go @@ -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 +} diff --git a/tfexec/validate_types.go b/tfexec/validate_types.go new file mode 100644 index 00000000..8035c10b --- /dev/null +++ b/tfexec/validate_types.go @@ -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"` +}