Skip to content

Commit

Permalink
Implement terraform fmt support
Browse files Browse the repository at this point in the history
  • Loading branch information
paultyng authored and appilon committed Sep 21, 2020
1 parent 0fd6d69 commit ecb3d92
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 17 deletions.
8 changes: 7 additions & 1 deletion tfexec/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ var (
)

func (tf *Terraform) parseError(err error, stderr string) error {
if _, ok := err.(*exec.ExitError); !ok {
ee, ok := err.(*exec.ExitError)
if !ok {
return err
}

Expand Down Expand Up @@ -87,6 +88,11 @@ func (tf *Terraform) parseError(err error, stderr string) error {
return &ErrWorkspaceExists{submatches[1]}
}
}
errString := strings.TrimSpace(stderr)
if errString == "" {
// if stderr is empty, return the ExitError directly, as it will have a better message
return ee
}
return errors.New(stderr)
}

Expand Down
145 changes: 145 additions & 0 deletions tfexec/fmt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package tfexec

import (
"bytes"
"context"
"fmt"
"os/exec"
"path/filepath"
"strings"
)

type formatConfig struct {
recursive bool
dir string
}

var defaultFormatConfig = formatConfig{
recursive: false,
}

type FormatOption interface {
configureFormat(*formatConfig)
}

func (opt *RecursiveOption) configureFormat(conf *formatConfig) {
conf.recursive = opt.recursive
}

func (opt *DirOption) configureFormat(conf *formatConfig) {
conf.dir = opt.path
}

func FormatString(ctx context.Context, execPath string, content string) (string, error) {
tf, err := NewTerraform(filepath.Dir(execPath), execPath)
if err != nil {
return "", err
}

return tf.FormatString(ctx, content)
}

func (tf *Terraform) FormatString(ctx context.Context, content string) (string, error) {
cmd, err := tf.formatCmd(ctx, nil, Dir("-"))
if err != nil {
return "", err
}

cmd.Stdin = strings.NewReader(content)

var outBuf bytes.Buffer
cmd.Stdout = mergeWriters(cmd.Stdout, &outBuf)

err = tf.runTerraformCmd(cmd)
if err != nil {
return "", err
}

return outBuf.String(), nil
}

func (tf *Terraform) FormatWrite(ctx context.Context, opts ...FormatOption) error {
for _, o := range opts {
switch o := o.(type) {
case *DirOption:
if o.path == "-" {
return fmt.Errorf("a path of \"-\" is not supported for this method, please use FormatString")
}
}
}

cmd, err := tf.formatCmd(ctx, []string{"-write=true", "-list=false", "-diff=false"}, opts...)
if err != nil {
return err
}

return tf.runTerraformCmd(cmd)
}

func (tf *Terraform) FormatCheck(ctx context.Context, opts ...FormatOption) (bool, []string, error) {
for _, o := range opts {
switch o := o.(type) {
case *DirOption:
if o.path == "-" {
return false, nil, fmt.Errorf("a path of \"-\" is not supported for this method, please use FormatString")
}
}
}

cmd, err := tf.formatCmd(ctx, []string{"-write=false", "-list=true", "-diff=false", "-check=true"}, opts...)
if err != nil {
return false, nil, err
}

var outBuf bytes.Buffer
cmd.Stdout = mergeWriters(cmd.Stdout, &outBuf)

err = tf.runTerraformCmd(cmd)
if err == nil {
return true, nil, nil
}
if cmd.ProcessState.ExitCode() == 3 {
// unformatted, parse the file list

files := []string{}
lines := strings.Split(strings.Replace(outBuf.String(), "\r\n", "\n", -1), "\n")
for _, l := range lines {
l = strings.TrimSpace(l)
if l == "" {
continue
}
files = append(files, l)
}

return false, files, nil
}
return false, nil, err
}

func (tf *Terraform) formatCmd(ctx context.Context, args []string, opts ...FormatOption) (*exec.Cmd, error) {
c := defaultFormatConfig

for _, o := range opts {
switch o.(type) {
case *RecursiveOption:
err := tf.compatible(ctx, tf0_12_0, nil)
if err != nil {
return nil, fmt.Errorf("-recursive was added to fmt in Terraform 0.12: %w", err)
}
}

o.configureFormat(&c)
}

args = append([]string{"fmt", "-no-color"}, args...)

if c.recursive {
args = append(args, "-recursive")
}

if c.dir != "" {
args = append(args, c.dir)
}

return tf.buildTerraformCmd(ctx, nil, args...), nil
}
86 changes: 86 additions & 0 deletions tfexec/internal/e2etest/fmt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package e2etest

import (
"context"
"path/filepath"
"reflect"
"strings"
"testing"

"github.com/hashicorp/go-version"

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

func TestFormatString(t *testing.T) {
runTest(t, "", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
unformatted := strings.TrimSpace(`
resource "foo" "bar" {
baz = 1
qux = 2
}
`)

expected := strings.TrimSpace(`
resource "foo" "bar" {
baz = 1
qux = 2
}
`)

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

actual = strings.TrimSpace(actual)

if actual != expected {
t.Fatalf("expected:\n%s\ngot:\n%s\n", expected, actual)
}
})
}

func TestFormatCheck(t *testing.T) {
runTest(t, "unformatted", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
checksums := map[string]uint32{
"file1.tf": checkSum(t, filepath.Join(tf.WorkingDir(), "file1.tf")),
"file2.tf": checkSum(t, filepath.Join(tf.WorkingDir(), "file2.tf")),
}

formatted, files, err := tf.FormatCheck(context.Background())
if err != nil {
t.Fatalf("error from FormatCheck: %T %q", err, err)
}

if formatted {
t.Fatal("expected unformatted")
}

if !reflect.DeepEqual(files, []string{"file1.tf", "file2.tf"}) {
t.Fatalf("unexpected files list: %#v", files)
}

for file, checksum := range checksums {
if checksum != checkSum(t, filepath.Join(tf.WorkingDir(), file)) {
t.Fatalf("%s should not have changed", file)
}
}
})
}

func TestFormatWrite(t *testing.T) {
runTest(t, "unformatted", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.FormatWrite(context.Background())
if err != nil {
t.Fatalf("error from FormatWrite: %T %q", err, err)
}

for file, golden := range map[string]string{
"file1.tf": "file1.golden.txt",
"file2.tf": "file2.golden.txt",
} {
textFilesEqual(t, filepath.Join(tf.WorkingDir(), golden), filepath.Join(tf.WorkingDir(), file))
}
})
}
4 changes: 4 additions & 0 deletions tfexec/internal/e2etest/testdata/unformatted/file1.golden.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "foo" "bar" {
baz = 1
qux = 2
}
4 changes: 4 additions & 0 deletions tfexec/internal/e2etest/testdata/unformatted/file1.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "foo" "bar" {
baz = 1
qux = 2
}
4 changes: 4 additions & 0 deletions tfexec/internal/e2etest/testdata/unformatted/file2.golden.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "foo" "baz" {
baz = 1
qux = 2
}
4 changes: 4 additions & 0 deletions tfexec/internal/e2etest/testdata/unformatted/file2.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "foo" "baz" {
baz = 1
qux = 2
}
37 changes: 21 additions & 16 deletions tfexec/internal/e2etest/util_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package e2etest

import (
"bufio"
"bytes"
"context"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"os"
Expand Down Expand Up @@ -163,27 +162,33 @@ func copyFile(path string, dstPath string) error {
return nil
}

// filesEqual returns true iff the two files have the same contents.
func filesEqual(file1, file2 string) (bool, error) {
sf, err := os.Open(file1)
// filesEqual asserts that two files have the same contents.
func textFilesEqual(t *testing.T, expected, actual string) {
eb, err := ioutil.ReadFile(expected)
if err != nil {
return false, err
t.Fatal(err)
}

df, err := os.Open(file2)
ab, err := ioutil.ReadFile(actual)
if err != nil {
return false, err
t.Fatal(err)
}

sscan := bufio.NewScanner(sf)
dscan := bufio.NewScanner(df)
es := string(eb)
es = strings.ReplaceAll(es, "\r\n", "\n")

for sscan.Scan() {
dscan.Scan()
if !bytes.Equal(sscan.Bytes(), dscan.Bytes()) {
return true, nil
}
as := string(ab)
as = strings.ReplaceAll(as, "\r\n", "\n")

if as != es {
t.Fatalf("expected:\n%s\n\ngot:\n%s\n", es, as)
}
}

return false, nil
func checkSum(t *testing.T, filename string) uint32 {
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
return crc32.ChecksumIEEE(b)
}
8 changes: 8 additions & 0 deletions tfexec/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ func Reconfigure(reconfigure bool) *ReconfigureOption {
return &ReconfigureOption{reconfigure}
}

type RecursiveOption struct {
recursive bool
}

func Recursive(r bool) *RecursiveOption {
return &RecursiveOption{r}
}

type RefreshOption struct {
refresh bool
}
Expand Down

0 comments on commit ecb3d92

Please sign in to comment.