tfdiags: Diagnostics.ErrWithWarnings and .NonFatalErr
There is some existing practice in the "terraform" package of returning a special error type ValidationError from EvalNode implementations in order to return warnings without halting the graph walk even though a non-nil error was returned. This is a diagnostics-flavored version of that approach, allowing us to avoid totally reworking the EvalNode concept around diagnostics and retaining the ability to return non-fatal errors. NonFatalErr is equivalent to the former terraform.ValidationError, while ErrWithWarnings is a helper that automatically treats any errors as fatal but returns NonFatalError if the diagnostics contains only warnings.
This commit is contained in:
parent
fca07d1a61
commit
3822650e15
|
@ -136,6 +136,42 @@ func (diags Diagnostics) Err() error {
|
||||||
return diagnosticsAsError{diags}
|
return diagnosticsAsError{diags}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrWithWarnings is similar to Err except that it will also return a non-nil
|
||||||
|
// error if the receiver contains only warnings.
|
||||||
|
//
|
||||||
|
// In the warnings-only situation, the result is guaranteed to be of dynamic
|
||||||
|
// type NonFatalError, allowing diagnostics-aware callers to type-assert
|
||||||
|
// and unwrap it, treating it as non-fatal.
|
||||||
|
//
|
||||||
|
// This should be used only in contexts where the caller is able to recognize
|
||||||
|
// and handle NonFatalError. For normal callers that expect a lack of errors
|
||||||
|
// to be signaled by nil, use just Diagnostics.Err.
|
||||||
|
func (diags Diagnostics) ErrWithWarnings() error {
|
||||||
|
if len(diags) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags.Err()
|
||||||
|
}
|
||||||
|
return NonFatalError{diags}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NonFatalErr is similar to Err except that it always returns either nil
|
||||||
|
// (if there are no diagnostics at all) or NonFatalError.
|
||||||
|
//
|
||||||
|
// This allows diagnostics to be returned over an error return channel while
|
||||||
|
// being explicit that the diagnostics should not halt processing.
|
||||||
|
//
|
||||||
|
// This should be used only in contexts where the caller is able to recognize
|
||||||
|
// and handle NonFatalError. For normal callers that expect a lack of errors
|
||||||
|
// to be signaled by nil, use just Diagnostics.Err.
|
||||||
|
func (diags Diagnostics) NonFatalErr() error {
|
||||||
|
if len(diags) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NonFatalError{diags}
|
||||||
|
}
|
||||||
|
|
||||||
type diagnosticsAsError struct {
|
type diagnosticsAsError struct {
|
||||||
Diagnostics
|
Diagnostics
|
||||||
}
|
}
|
||||||
|
@ -179,3 +215,44 @@ func (dae diagnosticsAsError) WrappedErrors() []error {
|
||||||
}
|
}
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NonFatalError is a special error type, returned by
|
||||||
|
// Diagnostics.ErrWithWarnings and Diagnostics.NonFatalErr,
|
||||||
|
// that indicates that the wrapped diagnostics should be treated as non-fatal.
|
||||||
|
// Callers can conditionally type-assert an error to this type in order to
|
||||||
|
// detect the non-fatal scenario and handle it in a different way.
|
||||||
|
type NonFatalError struct {
|
||||||
|
Diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func (woe NonFatalError) Error() string {
|
||||||
|
diags := woe.Diagnostics
|
||||||
|
switch {
|
||||||
|
case len(diags) == 0:
|
||||||
|
// should never happen, since we don't create this wrapper if
|
||||||
|
// there are no diagnostics in the list.
|
||||||
|
return "no errors or warnings"
|
||||||
|
case len(diags) == 1:
|
||||||
|
desc := diags[0].Description()
|
||||||
|
if desc.Detail == "" {
|
||||||
|
return desc.Summary
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail)
|
||||||
|
default:
|
||||||
|
var ret bytes.Buffer
|
||||||
|
if diags.HasErrors() {
|
||||||
|
fmt.Fprintf(&ret, "%d problems:\n", len(diags))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&ret, "%d warnings:\n", len(diags))
|
||||||
|
}
|
||||||
|
for _, diag := range woe.Diagnostics {
|
||||||
|
desc := diag.Description()
|
||||||
|
if desc.Detail == "" {
|
||||||
|
fmt.Fprintf(&ret, "\n- %s", desc.Summary)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -280,3 +280,160 @@ func TestDiagnosticsErr(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDiagnosticsErrWithWarnings(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
err := diags.ErrWithWarnings()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got non-nil error %#v; want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("warning only", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(SimpleWarning("bad"))
|
||||||
|
err := diags.ErrWithWarnings()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("got nil error; want NonFatalError")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := err.(NonFatalError); !ok {
|
||||||
|
t.Errorf("got %T; want NonFatalError", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("one error", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(errors.New("didn't work"))
|
||||||
|
err := diags.ErrWithWarnings()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
||||||
|
}
|
||||||
|
if got, want := err.Error(), "didn't work"; got != want {
|
||||||
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("two errors", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(errors.New("didn't work"))
|
||||||
|
diags = diags.Append(errors.New("didn't work either"))
|
||||||
|
err := diags.ErrWithWarnings()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
||||||
|
}
|
||||||
|
want := strings.TrimSpace(`
|
||||||
|
2 problems:
|
||||||
|
|
||||||
|
- didn't work
|
||||||
|
- didn't work either
|
||||||
|
`)
|
||||||
|
if got := err.Error(); got != want {
|
||||||
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("error and warning", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(errors.New("didn't work"))
|
||||||
|
diags = diags.Append(SimpleWarning("didn't work either"))
|
||||||
|
err := diags.ErrWithWarnings()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
||||||
|
}
|
||||||
|
// Since this "as error" mode is just a fallback for
|
||||||
|
// non-diagnostics-aware situations like tests, we don't actually
|
||||||
|
// distinguish warnings and errors here since the point is to just
|
||||||
|
// get the messages rendered. User-facing code should be printing
|
||||||
|
// each diagnostic separately, so won't enter this codepath,
|
||||||
|
want := strings.TrimSpace(`
|
||||||
|
2 problems:
|
||||||
|
|
||||||
|
- didn't work
|
||||||
|
- didn't work either
|
||||||
|
`)
|
||||||
|
if got := err.Error(); got != want {
|
||||||
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiagnosticsNonFatalErr(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
err := diags.NonFatalErr()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got non-nil error %#v; want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("warning only", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(SimpleWarning("bad"))
|
||||||
|
err := diags.NonFatalErr()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("got nil error; want NonFatalError")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := err.(NonFatalError); !ok {
|
||||||
|
t.Errorf("got %T; want NonFatalError", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("one error", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(errors.New("didn't work"))
|
||||||
|
err := diags.NonFatalErr()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
||||||
|
}
|
||||||
|
if got, want := err.Error(), "didn't work"; got != want {
|
||||||
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if _, ok := err.(NonFatalError); !ok {
|
||||||
|
t.Errorf("got %T; want NonFatalError", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("two errors", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(errors.New("didn't work"))
|
||||||
|
diags = diags.Append(errors.New("didn't work either"))
|
||||||
|
err := diags.NonFatalErr()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
||||||
|
}
|
||||||
|
want := strings.TrimSpace(`
|
||||||
|
2 problems:
|
||||||
|
|
||||||
|
- didn't work
|
||||||
|
- didn't work either
|
||||||
|
`)
|
||||||
|
if got := err.Error(); got != want {
|
||||||
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if _, ok := err.(NonFatalError); !ok {
|
||||||
|
t.Errorf("got %T; want NonFatalError", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("error and warning", func(t *testing.T) {
|
||||||
|
var diags Diagnostics
|
||||||
|
diags = diags.Append(errors.New("didn't work"))
|
||||||
|
diags = diags.Append(SimpleWarning("didn't work either"))
|
||||||
|
err := diags.NonFatalErr()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil error %#v; want non-nil", err)
|
||||||
|
}
|
||||||
|
// Since this "as error" mode is just a fallback for
|
||||||
|
// non-diagnostics-aware situations like tests, we don't actually
|
||||||
|
// distinguish warnings and errors here since the point is to just
|
||||||
|
// get the messages rendered. User-facing code should be printing
|
||||||
|
// each diagnostic separately, so won't enter this codepath,
|
||||||
|
want := strings.TrimSpace(`
|
||||||
|
2 problems:
|
||||||
|
|
||||||
|
- didn't work
|
||||||
|
- didn't work either
|
||||||
|
`)
|
||||||
|
if got := err.Error(); got != want {
|
||||||
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if _, ok := err.(NonFatalError); !ok {
|
||||||
|
t.Errorf("got %T; want NonFatalError", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue