-
Notifications
You must be signed in to change notification settings - Fork 351
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
backend: parse redpanda admin api errors
- Loading branch information
Showing
2 changed files
with
226 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Copyright 2024 Redpanda Data, Inc. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.md | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0 | ||
|
||
package errors | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"net/http" | ||
"strconv" | ||
|
||
"connectrpc.com/connect" | ||
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/adminapi" | ||
|
||
v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1alpha1" | ||
) | ||
|
||
// NewConnectErrorFromRedpandaAdminAPIError enhances error handling by providing | ||
// more insightful connect.Error messages to users. It unwraps errors from the | ||
// Redpanda Admin API, allowing for better feedback. It decodes HTTP response | ||
// errors, extracts relevant information, and constructs detailed error | ||
// messages. Additionally, it handles timeouts gracefully by generating specific | ||
// error messages for canceled requests. | ||
func NewConnectErrorFromRedpandaAdminAPIError(err error) *connect.Error { | ||
var httpErr *adminapi.HTTPResponseError | ||
if errors.As(err, &httpErr) { | ||
connectCode := CodeFromHTTPStatus(httpErr.Response.StatusCode) | ||
|
||
adminApiErr, err := httpErr.DecodeGenericErrorBody() | ||
if err != nil { | ||
return NewConnectError( | ||
connectCode, | ||
errors.New(httpErr.Error()), | ||
NewErrorInfo( | ||
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(), KeyVal{ | ||
Key: "adminapi_status_code", | ||
Value: strconv.Itoa(httpErr.Response.StatusCode), | ||
}, | ||
), | ||
) | ||
} | ||
|
||
// Bubble up original Redpanda adminapi error message | ||
return NewConnectError( | ||
connectCode, | ||
errors.New(adminApiErr.Message), | ||
NewErrorInfo( | ||
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(), KeyVal{ | ||
Key: "adminapi_status_code", | ||
Value: strconv.Itoa(httpErr.Response.StatusCode), | ||
}, | ||
), | ||
) | ||
} | ||
|
||
// Write a proper error for requests that timed-out | ||
if errors.Is(err, context.Canceled) { | ||
return NewConnectError( | ||
connect.CodeCanceled, | ||
errors.New("the request to the Redpanda admin API timed out"), | ||
NewErrorInfo(v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String()), | ||
) | ||
} | ||
|
||
return NewConnectError( | ||
connect.CodeInternal, | ||
err, | ||
NewErrorInfo(v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String()), | ||
) | ||
} | ||
|
||
// CodeFromHTTPStatus converts an HTTP response status code into the | ||
// corresponding connect.Code. If no status code matches, CodeUnknown will be | ||
// returned. | ||
func CodeFromHTTPStatus(status int) connect.Code { | ||
switch status { | ||
case http.StatusOK: | ||
return 0 | ||
case 499: | ||
return connect.CodeCanceled | ||
case http.StatusInternalServerError: | ||
return connect.CodeInternal | ||
case http.StatusBadRequest: | ||
return connect.CodeInvalidArgument | ||
case http.StatusGatewayTimeout: | ||
return connect.CodeDeadlineExceeded | ||
case http.StatusNotFound: | ||
return connect.CodeNotFound | ||
case http.StatusConflict: | ||
return connect.CodeAlreadyExists | ||
case http.StatusForbidden: | ||
return connect.CodePermissionDenied | ||
case http.StatusUnauthorized: | ||
return connect.CodeUnauthenticated | ||
case http.StatusTooManyRequests: | ||
return connect.CodeResourceExhausted | ||
case http.StatusNotImplemented: | ||
return connect.CodeUnimplemented | ||
case http.StatusServiceUnavailable: | ||
return connect.CodeUnavailable | ||
default: | ||
return connect.CodeUnknown | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// Copyright 2024 Redpanda Data, Inc. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.md | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0 | ||
|
||
package errors | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"strconv" | ||
"testing" | ||
|
||
"connectrpc.com/connect" | ||
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/adminapi" | ||
"github.com/stretchr/testify/assert" | ||
|
||
v1alpha1 "github.com/redpanda-data/console/backend/pkg/protogen/redpanda/api/dataplane/v1alpha1" | ||
) | ||
|
||
// HTTPResponseErrorInterface abstracts adminapi.HTTPResponseError for mocking. | ||
// | ||
//go:generate mockgen -destination=./mocks/http_response_error.go -package=mocks github.com/redpanda-data/console/backend/pkg/api/connect/errors HTTPResponseErrorInterface | ||
type HTTPResponseErrorInterface interface { | ||
Error() string | ||
DecodeGenericErrorBody() (*adminapi.GenericErrorBody, error) | ||
GetResponse() *http.Response | ||
} | ||
|
||
// Define the test cases | ||
func TestNewConnectErrorFromRedpandaAdminAPIError(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
inputError error | ||
expectedResult *connect.Error | ||
}{ | ||
{ | ||
name: "HTTPResponseError with undecodable body", | ||
inputError: func() error { | ||
/* | ||
mockErr := new(MockHTTPResponseError) | ||
mockErr.On("Error").Return("mock error message") | ||
mockErr.Response = &http.Response{StatusCode: http.StatusInternalServerError} | ||
return mockErr*/ | ||
return nil | ||
}(), | ||
expectedResult: NewConnectError( | ||
CodeFromHTTPStatus(http.StatusInternalServerError), | ||
errors.New("mock error message"), | ||
NewErrorInfo( | ||
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(), | ||
KeyVal{ | ||
Key: "adminapi_status_code", | ||
Value: strconv.Itoa(http.StatusInternalServerError), | ||
}, | ||
), | ||
), | ||
}, | ||
{ | ||
name: "HTTPResponseError with decodable body", | ||
inputError: func() error { | ||
/* | ||
mockErr := new(MockHTTPResponseError) | ||
mockErr.On("Error").Return("mock error message") | ||
mockErr.On("DecodeGenericErrorBodyBody").Return( | ||
&adminapi.GenericErrorBody{ | ||
Message: "decoded message", | ||
Code: http.StatusNotFound, | ||
}, nil, | ||
) | ||
mockErr.Response = &http.Response{StatusCode: http.StatusOK} | ||
return mockErr | ||
*/ | ||
return nil | ||
}(), | ||
expectedResult: NewConnectError( | ||
connect.CodeNotFound, | ||
errors.New("decoded message"), | ||
NewErrorInfo( | ||
v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String(), | ||
KeyVal{ | ||
Key: "adminapi_status_code", | ||
Value: strconv.Itoa(http.StatusNotFound), | ||
}, | ||
), | ||
), | ||
}, | ||
{ | ||
name: "Context canceled error", | ||
inputError: func() error { | ||
return fmt.Errorf("some random msg with a wrapped cancelled context err: %w", context.Canceled) | ||
}(), | ||
expectedResult: NewConnectError( | ||
connect.CodeCanceled, | ||
errors.New("the request to the Redpanda admin API timed out"), | ||
NewErrorInfo(v1alpha1.Reason_REASON_REDPANDA_ADMIN_API_ERROR.String()), | ||
), | ||
}, | ||
} | ||
|
||
// Run the test cases | ||
for _, tt := range tests { | ||
t.Run( | ||
tt.name, func(t *testing.T) { | ||
result := NewConnectErrorFromRedpandaAdminAPIError(tt.inputError) | ||
assert.Equal(t, tt.expectedResult, result) | ||
}, | ||
) | ||
} | ||
} |