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