2020-11-11 11:49:39 -05:00
|
|
|
package hcl2template
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
2021-01-08 21:22:26 -05:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2020-11-11 11:49:39 -05:00
|
|
|
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclwrite"
|
|
|
|
)
|
|
|
|
|
|
|
|
type HCL2Formatter struct {
|
2021-01-08 21:22:26 -05:00
|
|
|
ShowDiff, Write, Recursive bool
|
|
|
|
Output io.Writer
|
|
|
|
parser *hclparse.Parser
|
2020-11-11 11:49:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewHCL2Formatter creates a new formatter, ready to format configuration files.
|
|
|
|
func NewHCL2Formatter() *HCL2Formatter {
|
|
|
|
return &HCL2Formatter{
|
|
|
|
parser: hclparse.NewParser(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-08 21:22:26 -05:00
|
|
|
func isHcl2FileOrVarFile(path string) bool {
|
|
|
|
if strings.HasSuffix(path, hcl2FileExt) || strings.HasSuffix(path, hcl2VarFileExt) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *HCL2Formatter) formatFile(path string, diags hcl.Diagnostics, bytesModified int) (int, hcl.Diagnostics) {
|
|
|
|
data, err := f.processFile(path)
|
|
|
|
if err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: fmt.Sprintf("encountered an error while formatting %s", path),
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
bytesModified += len(data)
|
|
|
|
return bytesModified, diags
|
|
|
|
}
|
|
|
|
|
2020-11-11 11:49:39 -05:00
|
|
|
// Format all HCL2 files in path and return the total bytes formatted.
|
|
|
|
// If any error is encountered, zero bytes will be returned.
|
|
|
|
//
|
|
|
|
// Path can be a directory or a file.
|
|
|
|
func (f *HCL2Formatter) Format(path string) (int, hcl.Diagnostics) {
|
2021-01-08 21:22:26 -05:00
|
|
|
var diags hcl.Diagnostics
|
|
|
|
var bytesModified int
|
2020-11-11 11:49:39 -05:00
|
|
|
|
2021-01-08 21:22:26 -05:00
|
|
|
if path == "" {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "path is empty, cannot format",
|
|
|
|
Detail: "path is empty, cannot format",
|
|
|
|
})
|
|
|
|
return bytesModified, diags
|
2020-11-11 11:49:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if f.parser == nil {
|
|
|
|
f.parser = hclparse.NewParser()
|
|
|
|
}
|
|
|
|
|
2021-03-14 19:42:53 -04:00
|
|
|
if path == "-" {
|
2021-03-15 06:49:03 -04:00
|
|
|
return f.formatFile(path, diags, bytesModified)
|
2021-03-14 19:42:53 -04:00
|
|
|
}
|
|
|
|
|
2021-01-08 21:22:26 -05:00
|
|
|
isDir, err := isDir(path)
|
|
|
|
if err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Cannot tell wether " + path + " is a directory",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return bytesModified, diags
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isDir {
|
2021-03-15 06:48:13 -04:00
|
|
|
return f.formatFile(path, diags, bytesModified)
|
|
|
|
}
|
|
|
|
|
|
|
|
fileInfos, err := ioutil.ReadDir(path)
|
|
|
|
if err != nil {
|
|
|
|
diag := &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Cannot read hcl directory",
|
|
|
|
Detail: err.Error(),
|
2021-01-08 21:22:26 -05:00
|
|
|
}
|
2021-03-15 06:48:13 -04:00
|
|
|
diags = append(diags, diag)
|
|
|
|
return bytesModified, diags
|
|
|
|
}
|
2021-01-08 21:22:26 -05:00
|
|
|
|
2021-03-15 06:48:13 -04:00
|
|
|
for _, fileInfo := range fileInfos {
|
|
|
|
filename := filepath.Join(path, fileInfo.Name())
|
|
|
|
if fileInfo.IsDir() {
|
|
|
|
if f.Recursive {
|
|
|
|
var tempDiags hcl.Diagnostics
|
|
|
|
var tempBytesModified int
|
|
|
|
tempBytesModified, tempDiags = f.Format(filename)
|
|
|
|
bytesModified += tempBytesModified
|
|
|
|
diags = diags.Extend(tempDiags)
|
2021-01-08 21:22:26 -05:00
|
|
|
}
|
2021-03-15 06:48:13 -04:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if isHcl2FileOrVarFile(filename) {
|
|
|
|
bytesModified, diags = f.formatFile(filename, diags, bytesModified)
|
2020-11-11 11:49:39 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return bytesModified, diags
|
|
|
|
}
|
|
|
|
|
|
|
|
// processFile formats the source contents of filename and return the formatted data.
|
|
|
|
// overwriting the contents of the original when the f.Write is true; a diff of the changes
|
|
|
|
// will be outputted if f.ShowDiff is true.
|
|
|
|
func (f *HCL2Formatter) processFile(filename string) ([]byte, error) {
|
2021-01-08 21:22:26 -05:00
|
|
|
|
2020-11-11 11:49:39 -05:00
|
|
|
if f.Output == nil {
|
|
|
|
f.Output = os.Stdout
|
|
|
|
}
|
|
|
|
|
2021-01-19 18:30:34 -05:00
|
|
|
var in io.Reader
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if filename == "-" {
|
|
|
|
in = os.Stdin
|
|
|
|
f.ShowDiff = false
|
|
|
|
} else {
|
|
|
|
in, err = os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to open %s: %s", filename, err)
|
|
|
|
}
|
2020-11-11 11:49:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
inSrc, err := ioutil.ReadAll(in)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to read %s: %s", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, diags := f.parser.ParseHCL(inSrc, filename)
|
|
|
|
if diags.HasErrors() {
|
|
|
|
return nil, fmt.Errorf("failed to parse HCL %s", filename)
|
|
|
|
}
|
|
|
|
|
|
|
|
outSrc := hclwrite.Format(inSrc)
|
|
|
|
|
|
|
|
if bytes.Equal(inSrc, outSrc) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2021-01-20 14:30:50 -05:00
|
|
|
if filename != "-" {
|
|
|
|
s := []byte(fmt.Sprintf("%s\n", filename))
|
|
|
|
_, _ = f.Output.Write(s)
|
|
|
|
}
|
2020-11-11 11:49:39 -05:00
|
|
|
|
|
|
|
if f.Write {
|
2021-01-19 18:30:34 -05:00
|
|
|
if filename == "-" {
|
2021-01-19 18:34:18 -05:00
|
|
|
_, _ = f.Output.Write(outSrc)
|
2021-01-19 18:30:34 -05:00
|
|
|
} else {
|
|
|
|
if err := ioutil.WriteFile(filename, outSrc, 0644); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-11-11 11:49:39 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if f.ShowDiff {
|
|
|
|
diff, err := bytesDiff(inSrc, outSrc, filename)
|
|
|
|
if err != nil {
|
|
|
|
return outSrc, fmt.Errorf("failed to generate diff for %s: %s", filename, err)
|
|
|
|
}
|
|
|
|
_, _ = f.Output.Write(diff)
|
|
|
|
}
|
|
|
|
|
|
|
|
return outSrc, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// bytesDiff returns the unified diff of b1 and b2
|
|
|
|
// Shamelessly copied from Terraform's fmt command.
|
|
|
|
func bytesDiff(b1, b2 []byte, path string) (data []byte, err error) {
|
|
|
|
f1, err := ioutil.TempFile("", "")
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer os.Remove(f1.Name())
|
|
|
|
defer f1.Close()
|
|
|
|
|
|
|
|
f2, err := ioutil.TempFile("", "")
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer os.Remove(f2.Name())
|
|
|
|
defer f2.Close()
|
|
|
|
|
|
|
|
_, _ = f1.Write(b1)
|
|
|
|
_, _ = f2.Write(b2)
|
|
|
|
|
|
|
|
data, err = exec.Command("diff", "--label=old/"+path, "--label=new/"+path, "-u", f1.Name(), f2.Name()).CombinedOutput()
|
|
|
|
if len(data) > 0 {
|
|
|
|
// diff exits with a non-zero status when the files don't match.
|
|
|
|
// Ignore that failure as long as we get output.
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|