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