report all errors from module validation

It can be tedious fixing a new module with many errors when Terraform
only outputs the first random error it encounters.

Accumulate all errors from validation, and format them for the user.
This commit is contained in:
James Bardin 2017-03-02 14:10:41 -05:00
parent 0fa3c71d09
commit 2a949093ed
3 changed files with 67 additions and 32 deletions

View File

@ -1 +1,2 @@
variable "memory" {} variable "memory" {}
variable "feature" {}

View File

@ -259,7 +259,7 @@ func (t *Tree) Validate() error {
} }
// If something goes wrong, here is our error template // If something goes wrong, here is our error template
newErr := &TreeError{Name: []string{t.Name()}} newErr := &treeError{Name: []string{t.Name()}}
// Terraform core does not handle root module children named "root". // Terraform core does not handle root module children named "root".
// We plan to fix this in the future but this bug was brought up in // We plan to fix this in the future but this bug was brought up in
@ -271,15 +271,14 @@ func (t *Tree) Validate() error {
// Validate our configuration first. // Validate our configuration first.
if err := t.config.Validate(); err != nil { if err := t.config.Validate(); err != nil {
newErr.Err = err newErr.Add(err)
return newErr
} }
// If we're the root, we do extra validation. This validation usually // If we're the root, we do extra validation. This validation usually
// requires the entire tree (since children don't have parent pointers). // requires the entire tree (since children don't have parent pointers).
if len(t.path) == 0 { if len(t.path) == 0 {
if err := t.validateProviderAlias(); err != nil { if err := t.validateProviderAlias(); err != nil {
return err newErr.Add(err)
} }
} }
@ -293,7 +292,7 @@ func (t *Tree) Validate() error {
continue continue
} }
verr, ok := err.(*TreeError) verr, ok := err.(*treeError)
if !ok { if !ok {
// Unknown error, just return... // Unknown error, just return...
return err return err
@ -301,7 +300,7 @@ func (t *Tree) Validate() error {
// Append ourselves to the error and then return // Append ourselves to the error and then return
verr.Name = append(verr.Name, t.Name()) verr.Name = append(verr.Name, t.Name())
return verr newErr.AddChild(verr)
} }
// Go over all the modules and verify that any parameters are valid // Go over all the modules and verify that any parameters are valid
@ -327,10 +326,9 @@ func (t *Tree) Validate() error {
// Compare to the keys in our raw config for the module // Compare to the keys in our raw config for the module
for k, _ := range m.RawConfig.Raw { for k, _ := range m.RawConfig.Raw {
if _, ok := varMap[k]; !ok { if _, ok := varMap[k]; !ok {
newErr.Err = fmt.Errorf( newErr.Add(fmt.Errorf(
"module %s: %s is not a valid parameter", "module %s: %s is not a valid parameter",
m.Name, k) m.Name, k))
return newErr
} }
// Remove the required // Remove the required
@ -339,10 +337,9 @@ func (t *Tree) Validate() error {
// If we have any required left over, they aren't set. // If we have any required left over, they aren't set.
for k, _ := range requiredMap { for k, _ := range requiredMap {
newErr.Err = fmt.Errorf( newErr.Add(fmt.Errorf(
"module %s: required variable %s not set", "module %s: required variable %q not set",
m.Name, k) m.Name, k))
return newErr
} }
} }
@ -369,33 +366,61 @@ func (t *Tree) Validate() error {
} }
} }
if !found { if !found {
newErr.Err = fmt.Errorf( newErr.Add(fmt.Errorf(
"%s: %s is not a valid output for module %s", "%s: %s is not a valid output for module %s",
source, mv.Field, mv.Name) source, mv.Field, mv.Name))
return newErr
} }
} }
} }
return newErr.ErrOrNil()
}
// treeError is an error use by Tree.Validate to accumulates all
// validation errors.
type treeError struct {
Name []string
Errs []error
Children []*treeError
}
func (e *treeError) Add(err error) {
e.Errs = append(e.Errs, err)
}
func (e *treeError) AddChild(err *treeError) {
e.Children = append(e.Children, err)
}
func (e *treeError) ErrOrNil() error {
if len(e.Errs) > 0 || len(e.Children) > 0 {
return e
}
return nil return nil
} }
// TreeError is an error returned by Tree.Validate if an error occurs func (e *treeError) Error() string {
// with validation. name := strings.Join(e.Name, ".")
type TreeError struct { var out bytes.Buffer
Name []string fmt.Fprintf(&out, "module %s: ", name)
Err error
}
func (e *TreeError) Error() string { if len(e.Errs) == 1 {
// Build up the name // single like error
var buf bytes.Buffer out.WriteString(e.Errs[0].Error())
for _, n := range e.Name { } else {
buf.WriteString(n) // multi-line error
buf.WriteString(".") for _, err := range e.Errs {
fmt.Fprintf(&out, "\n %s", err)
}
} }
buf.Truncate(buf.Len() - 1)
// Format the value if len(e.Children) > 0 {
return fmt.Sprintf("module %s: %s", buf.String(), e.Err) // start the next error on a new line
out.WriteString("\n ")
}
for _, child := range e.Children {
out.WriteString(child.Error())
}
return out.String()
} }

View File

@ -410,9 +410,18 @@ func TestTreeValidate_requiredChildVar(t *testing.T) {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if err := tree.Validate(); err == nil { err := tree.Validate()
if err == nil {
t.Fatal("should error") t.Fatal("should error")
} }
// ensure both variables are mentioned in the output
errMsg := err.Error()
for _, v := range []string{"feature", "memory"} {
if !strings.Contains(errMsg, v) {
t.Fatalf("no mention of missing variable %q", v)
}
}
} }
const treeLoadStr = ` const treeLoadStr = `