[#589] Add LimitExceeded error #593

Merged
alexvanin merged 1 commit from r.loginov/frostfs-s3-gw:feature/589-return-quota-limit-reached_status into master 2025-01-17 06:31:09 +00:00
5 changed files with 83 additions and 17 deletions

View file

@ -4,6 +4,9 @@ This document outlines major changes between releases.
## [Unreleased] ## [Unreleased]
### Added
- Add LimitExceeded error (#589)
## [0.32.0] - Khumbu - 2024-12-20 ## [0.32.0] - Khumbu - 2024-12-20
### Added ### Added

View file

@ -290,6 +290,9 @@ const (
//CORS configuration errors. //CORS configuration errors.
ErrCORSUnsupportedMethod ErrCORSUnsupportedMethod
ErrCORSWildcardExposeHeaders ErrCORSWildcardExposeHeaders
// Limits errors.
ErrLimitExceeded
) )
// error code to Error structure, these fields carry respective // 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", Description: "Content-Range header is mandatory for this type of request",
HTTPStatusCode: http.StatusBadRequest, 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. // Add your error structure here.
} }
@ -1833,6 +1844,10 @@ func TransformToS3Error(err error) error {
return GetAPIError(ErrBucketAlreadyExists) return GetAPIError(ErrBucketAlreadyExists)
} }
if errors.Is(err, frostfs.ErrQuotaLimitReached) {
return GetAPIError(ErrLimitExceeded)
}
return GetAPIError(ErrInternalError) return GetAPIError(ErrInternalError)
} }

View file

@ -240,6 +240,9 @@ var (
// ErrGlobalDomainIsAlreadyTaken is returned from FrostFS in case of global domain is already taken. // ErrGlobalDomainIsAlreadyTaken is returned from FrostFS in case of global domain is already taken.
ErrGlobalDomainIsAlreadyTaken = errors.New("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. // 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 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) return fmt.Errorf("%s: %w: %s", msg, frostfs.ErrAccessDenied, reason)
} }

View file

@ -8,31 +8,49 @@ import (
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "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" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
func TestErrorChecking(t *testing.T) { func TestHandleObjectError(t *testing.T) {
reason := "some reason" msg := "some msg"
err := new(apistatus.ObjectAccessDenied)
err.WriteReason(reason)
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 { t.Run("simple access denied", func(t *testing.T) {
wrappedError = fmt.Errorf("%w: %s", frostfs.ErrAccessDenied, fetchedReason) reason := "some reason"
} inputErr := new(apistatus.ObjectAccessDenied)
inputErr.WriteReason(reason)
require.ErrorIs(t, wrappedError, frostfs.ErrAccessDenied) err := handleObjectError(msg, inputErr)
require.Contains(t, wrappedError.Error(), reason) 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) { 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) { t.Run("deadline exceeded", func(t *testing.T) {
@ -40,11 +58,35 @@ func TestErrorTimeoutChecking(t *testing.T) {
defer cancel() defer cancel()
<-ctx.Done() <-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) { t.Run("grpc deadline exceeded", func(t *testing.T) {
err := fmt.Errorf("wrap grpc error: %w", status.Error(codes.DeadlineExceeded, "error")) inputErr := fmt.Errorf("wrap grpc error: %w", status.Error(codes.DeadlineExceeded, "error"))
require.True(t, frosterr.IsTimeoutError(err))
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)
}) })
} }