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

chore(cli): add S3 bucket object tree to svc show #4966

Merged
merged 16 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions internal/pkg/aws/ecr/ecr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,15 +413,15 @@ func TestClearRepository(t *testing.T) {
},
wantError: nil,
},
"returns error if fail to check repo existance": {
"returns error if fail to check repo existence": {
mockECRClient: func(m *mocks.Mockapi) {
m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
RepositoryName: aws.String(mockRepoName),
}).Return(nil, mockAwsError)
},
wantError: fmt.Errorf("ecr repo mockRepoName describe images: %w", mockAwsError),
},
"returns error if fail to check repo existance because of non-awserr error type": {
"returns error if fail to check repo existence because of non-awserr error type": {
mockECRClient: func(m *mocks.Mockapi) {
m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
RepositoryName: aws.String(mockRepoName),
Expand Down
15 changes: 15 additions & 0 deletions internal/pkg/aws/s3/mocks/mock_s3.go

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

96 changes: 90 additions & 6 deletions internal/pkg/aws/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package s3
import (
"errors"
"fmt"
"github.com/xlab/treeprint"
"io"
"mime"
"path/filepath"
Expand All @@ -30,6 +31,9 @@ const (

// Object location prefixes.
s3URIPrefix = "s3://"

// Delimiter for ListObjectsV2Input.
slashDelimiter = "/"
)

type s3ManagerAPI interface {
Expand All @@ -38,6 +42,7 @@ type s3ManagerAPI interface {

type s3API interface {
ListObjectVersions(input *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error)
ListObjectsV2(input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error)
DeleteObjects(input *s3.DeleteObjectsInput) (*s3.DeleteObjectsOutput, error)
HeadBucket(input *s3.HeadBucketInput) (*s3.HeadBucketOutput, error)
}
Expand Down Expand Up @@ -77,13 +82,12 @@ func (s *S3) EmptyBucket(bucket string) error {
var listResp *s3.ListObjectVersionsOutput
var err error

// Bucket is exists check to make sure the bucket exists before proceeding in emptying it
isExists, err := s.isBucketExists(bucket)
bucketExists, err := s.bucketExists(bucket)
if err != nil {
return fmt.Errorf("unable to determine the existance of bucket %s: %w", bucket, err)
return fmt.Errorf("unable to determine the existence of bucket %s: %w", bucket, err)
}

if !isExists {
if !bucketExists {
return nil
}

Expand Down Expand Up @@ -190,8 +194,7 @@ func FormatARN(partition, location string) string {
return fmt.Sprintf("arn:%s:s3:::%s", partition, location)
}

// Check whether the bucket exists before proceeding with empty the bucket
func (s *S3) isBucketExists(bucket string) (bool, error) {
func (s *S3) bucketExists(bucket string) (bool, error) {
input := &s3.HeadBucketInput{
Bucket: aws.String(bucket),
}
Expand All @@ -206,6 +209,87 @@ func (s *S3) isBucketExists(bucket string) (bool, error) {
return true, nil
}

// GetBucketTree retrieves the objects in an S3 bucket and creates an ASCII tree representing their folder structure.
func (s *S3) GetBucketTree(bucket string) (string, error) {
exists, err := s.bucketExists(bucket)
if err != nil {
return "", err
}
if !exists {
return "", nil
}

var contents []*s3.Object
var prefixes []*s3.CommonPrefix
listResp := &s3.ListObjectsV2Output{}
for {
listParams := &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Delimiter: aws.String(slashDelimiter),
ContinuationToken: listResp.NextContinuationToken,
}
listResp, err = s.s3Client.ListObjectsV2(listParams)
huanjani marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", fmt.Errorf("list objects for bucket %s: %w", bucket, err)
}
contents = append(contents, listResp.Contents...)
prefixes = append(prefixes, listResp.CommonPrefixes...)
if listResp.NextContinuationToken == nil {
break
}
}

tree := treeprint.New()
// Add top-level files.
for _, object := range contents {
tree.AddNode(aws.StringValue(object.Key))
}
// Recursively add folders and their children.
if err := s.addNodes(tree, prefixes, bucket); err != nil {
return "", err
}
return tree.String(), nil
}

func (s *S3) addNodes(tree treeprint.Tree, prefixes []*s3.CommonPrefix, bucket string) error {
huanjani marked this conversation as resolved.
Show resolved Hide resolved
if len(prefixes) == 0 {
return nil
}

listResp := &s3.ListObjectsV2Output{}
var err error
for _, prefix := range prefixes {
var respContents []*s3.Object
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like we can look to parallelize these calls in the future!

var respPrefixes []*s3.CommonPrefix
branch := tree.AddBranch(filepath.Base(aws.StringValue(prefix.Prefix)))
for {
listParams := &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Delimiter: aws.String(slashDelimiter),
ContinuationToken: listResp.ContinuationToken,
Prefix: prefix.Prefix,
}
listResp, err = s.s3Client.ListObjectsV2(listParams)
huanjani marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("list objects for bucket %s: %w", bucket, err)
}
respContents = append(respContents, listResp.Contents...)
respPrefixes = append(respPrefixes, listResp.CommonPrefixes...)
if listResp.NextContinuationToken == nil {
break
}
}
for _, file := range respContents {
fileName := filepath.Base(aws.StringValue(file.Key))
branch.AddNode(fileName)
}
if err := s.addNodes(branch, respPrefixes, bucket); err != nil {
return err
}
}
return nil
}

func (s *S3) upload(bucket, key string, buf io.Reader) (string, error) {
in := &s3manager.UploadInput{
Body: buf,
Expand Down
Loading