From 3e3b36ead8105767f416e933786e47a70decc11a Mon Sep 17 00:00:00 2001 From: Roman Loginov Date: Sun, 22 Dec 2024 15:14:21 +0300 Subject: [PATCH] [#589] Add LimitExceeded error The Access Denied status may be received from APE due to exceeding the quota. In this situation, you need to return the appropriate error. The Conflict status is used because this error was made based on the LimitExceeded error from aws iam error https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateUser.html#API_CreateUser_Errors. Signed-off-by: Roman Loginov --- api/errors/errors.go | 15 +++++++ api/layer/frostfs/frostfs.go | 3 ++ internal/frostfs/frostfs.go | 3 ++ internal/frostfs/frostfs_test.go | 76 +++++++++++++++++++++++++------- 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/api/errors/errors.go b/api/errors/errors.go index 7ce7c5f..720868b 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -289,6 +289,9 @@ const ( //CORS configuration errors. ErrCORSUnsupportedMethod ErrCORSWildcardExposeHeaders + + // Limits errors. + ErrLimitExceeded ) // error code to Error structure, these fields carry respective @@ -1763,6 +1766,14 @@ var errorCodes = errorCodeMap{ Description: "Content-Range header is mandatory for this type of request", HTTPStatusCode: http.StatusBadRequest, }, + // The Conflict status is used because this error was made based on the LimitExceeded error + // from aws iam error https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateUser.html#API_CreateUser_Errors. + ErrLimitExceeded: { + ErrCode: ErrLimitExceeded, + Code: "LimitExceeded", + Description: "You have reached the quota limit.", + HTTPStatusCode: http.StatusConflict, + }, // Add your error structure here. } @@ -1826,6 +1837,10 @@ func TransformToS3Error(err error) error { return GetAPIError(ErrBucketAlreadyExists) } + if errors.Is(err, frostfs.ErrQuotaLimitReached) { + return GetAPIError(ErrLimitExceeded) + } + return GetAPIError(ErrInternalError) } diff --git a/api/layer/frostfs/frostfs.go b/api/layer/frostfs/frostfs.go index ba01350..36a8a8e 100644 --- a/api/layer/frostfs/frostfs.go +++ b/api/layer/frostfs/frostfs.go @@ -240,6 +240,9 @@ var ( // ErrGlobalDomainIsAlreadyTaken is returned from FrostFS in case of global domain is already taken. ErrGlobalDomainIsAlreadyTaken = errors.New("global domain is already taken") + + // ErrQuotaLimitReached is returned from FrostFS in case of quota exceeded. + ErrQuotaLimitReached = errors.New("quota limit reached") ) // FrostFS represents virtual connection to FrostFS network. diff --git a/internal/frostfs/frostfs.go b/internal/frostfs/frostfs.go index 7b4fba9..7be2e42 100644 --- a/internal/frostfs/frostfs.go +++ b/internal/frostfs/frostfs.go @@ -477,6 +477,9 @@ func handleObjectError(msg string, err error) error { } if reason, ok := frosterr.IsErrObjectAccessDenied(err); ok { + if strings.Contains(reason, "limit reached") { + return fmt.Errorf("%s: %w: %s", msg, frostfs.ErrQuotaLimitReached, reason) + } return fmt.Errorf("%s: %w: %s", msg, frostfs.ErrAccessDenied, reason) } diff --git a/internal/frostfs/frostfs_test.go b/internal/frostfs/frostfs_test.go index ef5d9ca..35c6ad9 100644 --- a/internal/frostfs/frostfs_test.go +++ b/internal/frostfs/frostfs_test.go @@ -8,31 +8,49 @@ import ( "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" - frosterr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -func TestErrorChecking(t *testing.T) { - reason := "some reason" - err := new(apistatus.ObjectAccessDenied) - err.WriteReason(reason) +func TestHandleObjectError(t *testing.T) { + msg := "some msg" - var wrappedError error + t.Run("nil error", func(t *testing.T) { + err := handleObjectError(msg, nil) + require.Nil(t, err) + }) - if fetchedReason, ok := frosterr.IsErrObjectAccessDenied(err); ok { - wrappedError = fmt.Errorf("%w: %s", frostfs.ErrAccessDenied, fetchedReason) - } + t.Run("simple access denied", func(t *testing.T) { + reason := "some reason" + inputErr := new(apistatus.ObjectAccessDenied) + inputErr.WriteReason(reason) - require.ErrorIs(t, wrappedError, frostfs.ErrAccessDenied) - require.Contains(t, wrappedError.Error(), reason) -} + err := handleObjectError(msg, inputErr) + require.ErrorIs(t, err, frostfs.ErrAccessDenied) + require.Contains(t, err.Error(), reason) + require.Contains(t, err.Error(), msg) + }) + + t.Run("access denied - quota reached", func(t *testing.T) { + reason := "Quota limit reached" + inputErr := new(apistatus.ObjectAccessDenied) + inputErr.WriteReason(reason) + + err := handleObjectError(msg, inputErr) + require.ErrorIs(t, err, frostfs.ErrQuotaLimitReached) + require.Contains(t, err.Error(), reason) + require.Contains(t, err.Error(), msg) + }) -func TestErrorTimeoutChecking(t *testing.T) { t.Run("simple timeout", func(t *testing.T) { - require.True(t, frosterr.IsTimeoutError(errors.New("timeout"))) + inputErr := errors.New("timeout") + + err := handleObjectError(msg, inputErr) + require.ErrorIs(t, err, frostfs.ErrGatewayTimeout) + require.Contains(t, err.Error(), inputErr.Error()) + require.Contains(t, err.Error(), msg) }) t.Run("deadline exceeded", func(t *testing.T) { @@ -40,11 +58,35 @@ func TestErrorTimeoutChecking(t *testing.T) { defer cancel() <-ctx.Done() - require.True(t, frosterr.IsTimeoutError(ctx.Err())) + err := handleObjectError(msg, ctx.Err()) + require.ErrorIs(t, err, frostfs.ErrGatewayTimeout) + require.Contains(t, err.Error(), ctx.Err().Error()) + require.Contains(t, err.Error(), msg) }) t.Run("grpc deadline exceeded", func(t *testing.T) { - err := fmt.Errorf("wrap grpc error: %w", status.Error(codes.DeadlineExceeded, "error")) - require.True(t, frosterr.IsTimeoutError(err)) + inputErr := fmt.Errorf("wrap grpc error: %w", status.Error(codes.DeadlineExceeded, "error")) + + err := handleObjectError(msg, inputErr) + require.ErrorIs(t, err, frostfs.ErrGatewayTimeout) + require.Contains(t, err.Error(), err.Error()) + require.Contains(t, err.Error(), msg) + }) + + t.Run("global domain already", func(t *testing.T) { + inputErr := errors.New("global domain is already taken") + + err := handleObjectError(msg, inputErr) + require.ErrorIs(t, err, frostfs.ErrGlobalDomainIsAlreadyTaken) + require.Contains(t, err.Error(), err.Error()) + require.Contains(t, err.Error(), msg) + }) + + t.Run("unknown error", func(t *testing.T) { + inputErr := errors.New("unknown error") + + err := handleObjectError(msg, inputErr) + require.ErrorIs(t, err, inputErr) + require.Contains(t, err.Error(), msg) }) } -- 2.45.2