* Add packer fmt command This change adds a new command that allows users to format one or more HCL2 Packer configuration template files. Related to: #9174 * command/fmt: Add check flag Packer's fmt command now supports a check flag that will output the name of any file that would be changed by the HCL2 formatting engine. The check flag is mutually exclusive with the write flag and will only check if formatting is needed. The update write flag will now overwrite the source files with the newly formatted HCL2 source unless the `-write=false` or `-check` is passed at the command line. * Returns a diagnostic error if Format is unable to show a diff - equivalent to `terraform fmt` * Updates testing to run against #Format and not the private methods of the HCL2Formatter; fixes ShowDiff test failure on Windows * Updates comments for exported functions * Add docs for fmt command
152 lines
3.5 KiB
Go
152 lines
3.5 KiB
Go
package hcl2template
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
|
"github.com/hashicorp/hcl/v2/hclwrite"
|
|
)
|
|
|
|
type HCL2Formatter struct {
|
|
ShowDiff, Write bool
|
|
Output io.Writer
|
|
parser *hclparse.Parser
|
|
}
|
|
|
|
// NewHCL2Formatter creates a new formatter, ready to format configuration files.
|
|
func NewHCL2Formatter() *HCL2Formatter {
|
|
return &HCL2Formatter{
|
|
parser: hclparse.NewParser(),
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
hclFiles, _, diags := GetHCL2Files(path, hcl2FileExt, hcl2JsonFileExt)
|
|
if diags.HasErrors() {
|
|
return 0, diags
|
|
}
|
|
|
|
hclVarFiles, _, diags := GetHCL2Files(path, hcl2VarFileExt, hcl2VarJsonFileExt)
|
|
if diags.HasErrors() {
|
|
return 0, diags
|
|
}
|
|
|
|
allHclFiles := append(hclFiles, hclVarFiles...)
|
|
|
|
if len(allHclFiles) == 0 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Cannot tell whether %s contains HCL2 configuration data", path),
|
|
})
|
|
|
|
return 0, diags
|
|
}
|
|
|
|
if f.parser == nil {
|
|
f.parser = hclparse.NewParser()
|
|
}
|
|
|
|
var bytesModified int
|
|
for _, fn := range allHclFiles {
|
|
data, err := f.processFile(fn)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("encountered an error while formatting %s", fn),
|
|
Detail: err.Error(),
|
|
})
|
|
}
|
|
bytesModified += len(data)
|
|
}
|
|
|
|
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) {
|
|
if f.Output == nil {
|
|
f.Output = os.Stdout
|
|
}
|
|
|
|
in, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open %s: %s", filename, err)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
s := []byte(fmt.Sprintf("%s\n", filename))
|
|
_, _ = f.Output.Write(s)
|
|
|
|
if f.Write {
|
|
if err := ioutil.WriteFile(filename, outSrc, 0644); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|