Skip to content

Commit

Permalink
Merge pull request #35 from storageos/feature/DEV-1911-custom-errors-…
Browse files Browse the repository at this point in the history
…package

Add custom errors package
  • Loading branch information
JoeReid authored Jan 26, 2018
2 parents c7009bb + 268befe commit e06f2d8
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 17 deletions.
9 changes: 6 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"github.com/storageos/go-api/netutil"
"github.com/storageos/go-api/serror"
"io"
"io/ioutil"
"net"
Expand All @@ -25,9 +26,6 @@ const (
DefaultVersion = 1
)

// ErrInvalidEndpoint is returned when the endpoint is not a valid HTTP URL.
type InvalidNodeError = netutil.InvalidNodeError

var (
// ErrConnectionRefused is returned when the client cannot connect to the given endpoint.
ErrConnectionRefused = errors.New("cannot connect to StorageOS API endpoint")
Expand Down Expand Up @@ -277,6 +275,11 @@ func (c *Client) do(method, urlpath string, doOptions doOptions) (*http.Response

resp, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
// If it is a custom error, return it. It probably knows more than us
if serror.IsStorageOSError(err) {
return nil, err
}

if strings.Contains(err.Error(), "connection refused") {
return nil, ErrConnectionRefused
}
Expand Down
8 changes: 5 additions & 3 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"bytes"
"context"
"fmt"
//"github.com/storageos/go-api/netutil"
"github.com/storageos/go-api/serror"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -68,8 +68,10 @@ func TestNewClientInvalidEndpoint(t *testing.T) {
if client != nil {
t.Errorf("Want <nil> client for invalid endpoint (%v), got %#v.", c, client)
}
if _, ok := err.(*InvalidNodeError); !ok {
t.Errorf("NewClient(%q): Got invalid error for invalid endpoint. Want (*netutil.InvalidNodeError). Got %#v.", c, err)
if serror.IsStorageOSError(err) {
if serror.ErrorKind(err) != serror.InvalidHostConfig {
t.Errorf("NewClient(%q): Got invalid error for invalid endpoint. Want serror.InvalidHostConfig. Got %#v.", c, err)
}
}
}
}
Expand Down
20 changes: 11 additions & 9 deletions netutil/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ package netutil
import (
"errors"
"fmt"
"github.com/storageos/go-api/serror"
"strings"
)

var ErrAllFailed = errors.New("failed to dial all provided addresses")
var ErrNoAddresses = errors.New("the MultiDialer instance has not been initialised with client addresses")
func errAllFailed(addrs []string) error {
msg := fmt.Sprintf("failed to dial all known cluster members, (%s)", strings.Join(addrs, ","))
help := "ensure that the value of $STORAGEOS_HOST (or the -H flag) is correct, and that there are healthy StorageOS nodes in this cluster"

type InvalidNodeError struct {
cause error
}

func (i *InvalidNodeError) Error() string {
return fmt.Sprintf("invalid node format: %s", i.cause.Error())
return serror.NewTypedStorageOSError(serror.APIUncontactable, nil, msg, help)
}

func newInvalidNodeError(err error) error {
return &InvalidNodeError{err}
msg := fmt.Sprintf("invalid node format: %s", err)
help := "please check the format of $STORAGEOS_HOST (or the -H flag) complies with the StorageOS JOIN format"

return serror.NewTypedStorageOSError(serror.InvalidHostConfig, err, msg, help)
}

var errNoAddresses = errors.New("the MultiDialer instance has not been initialised with client addresses")
var errUnsupportedScheme = errors.New("unsupported URL scheme")
var errInvalidPortNumber = errors.New("invalid port number")
4 changes: 2 additions & 2 deletions netutil/multidialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func NewMultiDialer(nodes []string, dialer *net.Dialer) (*MultiDialer, error) {
// ignored.
func (m *MultiDialer) DialContext(ctx context.Context, network, ignoredAddress string) (net.Conn, error) {
if len(m.Addresses) == 0 {
return nil, ErrNoAddresses
return nil, newInvalidNodeError(errNoAddresses)
}

// Shuffle a copy of the addresses (for even load balancing)
Expand Down Expand Up @@ -96,7 +96,7 @@ func (m *MultiDialer) DialContext(ctx context.Context, network, ignoredAddress s
}

// We failed to dail all of the addresses we have
return nil, ErrAllFailed
return nil, errAllFailed(m.Addresses)
}

// Dial returns the result of a call to m.DialContext passing in the background context
Expand Down
11 changes: 11 additions & 0 deletions serror/error_kind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package serror

//go:generate stringer -type=StorageOSErrorKind error_kind.go
type StorageOSErrorKind int

// Known error kinds
const (
UnknownError StorageOSErrorKind = iota
APIUncontactable
InvalidHostConfig
)
37 changes: 37 additions & 0 deletions serror/kind_lookup_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package serror

import (
"encoding/json"
"fmt"
"strings"
)

var kindLookupMap map[string]StorageOSErrorKind

func init() {
kindLookupMap = make(map[string]StorageOSErrorKind)

// Populate the lookup map with all the known constants
for i := StorageOSErrorKind(0); !strings.HasPrefix(i.String(), "StorageOSErrorKind("); i++ {
kindLookupMap[i.String()] = i
}
}

func (s *StorageOSErrorKind) UnmarshalJSON(b []byte) error {
str := ""
if err := json.Unmarshal(b, &str); err != nil {
return err
}

v, ok := kindLookupMap[str]
if !ok {
return fmt.Errorf("Failed to unmarshal ErrorKind %s", s)
}

*s = v
return nil
}

func (s *StorageOSErrorKind) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
34 changes: 34 additions & 0 deletions serror/storageos_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package serror

import (
"encoding/json"
)

type StorageOSError interface {
// embedding error provides compatibility with standard error handling code
error

// Encoding/decoding methods to help errors traverse API boundaries
json.Marshaler
json.Unmarshaler

Err() error // Returns the underlying error that caused this event
String() string // A short string representing the error (for logging etc)
Help() string // A larger string that should provide informative debug instruction to users
Kind() StorageOSErrorKind // A type representing a set of known error conditions, helpful to switch on
Extra() map[string]string // A container for error specific information

// TODO: should we include callstack traces here? We could have a debug mode for it.
}

func ErrorKind(err error) StorageOSErrorKind {
if serr, ok := err.(StorageOSError); ok {
return serr.Kind()
}
return UnknownError
}

func IsStorageOSError(err error) bool {
_, ok := err.(StorageOSError)
return ok
}
16 changes: 16 additions & 0 deletions serror/storageoserrorkind_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions serror/typed_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package serror

import (
"encoding/json"
)

func NewTypedStorageOSError(kind StorageOSErrorKind, err error, msg string, help string) StorageOSError {
return &typedStorageOSError{
internal: &internal_TypedStorageOSError{
ErrorKind: &kind,
Cause: err,
ErrMessage: msg,
HelpMessage: help,
},
}
}

func NewUntypedStorageOSError(err error, msg string, help string) StorageOSError {
var kind StorageOSErrorKind = UnknownError

return &typedStorageOSError{
internal: &internal_TypedStorageOSError{
ErrorKind: &kind,
Cause: err,
ErrMessage: msg,
HelpMessage: help,
},
}
}

type internal_TypedStorageOSError struct {
ErrorKind *StorageOSErrorKind `json:"error_kind"`
Cause error `json:"caused_by"`
ErrMessage string `json:"error_message"`
HelpMessage string `json:"help_message"`
ExtraMap map[string]string `json:"extra"`
}

type typedStorageOSError struct {
internal *internal_TypedStorageOSError
}

func (t *typedStorageOSError) MarshalJSON() ([]byte, error) {
return json.Marshal(t.internal)
}

func (t *typedStorageOSError) UnmarshalJSON(d []byte) error {
internal := &internal_TypedStorageOSError{}

err := json.Unmarshal(d, internal)
if err != nil {
return err
}

t.internal = internal
return nil
}

func (t *typedStorageOSError) Error() string { return t.String() }
func (t *typedStorageOSError) Err() error { return t.internal.Cause }
func (t *typedStorageOSError) String() string { return t.internal.ErrMessage }
func (t *typedStorageOSError) Help() string { return t.internal.HelpMessage }
func (t *typedStorageOSError) Kind() StorageOSErrorKind { return *t.internal.ErrorKind }
func (t *typedStorageOSError) Extra() map[string]string { return t.internal.ExtraMap }

0 comments on commit e06f2d8

Please sign in to comment.