mirror of https://github.com/pulumi/pulumi.git
205 lines
6.8 KiB
Go
205 lines
6.8 KiB
Go
// Copyright 2016-2023, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package result
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
)
|
|
|
|
type bailError struct {
|
|
err error
|
|
}
|
|
|
|
// BailError is the replacement for Result now that Go supports wrapping errors. It is used to indicate that
|
|
// a computation failed, but that it failed gracefully, i.e. it is not a bug in Pulumi. BailError implements
|
|
// the error interface but will prefix it's error string with BAIL, which if ever seen in user facing messages
|
|
// indicates that a check for bailing was missed. It also blocks `Unwrap` calls, instead to get access to the
|
|
// inner error use the `IsBail` function.
|
|
func BailError(err error) error {
|
|
contract.Requiref(err != nil, "err", "must not be nil")
|
|
|
|
return &bailError{err: err}
|
|
}
|
|
|
|
func (b *bailError) Error() string {
|
|
return fmt.Sprintf("BAIL: %v", b.err)
|
|
}
|
|
|
|
// BailErrorf is a helper for BailError(fmt.Errorf(...)).
|
|
func BailErrorf(format string, args ...interface{}) error {
|
|
return BailError(fmt.Errorf(format, args...))
|
|
}
|
|
|
|
// FprintBailf writes a formatted string to the given writer and returns a BailError with the same message.
|
|
func FprintBailf(w io.Writer, msg string, args ...any) error {
|
|
msg = fmt.Sprintf(msg, args...)
|
|
fmt.Fprintln(w, msg)
|
|
return BailError(errors.New(msg))
|
|
}
|
|
|
|
// IsBail reports whether any error in err's tree is a `BailError`.
|
|
func IsBail(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
var bail *bailError
|
|
ok := errors.As(err, &bail)
|
|
return ok
|
|
}
|
|
|
|
// Result represents the result of a computation that can fail. The Result type revolves around two
|
|
// notions of failure:
|
|
//
|
|
// 1. Computations can fail, but they can fail gracefully. Computations that fail gracefully do so
|
|
// by logging a diagnostic and returning a non-nil "bail" result.
|
|
//
|
|
// 2. Computations can fail due to bugs in Pulumi. Computations that fail in this manner do so by
|
|
// constructing a Result using the `Error`, `Errorf`, or `FromError` constructor functions.
|
|
//
|
|
// Result is an interface so that it can be nullable. A function returning a pointer Result has the
|
|
// following semantics:
|
|
//
|
|
// - If the result is `nil`, the caller should proceed. The callee believes
|
|
// that the overarching plan can still continue, even if it logged
|
|
// diagnostics.
|
|
//
|
|
// - If the result is non-nil, the caller should not proceed. Most often, the
|
|
// caller should return this Result to its caller.
|
|
//
|
|
// At the highest level, when a function wishes to return only an `error`, the `Error` member
|
|
// function can be used to turn a nullable `Result` into an `error`.
|
|
type Result interface {
|
|
Error() error
|
|
IsBail() bool
|
|
}
|
|
|
|
type simpleResult struct {
|
|
err error
|
|
}
|
|
|
|
func (r *simpleResult) Error() error { return r.err }
|
|
func (r *simpleResult) IsBail() bool { return r.err == nil }
|
|
func (r *simpleResult) String() string {
|
|
if r.err == nil {
|
|
return "Bail"
|
|
}
|
|
return fmt.Sprintf("Error: %s", r.err)
|
|
}
|
|
|
|
func (r *simpleResult) GoString() string {
|
|
if r.err == nil {
|
|
return "&simpleResult{}"
|
|
}
|
|
return fmt.Sprintf("&simpleResult{err: %#v}", r.err)
|
|
}
|
|
|
|
// Bail produces a Result that represents a computation that failed to complete
|
|
// successfully but is not a bug in Pulumi.
|
|
func Bail() Result {
|
|
return &simpleResult{err: nil}
|
|
}
|
|
|
|
// Errorf produces a Result that represents an internal Pulumi error,
|
|
// constructed from the given format string and arguments.
|
|
func Errorf(msg string, args ...interface{}) Result {
|
|
err := fmt.Errorf(msg, args...)
|
|
return FromError(err)
|
|
}
|
|
|
|
// Error produces a Result that represents an internal Pulumi error,
|
|
// constructed from the given message.
|
|
func Error(msg string) Result {
|
|
err := errors.New(msg)
|
|
return FromError(err)
|
|
}
|
|
|
|
// FromError produces a Result that wraps an internal Pulumi error. Do not call this with a 'nil' error. A
|
|
// 'nil' error means that there was no problem, and in that case a 'nil' result should be used instead. If
|
|
// this is called with an error from `BailError` it will discard the inner error of that and return a bail
|
|
// result.
|
|
func FromError(err error) Result {
|
|
if err == nil {
|
|
panic("FromError should not be called with a nil-error. " +
|
|
"If there is no error, then a nil result should be returned. " +
|
|
"Caller should check for this first.")
|
|
}
|
|
|
|
if IsBail(err) {
|
|
return &simpleResult{err: nil}
|
|
}
|
|
|
|
return &simpleResult{err: err}
|
|
}
|
|
|
|
// WrapIfNonNil returns a non-nil Result if [err] is non-nil. Otherwise it returns nil.
|
|
func WrapIfNonNil(err error) Result {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
return FromError(err)
|
|
}
|
|
|
|
// TODO returns an error that can be used in places that have not yet been
|
|
// adapted to use Results. Their use is intended to be temporary until Results
|
|
// are plumbed throughout the Pulumi codebase.
|
|
func TODO() error {
|
|
return errors.New("bailing due to error")
|
|
}
|
|
|
|
// Merge combines two results into one final result. It properly respects all three forms of Result
|
|
// (i.e. nil/bail/error) for both results, and combines all sensibly into a final form that represents
|
|
// the information of both.
|
|
func Merge(res1 Result, res2 Result) Result {
|
|
switch {
|
|
// If both are nil, then there's no problem. Return 'nil' to properly convey that outwards.
|
|
case res1 == nil && res2 == nil:
|
|
return nil
|
|
|
|
// Otherwise, if one is nil, and the other is not, then the non-nil takes precedence.
|
|
// i.e. an actual error (or bail) takes precedence
|
|
case res1 == nil:
|
|
return res2
|
|
case res2 == nil:
|
|
return res1
|
|
|
|
// If both results have asked to bail, then just bail. That properly respects both requests.
|
|
case res1.IsBail() && res2.IsBail():
|
|
return Bail()
|
|
|
|
// We have two non-nil results and one, or both, of the results indicate an error.
|
|
|
|
// If we have a request to Bail and a request to error then the request to error takes
|
|
// precedence. The concept of bailing is that we've printed an error already and should just
|
|
// quickly finish the entire pulumi execution. However, for an error, we are indicating a bug
|
|
// happened, and that we haven't printed it, and that it should print at the end. So we need
|
|
// to respect the error form here and pass it all the way back.
|
|
case res1.IsBail():
|
|
return res2
|
|
case res2.IsBail():
|
|
return res1
|
|
|
|
// Both results are errors. Combine them into one joint error and return that.
|
|
default:
|
|
return FromError(multierror.Append(res1.Error(), res2.Error()))
|
|
}
|
|
}
|