Skip to content

Commit 2e6df56

Browse files
authored
feat(cli): enable the new progress tracker for deployments (#1881)
_By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent b469c86 commit 2e6df56

File tree

15 files changed

+481
-353
lines changed

15 files changed

+481
-353
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ gen-mocks: tools
151151
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/describe/mocks/mock_describe.go -source=./internal/pkg/describe/describe.go
152152
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/describe/mocks/mock_stack.go -source=./internal/pkg/describe/stack.go
153153
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/describe/mocks/mock_status.go -source=./internal/pkg/describe/status.go
154-
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/describe/mocks/mock_pipeline.go -source=./internal/pkg/describe/pipeline.go
154+
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/describe/mocks/mock_pipeline_show.go -source=./internal/pkg/describe/pipeline_show.go
155155
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/describe/mocks/mock_pipeline_status.go -source=./internal/pkg/describe/pipeline_status.go
156156
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/ecr/mocks/mock_ecr.go -source=./internal/pkg/aws/ecr/ecr.go
157157
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/ecs/mocks/mock_ecs.go -source=./internal/pkg/aws/ecs/ecs.go

internal/pkg/aws/cloudformation/cloudformation.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,14 @@ func (c *CloudFormation) WaitForCreate(ctx context.Context, stackName string) er
105105

106106
// Update updates an existing CloudFormation with the new configuration.
107107
// If there are no changes for the stack, deletes the empty change set and returns ErrChangeSetEmpty.
108-
func (c *CloudFormation) Update(stack *Stack) error {
108+
func (c *CloudFormation) Update(stack *Stack) (changeSetID string, err error) {
109109
descr, err := c.Describe(stack.Name)
110110
if err != nil {
111-
return err
111+
return "", err
112112
}
113113
status := StackStatus(aws.StringValue(descr.StackStatus))
114114
if status.InProgress() {
115-
return &ErrStackUpdateInProgress{
115+
return "", &ErrStackUpdateInProgress{
116116
Name: stack.Name,
117117
}
118118
}
@@ -121,7 +121,7 @@ func (c *CloudFormation) Update(stack *Stack) error {
121121

122122
// UpdateAndWait calls Update and then blocks until the stack is updated or until the max attempt window expires.
123123
func (c *CloudFormation) UpdateAndWait(stack *Stack) error {
124-
if err := c.Update(stack); err != nil {
124+
if _, err := c.Update(stack); err != nil {
125125
return err
126126
}
127127
return c.WaitForUpdate(context.Background(), stack.Name)
@@ -306,12 +306,15 @@ func (c *CloudFormation) create(stack *Stack) (string, error) {
306306
return cs.name, nil
307307
}
308308

309-
func (c *CloudFormation) update(stack *Stack) error {
309+
func (c *CloudFormation) update(stack *Stack) (string, error) {
310310
cs, err := newUpdateChangeSet(c.client, stack.Name)
311311
if err != nil {
312-
return err
312+
return "", err
313313
}
314-
return cs.createAndExecute(stack.stackConfig)
314+
if err := cs.createAndExecute(stack.stackConfig); err != nil {
315+
return "", err
316+
}
317+
return cs.name, nil
315318
}
316319

317320
func (c *CloudFormation) deleteAndWait(in *cloudformation.DeleteStackInput) error {

internal/pkg/aws/cloudformation/cloudformation_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,15 @@ func TestCloudFormation_Update(t *testing.T) {
293293
}
294294

295295
// WHEN
296-
err := c.Update(mockStack)
296+
id, err := c.Update(mockStack)
297297

298298
// THEN
299-
require.Equal(t, tc.wantedErr, err)
299+
if tc.wantedErr != nil {
300+
require.EqualError(t, err, tc.wantedErr.Error())
301+
} else {
302+
require.NoError(t, err)
303+
require.Equal(t, mockChangeSetID, id)
304+
}
300305
})
301306
}
302307
}

internal/pkg/cli/job_deploy.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cli
66
import (
77
"errors"
88
"fmt"
9+
"os"
910
"strings"
1011

1112
"github.com/aws/copilot-cli/internal/pkg/deploy"
@@ -271,17 +272,10 @@ func (o *deployJobOpts) deployJob(addonsURL string) error {
271272
if err != nil {
272273
return err
273274
}
274-
o.spinner.Start(
275-
fmt.Sprintf("Deploying %s to %s",
276-
fmt.Sprintf("%s:%s", color.HighlightUserInput(o.name), color.HighlightUserInput(o.imageTag)),
277-
color.HighlightUserInput(o.targetEnvironment.Name),
278-
),
279-
)
280-
if err := o.jobCFN.DeployService(conf, awscloudformation.WithRoleARN(o.targetEnvironment.ExecutionRoleARN)); err != nil {
275+
if err := o.jobCFN.DeployService(os.Stderr, conf, awscloudformation.WithRoleARN(o.targetEnvironment.ExecutionRoleARN)); err != nil {
281276
o.spinner.Stop(log.Serrorf("Failed to deploy job.\n\n"))
282277
return fmt.Errorf("deploy job: %w", err)
283278
}
284-
o.spinner.Stop("\n\n")
285279
log.Successf("Deployed %s.\n", color.HighlightUserInput(o.name))
286280
return nil
287281
}

internal/pkg/cli/svc_deploy.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cli
66
import (
77
"errors"
88
"fmt"
9+
"os"
910
"path/filepath"
1011
"strings"
1112

@@ -433,16 +434,10 @@ func (o *deploySvcOpts) deploySvc(addonsURL string) error {
433434
if err != nil {
434435
return err
435436
}
436-
o.spinner.Start(
437-
fmt.Sprintf("Deploying %s to %s.",
438-
fmt.Sprintf("%s:%s", color.HighlightUserInput(o.name), color.HighlightUserInput(o.imageTag)),
439-
color.HighlightUserInput(o.targetEnvironment.Name)))
440437

441-
if err := o.svcCFN.DeployService(conf, awscloudformation.WithRoleARN(o.targetEnvironment.ExecutionRoleARN)); err != nil {
442-
o.spinner.Stop(log.Serrorf("Failed to deploy service.\n\n"))
438+
if err := o.svcCFN.DeployService(os.Stderr, conf, awscloudformation.WithRoleARN(o.targetEnvironment.ExecutionRoleARN)); err != nil {
443439
return fmt.Errorf("deploy service: %w", err)
444440
}
445-
o.spinner.Stop("\n\n")
446441
return nil
447442
}
448443

internal/pkg/deploy/cloudformation/cloudformation.go

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
sdkcloudformation "github.com/aws/aws-sdk-go/service/cloudformation"
1717
"github.com/aws/copilot-cli/internal/pkg/aws/cloudformation"
1818
"github.com/aws/copilot-cli/internal/pkg/aws/cloudformation/stackset"
19+
"github.com/aws/copilot-cli/internal/pkg/aws/ecs"
1920
"github.com/aws/copilot-cli/internal/pkg/stream"
2021
"github.com/aws/copilot-cli/internal/pkg/term/log"
2122
"github.com/aws/copilot-cli/internal/pkg/term/progress"
@@ -27,6 +28,9 @@ import (
2728
const (
2829
// waitForStackTimeout is how long we're willing to wait for a stack to go from in progress to a complete state.
2930
waitForStackTimeout = 1*time.Hour + 30*time.Minute
31+
32+
// CloudFormation resource types.
33+
ecsServiceResourceType = "AWS::ECS::Service"
3034
)
3135

3236
// StackConfiguration represents the set of methods needed to deploy a cloudformation stack.
@@ -37,12 +41,16 @@ type StackConfiguration interface {
3741
Tags() []*sdkcloudformation.Tag
3842
}
3943

44+
type ecsClient interface {
45+
stream.ECSServiceDescriber
46+
}
47+
4048
type cfnClient interface {
4149
// Methods augmented by the aws wrapper struct.
4250
Create(*cloudformation.Stack) (string, error)
4351
CreateAndWait(*cloudformation.Stack) error
4452
WaitForCreate(ctx context.Context, stackName string) error
45-
Update(*cloudformation.Stack) error
53+
Update(*cloudformation.Stack) (string, error)
4654
UpdateAndWait(*cloudformation.Stack) error
4755
WaitForUpdate(ctx context.Context, stackName string) error
4856
Delete(stackName string) error
@@ -72,15 +80,17 @@ type stackSetClient interface {
7280
// CloudFormation wraps the CloudFormationAPI interface
7381
type CloudFormation struct {
7482
cfnClient cfnClient
83+
ecsClient ecsClient
7584
regionalClient func(region string) cfnClient
7685
appStackSet stackSetClient
7786
box packd.Box
7887
}
7988

8089
// New returns a configured CloudFormation client.
8190
func New(sess *session.Session) CloudFormation {
82-
return CloudFormation{
91+
client := CloudFormation{
8392
cfnClient: cloudformation.New(sess),
93+
ecsClient: ecs.New(sess),
8494
regionalClient: func(region string) cfnClient {
8595
return cloudformation.New(sess.Copy(&aws.Config{
8696
Region: aws.String(region),
@@ -89,11 +99,12 @@ func New(sess *session.Session) CloudFormation {
8999
appStackSet: stackset.New(sess),
90100
box: templates.Box(),
91101
}
102+
return client
92103
}
93104

94105
// errorEvents returns the list of status reasons of failed resource events
95-
func (cf CloudFormation) errorEvents(conf StackConfiguration) ([]string, error) {
96-
events, err := cf.cfnClient.ErrorEvents(conf.StackName())
106+
func (cf CloudFormation) errorEvents(stackName string) ([]string, error) {
107+
events, err := cf.cfnClient.ErrorEvents(stackName)
97108
if err != nil {
98109
return nil, err
99110
}
@@ -112,7 +123,44 @@ type renderStackChangesInput struct {
112123
createChangeSet func() (string, error)
113124
}
114125

115-
func (cf CloudFormation) renderStackChanges(in renderStackChangesInput) error {
126+
func (cf CloudFormation) newRenderWorkloadInput(w progress.FileWriter, stack *cloudformation.Stack) *renderStackChangesInput {
127+
in := &renderStackChangesInput{
128+
w: w,
129+
stackName: stack.Name,
130+
stackDescription: fmt.Sprintf("Creating the infrastructure for stack %s", stack.Name),
131+
}
132+
in.createChangeSet = func() (changeSetID string, err error) {
133+
spinner := progress.NewSpinner(w)
134+
label := fmt.Sprintf("Proposing infrastructure changes for stack %s", stack.Name)
135+
spinner.Start(label)
136+
changeSetID, err = cf.cfnClient.Create(stack)
137+
if err == nil {
138+
// Successfully created the change set to create the stack.
139+
spinner.Stop(log.Ssuccessf("%s\n", label))
140+
return changeSetID, nil
141+
}
142+
143+
var errAlreadyExists *cloudformation.ErrStackAlreadyExists
144+
if !errors.As(err, &errAlreadyExists) {
145+
// Unexpected error trying to create a stack.
146+
spinner.Stop(log.Serrorf("%s\n", label))
147+
return "", cf.handleStackError(stack.Name, err)
148+
}
149+
150+
// We have to create an update stack change set instead.
151+
in.stackDescription = fmt.Sprintf("Updating the infrastructure for stack %s", stack.Name)
152+
changeSetID, err = cf.cfnClient.Update(stack)
153+
if err != nil {
154+
spinner.Stop(log.Serrorf("%s\n", label))
155+
return "", cf.handleStackError(stack.Name, err)
156+
}
157+
spinner.Stop(log.Ssuccessf("%s\n", label))
158+
return changeSetID, nil
159+
}
160+
return in
161+
}
162+
163+
func (cf CloudFormation) renderStackChanges(in *renderStackChangesInput) error {
116164
changeSetID, err := in.createChangeSet()
117165
if err != nil {
118166
return err
@@ -190,6 +238,12 @@ func (cf CloudFormation) changeRenderers(in changeRenderersInput) ([]progress.Re
190238
}
191239
var renderer progress.Renderer
192240
switch {
241+
case aws.StringValue(change.ResourceChange.ResourceType) == ecsServiceResourceType:
242+
renderer = progress.ListeningECSServiceResourceRenderer(in.stackStreamer, cf.ecsClient, logicalID, description, progress.ECSServiceRendererOpts{
243+
Group: in.g,
244+
Ctx: in.ctx,
245+
RenderOpts: in.opts,
246+
})
193247
case change.ResourceChange.ChangeSetId != nil:
194248
// The resource change is a nested stack.
195249
changeSetID := aws.StringValue(change.ResourceChange.ChangeSetId)

0 commit comments

Comments
 (0)