Skip to content

Commit

Permalink
handle all api errors
Browse files Browse the repository at this point in the history
  • Loading branch information
raulb committed Jan 31, 2025
1 parent 3779d64 commit f1b5e5e
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 67 deletions.
23 changes: 11 additions & 12 deletions cmd/conduit/cecdysis/decorators.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/conduitio/conduit/pkg/foundation/cerrors"
"github.com/conduitio/ecdysis"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

Expand Down Expand Up @@ -66,36 +65,36 @@ func (CommandWithExecuteWithClientDecorator) Decorate(_ *ecdysis.Ecdysis, cmd *c

client, err := api.NewClient(cmd.Context(), grpcAddress)
if err != nil {
// Not an error we need to escalate to the main CLI execution. We'll print it out and not execute further.
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return nil
return handleError(err)
}
defer client.Close()

ctx := ecdysis.ContextWithCobraCommand(cmd.Context(), cmd)
return handleExecuteError(v.ExecuteWithClient(ctx, client))
return handleError(v.ExecuteWithClient(ctx, client))
}

return nil
}

// check what type of error is to see if it's worth showing `cmd.Usage()` or not.
// if error is returned, usage will be shown automatically.
func handleExecuteError(err error) error {
// if any error is returned, usage will be shown automatically.
func handleError(err error) error {
errMsg := err.Error()
st, ok := status.FromError(err)
if ok && st.Code() == codes.NotFound {
errMsg := st.Message()

// an API error, we try to parse `desc`
if ok {
errMsg = st.Message()

// st.Message() is already an entire representation of the error
// need to grab the desc
descIndex := strings.Index(errMsg, "desc =")
if descIndex != -1 {
errMsg = errMsg[descIndex+len("desc = "):]
}
_, _ = fmt.Fprintf(os.Stderr, "%s\n", errMsg)
return nil
}
return err
_, _ = fmt.Fprintf(os.Stderr, "%v\n", errMsg)
return nil
}

// getGRPCAddress returns the gRPC address configured by the user. If no address is found, the default address is returned.
Expand Down
135 changes: 80 additions & 55 deletions cmd/conduit/cecdysis/decorators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
package cecdysis

import (
"bytes"
"io"
"os"
"strings"
"testing"

"github.com/conduitio/conduit/pkg/foundation/cerrors"
"github.com/matryer/is"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -26,102 +31,122 @@ func TestHandleError(t *testing.T) {
is := is.New(t)

testCases := []struct {
name string
inputError error
expected error
name string
inputError error
expected error
expectedStderr string
}{
{
name: "OK error",
inputError: status.Error(codes.OK, "ok error"),
expected: status.Error(codes.OK, "ok error"),
name: "regular error",
inputError: cerrors.New("some error"),
expectedStderr: "some error",
},
{
name: "Canceled error",
inputError: status.Error(codes.Canceled, "canceled error"),
expected: status.Error(codes.Canceled, "canceled error"),
name: "Canceled error",
inputError: status.Error(codes.Canceled, "canceled error"),
expectedStderr: "canceled error",
},
{
name: "Unknown error",
inputError: status.Error(codes.Unknown, "unknown error"),
expected: status.Error(codes.Unknown, "unknown error"),
name: "Unknown error",
inputError: status.Error(codes.Unknown, "unknown error"),
expectedStderr: "unknown error",
},
{
name: "InvalidArgument error",
inputError: status.Error(codes.InvalidArgument, "invalid argument error"),
expected: status.Error(codes.InvalidArgument, "invalid argument error"),
name: "InvalidArgument error",
inputError: status.Error(codes.InvalidArgument, "invalid argument error"),
expectedStderr: "invalid argument error",
},
{
name: "DeadlineExceeded error",
inputError: status.Error(codes.DeadlineExceeded, "deadline exceeded error"),
expected: status.Error(codes.DeadlineExceeded, "deadline exceeded error"),
name: "DeadlineExceeded error",
inputError: status.Error(codes.DeadlineExceeded, "deadline exceeded error"),
expectedStderr: "deadline exceeded error",
},
{
name: "NotFound error",
inputError: status.Error(codes.NotFound, "not found error"),
expected: nil,
name: "NotFound error",
inputError: status.Error(codes.NotFound, "not found error"),
expectedStderr: "not found error",
},
{
name: "AlreadyExists error",
inputError: status.Error(codes.AlreadyExists, "already exists error"),
expected: status.Error(codes.AlreadyExists, "already exists error"),
name: "NotFound error with description",
inputError: status.Error(codes.NotFound, "failed to get pipeline: rpc error: code = NotFound "+
"desc = failed to get pipeline by ID: pipeline instance not found (ID: foo): pipeline instance not found"),
expectedStderr: "failed to get pipeline by ID: pipeline instance not found (ID: foo): pipeline instance not found",
},
{
name: "PermissionDenied error",
inputError: status.Error(codes.PermissionDenied, "permission denied error"),
expected: status.Error(codes.PermissionDenied, "permission denied error"),
name: "AlreadyExists error",
inputError: status.Error(codes.AlreadyExists, "already exists error"),
expectedStderr: "already exists error",
},
{
name: "ResourceExhausted error",
inputError: status.Error(codes.ResourceExhausted, "resource exhausted error"),
expected: status.Error(codes.ResourceExhausted, "resource exhausted error"),
name: "PermissionDenied error",
inputError: status.Error(codes.PermissionDenied, "permission denied error"),
expectedStderr: "permission denied error",
},
{
name: "FailedPrecondition error",
inputError: status.Error(codes.FailedPrecondition, "failed precondition error"),
expected: status.Error(codes.FailedPrecondition, "failed precondition error"),
name: "ResourceExhausted error",
inputError: status.Error(codes.ResourceExhausted, "resource exhausted error"),
expectedStderr: "resource exhausted error",
},
{
name: "Aborted error",
inputError: status.Error(codes.Aborted, "aborted error"),
expected: status.Error(codes.Aborted, "aborted error"),
name: "FailedPrecondition error",
inputError: status.Error(codes.FailedPrecondition, "failed precondition error"),
expectedStderr: "failed precondition error",
},
{
name: "OutOfRange error",
inputError: status.Error(codes.OutOfRange, "out of range error"),
expected: status.Error(codes.OutOfRange, "out of range error"),
name: "Aborted error",
inputError: status.Error(codes.Aborted, "aborted error"),
expectedStderr: "aborted error",
},
{
name: "Unimplemented error",
inputError: status.Error(codes.Unimplemented, "unimplemented error"),
expected: status.Error(codes.Unimplemented, "unimplemented error"),
name: "OutOfRange error",
inputError: status.Error(codes.OutOfRange, "out of range error"),
expectedStderr: "out of range error",
},
{
name: "Internal error",
inputError: status.Error(codes.Internal, "internal error"),
expected: status.Error(codes.Internal, "internal error"),
name: "Unimplemented error",
inputError: status.Error(codes.Unimplemented, "unimplemented error"),
expectedStderr: "unimplemented error",
},
{
name: "Unavailable error",
inputError: status.Error(codes.Unavailable, "unavailable error"),
expected: status.Error(codes.Unavailable, "unavailable error"),
name: "Internal error",
inputError: status.Error(codes.Internal, "internal error"),
expectedStderr: "internal error",
},
{
name: "DataLoss error",
inputError: status.Error(codes.DataLoss, "data loss error"),
expected: status.Error(codes.DataLoss, "data loss error"),
name: "Unavailable error",
inputError: status.Error(codes.Unavailable, "unavailable error"),
expectedStderr: "unavailable error",
},
{
name: "Unauthenticated error",
inputError: status.Error(codes.Unauthenticated, "unauthenticated error"),
expected: status.Error(codes.Unauthenticated, "unauthenticated error"),
name: "DataLoss error",
inputError: status.Error(codes.DataLoss, "data loss error"),
expectedStderr: "data loss error",
},
{
name: "Unauthenticated error",
inputError: status.Error(codes.Unauthenticated, "unauthenticated error"),
expectedStderr: "unauthenticated error",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
is := is.New(t)
result := handleExecuteError(tc.inputError)

oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w

result := handleError(tc.inputError)

w.Close()
os.Stderr = oldStderr

var buf bytes.Buffer
io.Copy(&buf, r)

Check failure on line 146 in cmd/conduit/cecdysis/decorators_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `io.Copy` is not checked (errcheck)

is.Equal(result, tc.expected)
is.Equal(strings.TrimSpace(buf.String()), tc.expectedStderr)
})
}
}

0 comments on commit f1b5e5e

Please sign in to comment.