Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Delete versioned S3 objects #54

Merged
merged 2 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/di/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func NewS3App(ctx context.Context, profile model.AWSProfile, region model.Region
external.S3ObjectDownloaderSet,
external.S3ObjectUploaderSet,
external.S3ObjectCopierSet,
external.S3ObjectVersionsListerSet,
interactor.S3BucketCreatorSet,
interactor.S3BucketListerSet,
interactor.S3BucketDeleterSet,
Expand Down
3 changes: 2 additions & 1 deletion app/di/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/domain/model/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ func (k S3Key) IsAll() bool {
return k == "*"
}

// Join returns the S3Key with the key.
func (k S3Key) Join(key S3Key) S3Key {
if key.Empty() {
return k
Expand Down
17 changes: 17 additions & 0 deletions app/domain/service/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,20 @@ type S3ObjectCopierOutput struct{}
type S3ObjectCopier interface {
CopyS3Object(ctx context.Context, input *S3ObjectCopierInput) (*S3ObjectCopierOutput, error)
}

// S3ObjectVersionsListerInput is the input of the ListBucketObjectVersions method.
type S3ObjectVersionsListerInput struct {
// Bucket is the name of the bucket to list.
Bucket model.Bucket
}

// S3ObjectVersionsListerOutput is the output of the ListBucketObjectVersions method.
type S3ObjectVersionsListerOutput struct {
// Objects is the list of the objects.
Objects model.S3ObjectIdentifiers
}

// S3ObjectVersionsLister is the interface that wraps the basic ListBucketObjectVersions method.
type S3ObjectVersionsLister interface {
ListS3ObjectVersions(ctx context.Context, input *S3ObjectVersionsListerInput) (*S3ObjectVersionsListerOutput, error)
}
8 changes: 8 additions & 0 deletions app/external/mock/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,11 @@ type S3ObjectCopier func(ctx context.Context, input *service.S3ObjectCopierInput
func (m S3ObjectCopier) CopyS3Object(ctx context.Context, input *service.S3ObjectCopierInput) (*service.S3ObjectCopierOutput, error) {
return m(ctx, input)
}

// S3ObjectVersionsLister is a mock of the S3ObjectVersionsLister interface.
type S3ObjectVersionsLister func(ctx context.Context, input *service.S3ObjectVersionsListerInput) (*service.S3ObjectVersionsListerOutput, error)

// ListS3ObjectVersions calls the ListS3ObjectVersionsFunc.
func (m S3ObjectVersionsLister) ListS3ObjectVersions(ctx context.Context, input *service.S3ObjectVersionsListerInput) (*service.S3ObjectVersionsListerOutput, error) {
return m(ctx, input)
}
56 changes: 56 additions & 0 deletions app/external/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,59 @@ func (c *S3ObjectCopier) CopyS3Object(ctx context.Context, input *service.S3Obje
}
return &service.S3ObjectCopierOutput{}, nil
}

// S3ObjectVersionsLister implements the S3ObjectVersionsLister interface.
type S3ObjectVersionsLister struct {
client *s3.Client
}

// S3ObjectVersionsListerSet is a provider set for S3ObjectVersionsLister.
//
//nolint:gochecknoglobals
var S3ObjectVersionsListerSet = wire.NewSet(
NewS3ObjectVersionsLister,
wire.Bind(new(service.S3ObjectVersionsLister), new(*S3ObjectVersionsLister)),
)

var _ service.S3ObjectVersionsLister = (*S3ObjectVersionsLister)(nil)

// NewS3ObjectVersionsLister creates a new S3ObjectVersionsLister.
func NewS3ObjectVersionsLister(client *s3.Client) *S3ObjectVersionsLister {
return &S3ObjectVersionsLister{client: client}
}

// ListS3ObjectVersions lists the object versions in the bucket.
func (c *S3ObjectVersionsLister) ListS3ObjectVersions(ctx context.Context, input *service.S3ObjectVersionsListerInput) (*service.S3ObjectVersionsListerOutput, error) {
var objects model.S3ObjectIdentifiers
listObjectVersionsInput := &s3.ListObjectVersionsInput{
Bucket: aws.String(input.Bucket.String()),
MaxKeys: aws.Int32(model.MaxS3Keys),
}

for {
listObjectVersionsOutput, err := c.client.ListObjectVersions(ctx, listObjectVersionsInput)
if err != nil {
return nil, err
}
for _, version := range listObjectVersionsOutput.Versions {
objects = append(objects, model.S3ObjectIdentifier{
S3Key: model.S3Key(*version.Key),
VersionID: model.VersionID(*version.VersionId),
})
}
for _, deleteMarker := range listObjectVersionsOutput.DeleteMarkers {
objects = append(objects, model.S3ObjectIdentifier{
S3Key: model.S3Key(*deleteMarker.Key),
VersionID: model.VersionID(*deleteMarker.VersionId),
})
}

if !*listObjectVersionsOutput.IsTruncated {
break
}

listObjectVersionsInput.KeyMarker = listObjectVersionsOutput.NextKeyMarker
listObjectVersionsInput.VersionIdMarker = listObjectVersionsOutput.NextVersionIdMarker
}
return &service.S3ObjectVersionsListerOutput{Objects: objects}, nil
}
45 changes: 39 additions & 6 deletions app/interactor/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func (s *S3ObjectsLister) ListS3Objects(ctx context.Context, input *usecase.S3Ob
type S3ObjectsDeleter struct {
service.S3ObjectsDeleter
service.S3BucketLocationGetter
service.S3ObjectVersionsLister
}

// S3ObjectsDeleterSet is a provider set for S3ObjectsDeleter.
Expand All @@ -154,10 +155,15 @@ var S3ObjectsDeleterSet = wire.NewSet(
var _ usecase.S3ObjectsDeleter = (*S3ObjectsDeleter)(nil)

// NewS3ObjectsDeleter creates a new S3ObjectsDeleter.
func NewS3ObjectsDeleter(d service.S3ObjectsDeleter, l service.S3BucketLocationGetter) *S3ObjectsDeleter {
func NewS3ObjectsDeleter(
d service.S3ObjectsDeleter,
g service.S3BucketLocationGetter,
l service.S3ObjectVersionsLister,
) *S3ObjectsDeleter {
return &S3ObjectsDeleter{
S3ObjectsDeleter: d,
S3BucketLocationGetter: l,
S3BucketLocationGetter: g,
S3ObjectVersionsLister: l,
}
}

Expand All @@ -174,14 +180,41 @@ func (s *S3ObjectsDeleter) DeleteS3Objects(ctx context.Context, input *usecase.S
return nil, err
}

_, err = s.S3ObjectsDeleter.DeleteS3Objects(ctx, &service.S3ObjectsDeleterInput{
Bucket: input.Bucket,
Region: location.Region,
S3ObjectSets: input.S3ObjectIdentifiers,
versions, err := s.S3ObjectVersionsLister.ListS3ObjectVersions(ctx, &service.S3ObjectVersionsListerInput{
Bucket: input.Bucket,
})
if err != nil {
return nil, err
}
if len(versions.Objects) == 0 {
return &usecase.S3ObjectsDeleterOutput{}, nil // no objects to delete
}

targets := make(model.S3ObjectIdentifiers, 0, len(versions.Objects))
versionMap := make(map[model.S3Key][]model.VersionID)
for _, version := range versions.Objects {
versionMap[version.S3Key] = append(versionMap[version.S3Key], version.VersionID)
}

for _, inputIdentifier := range input.S3ObjectIdentifiers {
if versionIDs, ok := versionMap[inputIdentifier.S3Key]; ok {
for _, versionID := range versionIDs {
targets = append(targets, model.S3ObjectIdentifier{
S3Key: inputIdentifier.S3Key,
VersionID: versionID,
})
}
}
}

if _, err = s.S3ObjectsDeleter.DeleteS3Objects(ctx, &service.S3ObjectsDeleterInput{
Bucket: input.Bucket,
Region: location.Region,
S3ObjectSets: targets,
}); err != nil {
return nil, err
}

return &usecase.S3ObjectsDeleterOutput{}, nil
}

Expand Down
20 changes: 16 additions & 4 deletions app/interactor/s3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,13 @@ func TestS3ObjectsDeleter_DeleteS3Objects(t *testing.T) {
return &service.S3ObjectsDeleterOutput{}, nil
})

s3ObjectsDeleter := NewS3ObjectsDeleter(s3ObjectsDeleterMock, s3BucketLocationGetter)
s3ObjectVersionLister := mock.S3ObjectVersionsLister(func(ctx context.Context, input *service.S3ObjectVersionsListerInput) (*service.S3ObjectVersionsListerOutput, error) {
return &service.S3ObjectVersionsListerOutput{
Objects: model.S3ObjectIdentifiers{},
}, nil
})

s3ObjectsDeleter := NewS3ObjectsDeleter(s3ObjectsDeleterMock, s3BucketLocationGetter, s3ObjectVersionLister)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the s3ObjectVersionLister mock is correctly set up to return a meaningful response for testing the DeleteS3Objects functionality, even if it's an empty list in this case. Consider adding test cases or modifying the mock to return different sets of object versions to more thoroughly test the handling of versioned objects.

if _, err := s3ObjectsDeleter.DeleteS3Objects(context.Background(), &usecase.S3ObjectsDeleterInput{
Bucket: model.Bucket("bucket-name"),
S3ObjectIdentifiers: model.S3ObjectIdentifiers{
Expand Down Expand Up @@ -413,7 +419,13 @@ func TestS3ObjectsDeleter_DeleteS3Objects(t *testing.T) {
return nil, errors.New("some error")
})

s3ObjectsDeleter := NewS3ObjectsDeleter(s3ObjectsDeleterMock, s3BucketLocationGetter)
s3ObjectVersionLister := mock.S3ObjectVersionsLister(func(ctx context.Context, input *service.S3ObjectVersionsListerInput) (*service.S3ObjectVersionsListerOutput, error) {
return &service.S3ObjectVersionsListerOutput{
Objects: model.S3ObjectIdentifiers{},
}, nil
})

s3ObjectsDeleter := NewS3ObjectsDeleter(s3ObjectsDeleterMock, s3BucketLocationGetter, s3ObjectVersionLister)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous comment, verify that the s3ObjectVersionLister mock setup is appropriate for the test scenario. If testing error handling or specific logic related to object versions, adjust the mock's return values accordingly.

if _, err := s3ObjectsDeleter.DeleteS3Objects(context.Background(), &usecase.S3ObjectsDeleterInput{
Bucket: model.Bucket("bucket-name"),
S3ObjectIdentifiers: model.S3ObjectIdentifiers{
Expand Down Expand Up @@ -442,7 +454,7 @@ func TestS3ObjectsDeleter_DeleteS3Objects(t *testing.T) {
return nil, errors.New("some error")
})

s3ObjectsDeleter := NewS3ObjectsDeleter(nil, s3BucketLocationGetter)
s3ObjectsDeleter := NewS3ObjectsDeleter(nil, s3BucketLocationGetter, nil)
if _, err := s3ObjectsDeleter.DeleteS3Objects(context.Background(), &usecase.S3ObjectsDeleterInput{
Bucket: model.Bucket("bucket-name"),
S3ObjectIdentifiers: model.S3ObjectIdentifiers{
Expand All @@ -467,7 +479,7 @@ func TestS3ObjectsDeleter_DeleteS3Objects(t *testing.T) {
t.Run("If bucket name is too short, failed to delete objects", func(t *testing.T) {
t.Parallel()

s3ObjectsDeleter := NewS3ObjectsDeleter(nil, nil)
s3ObjectsDeleter := NewS3ObjectsDeleter(nil, nil, nil)
if _, err := s3ObjectsDeleter.DeleteS3Objects(context.Background(), &usecase.S3ObjectsDeleterInput{
Bucket: "b", // too short
}); err == nil {
Expand Down
Loading