Merge pull request #9346 from hashicorp/hcl-validation-command

command/validate: Add support for HCL2 config files
This commit is contained in:
Wilken Rivera 2020-06-05 14:44:13 -04:00 committed by GitHub
commit 085de68b82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 205 additions and 135 deletions

View File

@ -0,0 +1,15 @@
{
"builders":[
{
"type":"file",
"target":"chocolate.txt",
"content":"chocolate"
}
],
"provisioners": [
{
"type": "file",
"comment": "unknown field"
}
]
}

View File

@ -0,0 +1,10 @@
{
"builders":[
{
"type":"file",
"target":"chocolate.txt",
"content":"chocolate"
}
],
"provisioners": "not an array"
}

View File

@ -0,0 +1,9 @@
source "file" "chocolate" {
target = "chocolate.txt"
content = "chocolate"
}
build {
sources = ["source.file.cho"]
}

View File

@ -0,0 +1,9 @@
{
"builders":[
{
"type":"file",
"target":"chocolate.txt",
"content":"chocolate"
}
]
}

View File

@ -0,0 +1,8 @@
source "file" "chocolate" {
target = "chocolate.txt"
content = "chocolate"
}
build {
sources = ["source.file.chocolate"]
}

View File

@ -0,0 +1,13 @@
variable "target" {
type = string
default = "chocolate.txt"
}
source "file" "chocolate" {
target = var.target
content = "chocolate"
}
build {
sources = ["source.file.chocolate"]
}

View File

@ -2,16 +2,10 @@ package command
import ( import (
"context" "context"
"encoding/json"
"fmt"
"log"
"strings" "strings"
"github.com/hashicorp/packer/fix"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template"
"github.com/google/go-cmp/cmp"
"github.com/posener/complete" "github.com/posener/complete"
) )
@ -51,137 +45,27 @@ func (c *ValidateCommand) ParseArgs(args []string) (*ValidateArgs, int) {
} }
func (c *ValidateCommand) RunContext(ctx context.Context, cla *ValidateArgs) int { func (c *ValidateCommand) RunContext(ctx context.Context, cla *ValidateArgs) int {
// Parse the template packerStarter, ret := c.GetConfig(&cla.MetaArgs)
tpl, err := template.ParseFile(cla.Path) if ret != 0 {
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
return 1 return 1
} }
// If we're only checking syntax, then we're done already // If we're only checking syntax, then we're done already
if cla.SyntaxOnly { if cla.SyntaxOnly {
c.Ui.Say("Syntax-only check passed. Everything looks okay.")
return 0 return 0
} }
// Get the core _, diags := packerStarter.GetBuilds(packer.GetBuildsOptions{
core, err := c.Meta.Core(tpl, &cla.MetaArgs) Only: cla.Only,
if err != nil { Except: cla.Except,
c.Ui.Error(err.Error()) })
return 1
}
errs := make([]error, 0) fixerDiags := packerStarter.FixConfig(packer.FixConfigOptions{
warnings := make(map[string][]string) Mode: packer.Diff,
})
diags = append(diags, fixerDiags...)
// Get the builds we care about return writeDiags(c.Ui, nil, diags)
buildNames := core.BuildNames(c.CoreConfig.Only, c.CoreConfig.Except)
builds := make([]packer.Build, 0, len(buildNames))
for _, n := range buildNames {
b, err := core.Build(n)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to initialize build '%s': %s",
n, err))
return 1
}
builds = append(builds, b)
}
// Check the configuration of all builds
for _, b := range builds {
log.Printf("Preparing build: %s", b.Name())
warns, err := b.Prepare()
if len(warns) > 0 {
warnings[b.Name()] = warns
}
if err != nil {
errs = append(errs, fmt.Errorf("Errors validating build '%s'. %s", b.Name(), err))
}
}
// Check if any of the configuration is fixable
var rawTemplateData map[string]interface{}
input := make(map[string]interface{})
templateData := make(map[string]interface{})
json.Unmarshal(tpl.RawContents, &rawTemplateData)
for k, v := range rawTemplateData {
if vals, ok := v.([]interface{}); ok {
if len(vals) == 0 {
continue
}
}
templateData[strings.ToLower(k)] = v
input[strings.ToLower(k)] = v
}
// fix rawTemplateData into input
for _, name := range fix.FixerOrder {
var err error
fixer, ok := fix.Fixers[name]
if !ok {
panic("fixer not found: " + name)
}
input, err = fixer.Fix(input)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error checking against fixers: %s", err))
return 1
}
}
// delete empty top-level keys since the fixers seem to add them
// willy-nilly
for k := range input {
ml, ok := input[k].([]map[string]interface{})
if !ok {
continue
}
if len(ml) == 0 {
delete(input, k)
}
}
// marshal/unmarshal to make comparable to templateData
var fixedData map[string]interface{}
// Guaranteed to be valid json, so we can ignore errors
j, _ := json.Marshal(input)
json.Unmarshal(j, &fixedData)
if diff := cmp.Diff(templateData, fixedData); diff != "" {
c.Ui.Say("[warning] Fixable configuration found.")
c.Ui.Say("You may need to run `packer fix` to get your build to run")
c.Ui.Say("correctly. See debug log for more information.\n")
log.Printf("Fixable config differences:\n%s", diff)
}
if len(errs) > 0 {
c.Ui.Error("Template validation failed. Errors are shown below.\n")
for i, err := range errs {
c.Ui.Error(err.Error())
if (i + 1) < len(errs) {
c.Ui.Error("")
}
}
return 1
}
if len(warnings) > 0 {
c.Ui.Say("Template validation succeeded, but there were some warnings.")
c.Ui.Say("These are ONLY WARNINGS, and Packer will attempt to build the")
c.Ui.Say("template despite them, but they should be paid attention to.\n")
for build, warns := range warnings {
c.Ui.Say(fmt.Sprintf("Warnings for build '%s':\n", build))
for _, warning := range warns {
c.Ui.Say(fmt.Sprintf("* %s", warning))
}
}
return 0
}
c.Ui.Say("Template validated successfully.")
return 0
} }
func (*ValidateCommand) Help() string { func (*ValidateCommand) Help() string {

View File

@ -5,6 +5,62 @@ import (
"testing" "testing"
) )
func TestValidateCommand(t *testing.T) {
tt := []struct {
path string
exitCode int
}{
{path: filepath.Join(testFixture("validate"), "build.json")},
{path: filepath.Join(testFixture("validate"), "build.pkr.hcl")},
{path: filepath.Join(testFixture("validate"), "build_with_vars.pkr.hcl")},
{path: filepath.Join(testFixture("validate-invalid"), "bad_provisioner.json"), exitCode: 1},
{path: filepath.Join(testFixture("validate-invalid"), "missing_build_block.pkr.hcl"), exitCode: 1},
}
c := &ValidateCommand{
Meta: testMetaFile(t),
}
for _, tc := range tt {
t.Run(tc.path, func(t *testing.T) {
tc := tc
args := []string{tc.path}
if code := c.Run(args); code != tc.exitCode {
fatalCommand(t, c.Meta)
}
})
}
}
func TestValidateCommand_SyntaxOnly(t *testing.T) {
tt := []struct {
path string
exitCode int
}{
{path: filepath.Join(testFixture("validate"), "build.json")},
{path: filepath.Join(testFixture("validate"), "build.pkr.hcl")},
{path: filepath.Join(testFixture("validate"), "build_with_vars.pkr.hcl")},
{path: filepath.Join(testFixture("validate-invalid"), "bad_provisioner.json")},
{path: filepath.Join(testFixture("validate-invalid"), "missing_build_block.pkr.hcl")},
{path: filepath.Join(testFixture("validate-invalid"), "broken.json"), exitCode: 1},
}
c := &ValidateCommand{
Meta: testMetaFile(t),
}
c.CoreConfig.Version = "102.0.0"
for _, tc := range tt {
t.Run(tc.path, func(t *testing.T) {
tc := tc
args := []string{"-syntax-only", tc.path}
if code := c.Run(args); code != tc.exitCode {
fatalCommand(t, c.Meta)
}
})
}
}
func TestValidateCommandOKVersion(t *testing.T) { func TestValidateCommandOKVersion(t *testing.T) {
c := &ValidateCommand{ c := &ValidateCommand{
Meta: testMetaFile(t), Meta: testMetaFile(t),

View File

@ -450,3 +450,8 @@ func (p *PackerConfig) handleEval(line string) (out string, exit bool, diags hcl
return PrintableCtyValue(val), false, diags return PrintableCtyValue(val), false, diags
} }
func (p *PackerConfig) FixConfig(_ packer.FixConfigOptions) (diags hcl.Diagnostics) {
// No Fixers exist for HCL2 configs so there is nothing to do here for now.
return
}

View File

@ -159,7 +159,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error {
if fixable { if fixable {
unusedErr = fmt.Errorf("Deprecated configuration key: '%s'."+ unusedErr = fmt.Errorf("Deprecated configuration key: '%s'."+
" Please call `packer fix` against your template to "+ " Please call `packer fix` against your template to "+
"update your template to be compatable with the current "+ "update your template to be compatible with the current "+
"version of Packer. Visit "+ "version of Packer. Visit "+
"https://www.packer.io/docs/commands/fix/ for more detail.", "https://www.packer.io/docs/commands/fix/ for more detail.",
unused) unused)

View File

@ -1,6 +1,7 @@
package packer package packer
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"regexp" "regexp"
@ -9,6 +10,7 @@ import (
ttmp "text/template" ttmp "text/template"
"github.com/google/go-cmp/cmp"
multierror "github.com/hashicorp/go-multierror" multierror "github.com/hashicorp/go-multierror"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
@ -420,6 +422,66 @@ func (c *Core) EvaluateExpression(line string) (string, bool, hcl.Diagnostics) {
} }
} }
func (c *Core) FixConfig(opts FixConfigOptions) hcl.Diagnostics {
var diags hcl.Diagnostics
// Remove once we have support for the Inplace FixConfigMode
if opts.Mode != Diff {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("FixConfig only supports template diff; FixConfigMode %d not supported", opts.Mode),
})
return diags
}
var rawTemplateData map[string]interface{}
input := make(map[string]interface{})
templateData := make(map[string]interface{})
if err := json.Unmarshal(c.Template.RawContents, &rawTemplateData); err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("unable to read the contents of the JSON configuration file: %s", err),
Detail: err.Error(),
})
return diags
}
// Hold off on Diff for now - need to think about displaying to user.
// delete empty top-level keys since the fixers seem to add them
// willy-nilly
for k := range input {
ml, ok := input[k].([]map[string]interface{})
if !ok {
continue
}
if len(ml) == 0 {
delete(input, k)
}
}
// marshal/unmarshal to make comparable to templateData
var fixedData map[string]interface{}
// Guaranteed to be valid json, so we can ignore errors
j, _ := json.Marshal(input)
if err := json.Unmarshal(j, &fixedData); err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("unable to read the contents of the JSON configuration file: %s", err),
Detail: err.Error(),
})
return diags
}
if diff := cmp.Diff(templateData, fixedData); diff != "" {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Fixable configuration found.\nPlease run `packer fix` to get your build to run correctly.\nSee debug log for more information.",
Detail: diff,
})
}
return diags
}
// validate does a full validation of the template. // validate does a full validation of the template.
// //
// This will automatically call template.validate() in addition to doing // This will automatically call template.validate() in addition to doing

View File

@ -28,6 +28,7 @@ type Evaluator interface {
type Handler interface { type Handler interface {
Evaluator Evaluator
BuildGetter BuildGetter
ConfigFixer
} }
//go:generate enumer -type FixConfigMode //go:generate enumer -type FixConfigMode
@ -44,7 +45,7 @@ const (
) )
type FixConfigOptions struct { type FixConfigOptions struct {
DiffOnly bool Mode FixConfigMode
} }
type ConfigFixer interface { type ConfigFixer interface {

View File

@ -33,13 +33,11 @@ Errors validating build 'vmware'. 1 error(s) occurred:
- `-syntax-only` - Only the syntax of the template is checked. The - `-syntax-only` - Only the syntax of the template is checked. The
configuration is not validated. configuration is not validated.
- `-except=foo,bar,baz` - Builds all the builds and post-processors except - `-except=foo,bar,baz` - Validates all the builds except those with the
those with the given comma-separated names. Build and post-processor names comma-separated names. Build names by default are the names of their
by default are the names of their builders, unless a specific `name` builders, unless a specific `name` attribute is specified within the configuration.
attribute is specified within the configuration. A post-processor with an
empty name will be ignored.
- `-only=foo,bar,baz` - Only build the builds with the given comma-separated - `-only=foo,bar,baz` - Only validate the builds with the given comma-separated
names. Build names by default are the names of their builders, unless a names. Build names by default are the names of their builders, unless a
specific `name` attribute is specified within the configuration. specific `name` attribute is specified within the configuration.