Skip to content

Commit e31db57

Browse files
authored
add formatting for hcl parsing error messages (#5972)
1 parent 8833fb8 commit e31db57

File tree

7 files changed

+94
-34
lines changed

7 files changed

+94
-34
lines changed

client/allocrunner/taskrunner/task_runner.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,6 @@ MAIN:
487487
// Run the task
488488
if err := tr.runDriver(); err != nil {
489489
tr.logger.Error("running driver failed", "error", err)
490-
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(err))
491490
tr.restartTracker.SetStartError(err)
492491
goto RESTART
493492
}
@@ -680,6 +679,7 @@ func (tr *TaskRunner) shouldRestart() (bool, time.Duration) {
680679
}
681680

682681
// runDriver runs the driver and waits for it to exit
682+
// runDriver emits an appropriate task event on success/failure
683683
func (tr *TaskRunner) runDriver() error {
684684

685685
taskConfig := tr.buildTaskConfig()
@@ -705,13 +705,17 @@ func (tr *TaskRunner) runDriver() error {
705705
tr.logger.Warn("some environment variables not available for rendering", "keys", strings.Join(keys, ", "))
706706
}
707707

708-
val, diag := hclutils.ParseHclInterface(tr.task.Config, tr.taskSchema, vars)
708+
val, diag, diagErrs := hclutils.ParseHclInterface(tr.task.Config, tr.taskSchema, vars)
709709
if diag.HasErrors() {
710-
return multierror.Append(errors.New("failed to parse config"), diag.Errs()...)
710+
parseErr := multierror.Append(errors.New("failed to parse config: "), diagErrs...)
711+
tr.EmitEvent(structs.NewTaskEvent(structs.TaskFailedValidation).SetValidationError(parseErr))
712+
return parseErr
711713
}
712714

713715
if err := taskConfig.EncodeDriverConfig(val); err != nil {
714-
return fmt.Errorf("failed to encode driver config: %v", err)
716+
encodeErr := fmt.Errorf("failed to encode driver config: %v", err)
717+
tr.EmitEvent(structs.NewTaskEvent(structs.TaskFailedValidation).SetValidationError(encodeErr))
718+
return encodeErr
715719
}
716720

717721
// If there's already a task handle (eg from a Restore) there's nothing
@@ -734,16 +738,21 @@ func (tr *TaskRunner) runDriver() error {
734738
if err == bstructs.ErrPluginShutdown {
735739
tr.logger.Info("failed to start task because plugin shutdown unexpectedly; attempting to recover")
736740
if err := tr.initDriver(); err != nil {
737-
return fmt.Errorf("failed to initialize driver after it exited unexpectedly: %v", err)
741+
taskErr := fmt.Errorf("failed to initialize driver after it exited unexpectedly: %v", err)
742+
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(taskErr))
743+
return taskErr
738744
}
739745

740746
handle, net, err = tr.driver.StartTask(taskConfig)
741747
if err != nil {
742-
return fmt.Errorf("failed to start task after driver exited unexpectedly: %v", err)
748+
taskErr := fmt.Errorf("failed to start task after driver exited unexpectedly: %v", err)
749+
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(taskErr))
750+
return taskErr
743751
}
744752
} else {
745-
// Do *NOT* wrap the error here without maintaining
746-
// whether or not is Recoverable.
753+
// Do *NOT* wrap the error here without maintaining whether or not is Recoverable.
754+
// You must emit a task event failure to be considered Recoverable
755+
tr.EmitEvent(structs.NewTaskEvent(structs.TaskDriverFailure).SetDriverError(err))
747756
return err
748757
}
749758
}

helper/pluginutils/hclutils/testing.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ func (b *HCLParser) parse(t *testing.T, config, out interface{}) {
7070
decSpec, diags := hclspecutils.Convert(b.spec)
7171
require.Empty(t, diags)
7272

73-
ctyValue, diag := ParseHclInterface(config, decSpec, b.vars)
73+
ctyValue, diag, errs := ParseHclInterface(config, decSpec, b.vars)
74+
require.Nil(t, errs)
7475
require.Empty(t, diag)
7576

7677
// encode

helper/pluginutils/hclutils/util.go

+27-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package hclutils
22

33
import (
44
"bytes"
5+
"errors"
56
"fmt"
67

78
"github.com/hashicorp/hcl2/hcl"
@@ -17,7 +18,7 @@ import (
1718
// ParseHclInterface is used to convert an interface value representing a hcl2
1819
// body and return the interpolated value. Vars may be nil if there are no
1920
// variables to interpolate.
20-
func ParseHclInterface(val interface{}, spec hcldec.Spec, vars map[string]cty.Value) (cty.Value, hcl.Diagnostics) {
21+
func ParseHclInterface(val interface{}, spec hcldec.Spec, vars map[string]cty.Value) (cty.Value, hcl.Diagnostics, []error) {
2122
evalCtx := &hcl.EvalContext{
2223
Variables: vars,
2324
Functions: GetStdlibFuncs(),
@@ -29,27 +30,29 @@ func ParseHclInterface(val interface{}, spec hcldec.Spec, vars map[string]cty.Va
2930
err := enc.Encode(val)
3031
if err != nil {
3132
// Convert to a hcl diagnostics message
32-
return cty.NilVal, hcl.Diagnostics([]*hcl.Diagnostic{
33-
{
33+
errorMessage := fmt.Sprintf("Label encoding failed: %v", err)
34+
return cty.NilVal,
35+
hcl.Diagnostics([]*hcl.Diagnostic{{
3436
Severity: hcl.DiagError,
35-
Summary: "Failed to JSON encode value",
36-
Detail: fmt.Sprintf("JSON encoding failed: %v", err),
37-
}})
37+
Summary: "Failed to encode label value",
38+
Detail: errorMessage,
39+
}}),
40+
[]error{errors.New(errorMessage)}
3841
}
3942

4043
// Parse the json as hcl2
4144
hclFile, diag := hjson.Parse(buf.Bytes(), "")
4245
if diag.HasErrors() {
43-
return cty.NilVal, diag
46+
return cty.NilVal, diag, formattedDiagnosticErrors(diag)
4447
}
4548

4649
value, decDiag := hcldec.Decode(hclFile.Body, spec, evalCtx)
4750
diag = diag.Extend(decDiag)
4851
if diag.HasErrors() {
49-
return cty.NilVal, diag
52+
return cty.NilVal, diag, formattedDiagnosticErrors(diag)
5053
}
5154

52-
return value, diag
55+
return value, diag, nil
5356
}
5457

5558
// GetStdlibFuncs returns the set of stdlib functions.
@@ -72,3 +75,18 @@ func GetStdlibFuncs() map[string]function.Function {
7275
"upper": stdlib.UpperFunc,
7376
}
7477
}
78+
79+
// TODO: update hcl2 library with better diagnostics formatting for streamed configs
80+
// - should be arbitrary labels not JSON https://github.com/hashicorp/hcl2/blob/4fba5e1a75e382aed7f7a7993f2c4836a5e1cd52/hcl/json/structure.go#L66
81+
// - should not print diagnostic subject https://github.com/hashicorp/hcl2/blob/4fba5e1a75e382aed7f7a7993f2c4836a5e1cd52/hcl/diagnostic.go#L77
82+
func formattedDiagnosticErrors(diag hcl.Diagnostics) []error {
83+
var errs []error
84+
for _, d := range diag {
85+
if d.Summary == "Extraneous JSON object property" {
86+
d.Summary = "Invalid label"
87+
}
88+
err := errors.New(fmt.Sprintf("%s: %s", d.Summary, d.Detail))
89+
errs = append(errs, err)
90+
}
91+
return errs
92+
}

helper/pluginutils/hclutils/util_test.go

+38-4
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,9 @@ func TestParseHclInterface_Hcl(t *testing.T) {
358358
t.Run(c.name, func(t *testing.T) {
359359
t.Logf("Val: % #v", pretty.Formatter(c.config))
360360
// Parse the interface
361-
ctyValue, diag := hclutils.ParseHclInterface(c.config, c.spec, c.vars)
361+
ctyValue, diag, errs := hclutils.ParseHclInterface(c.config, c.spec, c.vars)
362362
if diag.HasErrors() {
363-
for _, err := range diag.Errs() {
363+
for _, err := range errs {
364364
t.Error(err)
365365
}
366366
t.FailNow()
@@ -497,11 +497,45 @@ func TestParseUnknown(t *testing.T) {
497497
t.Run(c.name, func(t *testing.T) {
498498
inter := hclutils.HclConfigToInterface(t, c.hcl)
499499

500-
ctyValue, diag := hclutils.ParseHclInterface(inter, cSpec, vars)
500+
ctyValue, diag, errs := hclutils.ParseHclInterface(inter, cSpec, vars)
501501
t.Logf("parsed: %# v", pretty.Formatter(ctyValue))
502502

503+
require.NotNil(t, errs)
503504
require.True(t, diag.HasErrors())
504-
require.Contains(t, diag.Errs()[0].Error(), "no variable named")
505+
require.Contains(t, errs[0].Error(), "no variable named")
506+
})
507+
}
508+
}
509+
510+
func TestParseInvalid(t *testing.T) {
511+
dockerDriver := new(docker.Driver)
512+
dockerSpec, err := dockerDriver.TaskConfigSchema()
513+
require.NoError(t, err)
514+
spec, diags := hclspecutils.Convert(dockerSpec)
515+
require.False(t, diags.HasErrors())
516+
517+
cases := []struct {
518+
name string
519+
hcl string
520+
}{
521+
{
522+
"invalid_field",
523+
`config { image = "redis:3.2" bad_key = "whatever"}`,
524+
},
525+
}
526+
527+
vars := map[string]cty.Value{}
528+
529+
for _, c := range cases {
530+
t.Run(c.name, func(t *testing.T) {
531+
inter := hclutils.HclConfigToInterface(t, c.hcl)
532+
533+
ctyValue, diag, errs := hclutils.ParseHclInterface(inter, spec, vars)
534+
t.Logf("parsed: %# v", pretty.Formatter(ctyValue))
535+
536+
require.NotNil(t, errs)
537+
require.True(t, diag.HasErrors())
538+
require.Contains(t, errs[0].Error(), "Invalid label")
505539
})
506540
}
507541
}

helper/pluginutils/loader/init.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -457,10 +457,11 @@ func (l *PluginLoader) validatePluginConfig(id PluginID, info *pluginInfo) error
457457
}
458458

459459
// Parse the config using the spec
460-
val, diag := hclutils.ParseHclInterface(info.config, spec, nil)
460+
val, diag, diagErrs := hclutils.ParseHclInterface(info.config, spec, nil)
461461
if diag.HasErrors() {
462-
multierror.Append(&mErr, diag.Errs()...)
463-
return multierror.Prefix(&mErr, "failed parsing config:")
462+
multierror.Append(&mErr, diagErrs...)
463+
return multierror.Prefix(&mErr, "failed to parse config: ")
464+
464465
}
465466

466467
// Marshal the value

nomad/structs/structs.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -5937,15 +5937,15 @@ const (
59375937
TaskSetupFailure = "Setup Failure"
59385938

59395939
// TaskDriveFailure indicates that the task could not be started due to a
5940-
// failure in the driver.
5940+
// failure in the driver. TaskDriverFailure is considered Recoverable.
59415941
TaskDriverFailure = "Driver Failure"
59425942

59435943
// TaskReceived signals that the task has been pulled by the client at the
59445944
// given timestamp.
59455945
TaskReceived = "Received"
59465946

5947-
// TaskFailedValidation indicates the task was invalid and as such was not
5948-
// run.
5947+
// TaskFailedValidation indicates the task was invalid and as such was not run.
5948+
// TaskFailedValidation is not considered Recoverable.
59495949
TaskFailedValidation = "Failed Validation"
59505950

59515951
// TaskStarted signals that the task was started and its timestamp can be

plugins/shared/cmd/launcher/command/device.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
hclog "github.com/hashicorp/go-hclog"
14+
multierror "github.com/hashicorp/go-multierror"
1415
plugin "github.com/hashicorp/go-plugin"
1516
"github.com/hashicorp/hcl"
1617
"github.com/hashicorp/hcl/hcl/ast"
@@ -196,13 +197,9 @@ func (c *Device) setConfig(spec hcldec.Spec, apiVersion string, config []byte, n
196197

197198
c.logger.Trace("raw hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(configVal)))
198199

199-
val, diag := hclutils.ParseHclInterface(configVal, spec, nil)
200+
val, diag, diagErrs := hclutils.ParseHclInterface(configVal, spec, nil)
200201
if diag.HasErrors() {
201-
errStr := "failed to parse config"
202-
for _, err := range diag.Errs() {
203-
errStr = fmt.Sprintf("%s\n* %s", errStr, err.Error())
204-
}
205-
return errors.New(errStr)
202+
return multierror.Append(errors.New("failed to parse config: "), diagErrs...)
206203
}
207204
c.logger.Trace("parsed hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(val)))
208205

0 commit comments

Comments
 (0)