Skip to content

Commit

Permalink
Split errors and exit error wrappers to separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
paultyng committed Oct 5, 2020
1 parent 729720f commit 07bb078
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 164 deletions.
166 changes: 2 additions & 164 deletions tfexec/errors.go
Original file line number Diff line number Diff line change
@@ -1,100 +1,8 @@
package tfexec

import (
"errors"
"fmt"
"os/exec"
"regexp"
"strings"
)
import "fmt"

var (
// The "Required variable not set:" case is for 0.11
missingVarErrRegexp = regexp.MustCompile(`Error: No value for required variable|Error: Required variable not set:`)
missingVarNameRegexp = regexp.MustCompile(`The root module input variable "(.+)" is not set, and has no default|Error: Required variable not set: (.+)`)

usageRegexp = regexp.MustCompile(`Too many command line arguments|^Usage: .*Options:.*|Error: Invalid -\d+ option`)

// "Could not load plugin" is present in 0.13
noInitErrRegexp = regexp.MustCompile(`Error: Could not satisfy plugin requirements|Error: Could not load plugin`)

noConfigErrRegexp = regexp.MustCompile(`Error: No configuration files`)

workspaceDoesNotExistRegexp = regexp.MustCompile(`Workspace "(.+)" doesn't exist.`)

workspaceAlreadyExistsRegexp = regexp.MustCompile(`Workspace "(.+)" already exists`)

tfVersionMismatchErrRegexp = regexp.MustCompile(`Error: The currently running version of Terraform doesn't meet the|Error: Unsupported Terraform Core version`)
tfVersionMismatchConstraintRegexp = regexp.MustCompile(`required_version = "(.+)"|Required version: (.+)\b`)
)

func (tf *Terraform) parseError(err error, stderr string) error {
ee, ok := err.(*exec.ExitError)
if !ok {
return err
}

switch {
case tfVersionMismatchErrRegexp.MatchString(stderr):
constraint := ""
constraints := tfVersionMismatchConstraintRegexp.FindStringSubmatch(stderr)
for i := 1; i < len(constraints); i++ {
constraint = strings.TrimSpace(constraints[i])
if constraint != "" {
break
}
}

if constraint == "" {
// hardcode a value here for weird cases (incl. 0.12)
constraint = "unknown"
}

// only set this if it happened to be cached already
ver := ""
if tf != nil && tf.execVersion != nil {
ver = tf.execVersion.String()
}

return &ErrTFVersionMismatch{
Constraint: constraint,
TFVersion: ver,
}
case missingVarErrRegexp.MatchString(stderr):
name := ""
names := missingVarNameRegexp.FindStringSubmatch(stderr)
for i := 1; i < len(names); i++ {
name = strings.TrimSpace(names[i])
if name != "" {
break
}
}

return &ErrMissingVar{name}
case usageRegexp.MatchString(stderr):
return &ErrCLIUsage{stderr: stderr}
case noInitErrRegexp.MatchString(stderr):
return &ErrNoInit{stderr: stderr}
case noConfigErrRegexp.MatchString(stderr):
return &ErrNoConfig{stderr: stderr}
case workspaceDoesNotExistRegexp.MatchString(stderr):
submatches := workspaceDoesNotExistRegexp.FindStringSubmatch(stderr)
if len(submatches) == 2 {
return &ErrNoWorkspace{submatches[1]}
}
case workspaceAlreadyExistsRegexp.MatchString(stderr):
submatches := workspaceAlreadyExistsRegexp.FindStringSubmatch(stderr)
if len(submatches) == 2 {
return &ErrWorkspaceExists{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 errors.New(stderr)
}
// this file contains non-parsed exported errors

type ErrNoSuitableBinary struct {
err error
Expand All @@ -104,19 +12,6 @@ func (e *ErrNoSuitableBinary) Error() string {
return fmt.Sprintf("no suitable terraform binary could be found: %s", e.err.Error())
}

// 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 {
TFVersion string

// Constraint is not returned in the error messaging on 0.12
Constraint string
}

func (e *ErrTFVersionMismatch) Error() string {
return "terraform core version not supported by configuration"
}

// 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 @@ -129,38 +24,6 @@ func (e *ErrVersionMismatch) Error() string {
return fmt.Sprintf("unexpected version %s (min: %s, max: %s)", e.Actual, e.MinInclusive, e.MaxExclusive)
}

type ErrNoInit struct {
stderr string
}

func (e *ErrNoInit) Error() string {
return e.stderr
}

type ErrNoConfig struct {
stderr string
}

func (e *ErrNoConfig) Error() string {
return e.stderr
}

// ErrCLIUsage is returned when the combination of flags or arguments is incorrect.
//
// CLI indicates usage errors in three different ways: either
// 1. Exit 1, with a custom error message on stderr.
// 2. Exit 1, with command usage logged to stderr.
// 3. Exit 127, with command usage logged to stdout.
// Currently cases 1 and 2 are handled.
// TODO KEM: Handle exit 127 case. How does this work on non-Unix platforms?
type ErrCLIUsage struct {
stderr string
}

func (e *ErrCLIUsage) Error() string {
return e.stderr
}

// 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 {
Expand All @@ -170,28 +33,3 @@ type ErrManualEnvVar struct {
func (err *ErrManualEnvVar) Error() string {
return fmt.Sprintf("manual setting of env var %q detected", err.name)
}

type ErrMissingVar struct {
VariableName string
}

func (err *ErrMissingVar) Error() string {
return fmt.Sprintf("variable %q was required but not supplied", err.VariableName)
}

type ErrNoWorkspace struct {
Name string
}

func (err *ErrNoWorkspace) Error() string {
return fmt.Sprintf("workspace %q does not exist", err.Name)
}

// ErrWorkspaceExists is returned when creating a workspace that already exists
type ErrWorkspaceExists struct {
Name string
}

func (err *ErrWorkspaceExists) Error() string {
return fmt.Sprintf("workspace %q already exists", err.Name)
}
169 changes: 169 additions & 0 deletions tfexec/exit_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package tfexec

import (
"errors"
"fmt"
"os/exec"
"regexp"
"strings"
)

// this file contains errors parsed from stderr

var (
// The "Required variable not set:" case is for 0.11
missingVarErrRegexp = regexp.MustCompile(`Error: No value for required variable|Error: Required variable not set:`)
missingVarNameRegexp = regexp.MustCompile(`The root module input variable "(.+)" is not set, and has no default|Error: Required variable not set: (.+)`)

usageRegexp = regexp.MustCompile(`Too many command line arguments|^Usage: .*Options:.*|Error: Invalid -\d+ option`)

// "Could not load plugin" is present in 0.13
noInitErrRegexp = regexp.MustCompile(`Error: Could not satisfy plugin requirements|Error: Could not load plugin`)

noConfigErrRegexp = regexp.MustCompile(`Error: No configuration files`)

workspaceDoesNotExistRegexp = regexp.MustCompile(`Workspace "(.+)" doesn't exist.`)

workspaceAlreadyExistsRegexp = regexp.MustCompile(`Workspace "(.+)" already exists`)

tfVersionMismatchErrRegexp = regexp.MustCompile(`Error: The currently running version of Terraform doesn't meet the|Error: Unsupported Terraform Core version`)
tfVersionMismatchConstraintRegexp = regexp.MustCompile(`required_version = "(.+)"|Required version: (.+)\b`)
)

func (tf *Terraform) parseError(err error, stderr string) error {
ee, ok := err.(*exec.ExitError)
if !ok {
return err
}

switch {
case tfVersionMismatchErrRegexp.MatchString(stderr):
constraint := ""
constraints := tfVersionMismatchConstraintRegexp.FindStringSubmatch(stderr)
for i := 1; i < len(constraints); i++ {
constraint = strings.TrimSpace(constraints[i])
if constraint != "" {
break
}
}

if constraint == "" {
// hardcode a value here for weird cases (incl. 0.12)
constraint = "unknown"
}

// only set this if it happened to be cached already
ver := ""
if tf != nil && tf.execVersion != nil {
ver = tf.execVersion.String()
}

return &ErrTFVersionMismatch{
Constraint: constraint,
TFVersion: ver,
}
case missingVarErrRegexp.MatchString(stderr):
name := ""
names := missingVarNameRegexp.FindStringSubmatch(stderr)
for i := 1; i < len(names); i++ {
name = strings.TrimSpace(names[i])
if name != "" {
break
}
}

return &ErrMissingVar{name}
case usageRegexp.MatchString(stderr):
return &ErrCLIUsage{stderr: stderr}
case noInitErrRegexp.MatchString(stderr):
return &ErrNoInit{stderr: stderr}
case noConfigErrRegexp.MatchString(stderr):
return &ErrNoConfig{stderr: stderr}
case workspaceDoesNotExistRegexp.MatchString(stderr):
submatches := workspaceDoesNotExistRegexp.FindStringSubmatch(stderr)
if len(submatches) == 2 {
return &ErrNoWorkspace{submatches[1]}
}
case workspaceAlreadyExistsRegexp.MatchString(stderr):
submatches := workspaceAlreadyExistsRegexp.FindStringSubmatch(stderr)
if len(submatches) == 2 {
return &ErrWorkspaceExists{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 errors.New(stderr)
}

type ErrMissingVar struct {
VariableName string
}

func (err *ErrMissingVar) Error() string {
return fmt.Sprintf("variable %q was required but not supplied", err.VariableName)
}

type ErrNoWorkspace struct {
Name string
}

func (err *ErrNoWorkspace) Error() string {
return fmt.Sprintf("workspace %q does not exist", err.Name)
}

// ErrWorkspaceExists is returned when creating a workspace that already exists
type ErrWorkspaceExists struct {
Name string
}

func (err *ErrWorkspaceExists) Error() string {
return fmt.Sprintf("workspace %q already exists", err.Name)
}

type ErrNoInit struct {
stderr string
}

func (e *ErrNoInit) Error() string {
return e.stderr
}

type ErrNoConfig struct {
stderr string
}

func (e *ErrNoConfig) Error() string {
return e.stderr
}

// ErrCLIUsage is returned when the combination of flags or arguments is incorrect.
//
// CLI indicates usage errors in three different ways: either
// 1. Exit 1, with a custom error message on stderr.
// 2. Exit 1, with command usage logged to stderr.
// 3. Exit 127, with command usage logged to stdout.
// Currently cases 1 and 2 are handled.
// TODO KEM: Handle exit 127 case. How does this work on non-Unix platforms?
type ErrCLIUsage struct {
stderr string
}

func (e *ErrCLIUsage) Error() string {
return e.stderr
}

// 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 {
TFVersion string

// Constraint is not returned in the error messaging on 0.12
Constraint string
}

func (e *ErrTFVersionMismatch) Error() string {
return "terraform core version not supported by configuration"
}

0 comments on commit 07bb078

Please sign in to comment.