From 8d2eeebce3f163f444b130a4b579a1703ae86349 Mon Sep 17 00:00:00 2001 From: niksis02 Date: Tue, 4 Nov 2025 23:47:48 +0400 Subject: [PATCH] feat: adds tagging support for object versions in posix Closes #1343 Object version tagging support was previously missing in the gateway. The support is added with this PR. If versioning is not enabled at the gateway level and a user attempts to put, get, or delete object version tags, the gateway returns an `InvalidArgument`(Invalid versionId) --- backend/azure/azure.go | 6 +- backend/backend.go | 12 +-- backend/posix/posix.go | 71 ++++++++++--- backend/s3proxy/s3.go | 23 ++-- s3api/controllers/backend_moq_test.go | 42 +++++--- s3api/controllers/object-delete.go | 3 +- s3api/controllers/object-delete_test.go | 2 +- s3api/controllers/object-get.go | 3 +- s3api/controllers/object-get_test.go | 2 +- s3api/controllers/object-put.go | 3 +- s3api/controllers/object-put_test.go | 2 +- tests/integration/group-tests.go | 9 ++ tests/integration/versioning.go | 136 ++++++++++++++++++++++++ 13 files changed, 265 insertions(+), 49 deletions(-) diff --git a/backend/azure/azure.go b/backend/azure/azure.go index cb591f8..57b9dd4 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -1085,7 +1085,7 @@ func (az *Azure) CopyObject(ctx context.Context, input s3response.CopyObjectInpu }, nil } -func (az *Azure) PutObjectTagging(ctx context.Context, bucket, object string, tags map[string]string) error { +func (az *Azure) PutObjectTagging(ctx context.Context, bucket, object, _ string, tags map[string]string) error { client, err := az.getBlobClient(bucket, object) if err != nil { return err @@ -1099,7 +1099,7 @@ func (az *Azure) PutObjectTagging(ctx context.Context, bucket, object string, ta return nil } -func (az *Azure) GetObjectTagging(ctx context.Context, bucket, object string) (map[string]string, error) { +func (az *Azure) GetObjectTagging(ctx context.Context, bucket, object, _ string) (map[string]string, error) { client, err := az.getBlobClient(bucket, object) if err != nil { return nil, err @@ -1113,7 +1113,7 @@ func (az *Azure) GetObjectTagging(ctx context.Context, bucket, object string) (m return parseAzTags(tags.BlobTagSet), nil } -func (az *Azure) DeleteObjectTagging(ctx context.Context, bucket, object string) error { +func (az *Azure) DeleteObjectTagging(ctx context.Context, bucket, object, _ string) error { client, err := az.getBlobClient(bucket, object) if err != nil { return err diff --git a/backend/backend.go b/backend/backend.go index 873e798..2f9f69c 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -83,9 +83,9 @@ type Backend interface { DeleteBucketTagging(_ context.Context, bucket string) error // object tagging operations - GetObjectTagging(_ context.Context, bucket, object string) (map[string]string, error) - PutObjectTagging(_ context.Context, bucket, object string, tags map[string]string) error - DeleteObjectTagging(_ context.Context, bucket, object string) error + GetObjectTagging(_ context.Context, bucket, object, versionId string) (map[string]string, error) + PutObjectTagging(_ context.Context, bucket, object, versionId string, tags map[string]string) error + DeleteObjectTagging(_ context.Context, bucket, object, versionId string) error // object lock operations PutObjectLockConfiguration(_ context.Context, bucket string, config []byte) error @@ -251,13 +251,13 @@ func (BackendUnsupported) DeleteBucketTagging(_ context.Context, bucket string) return s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) GetObjectTagging(_ context.Context, bucket, object string) (map[string]string, error) { +func (BackendUnsupported) GetObjectTagging(_ context.Context, bucket, object, versionId string) (map[string]string, error) { return nil, s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) PutObjectTagging(_ context.Context, bucket, object string, tags map[string]string) error { +func (BackendUnsupported) PutObjectTagging(_ context.Context, bucket, object, versionId string, tags map[string]string) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } -func (BackendUnsupported) DeleteObjectTagging(_ context.Context, bucket, object string) error { +func (BackendUnsupported) DeleteObjectTagging(_ context.Context, bucket, object, versionId string) error { return s3err.GetAPIError(s3err.ErrNotImplemented) } diff --git a/backend/posix/posix.go b/backend/posix/posix.go index d77f8d4..13db839 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -1313,7 +1313,7 @@ func (p *Posix) CreateMultipartUpload(ctx context.Context, mpu s3response.Create // set object tagging if tags != nil { - err := p.PutObjectTagging(ctx, bucket, filepath.Join(objdir, uploadID), tags) + err := p.PutObjectTagging(ctx, bucket, filepath.Join(objdir, uploadID), "", tags) if err != nil { // cleanup object if returning error os.RemoveAll(filepath.Join(tmppath, uploadID)) @@ -3149,7 +3149,7 @@ func (p *Posix) PutObject(ctx context.Context, po s3response.PutObjectInput) (s3 // Set object tagging if tags != nil { - err := p.PutObjectTagging(ctx, *po.Bucket, *po.Key, tags) + err := p.PutObjectTagging(ctx, *po.Bucket, *po.Key, "", tags) if errors.Is(err, fs.ErrNotExist) { return s3response.PutObjectOutput{ ETag: etag, @@ -3722,7 +3722,7 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO objMeta := p.loadObjectMetaData(nil, bucket, object, &fid, userMetaData) var tagCount *int32 - tags, err := p.getAttrTags(bucket, object) + tags, err := p.getAttrTags(bucket, object, versionId) if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) { return nil, err } @@ -3802,7 +3802,7 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO objMeta := p.loadObjectMetaData(f, bucket, object, &fi, userMetaData) var tagCount *int32 - tags, err := p.getAttrTags(bucket, object) + tags, err := p.getAttrTags(bucket, object, versionId) if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) { return nil, err } @@ -4319,7 +4319,7 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput return s3response.CopyObjectOutput{}, err } - err = p.PutObjectTagging(ctx, dstBucket, dstObject, tags) + err = p.PutObjectTagging(ctx, dstBucket, dstObject, "", tags) if err != nil { return s3response.CopyObjectOutput{}, err } @@ -4742,7 +4742,7 @@ func (p *Posix) GetBucketTagging(_ context.Context, bucket string) (map[string]s return nil, fmt.Errorf("stat bucket: %w", err) } - tags, err := p.getAttrTags(bucket, "") + tags, err := p.getAttrTags(bucket, "", "") if err != nil { return nil, err } @@ -4757,7 +4757,7 @@ func (p *Posix) DeleteBucketTagging(ctx context.Context, bucket string) error { return p.PutBucketTagging(ctx, bucket, nil) } -func (p *Posix) GetObjectTagging(_ context.Context, bucket, object string) (map[string]string, error) { +func (p *Posix) GetObjectTagging(_ context.Context, bucket, object, versionId string) (map[string]string, error) { if !p.isBucketValid(bucket) { return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -4769,13 +4769,35 @@ func (p *Posix) GetObjectTagging(_ context.Context, bucket, object string) (map[ return nil, fmt.Errorf("stat bucket: %w", err) } - return p.getAttrTags(bucket, object) + if versionId != "" { + if !p.versioningEnabled() { + //TODO: Maybe we need to return our custom error here? + return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + } + vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) + if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return nil, fmt.Errorf("get obj versionId: %w", err) + } + + if string(vId) != versionId { + bucket = filepath.Join(p.versioningDir, bucket) + object = filepath.Join(genObjVersionKey(object), versionId) + } + } + + return p.getAttrTags(bucket, object, versionId) } -func (p *Posix) getAttrTags(bucket, object string) (map[string]string, error) { +func (p *Posix) getAttrTags(bucket, object, versionId string) (map[string]string, error) { tags := make(map[string]string) b, err := p.meta.RetrieveAttribute(nil, bucket, object, tagHdr) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { + if versionId != "" { + return nil, s3err.GetAPIError(s3err.ErrNoSuchVersion) + } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, meta.ErrNoSuchKey) { @@ -4793,7 +4815,7 @@ func (p *Posix) getAttrTags(bucket, object string) (map[string]string, error) { return tags, nil } -func (p *Posix) PutObjectTagging(_ context.Context, bucket, object string, tags map[string]string) error { +func (p *Posix) PutObjectTagging(_ context.Context, bucket, object, versionId string, tags map[string]string) error { if !p.isBucketValid(bucket) { return s3err.GetAPIError(s3err.ErrInvalidBucketName) } @@ -4805,9 +4827,31 @@ func (p *Posix) PutObjectTagging(_ context.Context, bucket, object string, tags return fmt.Errorf("stat bucket: %w", err) } + if versionId != "" { + if !p.versioningEnabled() { + //TODO: Maybe we need to return our custom error here? + return s3err.GetAPIError(s3err.ErrInvalidVersionId) + } + vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) + if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { + return s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return fmt.Errorf("get obj versionId: %w", err) + } + + if string(vId) != versionId { + bucket = filepath.Join(p.versioningDir, bucket) + object = filepath.Join(genObjVersionKey(object), versionId) + } + } + if tags == nil { err = p.meta.DeleteAttribute(bucket, object, tagHdr) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { + if versionId != "" { + return s3err.GetAPIError(s3err.ErrNoSuchVersion) + } return s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, meta.ErrNoSuchKey) { @@ -4826,6 +4870,9 @@ func (p *Posix) PutObjectTagging(_ context.Context, bucket, object string, tags err = p.meta.StoreAttribute(nil, bucket, object, tagHdr, b) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { + if versionId != "" { + return s3err.GetAPIError(s3err.ErrNoSuchVersion) + } return s3err.GetAPIError(s3err.ErrNoSuchKey) } if err != nil { @@ -4835,11 +4882,11 @@ func (p *Posix) PutObjectTagging(_ context.Context, bucket, object string, tags return nil } -func (p *Posix) DeleteObjectTagging(ctx context.Context, bucket, object string) error { +func (p *Posix) DeleteObjectTagging(ctx context.Context, bucket, object, versionId string) error { if !p.isBucketValid(bucket) { return s3err.GetAPIError(s3err.ErrInvalidBucketName) } - return p.PutObjectTagging(ctx, bucket, object, nil) + return p.PutObjectTagging(ctx, bucket, object, versionId, nil) } func (p *Posix) PutBucketPolicy(ctx context.Context, bucket string, policy []byte) error { diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go index 87966c0..337d388 100644 --- a/backend/s3proxy/s3.go +++ b/backend/s3proxy/s3.go @@ -1445,7 +1445,7 @@ func (s *S3Proxy) PutBucketAcl(ctx context.Context, bucket string, data []byte) return handleError(s.putMetaBucketObj(ctx, bucket, data, metaPrefixAcl)) } -func (s *S3Proxy) PutObjectTagging(ctx context.Context, bucket, object string, tags map[string]string) error { +func (s *S3Proxy) PutObjectTagging(ctx context.Context, bucket, object, versionId string, tags map[string]string) error { if bucket == s.metaBucket { return s3err.GetAPIError(s3err.ErrAccessDenied) } @@ -1460,20 +1460,22 @@ func (s *S3Proxy) PutObjectTagging(ctx context.Context, bucket, object string, t } _, err := s.client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{ - Bucket: &bucket, - Key: &object, - Tagging: tagging, + Bucket: &bucket, + Key: &object, + VersionId: &versionId, + Tagging: tagging, }) return handleError(err) } -func (s *S3Proxy) GetObjectTagging(ctx context.Context, bucket, object string) (map[string]string, error) { +func (s *S3Proxy) GetObjectTagging(ctx context.Context, bucket, object, versionId string) (map[string]string, error) { if bucket == s.metaBucket { return nil, s3err.GetAPIError(s3err.ErrAccessDenied) } output, err := s.client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ - Bucket: &bucket, - Key: &object, + Bucket: &bucket, + Key: &object, + VersionId: &versionId, }) if err != nil { return nil, handleError(err) @@ -1487,13 +1489,14 @@ func (s *S3Proxy) GetObjectTagging(ctx context.Context, bucket, object string) ( return tags, nil } -func (s *S3Proxy) DeleteObjectTagging(ctx context.Context, bucket, object string) error { +func (s *S3Proxy) DeleteObjectTagging(ctx context.Context, bucket, object, versionId string) error { if bucket == s.metaBucket { return s3err.GetAPIError(s3err.ErrAccessDenied) } _, err := s.client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{ - Bucket: &bucket, - Key: &object, + Bucket: &bucket, + Key: &object, + VersionId: &versionId, }) return handleError(err) } diff --git a/s3api/controllers/backend_moq_test.go b/s3api/controllers/backend_moq_test.go index a0d7710..1a0b0de 100644 --- a/s3api/controllers/backend_moq_test.go +++ b/s3api/controllers/backend_moq_test.go @@ -59,7 +59,7 @@ var _ backend.Backend = &BackendMock{} // DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) { // panic("mock out the DeleteObject method") // }, -// DeleteObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string) error { +// DeleteObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) error { // panic("mock out the DeleteObjectTagging method") // }, // DeleteObjectsFunc: func(contextMoqParam context.Context, deleteObjectsInput *s3.DeleteObjectsInput) (s3response.DeleteResult, error) { @@ -101,7 +101,7 @@ var _ backend.Backend = &BackendMock{} // GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) ([]byte, error) { // panic("mock out the GetObjectRetention method") // }, -// GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) { +// GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string) (map[string]string, error) { // panic("mock out the GetObjectTagging method") // }, // HeadBucketFunc: func(contextMoqParam context.Context, headBucketInput *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) { @@ -164,7 +164,7 @@ var _ backend.Backend = &BackendMock{} // PutObjectRetentionFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error { // panic("mock out the PutObjectRetention method") // }, -// PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { +// PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket string, object string, versionId string, tags map[string]string) error { // panic("mock out the PutObjectTagging method") // }, // RestoreObjectFunc: func(contextMoqParam context.Context, restoreObjectInput *s3.RestoreObjectInput) error { @@ -229,7 +229,7 @@ type BackendMock struct { DeleteObjectFunc func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) // DeleteObjectTaggingFunc mocks the DeleteObjectTagging method. - DeleteObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string) error + DeleteObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) error // DeleteObjectsFunc mocks the DeleteObjects method. DeleteObjectsFunc func(contextMoqParam context.Context, deleteObjectsInput *s3.DeleteObjectsInput) (s3response.DeleteResult, error) @@ -271,7 +271,7 @@ type BackendMock struct { GetObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) ([]byte, error) // GetObjectTaggingFunc mocks the GetObjectTagging method. - GetObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) + GetObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string, versionId string) (map[string]string, error) // HeadBucketFunc mocks the HeadBucket method. HeadBucketFunc func(contextMoqParam context.Context, headBucketInput *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) @@ -334,7 +334,7 @@ type BackendMock struct { PutObjectRetentionFunc func(contextMoqParam context.Context, bucket string, object string, versionId string, retention []byte) error // PutObjectTaggingFunc mocks the PutObjectTagging method. - PutObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error + PutObjectTaggingFunc func(contextMoqParam context.Context, bucket string, object string, versionId string, tags map[string]string) error // RestoreObjectFunc mocks the RestoreObject method. RestoreObjectFunc func(contextMoqParam context.Context, restoreObjectInput *s3.RestoreObjectInput) error @@ -452,6 +452,8 @@ type BackendMock struct { Bucket string // Object is the object argument value. Object string + // VersionId is the versionId argument value. + VersionId string } // DeleteObjects holds details about calls to the DeleteObjects method. DeleteObjects []struct { @@ -560,6 +562,8 @@ type BackendMock struct { Bucket string // Object is the object argument value. Object string + // VersionId is the versionId argument value. + VersionId string } // HeadBucket holds details about calls to the HeadBucket method. HeadBucket []struct { @@ -733,6 +737,8 @@ type BackendMock struct { Bucket string // Object is the object argument value. Object string + // VersionId is the versionId argument value. + VersionId string // Tags is the tags argument value. Tags map[string]string } @@ -1268,7 +1274,7 @@ func (mock *BackendMock) DeleteObjectCalls() []struct { } // DeleteObjectTagging calls DeleteObjectTaggingFunc. -func (mock *BackendMock) DeleteObjectTagging(contextMoqParam context.Context, bucket string, object string) error { +func (mock *BackendMock) DeleteObjectTagging(contextMoqParam context.Context, bucket string, object string, versionId string) error { if mock.DeleteObjectTaggingFunc == nil { panic("BackendMock.DeleteObjectTaggingFunc: method is nil but Backend.DeleteObjectTagging was just called") } @@ -1276,15 +1282,17 @@ func (mock *BackendMock) DeleteObjectTagging(contextMoqParam context.Context, bu ContextMoqParam context.Context Bucket string Object string + VersionId string }{ ContextMoqParam: contextMoqParam, Bucket: bucket, Object: object, + VersionId: versionId, } mock.lockDeleteObjectTagging.Lock() mock.calls.DeleteObjectTagging = append(mock.calls.DeleteObjectTagging, callInfo) mock.lockDeleteObjectTagging.Unlock() - return mock.DeleteObjectTaggingFunc(contextMoqParam, bucket, object) + return mock.DeleteObjectTaggingFunc(contextMoqParam, bucket, object, versionId) } // DeleteObjectTaggingCalls gets all the calls that were made to DeleteObjectTagging. @@ -1295,11 +1303,13 @@ func (mock *BackendMock) DeleteObjectTaggingCalls() []struct { ContextMoqParam context.Context Bucket string Object string + VersionId string } { var calls []struct { ContextMoqParam context.Context Bucket string Object string + VersionId string } mock.lockDeleteObjectTagging.RLock() calls = mock.calls.DeleteObjectTagging @@ -1792,7 +1802,7 @@ func (mock *BackendMock) GetObjectRetentionCalls() []struct { } // GetObjectTagging calls GetObjectTaggingFunc. -func (mock *BackendMock) GetObjectTagging(contextMoqParam context.Context, bucket string, object string) (map[string]string, error) { +func (mock *BackendMock) GetObjectTagging(contextMoqParam context.Context, bucket string, object string, versionId string) (map[string]string, error) { if mock.GetObjectTaggingFunc == nil { panic("BackendMock.GetObjectTaggingFunc: method is nil but Backend.GetObjectTagging was just called") } @@ -1800,15 +1810,17 @@ func (mock *BackendMock) GetObjectTagging(contextMoqParam context.Context, bucke ContextMoqParam context.Context Bucket string Object string + VersionId string }{ ContextMoqParam: contextMoqParam, Bucket: bucket, Object: object, + VersionId: versionId, } mock.lockGetObjectTagging.Lock() mock.calls.GetObjectTagging = append(mock.calls.GetObjectTagging, callInfo) mock.lockGetObjectTagging.Unlock() - return mock.GetObjectTaggingFunc(contextMoqParam, bucket, object) + return mock.GetObjectTaggingFunc(contextMoqParam, bucket, object, versionId) } // GetObjectTaggingCalls gets all the calls that were made to GetObjectTagging. @@ -1819,11 +1831,13 @@ func (mock *BackendMock) GetObjectTaggingCalls() []struct { ContextMoqParam context.Context Bucket string Object string + VersionId string } { var calls []struct { ContextMoqParam context.Context Bucket string Object string + VersionId string } mock.lockGetObjectTagging.RLock() calls = mock.calls.GetObjectTagging @@ -2600,7 +2614,7 @@ func (mock *BackendMock) PutObjectRetentionCalls() []struct { } // PutObjectTagging calls PutObjectTaggingFunc. -func (mock *BackendMock) PutObjectTagging(contextMoqParam context.Context, bucket string, object string, tags map[string]string) error { +func (mock *BackendMock) PutObjectTagging(contextMoqParam context.Context, bucket string, object string, versionId string, tags map[string]string) error { if mock.PutObjectTaggingFunc == nil { panic("BackendMock.PutObjectTaggingFunc: method is nil but Backend.PutObjectTagging was just called") } @@ -2608,17 +2622,19 @@ func (mock *BackendMock) PutObjectTagging(contextMoqParam context.Context, bucke ContextMoqParam context.Context Bucket string Object string + VersionId string Tags map[string]string }{ ContextMoqParam: contextMoqParam, Bucket: bucket, Object: object, + VersionId: versionId, Tags: tags, } mock.lockPutObjectTagging.Lock() mock.calls.PutObjectTagging = append(mock.calls.PutObjectTagging, callInfo) mock.lockPutObjectTagging.Unlock() - return mock.PutObjectTaggingFunc(contextMoqParam, bucket, object, tags) + return mock.PutObjectTaggingFunc(contextMoqParam, bucket, object, versionId, tags) } // PutObjectTaggingCalls gets all the calls that were made to PutObjectTagging. @@ -2629,12 +2645,14 @@ func (mock *BackendMock) PutObjectTaggingCalls() []struct { ContextMoqParam context.Context Bucket string Object string + VersionId string Tags map[string]string } { var calls []struct { ContextMoqParam context.Context Bucket string Object string + VersionId string Tags map[string]string } mock.lockPutObjectTagging.RLock() diff --git a/s3api/controllers/object-delete.go b/s3api/controllers/object-delete.go index 4e41bd5..b094f44 100644 --- a/s3api/controllers/object-delete.go +++ b/s3api/controllers/object-delete.go @@ -30,6 +30,7 @@ import ( func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error) { bucket := ctx.Params("bucket") key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + versionId := ctx.Query("versionId") acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) isBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) @@ -55,7 +56,7 @@ func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error) }, err } - err = c.be.DeleteObjectTagging(ctx.Context(), bucket, key) + err = c.be.DeleteObjectTagging(ctx.Context(), bucket, key, versionId) return &Response{ MetaOpts: &MetaOptions{ Status: http.StatusNoContent, diff --git a/s3api/controllers/object-delete_test.go b/s3api/controllers/object-delete_test.go index d905435..33e72e7 100644 --- a/s3api/controllers/object-delete_test.go +++ b/s3api/controllers/object-delete_test.go @@ -81,7 +81,7 @@ func TestS3ApiController_DeleteObjectTagging(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { be := &BackendMock{ - DeleteObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string) error { + DeleteObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object, versionId string) error { return tt.input.beErr }, GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { diff --git a/s3api/controllers/object-get.go b/s3api/controllers/object-get.go index dbd7257..9c72b38 100644 --- a/s3api/controllers/object-get.go +++ b/s3api/controllers/object-get.go @@ -35,6 +35,7 @@ import ( func (c S3ApiController) GetObjectTagging(ctx *fiber.Ctx) (*Response, error) { bucket := ctx.Params("bucket") key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + versionId := ctx.Query("versionId") acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) @@ -59,7 +60,7 @@ func (c S3ApiController) GetObjectTagging(ctx *fiber.Ctx) (*Response, error) { }, err } - data, err := c.be.GetObjectTagging(ctx.Context(), bucket, key) + data, err := c.be.GetObjectTagging(ctx.Context(), bucket, key, versionId) if err != nil { return &Response{ MetaOpts: &MetaOptions{ diff --git a/s3api/controllers/object-get_test.go b/s3api/controllers/object-get_test.go index 1987980..65ffda7 100644 --- a/s3api/controllers/object-get_test.go +++ b/s3api/controllers/object-get_test.go @@ -95,7 +95,7 @@ func TestS3ApiController_GetObjectTagging(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { be := &BackendMock{ - GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string) (map[string]string, error) { + GetObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (map[string]string, error) { return tt.input.beRes.(map[string]string), tt.input.beErr }, GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go index 2b76fe4..55d51dc 100644 --- a/s3api/controllers/object-put.go +++ b/s3api/controllers/object-put.go @@ -36,6 +36,7 @@ import ( func (c S3ApiController) PutObjectTagging(ctx *fiber.Ctx) (*Response, error) { bucket := ctx.Params("bucket") key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket)) + versionId := ctx.Query("versionId") acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx) @@ -69,7 +70,7 @@ func (c S3ApiController) PutObjectTagging(ctx *fiber.Ctx) (*Response, error) { }, err } - err = c.be.PutObjectTagging(ctx.Context(), bucket, key, tagging) + err = c.be.PutObjectTagging(ctx.Context(), bucket, key, versionId, tagging) return &Response{ MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, diff --git a/s3api/controllers/object-put_test.go b/s3api/controllers/object-put_test.go index 95c7bfa..dbf877b 100644 --- a/s3api/controllers/object-put_test.go +++ b/s3api/controllers/object-put_test.go @@ -115,7 +115,7 @@ func TestS3ApiController_PutObjectTagging(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { be := &BackendMock{ - PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object string, tags map[string]string) error { + PutObjectTaggingFunc: func(contextMoqParam context.Context, bucket, object, versionId string, tags map[string]string) error { return tt.input.beErr }, GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index caef6f1..9944a29 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -1004,6 +1004,11 @@ func TestVersioning(ts *TestState) { ts.Run(Versioning_GetObject_delete_marker_without_versionId) ts.Run(Versioning_GetObject_delete_marker) ts.Run(Versioning_GetObject_null_versionId_obj) + // object tagging actions + ts.Run(Versioning_PutObjectTagging_non_existing_object_version) + ts.Run(Versioning_GetObjectTagging_non_existing_object_version) + ts.Run(Versioning_DeleteObjectTagging_non_existing_object_version) + ts.Run(Versioning_PutGetDeleteObjectTagging_success) // GetObjectAttributes action ts.Run(Versioning_GetObjectAttributes_object_version) ts.Run(Versioning_GetObjectAttributes_delete_marker) @@ -1636,6 +1641,10 @@ func GetIntTests() IntTests { "Versioning_GetObject_delete_marker_without_versionId": Versioning_GetObject_delete_marker_without_versionId, "Versioning_GetObject_delete_marker": Versioning_GetObject_delete_marker, "Versioning_GetObject_null_versionId_obj": Versioning_GetObject_null_versionId_obj, + "Versioning_PutObjectTagging_non_existing_object_version": Versioning_PutObjectTagging_non_existing_object_version, + "Versioning_GetObjectTagging_non_existing_object_version": Versioning_GetObjectTagging_non_existing_object_version, + "Versioning_DeleteObjectTagging_non_existing_object_version": Versioning_DeleteObjectTagging_non_existing_object_version, + "Versioning_PutGetDeleteObjectTagging_success": Versioning_PutGetDeleteObjectTagging_success, "Versioning_GetObjectAttributes_object_version": Versioning_GetObjectAttributes_object_version, "Versioning_GetObjectAttributes_delete_marker": Versioning_GetObjectAttributes_delete_marker, "Versioning_DeleteObject_delete_object_version": Versioning_DeleteObject_delete_object_version, diff --git a/tests/integration/versioning.go b/tests/integration/versioning.go index ed78695..c43e614 100644 --- a/tests/integration/versioning.go +++ b/tests/integration/versioning.go @@ -2682,3 +2682,139 @@ func Versioning_concurrent_upload_object(s *S3Conf) error { return nil }, withVersioning(types.BucketVersioningStatusEnabled)) } + +func Versioning_PutObjectTagging_non_existing_object_version(s *S3Conf) error { + testName := "Versioning_PutObjectTagging_non_existing_object_version" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-object" + _, err := putObjectWithData(4, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + }, s3client) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + Tagging: &types.Tagging{ + TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}, + }, + VersionId: getPtr("01K97XE6PJQ1A4X5TJFDHK4EMC"), + }) + cancel() + return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)) + }, withVersioning(types.BucketVersioningStatusEnabled)) +} + +func Versioning_GetObjectTagging_non_existing_object_version(s *S3Conf) error { + testName := "Versioning_GetObjectTagging_non_existing_object_version" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-object" + _, err := putObjectWithData(4, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + }, s3client) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + VersionId: getPtr("01K97XE6PJQ1A4X5TJFDHK4EMC"), + }) + cancel() + return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)) + }, withVersioning(types.BucketVersioningStatusEnabled)) +} + +func Versioning_DeleteObjectTagging_non_existing_object_version(s *S3Conf) error { + testName := "Versioning_DeleteObjectTagging_non_existing_object_version" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-object" + _, err := putObjectWithData(4, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &obj, + }, s3client) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + VersionId: getPtr("01K97XE6PJQ1A4X5TJFDHK4EMC"), + }) + cancel() + return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)) + }, withVersioning(types.BucketVersioningStatusEnabled)) +} + +func Versioning_PutGetDeleteObjectTagging_success(s *S3Conf) error { + testName := "Versioning_PutGetDeleteObjectTagging_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-object" + versions, err := createObjVersions(s3client, bucket, obj, 5) + if err != nil { + return err + } + versionId := versions[2].VersionId + + tagging := types.Tagging{ + TagSet: []types.Tag{ + {Key: getPtr("key"), Value: getPtr("value")}, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + Tagging: &tagging, + VersionId: versionId, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + out, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + VersionId: versionId, + }) + cancel() + if err != nil { + return err + } + if !areTagsSame(tagging.TagSet, out.TagSet) { + return fmt.Errorf("expected the object version tags to be %v, instead got %v", tagging.TagSet, out.TagSet) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + VersionId: versionId, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{ + Bucket: &bucket, + Key: &obj, + VersionId: versionId, + }) + cancel() + return checkApiErr(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) + }, withVersioning(types.BucketVersioningStatusEnabled)) +}