From 8b3252cbd0f0012d6616b2eab3a1c3b6e41b2ab5 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 --- CHANGELOG.md | 3 ++ api/errors/errors.go | 15 +++++++ api/layer/frostfs/frostfs.go | 3 ++ internal/frostfs/frostfs.go | 3 ++ internal/frostfs/frostfs_test.go | 76 +++++++++++++++++++++++++------- 5 files changed, 83 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 773a086c5..465812fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ This document outlines major changes between releases. ## [Unreleased] +### Added +- Add LimitExceeded error (#589) + ## [0.32.0] - Khumbu - 2024-12-20 ### Added diff --git a/api/errors/errors.go b/api/errors/errors.go index 2e033a530..d35750c40 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -290,6 +290,9 @@ const ( //CORS configuration errors. ErrCORSUnsupportedMethod ErrCORSWildcardExposeHeaders + + // Limits errors. + ErrLimitExceeded ) // error code to Error structure, these fields carry respective @@ -1770,6 +1773,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. } @@ -1833,6 +1844,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 9b6124003..82cea743b 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 186d206d0..ffe752990 100644 --- a/internal/frostfs/frostfs.go +++ b/internal/frostfs/frostfs.go @@ -486,6 +486,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 ef5d9ca83..b52bca1c0 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(), inputErr.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(), inputErr.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) }) }