Skip to content

Commit

Permalink
Unwrap exec.ExitError on all our special errors
Browse files Browse the repository at this point in the history
And respond to errors.Is for Context errors
  • Loading branch information
paultyng committed Sep 30, 2020
1 parent 464bd75 commit 1618687
Show file tree
Hide file tree
Showing 20 changed files with 196 additions and 41 deletions.
2 changes: 1 addition & 1 deletion tfexec/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error {
if err != nil {
return err
}
return tf.runTerraformCmd(cmd)
return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) {
Expand Down
8 changes: 4 additions & 4 deletions tfexec/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,27 +135,27 @@ func (tf *Terraform) buildTerraformCmd(ctx context.Context, mergeEnv map[string]
return cmd
}

func (tf *Terraform) runTerraformCmdJSON(cmd *exec.Cmd, v interface{}) error {
func (tf *Terraform) runTerraformCmdJSON(ctx context.Context, cmd *exec.Cmd, v interface{}) error {
var outbuf = bytes.Buffer{}
cmd.Stdout = mergeWriters(cmd.Stdout, &outbuf)

err := tf.runTerraformCmd(cmd)
err := tf.runTerraformCmd(ctx, cmd)
if err != nil {
return err
}

return json.Unmarshal(outbuf.Bytes(), v)
}

func (tf *Terraform) runTerraformCmd(cmd *exec.Cmd) error {
func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
var errBuf strings.Builder

cmd.Stdout = mergeWriters(cmd.Stdout, tf.stdout)
cmd.Stderr = mergeWriters(cmd.Stderr, tf.stderr, &errBuf)

err := cmd.Run()
if err != nil {
return tf.parseError(err, errBuf.String())
return tf.wrapExitError(ctx, err, errBuf.String())
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion tfexec/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (tf *Terraform) Destroy(ctx context.Context, opts ...DestroyOption) error {
if err != nil {
return err
}
return tf.runTerraformCmd(cmd)
return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) {
Expand Down
8 changes: 6 additions & 2 deletions tfexec/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ func (e *ErrNoSuitableBinary) Error() string {
return fmt.Sprintf("no suitable terraform binary could be found: %s", e.err.Error())
}

func (e *ErrNoSuitableBinary) Unwrap() error {
return e.err
}

// ErrVersionMismatch is returned when the detected Terraform version is not compatible with the
// command or flags being used in this invocation.
type ErrVersionMismatch struct {
Expand All @@ -27,9 +31,9 @@ func (e *ErrVersionMismatch) Error() string {
// ErrManualEnvVar is returned when an env var that should be set programatically via an option or method
// is set via the manual environment passing functions.
type ErrManualEnvVar struct {
name string
Name string
}

func (err *ErrManualEnvVar) Error() string {
return fmt.Sprintf("manual setting of env var %q detected", err.name)
return fmt.Sprintf("manual setting of env var %q detected", err.Name)
}
95 changes: 81 additions & 14 deletions tfexec/exit_errors.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package tfexec

import (
"errors"
"context"
"fmt"
"os/exec"
"regexp"
Expand Down Expand Up @@ -30,12 +30,21 @@ var (
tfVersionMismatchConstraintRegexp = regexp.MustCompile(`required_version = "(.+)"|Required version: (.+)\b`)
)

func (tf *Terraform) parseError(err error, stderr string) error {
ee, ok := err.(*exec.ExitError)
func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error {
exitErr, ok := err.(*exec.ExitError)
if !ok {
// not an exit error, short circuit, nothing to wrap
return err
}

ctxErr := ctx.Err()

// nothing to parse, return early
errString := strings.TrimSpace(stderr)
if errString == "" {
return &unwrapper{exitErr, ctxErr}
}

switch {
case tfVersionMismatchErrRegexp.MatchString(stderr):
constraint := ""
Expand All @@ -59,6 +68,8 @@ func (tf *Terraform) parseError(err error, stderr string) error {
}

return &ErrTFVersionMismatch{
unwrapper: unwrapper{exitErr, ctxErr},

Constraint: constraint,
TFVersion: ver,
}
Expand All @@ -72,33 +83,77 @@ func (tf *Terraform) parseError(err error, stderr string) error {
}
}

return &ErrMissingVar{name}
return &ErrMissingVar{
unwrapper: unwrapper{exitErr, ctxErr},

VariableName: name,
}
case usageRegexp.MatchString(stderr):
return &ErrCLIUsage{stderr: stderr}
return &ErrCLIUsage{
unwrapper: unwrapper{exitErr, ctxErr},

stderr: stderr,
}
case noInitErrRegexp.MatchString(stderr):
return &ErrNoInit{stderr: stderr}
return &ErrNoInit{
unwrapper: unwrapper{exitErr, ctxErr},

stderr: stderr,
}
case noConfigErrRegexp.MatchString(stderr):
return &ErrNoConfig{stderr: stderr}
return &ErrNoConfig{
unwrapper: unwrapper{exitErr, ctxErr},

stderr: stderr,
}
case workspaceDoesNotExistRegexp.MatchString(stderr):
submatches := workspaceDoesNotExistRegexp.FindStringSubmatch(stderr)
if len(submatches) == 2 {
return &ErrNoWorkspace{submatches[1]}
return &ErrNoWorkspace{
unwrapper: unwrapper{exitErr, ctxErr},

Name: submatches[1],
}
}
case workspaceAlreadyExistsRegexp.MatchString(stderr):
submatches := workspaceAlreadyExistsRegexp.FindStringSubmatch(stderr)
if len(submatches) == 2 {
return &ErrWorkspaceExists{submatches[1]}
return &ErrWorkspaceExists{
unwrapper: unwrapper{exitErr, ctxErr},

Name: submatches[1],
}
}
}
errString := strings.TrimSpace(stderr)
if errString == "" {
// if stderr is empty, return the ExitError directly, as it will have a better message
return ee

return fmt.Errorf("%w\n%s", &unwrapper{exitErr, ctxErr}, stderr)
}

type unwrapper struct {
err error
ctxErr error
}

func (u *unwrapper) Unwrap() error {
return u.err
}

func (u *unwrapper) Is(target error) bool {
switch target {
case context.DeadlineExceeded, context.Canceled:
return u.ctxErr == context.DeadlineExceeded ||
u.ctxErr == context.Canceled
}
return errors.New(stderr)
return false
}

func (u *unwrapper) Error() string {
return u.err.Error()
}

type ErrMissingVar struct {
unwrapper

VariableName string
}

Expand All @@ -107,6 +162,8 @@ func (err *ErrMissingVar) Error() string {
}

type ErrNoWorkspace struct {
unwrapper

Name string
}

Expand All @@ -116,6 +173,8 @@ func (err *ErrNoWorkspace) Error() string {

// ErrWorkspaceExists is returned when creating a workspace that already exists
type ErrWorkspaceExists struct {
unwrapper

Name string
}

Expand All @@ -124,6 +183,8 @@ func (err *ErrWorkspaceExists) Error() string {
}

type ErrNoInit struct {
unwrapper

stderr string
}

Expand All @@ -132,6 +193,8 @@ func (e *ErrNoInit) Error() string {
}

type ErrNoConfig struct {
unwrapper

stderr string
}

Expand All @@ -148,6 +211,8 @@ func (e *ErrNoConfig) Error() string {
// Currently cases 1 and 2 are handled.
// TODO KEM: Handle exit 127 case. How does this work on non-Unix platforms?
type ErrCLIUsage struct {
unwrapper

stderr string
}

Expand All @@ -158,6 +223,8 @@ func (e *ErrCLIUsage) Error() string {
// ErrTFVersionMismatch is returned when the running Terraform version is not compatible with the
// value specified for required_version in the terraform block.
type ErrTFVersionMismatch struct {
unwrapper

TFVersion string

// Constraint is not returned in the error messaging on 0.12
Expand Down
6 changes: 3 additions & 3 deletions tfexec/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (tf *Terraform) FormatString(ctx context.Context, content string) (string,
var outBuf bytes.Buffer
cmd.Stdout = mergeWriters(cmd.Stdout, &outBuf)

err = tf.runTerraformCmd(cmd)
err = tf.runTerraformCmd(ctx, cmd)
if err != nil {
return "", err
}
Expand All @@ -76,7 +76,7 @@ func (tf *Terraform) FormatWrite(ctx context.Context, opts ...FormatOption) erro
return err
}

return tf.runTerraformCmd(cmd)
return tf.runTerraformCmd(ctx, cmd)
}

// FormatCheck returns true if the config files in the working or selected (via DirOption) directory are already formatted.
Expand All @@ -98,7 +98,7 @@ func (tf *Terraform) FormatCheck(ctx context.Context, opts ...FormatOption) (boo
var outBuf bytes.Buffer
cmd.Stdout = mergeWriters(cmd.Stdout, &outBuf)

err = tf.runTerraformCmd(cmd)
err = tf.runTerraformCmd(ctx, cmd)
if err == nil {
return true, nil, nil
}
Expand Down
2 changes: 1 addition & 1 deletion tfexec/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (tf *Terraform) Import(ctx context.Context, address, id string, opts ...Imp
if err != nil {
return err
}
return tf.runTerraformCmd(cmd)
return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) importCmd(ctx context.Context, address, id string, opts ...ImportOption) (*exec.Cmd, error) {
Expand Down
2 changes: 1 addition & 1 deletion tfexec/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (tf *Terraform) Init(ctx context.Context, opts ...InitOption) error {
if err != nil {
return err
}
return tf.runTerraformCmd(cmd)
return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) initCmd(ctx context.Context, opts ...InitOption) (*exec.Cmd, error) {
Expand Down
Loading

0 comments on commit 1618687

Please sign in to comment.