diff --git a/internal/pkg/aws/ecr/ecr_test.go b/internal/pkg/aws/ecr/ecr_test.go index 713162d8a74..64286d5f7a2 100644 --- a/internal/pkg/aws/ecr/ecr_test.go +++ b/internal/pkg/aws/ecr/ecr_test.go @@ -413,7 +413,7 @@ 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), @@ -421,7 +421,7 @@ func TestClearRepository(t *testing.T) { }, 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), diff --git a/internal/pkg/aws/s3/mocks/mock_s3.go b/internal/pkg/aws/s3/mocks/mock_s3.go index 5dda2a12914..5361e364c5c 100644 --- a/internal/pkg/aws/s3/mocks/mock_s3.go +++ b/internal/pkg/aws/s3/mocks/mock_s3.go @@ -123,6 +123,21 @@ func (mr *Mocks3APIMockRecorder) ListObjectVersions(input interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectVersions", reflect.TypeOf((*Mocks3API)(nil).ListObjectVersions), input) } +// ListObjectsV2 mocks base method. +func (m *Mocks3API) ListObjectsV2(input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListObjectsV2", input) + ret0, _ := ret[0].(*s3.ListObjectsV2Output) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListObjectsV2 indicates an expected call of ListObjectsV2. +func (mr *Mocks3APIMockRecorder) ListObjectsV2(input interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2", reflect.TypeOf((*Mocks3API)(nil).ListObjectsV2), input) +} + // MockNamedBinary is a mock of NamedBinary interface. type MockNamedBinary struct { ctrl *gomock.Controller diff --git a/internal/pkg/aws/s3/s3.go b/internal/pkg/aws/s3/s3.go index 642d01a5c3b..9e22d94c518 100644 --- a/internal/pkg/aws/s3/s3.go +++ b/internal/pkg/aws/s3/s3.go @@ -7,6 +7,7 @@ package s3 import ( "errors" "fmt" + "github.com/xlab/treeprint" "io" "mime" "path/filepath" @@ -30,6 +31,9 @@ const ( // Object location prefixes. s3URIPrefix = "s3://" + + // Delimiter for ListObjectsV2Input. + slashDelimiter = "/" ) type s3ManagerAPI interface { @@ -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) } @@ -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 } @@ -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), } @@ -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) + 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 { + if len(prefixes) == 0 { + return nil + } + + listResp := &s3.ListObjectsV2Output{} + var err error + for _, prefix := range prefixes { + var respContents []*s3.Object + 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) + 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, diff --git a/internal/pkg/aws/s3/s3_test.go b/internal/pkg/aws/s3/s3_test.go index a14f7854052..011342a37ad 100644 --- a/internal/pkg/aws/s3/s3_test.go +++ b/internal/pkg/aws/s3/s3_test.go @@ -274,7 +274,7 @@ func TestS3_EmptyBucket(t *testing.T) { }).Return(nil, awserr.New("Unknown", "message", nil)) }, - wantErr: fmt.Errorf("unable to determine the existance of bucket %s: %w", "mockBucket", + wantErr: fmt.Errorf("unable to determine the existence of bucket %s: %w", "mockBucket", awserr.New("Unknown", "message", nil)), }, "some objects failed to delete": { @@ -488,3 +488,215 @@ func TestS3_FormatARN(t *testing.T) { }) } } + +func TestS3_GetBucketTree(t *testing.T) { + type s3Mocks struct { + s3API *mocks.Mocks3API + s3ManagerAPI *mocks.Mocks3ManagerAPI + } + mockBucket := aws.String("bucketName") + delimiter := aws.String("/") + nonexistentError := awserr.New(errCodeNotFound, "msg", errors.New("some error")) + mockContinuationToken := "next" + + firstResp := s3.ListObjectsV2Output{ + CommonPrefixes: []*s3.CommonPrefix{ + {Prefix: aws.String("Images")}, + {Prefix: aws.String("css")}, + {Prefix: aws.String("top")}, + }, + Contents: []*s3.Object{ + {Key: aws.String("README.md")}, + {Key: aws.String("error.html")}, + {Key: aws.String("index.html")}, + }, + Delimiter: delimiter, + KeyCount: aws.Int64(14), + MaxKeys: aws.Int64(1000), + Name: mockBucket, + } + imagesResp := s3.ListObjectsV2Output{ + Contents: []*s3.Object{ + {Key: aws.String("firstImage.PNG")}, + {Key: aws.String("secondImage.PNG")}, + }, + Delimiter: delimiter, + KeyCount: aws.Int64(14), + MaxKeys: aws.Int64(1000), + Name: mockBucket, + } + cssResp := s3.ListObjectsV2Output{ + Contents: []*s3.Object{ + {Key: aws.String("Style.css")}, + {Key: aws.String("bootstrap.min.css")}, + }, + Delimiter: delimiter, + KeyCount: aws.Int64(14), + MaxKeys: aws.Int64(1000), + Name: mockBucket, + } + topResp := s3.ListObjectsV2Output{ + CommonPrefixes: []*s3.CommonPrefix{ + {Prefix: aws.String("middle")}, + }, + Delimiter: delimiter, + KeyCount: aws.Int64(14), + MaxKeys: aws.Int64(1000), + Name: mockBucket, + } + middleResp := s3.ListObjectsV2Output{ + Contents: []*s3.Object{ + {Key: aws.String("bottom.html")}, + }, + Delimiter: delimiter, + KeyCount: aws.Int64(14), + MaxKeys: aws.Int64(1000), + Name: mockBucket, + } + testCases := map[string]struct { + setupMocks func(mocks s3Mocks) + + wantTree string + wantErr error + }{ + "should return all objects within the bucket as a tree string": { + setupMocks: func(m s3Mocks) { + m.s3API.EXPECT().HeadBucket(&s3.HeadBucketInput{Bucket: mockBucket}).Return(&s3.HeadBucketOutput{}, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + ContinuationToken: nil, + Delimiter: delimiter, + Prefix: nil, + }).Return(&firstResp, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + Delimiter: delimiter, + Prefix: aws.String("Images"), + }).Return(&imagesResp, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + Delimiter: delimiter, + Prefix: aws.String("css"), + }).Return(&cssResp, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + Delimiter: delimiter, + Prefix: aws.String("top"), + }).Return(&topResp, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + Delimiter: delimiter, + Prefix: aws.String("middle"), + }).Return(&middleResp, nil) + }, + wantTree: `. +├── README.md +├── error.html +├── index.html +├── Images +│ ├── firstImage.PNG +│ └── secondImage.PNG +├── css +│ ├── Style.css +│ └── bootstrap.min.css +└── top + └── middle + └── bottom.html +`, + }, + "should handle multiple pages of objects": { + setupMocks: func(m s3Mocks) { + m.s3API.EXPECT().HeadBucket(&s3.HeadBucketInput{Bucket: mockBucket}).Return(&s3.HeadBucketOutput{}, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + ContinuationToken: nil, + Delimiter: delimiter, + Prefix: nil, + }).Return( + &s3.ListObjectsV2Output{ + Contents: []*s3.Object{ + {Key: aws.String("README.md")}, + }, + Delimiter: delimiter, + KeyCount: aws.Int64(14), + MaxKeys: aws.Int64(1000), + Name: mockBucket, + NextContinuationToken: &mockContinuationToken, + }, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + ContinuationToken: &mockContinuationToken, + Delimiter: delimiter, + Prefix: nil, + }).Return( + &s3.ListObjectsV2Output{ + Contents: []*s3.Object{ + {Key: aws.String("READMETOO.md")}, + }, + Delimiter: delimiter, + KeyCount: aws.Int64(14), + MaxKeys: aws.Int64(1000), + Name: mockBucket, + NextContinuationToken: nil, + }, nil) + }, + wantTree: `. +├── README.md +└── READMETOO.md +`, + }, + "return nil if bucket doesn't exist": { + setupMocks: func(m s3Mocks) { + m.s3API.EXPECT().HeadBucket(&s3.HeadBucketInput{Bucket: mockBucket}).Return(&s3.HeadBucketOutput{}, nonexistentError) + }, + }, + "return err if cannot determine if bucket exists": { + setupMocks: func(m s3Mocks) { + m.s3API.EXPECT().HeadBucket(&s3.HeadBucketInput{Bucket: mockBucket}).Return(&s3.HeadBucketOutput{}, errors.New("some error")) + }, + wantErr: errors.New("some error"), + }, + "should wrap error if fail to list objects": { + setupMocks: func(m s3Mocks) { + m.s3API.EXPECT().HeadBucket(&s3.HeadBucketInput{Bucket: mockBucket}).Return(&s3.HeadBucketOutput{}, nil) + m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: mockBucket, + ContinuationToken: nil, + Delimiter: delimiter, + Prefix: nil, + }).Return(nil, errors.New("some error")) + }, + wantErr: errors.New("list objects for bucket bucketName: some error"), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockS3Client := mocks.NewMocks3API(ctrl) + + mockS3Manager := mocks.NewMocks3ManagerAPI(ctrl) + + s3mocks := s3Mocks{ + s3API: mockS3Client, + s3ManagerAPI: mockS3Manager, + } + service := S3{ + s3Client: mockS3Client, + } + tc.setupMocks(s3mocks) + + gotTree, gotErr := service.GetBucketTree(aws.StringValue(mockBucket)) + if tc.wantErr != nil { + require.EqualError(t, gotErr, tc.wantErr.Error()) + return + } + require.NoError(t, gotErr) + require.Equal(t, tc.wantTree, gotTree) + }) + + } +} diff --git a/internal/pkg/describe/mocks/mock_service.go b/internal/pkg/describe/mocks/mock_service.go index ad70b9e9e0d..3e8e69b6986 100644 --- a/internal/pkg/describe/mocks/mock_service.go +++ b/internal/pkg/describe/mocks/mock_service.go @@ -708,3 +708,79 @@ func (mr *MockcwAlarmDescriberMockRecorder) AlarmDescriptions(arg0 interface{}) mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AlarmDescriptions", reflect.TypeOf((*MockcwAlarmDescriber)(nil).AlarmDescriptions), arg0) } + +// MockbucketDescriber is a mock of bucketDescriber interface. +type MockbucketDescriber struct { + ctrl *gomock.Controller + recorder *MockbucketDescriberMockRecorder +} + +// MockbucketDescriberMockRecorder is the mock recorder for MockbucketDescriber. +type MockbucketDescriberMockRecorder struct { + mock *MockbucketDescriber +} + +// NewMockbucketDescriber creates a new mock instance. +func NewMockbucketDescriber(ctrl *gomock.Controller) *MockbucketDescriber { + mock := &MockbucketDescriber{ctrl: ctrl} + mock.recorder = &MockbucketDescriberMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockbucketDescriber) EXPECT() *MockbucketDescriberMockRecorder { + return m.recorder +} + +// GetBucketTree mocks base method. +func (m *MockbucketDescriber) GetBucketTree(bucket string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBucketTree", bucket) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBucketTree indicates an expected call of GetBucketTree. +func (mr *MockbucketDescriberMockRecorder) GetBucketTree(bucket interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketTree", reflect.TypeOf((*MockbucketDescriber)(nil).GetBucketTree), bucket) +} + +// MockbucketNameGetter is a mock of bucketNameGetter interface. +type MockbucketNameGetter struct { + ctrl *gomock.Controller + recorder *MockbucketNameGetterMockRecorder +} + +// MockbucketNameGetterMockRecorder is the mock recorder for MockbucketNameGetter. +type MockbucketNameGetterMockRecorder struct { + mock *MockbucketNameGetter +} + +// NewMockbucketNameGetter creates a new mock instance. +func NewMockbucketNameGetter(ctrl *gomock.Controller) *MockbucketNameGetter { + mock := &MockbucketNameGetter{ctrl: ctrl} + mock.recorder = &MockbucketNameGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockbucketNameGetter) EXPECT() *MockbucketNameGetterMockRecorder { + return m.recorder +} + +// BucketName mocks base method. +func (m *MockbucketNameGetter) BucketName(app, env, svc string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BucketName", app, env, svc) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BucketName indicates an expected call of BucketName. +func (mr *MockbucketNameGetterMockRecorder) BucketName(app, env, svc interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BucketName", reflect.TypeOf((*MockbucketNameGetter)(nil).BucketName), app, env, svc) +} diff --git a/internal/pkg/describe/service.go b/internal/pkg/describe/service.go index f68e46f1a7a..f2001e601b1 100644 --- a/internal/pkg/describe/service.go +++ b/internal/pkg/describe/service.go @@ -91,6 +91,14 @@ type cwAlarmDescriber interface { AlarmDescriptions([]string) ([]*cloudwatch.AlarmDescription, error) } +type bucketDescriber interface { + GetBucketTree(bucket string) (string, error) +} + +type bucketNameGetter interface { + BucketName(app, env, svc string) (string, error) +} + type ecsSvcDesc struct { Service string `json:"service"` Type string `json:"type"` diff --git a/internal/pkg/describe/static_site.go b/internal/pkg/describe/static_site.go index 1fe9138e752..bf7d7c83ca9 100644 --- a/internal/pkg/describe/static_site.go +++ b/internal/pkg/describe/static_site.go @@ -7,6 +7,9 @@ import ( "bytes" "encoding/json" "fmt" + awsS3 "github.com/aws/copilot-cli/internal/pkg/aws/s3" + "github.com/aws/copilot-cli/internal/pkg/aws/sessions" + s3 "github.com/aws/copilot-cli/internal/pkg/s3" "strings" "text/tabwriter" @@ -30,6 +33,7 @@ type StaticSiteDescriber struct { store DeployedEnvServicesLister initWkldStackDescriber func(string) (workloadDescriber, error) wkldDescribers map[string]workloadDescriber + initS3Client func(string) (bucketDescriber, bucketNameGetter, error) } // NewStaticSiteDescriber instantiates a static site service describer. @@ -56,6 +60,17 @@ func NewStaticSiteDescriber(opt NewServiceConfig) (*StaticSiteDescriber, error) describer.wkldDescribers[env] = svcDescr return svcDescr, nil } + describer.initS3Client = func(env string) (bucketDescriber, bucketNameGetter, error) { + environment, err := opt.ConfigStore.GetEnvironment(opt.App, env) + if err != nil { + return nil, nil, fmt.Errorf("get environment %s: %w", env, err) + } + sess, err := sessions.ImmutableProvider().FromRole(environment.ManagerRoleARN, environment.Region) + if err != nil { + return nil, nil, err + } + return awsS3.New(sess), s3.New(sess), nil + } return describer, nil } @@ -86,7 +101,12 @@ func (d *StaticSiteDescriber) Describe() (HumanJSONStringer, error) { return nil, fmt.Errorf("list deployed environments for service %q: %w", d.svc, err) } var routes []*WebServiceRoute + var objects []*S3ObjectTree for _, env := range environments { + bucketDescriber, bucketNameDescriber, err := d.initS3Client(env) + if err != nil { + return nil, err + } uri, err := d.URI(env) if err != nil { return nil, fmt.Errorf("retrieve service URI: %w", err) @@ -97,7 +117,22 @@ func (d *StaticSiteDescriber) Describe() (HumanJSONStringer, error) { URL: uri.URI, }) } + bucketName, err := bucketNameDescriber.BucketName(d.app, env, d.svc) + if err != nil { + return nil, fmt.Errorf("get bucket name for %q env: %w", env, err) + } + tree, err := bucketDescriber.GetBucketTree(bucketName) + if err != nil { + return nil, fmt.Errorf("get tree representation of bucket contents: %w", err) + } + if tree != "" { + objects = append(objects, &S3ObjectTree{ + Environment: env, + Tree: tree, + }) + } } + resources := make(map[string][]*stack.Resource) if d.enableResources { for _, env := range environments { @@ -118,6 +153,7 @@ func (d *StaticSiteDescriber) Describe() (HumanJSONStringer, error) { App: d.app, Routes: routes, Resources: resources, + Objects: objects, environments: environments, }, nil @@ -133,12 +169,19 @@ func (d *StaticSiteDescriber) Manifest(env string) ([]byte, error) { return cfn.Manifest() } +// S3ObjectTree contains serialized parameters for an S3 object tree. +type S3ObjectTree struct { + Environment string + Tree string +} + // staticSiteDesc contains serialized parameters for a static site. type staticSiteDesc struct { Service string `json:"service"` Type string `json:"type"` App string `json:"application"` Routes []*WebServiceRoute `json:"routes"` + Objects []*S3ObjectTree `json:"objects,omitempty"` Resources deployedSvcResources `json:"resources,omitempty"` environments []string `json:"-"` @@ -172,6 +215,15 @@ func (w *staticSiteDesc) HumanString() string { fmt.Fprintf(writer, " %s\t%s\n", route.Environment, route.URL) } } + if len(w.Objects) != 0 { + fmt.Fprint(writer, color.Bold.Sprint("\nS3 Bucket Objects\n")) + writer.Flush() + for _, object := range w.Objects { + fmt.Fprintf(writer, "\n %s\t%s\n", "Environment", object.Environment) + fmt.Fprintf(writer, object.Tree) + } + writer.Flush() + } if len(w.Resources) != 0 { fmt.Fprint(writer, color.Bold.Sprint("\nResources\n")) writer.Flush() diff --git a/internal/pkg/describe/static_site_test.go b/internal/pkg/describe/static_site_test.go index 803bc7e3669..d9db6e8f8cf 100644 --- a/internal/pkg/describe/static_site_test.go +++ b/internal/pkg/describe/static_site_test.go @@ -17,6 +17,8 @@ import ( type staticSiteDescriberMocks struct { wkldDescriber *mocks.MockworkloadDescriber store *mocks.MockDeployedEnvServicesLister + awsS3Client *mocks.MockbucketDescriber + s3Client *mocks.MockbucketNameGetter } func TestStaticSiteDescriber_URI(t *testing.T) { @@ -107,6 +109,7 @@ func TestStaticSiteDescriber_Describe(t *testing.T) { mockSvc = "static" ) mockErr := errors.New("some error") + mockBucket := "bucketName" testCases := map[string]struct { shouldOutputResources bool @@ -124,13 +127,15 @@ func TestStaticSiteDescriber_Describe(t *testing.T) { }, wantedError: fmt.Errorf(`list deployed environments for service "static": some error`), }, - "success without resources flag": { + "success without resources flag or objects in bucket": { setupMocks: func(m staticSiteDescriberMocks) { gomock.InOrder( m.store.EXPECT().ListEnvironmentsDeployedTo(mockApp, mockSvc).Return([]string{"test"}, nil), m.wkldDescriber.EXPECT().Outputs().Return(map[string]string{ "CloudFrontDistributionDomainName": "dut843shvcmvn.cloudfront.net", }, nil), + m.s3Client.EXPECT().BucketName(mockApp, mockEnv, mockSvc).Return(mockBucket, nil), + m.awsS3Client.EXPECT().GetBucketTree(mockBucket).Return("", nil), ) }, wantedHuman: `About @@ -155,12 +160,14 @@ Routes m.wkldDescriber.EXPECT().Outputs().Return(map[string]string{ "CloudFrontDistributionDomainName": "dut843shvcmvn.cloudfront.net", }, nil), + m.s3Client.EXPECT().BucketName(mockApp, mockEnv, mockSvc).Return(mockBucket, nil), + m.awsS3Client.EXPECT().GetBucketTree(mockBucket).Return("", nil), m.wkldDescriber.EXPECT().StackResources().Return(nil, mockErr), ) }, wantedError: fmt.Errorf("retrieve service resources: some error"), }, - "success with resources flag": { + "success with resources flag and objects in bucket": { shouldOutputResources: true, setupMocks: func(m staticSiteDescriberMocks) { gomock.InOrder( @@ -168,6 +175,21 @@ Routes m.wkldDescriber.EXPECT().Outputs().Return(map[string]string{ "CloudFrontDistributionDomainName": "dut843shvcmvn.cloudfront.net", }, nil), + m.s3Client.EXPECT().BucketName(mockApp, mockEnv, mockSvc).Return(mockBucket, nil), + m.awsS3Client.EXPECT().GetBucketTree(mockBucket).Return(`. +├── README.md +├── error.html +├── index.html +├── Images +│ ├── firstImage.PNG +│ └── secondImage.PNG +├── css +│ ├── Style.css +│ └── bootstrap.min.css +└── top + └── middle + └── bottom.html +`, nil), m.wkldDescriber.EXPECT().StackResources().Return([]*stack.Resource{ { Type: "AWS::S3::Bucket", @@ -194,13 +216,30 @@ Routes ----------- --- test dut843shvcmvn.cloudfront.net +S3 Bucket Objects + + Environment test +. +├── README.md +├── error.html +├── index.html +├── Images +│ ├── firstImage.PNG +│ └── secondImage.PNG +├── css +│ ├── Style.css +│ └── bootstrap.min.css +└── top + └── middle + └── bottom.html + Resources test AWS::S3::Bucket demo-test-mystatic-bucket-h69vu7y72ga9 AWS::S3::BucketPolicy demo-test-mystatic-BucketPolicyForCloudFront-8AITX9Q7K13R `, - wantedJSON: "{\"service\":\"static\",\"type\":\"Static Site\",\"application\":\"phonetool\",\"routes\":[{\"environment\":\"test\",\"url\":\"dut843shvcmvn.cloudfront.net\"}],\"resources\":{\"test\":[{\"type\":\"AWS::S3::Bucket\",\"physicalID\":\"demo-test-mystatic-bucket-h69vu7y72ga9\",\"logicalID\":\"Bucket\"},{\"type\":\"AWS::S3::BucketPolicy\",\"physicalID\":\"demo-test-mystatic-BucketPolicyForCloudFront-8AITX9Q7K13R\",\"logicalID\":\"BucketPolicy\"}]}}\n", + wantedJSON: "{\"service\":\"static\",\"type\":\"Static Site\",\"application\":\"phonetool\",\"routes\":[{\"environment\":\"test\",\"url\":\"dut843shvcmvn.cloudfront.net\"}],\"objects\":[{\"Environment\":\"test\",\"Tree\":\".\\n├── README.md\\n├── error.html\\n├── index.html\\n├── Images\\n│ ├── firstImage.PNG\\n│ └── secondImage.PNG\\n├── css\\n│ ├── Style.css\\n│ └── bootstrap.min.css\\n└── top\\n └── middle\\n └── bottom.html\\n\"}],\"resources\":{\"test\":[{\"type\":\"AWS::S3::Bucket\",\"physicalID\":\"demo-test-mystatic-bucket-h69vu7y72ga9\",\"logicalID\":\"Bucket\"},{\"type\":\"AWS::S3::BucketPolicy\",\"physicalID\":\"demo-test-mystatic-BucketPolicyForCloudFront-8AITX9Q7K13R\",\"logicalID\":\"BucketPolicy\"}]}}\n", }, } @@ -212,6 +251,8 @@ Resources mocks := staticSiteDescriberMocks{ store: mocks.NewMockDeployedEnvServicesLister(ctrl), wkldDescriber: mocks.NewMockworkloadDescriber(ctrl), + awsS3Client: mocks.NewMockbucketDescriber(ctrl), + s3Client: mocks.NewMockbucketNameGetter(ctrl), } tc.setupMocks(mocks) @@ -223,6 +264,7 @@ Resources store: mocks.store, initWkldStackDescriber: func(string) (workloadDescriber, error) { return mocks.wkldDescriber, nil }, wkldDescribers: make(map[string]workloadDescriber), + initS3Client: func(string) (bucketDescriber, bucketNameGetter, error) { return mocks.awsS3Client, mocks.s3Client, nil }, } // WHEN