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 svc status output for Static Site service type #4985

Merged
merged 11 commits into from
Jun 20, 2023
10 changes: 5 additions & 5 deletions internal/pkg/aws/cloudwatch/cloudwatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ type AlarmStatus struct {
// AlarmDescription contains CloudWatch alarm config.
// Also available: MetricName, ComparisonOperator, DatapointsToAlarm, EvaluationPeriods, Threshold, Unit.
type AlarmDescription struct {
Name string `json:"name"`
Description string `json:"description"`
Environment string `json:"environment"`
Name string `json:"name"`
Description string `json:"description"`
Environment string `json:"environment"`
}

// New returns a CloudWatch struct configured against the input session.
Expand Down Expand Up @@ -181,8 +181,8 @@ func (cw *CloudWatch) metricAlarmsDescriptions(alarms []*cloudwatch.MetricAlarm)
continue
}
alarmDescriptionsList = append(alarmDescriptionsList, &AlarmDescription{
Name: aws.StringValue(alarm.AlarmName),
Description: aws.StringValue(alarm.AlarmDescription),
Name: aws.StringValue(alarm.AlarmName),
Description: aws.StringValue(alarm.AlarmDescription),
})
}
return alarmDescriptionsList
Expand Down
87 changes: 58 additions & 29 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/dustin/go-humanize"
"github.com/xlab/treeprint"
"io"
"mime"
Expand Down Expand Up @@ -194,68 +195,96 @@ func FormatARN(partition, location string) string {
return fmt.Sprintf("arn:%s:s3:::%s", partition, location)
}

func (s *S3) bucketExists(bucket string) (bool, error) {
input := &s3.HeadBucketInput{
Bucket: aws.String(bucket),
// BucketTree creates an ASCII tree representing the folder structure of a bucket's objects.
func (s *S3) BucketTree(bucket string) (string, error) {
outputs, err := s.listObjects(bucket, "/")
if err != nil || outputs == nil {
return "", nil
}
_, err := s.s3Client.HeadBucket(input)
var contents []*s3.Object
var prefixes []*s3.CommonPrefix
for _, output := range outputs {
contents = append(contents, output.Contents...)
prefixes = append(prefixes, output.CommonPrefixes...)
}
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
}

// BucketSizeAndCount returns the total size and number of objects in an S3 bucket.
func (s *S3) BucketSizeAndCount(bucket string) (string, int, error) {
outputs, err := s.listObjects(bucket, "")
if err != nil {
huanjani marked this conversation as resolved.
Show resolved Hide resolved
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == errCodeNotFound {
return false, nil
return "", 0, err
}
if outputs == nil {
return "", 0, nil
}
var size int64
var count int
for _, output := range outputs {
for _, object := range output.Contents {
size = size + aws.Int64Value(object.Size)
count = count + 1
huanjani marked this conversation as resolved.
Show resolved Hide resolved
}
return false, err
}

return true, nil
return humanize.Bytes(uint64(size)), count, 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) {
func (s *S3) listObjects(bucket, delimiter string) ([]s3.ListObjectsV2Output, error) {
exists, err := s.bucketExists(bucket)
if err != nil {
huanjani marked this conversation as resolved.
Show resolved Hide resolved
return "", err
return nil, err
}
if !exists {
return "", nil
return nil, nil
}

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

tree := treeprint.New()
// Add top-level files.
for _, object := range contents {
tree.AddNode(aws.StringValue(object.Key))
func (s *S3) bucketExists(bucket string) (bool, error) {
input := &s3.HeadBucketInput{
Bucket: aws.String(bucket),
}
// Recursively add folders and their children.
if err := s.addNodes(tree, prefixes, bucket); err != nil {
return "", err
_, err := s.s3Client.HeadBucket(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == errCodeNotFound {
return false, nil
}
huanjani marked this conversation as resolved.
Show resolved Hide resolved
return false, err
}
return tree.String(), nil
return true, 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 {
Expand Down
167 changes: 165 additions & 2 deletions internal/pkg/aws/s3/s3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ func TestS3_FormatARN(t *testing.T) {
}
}

func TestS3_GetBucketTree(t *testing.T) {
func TestS3_BucketTree(t *testing.T) {
type s3Mocks struct {
s3API *mocks.Mocks3API
s3ManagerAPI *mocks.Mocks3ManagerAPI
Expand Down Expand Up @@ -689,7 +689,7 @@ func TestS3_GetBucketTree(t *testing.T) {
}
tc.setupMocks(s3mocks)

gotTree, gotErr := service.GetBucketTree(aws.StringValue(mockBucket))
gotTree, gotErr := service.BucketTree(aws.StringValue(mockBucket))
if tc.wantErr != nil {
require.EqualError(t, gotErr, tc.wantErr.Error())
return
Expand All @@ -700,3 +700,166 @@ func TestS3_GetBucketTree(t *testing.T) {

}
}

func TestS3_BucketSizeAndCount(t *testing.T) {
type s3Mocks struct {
s3API *mocks.Mocks3API
s3ManagerAPI *mocks.Mocks3ManagerAPI
}
mockBucket := aws.String("bucketName")
nonexistentError := awserr.New(errCodeNotFound, "msg", errors.New("some error"))
mockContinuationToken := "next"

resp := s3.ListObjectsV2Output{
Contents: []*s3.Object{
{
Key: aws.String("README.md"),
Size: aws.Int64(111111),
},
{
Key: aws.String("error.html"),
Size: aws.Int64(222222),
},
{
Key: aws.String("index.html"),
Size: aws.Int64(333333),
},
},
KeyCount: aws.Int64(14),
MaxKeys: aws.Int64(1000),
Name: mockBucket,
}

testCases := map[string]struct {
setupMocks func(mocks s3Mocks)

wantSize string
wantCount int
wantErr error
}{
"should return correct size and count": {
setupMocks: func(m s3Mocks) {
m.s3API.EXPECT().HeadBucket(&s3.HeadBucketInput{Bucket: mockBucket}).Return(&s3.HeadBucketOutput{}, nil)
m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{
Bucket: mockBucket,
Delimiter: aws.String(""),
ContinuationToken: nil,
Prefix: nil,
}).Return(&resp, nil)
},
wantSize: "667 kB",
wantCount: 3,
},
"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,
Delimiter: aws.String(""),
ContinuationToken: nil,
Prefix: nil,
}).Return(
&s3.ListObjectsV2Output{
Contents: []*s3.Object{
{
Key: aws.String("README.md"),
Size: aws.Int64(123),
},
},
KeyCount: aws.Int64(14),
MaxKeys: aws.Int64(1000),
Name: mockBucket,
NextContinuationToken: &mockContinuationToken,
}, nil)
m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{
Bucket: mockBucket,
Delimiter: aws.String(""),
ContinuationToken: &mockContinuationToken,
Prefix: nil,
}).Return(
&s3.ListObjectsV2Output{
Contents: []*s3.Object{
{
Key: aws.String("READMETOO.md"),
Size: aws.Int64(321),
},
},
KeyCount: aws.Int64(14),
MaxKeys: aws.Int64(1000),
Name: mockBucket,
NextContinuationToken: nil,
}, nil)
},
wantSize: "444 B",
wantCount: 2,
},
"empty bucket": {
setupMocks: func(m s3Mocks) {
m.s3API.EXPECT().HeadBucket(&s3.HeadBucketInput{Bucket: mockBucket}).Return(&s3.HeadBucketOutput{}, nil)
m.s3API.EXPECT().ListObjectsV2(&s3.ListObjectsV2Input{
Bucket: mockBucket,
Delimiter: aws.String(""),
ContinuationToken: nil,
Prefix: nil,
}).Return(&s3.ListObjectsV2Output{
Contents: nil,
Name: mockBucket,
}, nil)
},
wantSize: "0 B",
wantCount: 0,
},
"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,
Delimiter: aws.String(""),
ContinuationToken: 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)

gotSize, gotCount, gotErr := service.BucketSizeAndCount(aws.StringValue(mockBucket))
if tc.wantErr != nil {
require.EqualError(t, gotErr, tc.wantErr.Error())
return
}
require.NoError(t, gotErr)
require.Equal(t, tc.wantSize, gotSize)
require.Equal(t, tc.wantCount, gotCount)
})

}
}
15 changes: 13 additions & 2 deletions internal/pkg/cli/svc_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,18 @@ func newSvcStatusOpts(vars svcStatusVars) (*svcStatusOpts, error) {
ConfigStore: configStore,
})
if err != nil {
return fmt.Errorf("creating status describer for apprunner service %s in application %s: %w", o.svcName, o.appName, err)
return fmt.Errorf("create status describer for App Runner service %s in application %s: %w", o.svcName, o.appName, err)
}
o.statusDescriber = d
} else if wkld.Type == manifestinfo.StaticSiteType {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe consider using a switch here?

d, err := describe.NewStaticSiteStatusDescriber(&describe.NewServiceStatusConfig{
App: o.appName,
Env: o.envName,
Svc: o.svcName,
ConfigStore: configStore,
})
if err != nil {
return fmt.Errorf("create status describer for Static Site service %s in application %s: %w", o.svcName, o.appName, err)
}
o.statusDescriber = d
} else {
Expand All @@ -85,7 +96,7 @@ func newSvcStatusOpts(vars svcStatusVars) (*svcStatusOpts, error) {
ConfigStore: configStore,
})
if err != nil {
return fmt.Errorf("creating status describer for service %s in application %s: %w", o.svcName, o.appName, err)
return fmt.Errorf("create status describer for service %s in application %s: %w", o.svcName, o.appName, err)
}
o.statusDescriber = d
}
Expand Down
Loading