Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return error reason #61

Merged
merged 8 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions cmd/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
package local

import (
"errors"
"fmt"
"io"
"net/url"
Expand All @@ -16,8 +15,6 @@ import (
"github.com/spf13/cobra"
)

var ErrTargetPlatformUndefined = errors.New("target platform is required") //nolint:revive

const (
long = `
k6build local builder creates a custom k6 binary artifacts that satisfies certain
Expand Down
101 changes: 101 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package k6build

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

// ErrReasonUnknown signals the reason for an APIError in unknown
var ErrReasonUnknown = errors.New("reason unknown")

// Error represents an error returned by the build service
// This custom error type facilitates extracting the reason of an error
// by using errors.Unwrap method.
// It also facilitates checking an error (or its reason) using errors.Is by
// comparing the error and its reason.
// This custom type has the following known limitations:
// - A nil Error 'e' will not satisfy errors.Is(e, nil)
// - Is method will not
type Error struct {
Err error `json:"error,omitempty"`
Reason error `json:"reason,omitempty"`
}

// Error returns the Error as a string
func (e *Error) Error() string {
return fmt.Sprintf("%s: %s", e.Err, e.Reason)
}

// Is returns true if the target error is the same as the Error or its reason
// It attempts several strategies:
// - compare error and reason to target's Error()
// - unwrap the error and reason and compare to target's Error
// - unwrap target and compares to the error recursively
func (e *Error) Is(target error) bool {
if target == nil {
return false
}

if e.Err.Error() == target.Error() {
return true
}

if e.Reason != nil && e.Reason.Error() == target.Error() {
return true
}

if u := errors.Unwrap(e.Err); u != nil && u.Error() == target.Error() {
return true
}

if u := errors.Unwrap(e.Reason); u != nil && u.Error() == target.Error() {
return true
}

return e.Is(errors.Unwrap(target))
}

// Unwrap returns the underlying reason for the Error
func (e *Error) Unwrap() error {
return e.Reason
}

// MarshalJSON implements the json.Marshaler interface for the Error type
func (e *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Err string `json:"error,omitempty"`
Reason string `json:"reason,omitempty"`
}{
Err: e.Err.Error(),
Reason: e.Reason.Error(),
})
}

// UnmarshalJSON implements the json.Unmarshaler interface for the Error type
func (e *Error) UnmarshalJSON(data []byte) error {
val := struct {
Err string `json:"error,omitempty"`
Reason string `json:"reason,omitempty"`
}{}

if err := json.Unmarshal(data, &val); err != nil {
return err
}

e.Err = errors.New(val.Err)
e.Reason = errors.New(val.Reason)
return nil
}

// NewError creates an Error from an error and a reason
// If the reason is nil, ErrReasonUnknown is used
func NewError(err error, reason error) *Error {
if reason == nil {
reason = ErrReasonUnknown
}
return &Error{
Err: err,
Reason: reason,
}
}
79 changes: 79 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package k6build

import (
"errors"
"fmt"
"testing"
)

func Test_Error(t *testing.T) {
t.Parallel()

var (
err = errors.New("error")
reason = errors.New("reason")
)

testCases := []struct {
title string
err error
reason error
expect []error
}{
{
title: "error and reason",
err: err,
reason: reason,
expect: []error{err, reason},
},
{
title: "error not reason",
err: err,
reason: nil,
expect: []error{err},
},
{
title: "multiple and reasons",
err: err,
reason: reason,
expect: []error{err, reason},
},
{
title: "wrapped err",
err: fmt.Errorf("wrapped %w", err),
reason: reason,
expect: []error{err, reason},
},
{
title: "wrapped reason",
err: err,
reason: fmt.Errorf("wrapped %w", reason),
expect: []error{err, reason},
},
{
title: "wrapped err in target",
err: err,
reason: reason,
expect: []error{fmt.Errorf("wrapped %w", err)},
},
{
title: "wrapped reason in target",
err: err,
reason: reason,
expect: []error{fmt.Errorf("wrapped %w", reason)},
},
}

for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
t.Parallel()

err := NewError(tc.err, tc.reason)
for _, expected := range tc.expect {
if !errors.Is(err, expected) {
t.Fatalf("expected %v got %v", expected, err)
}
}
})
}
}
17 changes: 16 additions & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ package api

import (
"bytes"
"errors"
"fmt"

"github.com/grafana/k6build"
)

var (
// ErrInvalidRequest signals the request could not be processed
// due to erroneous parameters
ErrInvalidRequest = errors.New("invalid request")
// ErrRequestFailed signals the request failed, probably due to a network error
ErrRequestFailed = errors.New("request failed")
// ErrBuildFailed signals the build process failed
ErrBuildFailed = errors.New("build failed")
)

// BuildRequest defines a request to the build service
type BuildRequest struct {
K6Constrains string `json:"k6,omitempty"`
Expand All @@ -28,6 +39,10 @@ func (r BuildRequest) String() string {

// BuildResponse defines the response for a BuildRequest
type BuildResponse struct {
Error string `json:"error,omitempty"`
// If not empty an error occurred processing the request
// This Error can be compared to the errors defined in this package using errors.Is
// to know the type of error, and use Unwrap to obtain its cause if available.
Error *k6build.Error `json:"error,omitempty"`
// Artifact metadata. If an error occurred, content is undefined
Artifact k6build.Artifact `json:"artifact,omitempty"`
}
15 changes: 14 additions & 1 deletion pkg/cache/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@
package api

import (
"errors"

"github.com/grafana/k6build"
"github.com/grafana/k6build/pkg/cache"
)

var (
// ErrInvalidRequest signals the request could not be processed
// due to erroneous parameters
ErrInvalidRequest = errors.New("invalid request")
// ErrRequestFailed signals the request failed, probably due to a network error
ErrRequestFailed = errors.New("request failed")
// ErrCacheAccess signals the access to the cache failed
ErrCacheAccess = errors.New("cache access failed")
)

// CacheResponse is the response to a cache server request
type CacheResponse struct {
Error string
Error *k6build.Error
Object cache.Object
}
39 changes: 17 additions & 22 deletions pkg/cache/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,13 @@ import (
"net/http"
"net/url"

"github.com/grafana/k6build"
"github.com/grafana/k6build/pkg/cache"
"github.com/grafana/k6build/pkg/cache/api"
)

var (
ErrAccessingServer = errors.New("making request") //nolint:revive
ErrInvalidConfig = errors.New("invalid configuration") //nolint:revive
ErrInvalidRequest = errors.New("invalid request") //nolint:revive
ErrInvalidResponse = errors.New("invalid response") //nolint:revive
ErrRequestFailed = errors.New("request failed") //nolint:revive

)
// ErrInvalidConfig signals an error with the client configuration
var ErrInvalidConfig = errors.New("invalid configuration")

// CacheClientConfig defines the configuration for accessing a remote cache service
type CacheClientConfig struct {
Expand All @@ -36,7 +31,7 @@ type CacheClient struct {
// NewCacheClient returns a client for a cache server
func NewCacheClient(config CacheClientConfig) (*CacheClient, error) {
if _, err := url.Parse(config.Server); err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidConfig, err)
return nil, k6build.NewError(ErrInvalidConfig, err)
}

return &CacheClient{
Expand All @@ -51,27 +46,27 @@ func (c *CacheClient) Get(_ context.Context, id string) (cache.Object, error) {
// TODO: use http.Request
resp, err := http.Get(url) //nolint:gosec,noctx
if err != nil {
return cache.Object{}, fmt.Errorf("%w %w", ErrAccessingServer, err)
return cache.Object{}, k6build.NewError(api.ErrRequestFailed, err)
}
defer func() {
_ = resp.Body.Close()
}()

if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return cache.Object{}, fmt.Errorf("%w with status", cache.ErrObjectNotFound)
return cache.Object{}, cache.ErrObjectNotFound
}
return cache.Object{}, fmt.Errorf("%w with status %s", ErrRequestFailed, resp.Status)
return cache.Object{}, k6build.NewError(api.ErrRequestFailed, fmt.Errorf("status %s", resp.Status))
}

cacheResponse := api.CacheResponse{}
err = json.NewDecoder(resp.Body).Decode(&cacheResponse)
if err != nil {
return cache.Object{}, fmt.Errorf("%w: %s", ErrInvalidResponse, err.Error())
return cache.Object{}, k6build.NewError(api.ErrRequestFailed, err)
}

if cacheResponse.Error != "" {
return cache.Object{}, fmt.Errorf("%w: %s", ErrRequestFailed, cacheResponse.Error)
if cacheResponse.Error != nil {
return cache.Object{}, cacheResponse.Error
}

return cacheResponse.Object, nil
Expand All @@ -86,23 +81,23 @@ func (c *CacheClient) Store(_ context.Context, id string, content io.Reader) (ca
content,
)
if err != nil {
return cache.Object{}, fmt.Errorf("%w %w", ErrAccessingServer, err)
return cache.Object{}, k6build.NewError(api.ErrRequestFailed, err)
}
defer func() {
_ = resp.Body.Close()
}()

if resp.StatusCode != http.StatusOK {
return cache.Object{}, fmt.Errorf("%w with status %s", ErrRequestFailed, resp.Status)
return cache.Object{}, k6build.NewError(api.ErrRequestFailed, fmt.Errorf("status %s", resp.Status))
}
cacheResponse := api.CacheResponse{}
err = json.NewDecoder(resp.Body).Decode(&cacheResponse)
if err != nil {
return cache.Object{}, fmt.Errorf("%w: %s", ErrInvalidResponse, err.Error())
return cache.Object{}, k6build.NewError(api.ErrRequestFailed, err)
}

if cacheResponse.Error != "" {
return cache.Object{}, fmt.Errorf("%w: %s", ErrRequestFailed, cacheResponse.Error)
if cacheResponse.Error != nil {
return cache.Object{}, cacheResponse.Error
}

return cacheResponse.Object, nil
Expand All @@ -112,11 +107,11 @@ func (c *CacheClient) Store(_ context.Context, id string, content io.Reader) (ca
func (c *CacheClient) Download(_ context.Context, object cache.Object) (io.ReadCloser, error) {
resp, err := http.Get(object.URL) //nolint:noctx,bodyclose
if err != nil {
return nil, fmt.Errorf("%w %w", ErrAccessingServer, err)
return nil, k6build.NewError(api.ErrRequestFailed, err)
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w with status %s", ErrRequestFailed, resp.Status)
return nil, k6build.NewError(api.ErrRequestFailed, fmt.Errorf("status %s", resp.Status))
}

return resp.Request.Body, nil
Expand Down
Loading
Loading