Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terraform fmt support #82

Merged
merged 2 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
149 changes: 149 additions & 0 deletions tfexec/fmt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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
}

// FormatString formats a passed string, given a path to Terraform.
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)
}

// FormatString formats a passed string.
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
}

// FormatWrite attempts to format and modify all config files in the working or selected (via DirOption) directory.
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)
}

// FormatCheck returns true if the config files in the working or selected (via DirOption) directory are already formatted.
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