diff --git a/internal/errors/code.go b/internal/errors/code.go index e4918ae7da..7e2185f268 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -56,7 +56,7 @@ const ( // Note: Storage errors are currently unused in OSS StorageFileClosed = 126 // StorageFileClose represents an error when a file has been closed and a read/write operation is attempted on it - StorageContainerClosed = 127 // StorageContainerClosed represents an error when a container has been closed and a read/write operation is attempted on it + StorageContainerClosed = 127 // StorageContainerClosed represents an error when a container has been closed and a I/O operation is attempted on it StorageFileReadOnly = 128 // StorageFileReadOnly represents an error when a file is readonly and a write operation is attempted on it StorageFileWriteOnly = 129 // StorageFileWriteOnly represents an error when a file is write only and a read operation is attempted on it StorageFileAlreadyExists = 130 // StorageFileAlreadyExists represents an error when a file already exists during an attempt to create it diff --git a/internal/plugin/loopback/options.go b/internal/plugin/loopback/options.go index 119052b0d1..0306736648 100644 --- a/internal/plugin/loopback/options.go +++ b/internal/plugin/loopback/options.go @@ -8,25 +8,77 @@ import ( "google.golang.org/grpc/codes" ) +// Method is used to determine if an error should be returned to a specific storage plugin method. +type Method uint8 + +const ( + // Any will return an error for any of the method defined in the storage plugin. + Any Method = iota + + // Will return an error for the OnCreateStorageBucket method + OnCreateStorageBucket + + // Will return an error for the OnUpdateStorageBucket method + OnUpdateStorageBucket + + // Will return an error for the OnDeleteStorageBucket method + OnDeleteStorageBucket + + // Will return an error for the ValidatePermissions method + ValidatePermissions + + // Will return an error for the HeadObject method + HeadObject + + // Will return an error for the GetObject method + GetObject + + // Will return an error for the PutObject method + PutObject +) + // PluginMockError is used to mock an error when interacting with an external object store. type PluginMockError struct { - bucketName string - bucketPrefix string - objectKey string - errMsg string - errCode codes.Code + BucketName string + BucketPrefix string + ObjectKey string + ErrMsg string + ErrCode codes.Code + ErrMethod Method } -func (e PluginMockError) match(bucket *storagebuckets.StorageBucket, key string) bool { - if key != "" && e.objectKey != key { +// match compares the given values from the parameters to the values provided in the mocked error. +// The bucket and key parameter values should be provided from the plugin request. The method +// value should be based on the plugin method that is calling this function. +// +// When match returns false, the mocked error should not be used for the plugin response. +// When match returns true, the mocked error should be used for the plugin response. +func (e PluginMockError) match(bucket *storagebuckets.StorageBucket, key string, method Method) bool { + // if the mocked error object key does not match the request's object key, return false. + // the object key comparison is ignored when the given key is empty because the following + // plugin methods do not provide key values: onCreateStorageBucket, onUpdateStorageBucket, + // onDeleteStorageBucket + if key != "" && e.ObjectKey != key { return false } - if e.bucketName != bucket.BucketName { + // if the mocked error bucket name does not match the request's bucket name, return false. + if e.BucketName != bucket.BucketName { return false } - if bucket.BucketPrefix != "" && e.bucketPrefix != bucket.BucketPrefix { + // if the request has a bucket prefix and it does not match the mocked error bucket prefix, return false. + if bucket.BucketPrefix != "" && e.BucketPrefix != bucket.BucketPrefix { + return false + } + // if the mocked error method is set to Any, return true. This means that the mocked error response + // will be utilized by all the plugin methods. + if e.ErrMethod == Any { + return true + } + // if the mocked error method does match the given method, return false. + if e.ErrMethod != method { return false } + // all checks comparison checks passed, return true. return true } diff --git a/internal/plugin/loopback/storage.go b/internal/plugin/loopback/storage.go index b260521468..ba847ce0b7 100644 --- a/internal/plugin/loopback/storage.go +++ b/internal/plugin/loopback/storage.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "os" + "path" "strings" "sync" "time" @@ -130,8 +131,8 @@ func (l *LoopbackStorage) onCreateStorageBucket(ctx context.Context, req *plgpb. return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) } for _, err := range l.errs { - if err.match(req.GetBucket(), "") { - return nil, status.Errorf(err.errCode, err.errMsg) + if err.match(req.GetBucket(), "", OnCreateStorageBucket) { + return nil, status.Errorf(err.ErrCode, err.ErrMsg) } } return &plgpb.OnCreateStorageBucketResponse{ @@ -158,8 +159,8 @@ func (l *LoopbackStorage) onUpdateStorageBucket(ctx context.Context, req *plgpb. return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) } for _, err := range l.errs { - if err.match(req.GetNewBucket(), "") { - return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + if err.match(req.GetNewBucket(), "", OnUpdateStorageBucket) { + return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } var sec *storagebuckets.StorageBucketPersisted @@ -206,8 +207,8 @@ func (l *LoopbackStorage) validatePermissions(ctx context.Context, req *plgpb.Va return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op) } for _, err := range l.errs { - if err.match(req.GetBucket(), "") { - return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + if err.match(req.GetBucket(), "", ValidatePermissions) { + return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } return &plgpb.ValidatePermissionsResponse{}, nil @@ -239,8 +240,8 @@ func (l *LoopbackStorage) headObject(ctx context.Context, req *plgpb.HeadObjectR return nil, status.Errorf(codes.NotFound, "%s: object %s not found", op, objectPath) } for _, err := range l.errs { - if err.match(req.GetBucket(), req.GetKey()) { - return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + if err.match(req.GetBucket(), req.GetKey(), HeadObject) { + return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } var contentLength int64 @@ -279,8 +280,8 @@ func (l *LoopbackStorage) getObject(req *plgpb.GetObjectRequest, stream plgpb.St return status.Errorf(codes.NotFound, "%s: object %s not found", op, objectPath) } for _, err := range l.errs { - if err.match(req.GetBucket(), req.GetKey()) { - return status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + if err.match(req.GetBucket(), req.GetKey(), GetObject) { + return status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } go func() { @@ -334,8 +335,8 @@ func (l *LoopbackStorage) putObject(ctx context.Context, req *plgpb.PutObjectReq return nil, status.Errorf(codes.InvalidArgument, "%s: bucket not found", op) } for _, err := range l.errs { - if err.match(req.GetBucket(), req.GetKey()) { - return nil, status.Errorf(err.errCode, "%s: %s", op, err.errMsg) + if err.match(req.GetBucket(), req.GetKey(), PutObject) { + return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg) } } @@ -354,8 +355,11 @@ func (l *LoopbackStorage) putObject(ctx context.Context, req *plgpb.PutObjectReq for _, p := range parts[:len(parts)-1] { // Directories should have trailing `/` in the key tempPath = fmt.Sprintf("%v%v/", tempPath, p) + emptyContent := int64(0) bucket[ObjectName(tempPath)] = &storagePluginStorageInfo{ - lastModified: &lastModified, + lastModified: &lastModified, + contentLength: &emptyContent, + DataChunks: []Chunk{}, } } @@ -369,7 +373,7 @@ func (l *LoopbackStorage) putObject(ctx context.Context, req *plgpb.PutObjectReq } // Now insert the object - objectPath := ObjectName(req.GetBucket().GetBucketPrefix() + req.GetKey()) + objectPath := ObjectName(path.Join(req.GetBucket().GetBucketPrefix(), req.GetKey())) bucket[objectPath] = &storagePluginStorageInfo{ DataChunks: objectChunks, contentLength: &contentLength, @@ -386,6 +390,53 @@ func (l *LoopbackStorage) putObject(ctx context.Context, req *plgpb.PutObjectReq }, nil } +// CloneBucket returns a clone of the bucket. +// returns nil when the bucket is not found. +func (l *LoopbackStorage) CloneBucket(name string) Bucket { + l.m.Lock() + defer l.m.Unlock() + if _, ok := l.buckets[BucketName(name)]; !ok { + return nil + } + bucket := Bucket{} + for objName, obj := range l.buckets[BucketName(name)] { + if obj != nil { + bucket[objName] = copyStorageInfo(obj) + } + } + return bucket +} + +// CloneStorageInfo returns a clone of the object stored in memory. +// Returns nil when the bucket or object is not found. +func (l *LoopbackStorage) CloneStorageInfo(bucketName, objectName string) *storagePluginStorageInfo { + l.m.Lock() + defer l.m.Unlock() + bucket, ok := l.buckets[BucketName(bucketName)] + if !ok { + return nil + } + obj, ok := bucket[ObjectName(objectName)] + if !ok { + return nil + } + return copyStorageInfo(obj) +} + +func copyStorageInfo(obj *storagePluginStorageInfo) *storagePluginStorageInfo { + chunks := make([]Chunk, len(obj.DataChunks)) + for i, c := range obj.DataChunks { + chunks[i] = copyBytes(c) + } + contentLength := *obj.contentLength + lastModified := *obj.lastModified + return &storagePluginStorageInfo{ + DataChunks: chunks, + lastModified: &lastModified, + contentLength: &contentLength, + } +} + func MockObject(data []Chunk) *storagePluginStorageInfo { lastModified := time.Now() dataChunks := make([]Chunk, len(data)) diff --git a/internal/plugin/loopback/storage_test.go b/internal/plugin/loopback/storage_test.go index 32d7f3b2d4..5c9948bc51 100644 --- a/internal/plugin/loopback/storage_test.go +++ b/internal/plugin/loopback/storage_test.go @@ -33,9 +33,9 @@ func TestLoopbackOnCreateStorageBucket(t *testing.T) { "aws_s3_err": {}, }), WithMockError(PluginMockError{ - bucketName: "aws_s3_err", - errMsg: "invalid credentials", - errCode: codes.PermissionDenied, + BucketName: "aws_s3_err", + ErrMsg: "invalid credentials", + ErrCode: codes.PermissionDenied, }), ) assert.NoError(err) @@ -131,9 +131,9 @@ func TestLoopbackOnUpdateStorageBucket(t *testing.T) { "aws_s3_err": {}, }), WithMockError(PluginMockError{ - bucketName: "aws_s3_err", - errMsg: "invalid credentials", - errCode: codes.PermissionDenied, + BucketName: "aws_s3_err", + ErrMsg: "invalid credentials", + ErrCode: codes.PermissionDenied, }), ) assert.NoError(err) @@ -306,9 +306,9 @@ func TestLoopbackValidatePermissions(t *testing.T) { "aws_s3_err": {}, }), WithMockError(PluginMockError{ - bucketName: "aws_s3_err", - errMsg: "invalid credentials", - errCode: codes.PermissionDenied, + BucketName: "aws_s3_err", + ErrMsg: "invalid credentials", + ErrCode: codes.PermissionDenied, }), ) assert.NoError(err) @@ -411,10 +411,10 @@ func TestLoopbackHeadObject(t *testing.T) { plg, err := NewLoopbackPlugin( WithMockBuckets(mockStorageMapData), WithMockError(PluginMockError{ - bucketName: "aws_s3_err", - objectKey: "mock_object", - errMsg: "invalid credentials", - errCode: codes.PermissionDenied, + BucketName: "aws_s3_err", + ObjectKey: "mock_object", + ErrMsg: "invalid credentials", + ErrCode: codes.PermissionDenied, }), ) assert.NoError(err) @@ -545,10 +545,10 @@ func TestLoopbackGetObject(t *testing.T) { plg, err := NewLoopbackPlugin( WithMockBuckets(mockStorageMapData), WithMockError(PluginMockError{ - bucketName: "aws_s3_err", - objectKey: "mock_object", - errMsg: "invalid credentials", - errCode: codes.PermissionDenied, + BucketName: "aws_s3_err", + ObjectKey: "mock_object", + ErrMsg: "invalid credentials", + ErrCode: codes.PermissionDenied, }), ) assert.NoError(err) @@ -693,10 +693,10 @@ func TestLoopbackPutObject(t *testing.T) { plg, err := NewLoopbackPlugin( WithMockBuckets(mockStorageMapData), WithMockError(PluginMockError{ - bucketName: "object_store_err", - objectKey: "mock_object", - errMsg: "invalid credentials", - errCode: codes.PermissionDenied, + BucketName: "object_store_err", + ObjectKey: "mock_object", + ErrMsg: "invalid credentials", + ErrCode: codes.PermissionDenied, }), ) require.NoError(err)