add formatting for hcl parsing error messages (#5972)

This commit is contained in:
Jasmine Dahilig 2019-07-19 10:04:39 -07:00 committed by GitHub
parent d24f56290b
commit 2157f6ddf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 94 additions and 34 deletions

View file

@ -487,7 +487,6 @@ MAIN:
// Run the task // Run the task
if err := tr.runDriver(); err != nil { if err := tr.runDriver(); err != nil {
tr.logger.Error("running driver failed", "error", err) tr.logger.Error("running driver failed", "error", err)
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(err))
tr.restartTracker.SetStartError(err) tr.restartTracker.SetStartError(err)
goto RESTART goto RESTART
} }
@ -680,6 +679,7 @@ func (tr *TaskRunner) shouldRestart() (bool, time.Duration) {
} }
// runDriver runs the driver and waits for it to exit // runDriver runs the driver and waits for it to exit
// runDriver emits an appropriate task event on success/failure
func (tr *TaskRunner) runDriver() error { func (tr *TaskRunner) runDriver() error {
taskConfig := tr.buildTaskConfig() taskConfig := tr.buildTaskConfig()
@ -705,13 +705,17 @@ func (tr *TaskRunner) runDriver() error {
tr.logger.Warn("some environment variables not available for rendering", "keys", strings.Join(keys, ", ")) tr.logger.Warn("some environment variables not available for rendering", "keys", strings.Join(keys, ", "))
} }
val, diag := hclutils.ParseHclInterface(tr.task.Config, tr.taskSchema, vars) val, diag, diagErrs := hclutils.ParseHclInterface(tr.task.Config, tr.taskSchema, vars)
if diag.HasErrors() { if diag.HasErrors() {
return multierror.Append(errors.New("failed to parse config"), diag.Errs()...) parseErr := multierror.Append(errors.New("failed to parse config: "), diagErrs...)
tr.EmitEvent(structs.NewTaskEvent(structs.TaskFailedValidation).SetValidationError(parseErr))
return parseErr
} }
if err := taskConfig.EncodeDriverConfig(val); err != nil { if err := taskConfig.EncodeDriverConfig(val); err != nil {
return fmt.Errorf("failed to encode driver config: %v", err) encodeErr := fmt.Errorf("failed to encode driver config: %v", err)
tr.EmitEvent(structs.NewTaskEvent(structs.TaskFailedValidation).SetValidationError(encodeErr))
return encodeErr
} }
// If there's already a task handle (eg from a Restore) there's nothing // If there's already a task handle (eg from a Restore) there's nothing
@ -734,16 +738,21 @@ func (tr *TaskRunner) runDriver() error {
if err == bstructs.ErrPluginShutdown { if err == bstructs.ErrPluginShutdown {
tr.logger.Info("failed to start task because plugin shutdown unexpectedly; attempting to recover") tr.logger.Info("failed to start task because plugin shutdown unexpectedly; attempting to recover")
if err := tr.initDriver(); err != nil { if err := tr.initDriver(); err != nil {
return fmt.Errorf("failed to initialize driver after it exited unexpectedly: %v", err) taskErr := fmt.Errorf("failed to initialize driver after it exited unexpectedly: %v", err)
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(taskErr))
return taskErr
} }
handle, net, err = tr.driver.StartTask(taskConfig) handle, net, err = tr.driver.StartTask(taskConfig)
if err != nil { if err != nil {
return fmt.Errorf("failed to start task after driver exited unexpectedly: %v", err) taskErr := fmt.Errorf("failed to start task after driver exited unexpectedly: %v", err)
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(taskErr))
return taskErr
} }
} else { } else {
// Do *NOT* wrap the error here without maintaining // Do *NOT* wrap the error here without maintaining whether or not is Recoverable.
// whether or not is Recoverable. // You must emit a task event failure to be considered Recoverable
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(err))
return err return err
} }
} }

View file

@ -70,7 +70,8 @@ func (b *HCLParser) parse(t *testing.T, config, out interface{}) {
decSpec, diags := hclspecutils.Convert(b.spec) decSpec, diags := hclspecutils.Convert(b.spec)
require.Empty(t, diags) require.Empty(t, diags)
ctyValue, diag := ParseHclInterface(config, decSpec, b.vars) ctyValue, diag, errs := ParseHclInterface(config, decSpec, b.vars)
require.Nil(t, errs)
require.Empty(t, diag) require.Empty(t, diag)
// encode // encode

View file

@ -2,6 +2,7 @@ package hclutils
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
@ -17,7 +18,7 @@ import (
// ParseHclInterface is used to convert an interface value representing a hcl2 // ParseHclInterface is used to convert an interface value representing a hcl2
// body and return the interpolated value. Vars may be nil if there are no // body and return the interpolated value. Vars may be nil if there are no
// variables to interpolate. // variables to interpolate.
func ParseHclInterface(val interface{}, spec hcldec.Spec, vars map[string]cty.Value) (cty.Value, hcl.Diagnostics) { func ParseHclInterface(val interface{}, spec hcldec.Spec, vars map[string]cty.Value) (cty.Value, hcl.Diagnostics, []error) {
evalCtx := &hcl.EvalContext{ evalCtx := &hcl.EvalContext{
Variables: vars, Variables: vars,
Functions: GetStdlibFuncs(), Functions: GetStdlibFuncs(),
@ -29,27 +30,29 @@ func ParseHclInterface(val interface{}, spec hcldec.Spec, vars map[string]cty.Va
err := enc.Encode(val) err := enc.Encode(val)
if err != nil { if err != nil {
// Convert to a hcl diagnostics message // Convert to a hcl diagnostics message
return cty.NilVal, hcl.Diagnostics([]*hcl.Diagnostic{ errorMessage := fmt.Sprintf("Label encoding failed: %v", err)
{ return cty.NilVal,
hcl.Diagnostics([]*hcl.Diagnostic{{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Failed to JSON encode value", Summary: "Failed to encode label value",
Detail: fmt.Sprintf("JSON encoding failed: %v", err), Detail: errorMessage,
}}) }}),
[]error{errors.New(errorMessage)}
} }
// Parse the json as hcl2 // Parse the json as hcl2
hclFile, diag := hjson.Parse(buf.Bytes(), "") hclFile, diag := hjson.Parse(buf.Bytes(), "")
if diag.HasErrors() { if diag.HasErrors() {
return cty.NilVal, diag return cty.NilVal, diag, formattedDiagnosticErrors(diag)
} }
value, decDiag := hcldec.Decode(hclFile.Body, spec, evalCtx) value, decDiag := hcldec.Decode(hclFile.Body, spec, evalCtx)
diag = diag.Extend(decDiag) diag = diag.Extend(decDiag)
if diag.HasErrors() { if diag.HasErrors() {
return cty.NilVal, diag return cty.NilVal, diag, formattedDiagnosticErrors(diag)
} }
return value, diag return value, diag, nil
} }
// GetStdlibFuncs returns the set of stdlib functions. // GetStdlibFuncs returns the set of stdlib functions.
@ -72,3 +75,18 @@ func GetStdlibFuncs() map[string]function.Function {
"upper": stdlib.UpperFunc, "upper": stdlib.UpperFunc,
} }
} }
// TODO: update hcl2 library with better diagnostics formatting for streamed configs
// - should be arbitrary labels not JSON https://github.com/hashicorp/hcl2/blob/4fba5e1a75e382aed7f7a7993f2c4836a5e1cd52/hcl/json/structure.go#L66
// - should not print diagnostic subject https://github.com/hashicorp/hcl2/blob/4fba5e1a75e382aed7f7a7993f2c4836a5e1cd52/hcl/diagnostic.go#L77
func formattedDiagnosticErrors(diag hcl.Diagnostics) []error {
var errs []error
for _, d := range diag {
if d.Summary == "Extraneous JSON object property" {
d.Summary = "Invalid label"
}
err := errors.New(fmt.Sprintf("%s: %s", d.Summary, d.Detail))
errs = append(errs, err)
}
return errs
}

View file

@ -358,9 +358,9 @@ func TestParseHclInterface_Hcl(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Logf("Val: % #v", pretty.Formatter(c.config)) t.Logf("Val: % #v", pretty.Formatter(c.config))
// Parse the interface // Parse the interface
ctyValue, diag := hclutils.ParseHclInterface(c.config, c.spec, c.vars) ctyValue, diag, errs := hclutils.ParseHclInterface(c.config, c.spec, c.vars)
if diag.HasErrors() { if diag.HasErrors() {
for _, err := range diag.Errs() { for _, err := range errs {
t.Error(err) t.Error(err)
} }
t.FailNow() t.FailNow()
@ -497,11 +497,45 @@ func TestParseUnknown(t *testing.T) {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
inter := hclutils.HclConfigToInterface(t, c.hcl) inter := hclutils.HclConfigToInterface(t, c.hcl)
ctyValue, diag := hclutils.ParseHclInterface(inter, cSpec, vars) ctyValue, diag, errs := hclutils.ParseHclInterface(inter, cSpec, vars)
t.Logf("parsed: %# v", pretty.Formatter(ctyValue)) t.Logf("parsed: %# v", pretty.Formatter(ctyValue))
require.NotNil(t, errs)
require.True(t, diag.HasErrors()) require.True(t, diag.HasErrors())
require.Contains(t, diag.Errs()[0].Error(), "no variable named") require.Contains(t, errs[0].Error(), "no variable named")
})
}
}
func TestParseInvalid(t *testing.T) {
dockerDriver := new(docker.Driver)
dockerSpec, err := dockerDriver.TaskConfigSchema()
require.NoError(t, err)
spec, diags := hclspecutils.Convert(dockerSpec)
require.False(t, diags.HasErrors())
cases := []struct {
name string
hcl string
}{
{
"invalid_field",
`config { image = "redis:3.2" bad_key = "whatever"}`,
},
}
vars := map[string]cty.Value{}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
inter := hclutils.HclConfigToInterface(t, c.hcl)
ctyValue, diag, errs := hclutils.ParseHclInterface(inter, spec, vars)
t.Logf("parsed: %# v", pretty.Formatter(ctyValue))
require.NotNil(t, errs)
require.True(t, diag.HasErrors())
require.Contains(t, errs[0].Error(), "Invalid label")
}) })
} }
} }

View file

@ -457,10 +457,11 @@ func (l *PluginLoader) validatePluginConfig(id PluginID, info *pluginInfo) error
} }
// Parse the config using the spec // Parse the config using the spec
val, diag := hclutils.ParseHclInterface(info.config, spec, nil) val, diag, diagErrs := hclutils.ParseHclInterface(info.config, spec, nil)
if diag.HasErrors() { if diag.HasErrors() {
multierror.Append(&mErr, diag.Errs()...) multierror.Append(&mErr, diagErrs...)
return multierror.Prefix(&mErr, "failed parsing config:") return multierror.Prefix(&mErr, "failed to parse config: ")
} }
// Marshal the value // Marshal the value

View file

@ -5937,15 +5937,15 @@ const (
TaskSetupFailure = "Setup Failure" TaskSetupFailure = "Setup Failure"
// TaskDriveFailure indicates that the task could not be started due to a // TaskDriveFailure indicates that the task could not be started due to a
// failure in the driver. // failure in the driver. TaskDriverFailure is considered Recoverable.
TaskDriverFailure = "Driver Failure" TaskDriverFailure = "Driver Failure"
// TaskReceived signals that the task has been pulled by the client at the // TaskReceived signals that the task has been pulled by the client at the
// given timestamp. // given timestamp.
TaskReceived = "Received" TaskReceived = "Received"
// TaskFailedValidation indicates the task was invalid and as such was not // TaskFailedValidation indicates the task was invalid and as such was not run.
// run. // TaskFailedValidation is not considered Recoverable.
TaskFailedValidation = "Failed Validation" TaskFailedValidation = "Failed Validation"
// TaskStarted signals that the task was started and its timestamp can be // TaskStarted signals that the task was started and its timestamp can be

View file

@ -11,6 +11,7 @@ import (
"time" "time"
hclog "github.com/hashicorp/go-hclog" hclog "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/ast"
@ -196,13 +197,9 @@ func (c *Device) setConfig(spec hcldec.Spec, apiVersion string, config []byte, n
c.logger.Trace("raw hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(configVal))) c.logger.Trace("raw hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(configVal)))
val, diag := hclutils.ParseHclInterface(configVal, spec, nil) val, diag, diagErrs := hclutils.ParseHclInterface(configVal, spec, nil)
if diag.HasErrors() { if diag.HasErrors() {
errStr := "failed to parse config" return multierror.Append(errors.New("failed to parse config: "), diagErrs...)
for _, err := range diag.Errs() {
errStr = fmt.Sprintf("%s\n* %s", errStr, err.Error())
}
return errors.New(errStr)
} }
c.logger.Trace("parsed hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(val))) c.logger.Trace("parsed hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(val)))