[#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 <r.loginov@yadro.com>
This commit is contained in:
Roman Loginov 2024-12-22 15:14:21 +03:00 committed by Alexey Vanin
parent d150f8ddcb
commit 8b3252cbd0
5 changed files with 83 additions and 17 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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.

View file

@ -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)
}

View file

@ -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) {
func TestHandleObjectError(t *testing.T) {
msg := "some msg"
t.Run("nil error", func(t *testing.T) {
err := handleObjectError(msg, nil)
require.Nil(t, err)
})
t.Run("simple access denied", func(t *testing.T) {
reason := "some reason"
err := new(apistatus.ObjectAccessDenied)
err.WriteReason(reason)
inputErr := new(apistatus.ObjectAccessDenied)
inputErr.WriteReason(reason)
var wrappedError error
err := handleObjectError(msg, inputErr)
require.ErrorIs(t, err, frostfs.ErrAccessDenied)
require.Contains(t, err.Error(), reason)
require.Contains(t, err.Error(), msg)
})
if fetchedReason, ok := frosterr.IsErrObjectAccessDenied(err); ok {
wrappedError = fmt.Errorf("%w: %s", frostfs.ErrAccessDenied, fetchedReason)
}
t.Run("access denied - quota reached", func(t *testing.T) {
reason := "Quota limit reached"
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.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)
})
}