Repository: aws/amazon-ecs-cli-v2
Pull request #359 (2019-11-15 02:43:35)

Restructure deploy logic for env for consistency
<!-- Provide summary of changes -->

<!-- Issue number, if available. E.g. "Fixes #31", "Addresses #42, 77" -->

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.



Patch:
diff --git a/internal/pkg/deploy/cloudformation/cloudformation.go b/internal/pkg/deploy/cloudformation/cloudformation.go
index 556b52e2..e1158494 100644
--- a/internal/pkg/deploy/cloudformation/cloudformation.go
+++ b/internal/pkg/deploy/cloudformation/cloudformation.go
@@ -67,55 +67,6 @@ func New(sess *session.Session) CloudFormation {
 	}
 }
 
-// DeployEnvironment creates the CloudFormation stack for an environment by creating and executing a change set.
-//
-// If the deployment succeeds, returns nil.
-// If the stack already exists, returns a ErrStackAlreadyExists.
-// If the change set to create the stack cannot be executed, returns a ErrNotExecutableChangeSet.
-// Otherwise, returns a wrapped error.
-func (cf CloudFormation) DeployEnvironment(env *deploy.CreateEnvironmentInput) error {
-	return cf.deploy(newEnvStackConfig(env, cf.box))
-}
-
-// StreamEnvironmentCreation streams resource update events while a deployment is taking place.
-// Once the CloudFormation stack operation halts, the update channel is closed and a
-// CreateEnvironmentResponse is sent to the second channel.
-func (cf CloudFormation) StreamEnvironmentCreation(env *deploy.CreateEnvironmentInput) (<-chan []deploy.ResourceEvent, <-chan deploy.CreateEnvironmentResponse) {
-	done := make(chan struct{})
-	events := make(chan []deploy.ResourceEvent)
-	resp := make(chan deploy.CreateEnvironmentResponse, 1)
-
-	stack := newEnvStackConfig(env, cf.box)
-	go cf.streamResourceEvents(done, events, stack.StackName())
-	go cf.streamEnvironmentResponse(done, resp, stack)
-	return events, resp
-}
-
-func (cf CloudFormation) deploy(stackConfig stackConfiguration) error {
-	template, err := stackConfig.Template()
-	if err != nil {
-		return fmt.Errorf("template creation: %w", err)
-	}
-
-	in, err := createChangeSetInput(stackConfig.StackName(), template, withCreateChangeSetType(), withTags(stackConfig.Tags()), withParameters(stackConfig.Parameters()))
-	if err != nil {
-		return err
-	}
-
-	if err := cf.deployChangeSet(in); err != nil {
-		if stackExists(err) {
-			// Explicitly return a StackAlreadyExists error for the caller to decide if they want to ignore the
-			// operation or fail the program.
-			return &ErrStackAlreadyExists{
-				stackName: stackConfig.StackName(),
-				parentErr: err,
-			}
-		}
-		return err
-	}
-	return nil
-}
-
 // streamResourceEvents sends a list of ResourceEvent every 3 seconds to the events channel.
 // The events channel is closed only when the done channel receives a message.
 // If an error occurs while describing stack events, it is ignored so that the stream is not interrupted.
@@ -153,22 +104,6 @@ func (cf CloudFormation) streamResourceEvents(done <-chan struct{}, events chan
 	}
 }
 
-// streamEnvironmentResponse sends a CreateEnvironmentResponse to the response channel once the stack creation halts.
-// The done channel is closed once this method exits to notify other streams that they should stop working.
-func (cf CloudFormation) streamEnvironmentResponse(done chan struct{}, resp chan deploy.CreateEnvironmentResponse, stack *envStackConfig) {
-	defer close(done)
-	deployed, err := cf.waitForStackCreation(stack)
-	if err != nil {
-		resp <- deploy.CreateEnvironmentResponse{Err: err}
-		return
-	}
-	env, err := stack.ToEnv(deployed)
-	resp <- deploy.CreateEnvironmentResponse{
-		Env: env,
-		Err: err,
-	}
-}
-
 // describeStackEvents gathers all stack resource events in **chronological** order.
 // If an error occurs while collecting events, returns a wrapped error.
 func (cf CloudFormation) describeStackEvents(stackName string) ([]*cloudformation.StackEvent, error) {
@@ -231,6 +166,31 @@ func (cf CloudFormation) describeStackWithClient(describeStackInput *cloudformat
 	return describeStackOutput.Stacks[0], nil
 }
 
+func (cf CloudFormation) deploy(stackConfig stackConfiguration) error {
+	template, err := stackConfig.Template()
+	if err != nil {
+		return fmt.Errorf("template creation: %w", err)
+	}
+
+	in, err := createChangeSetInput(stackConfig.StackName(), template, withCreateChangeSetType(), withTags(stackConfig.Tags()), withParameters(stackConfig.Parameters()))
+	if err != nil {
+		return err
+	}
+
+	if err := cf.deployChangeSet(in); err != nil {
+		if stackExists(err) {
+			// Explicitly return a StackAlreadyExists error for the caller to decide if they want to ignore the
+			// operation or fail the program.
+			return &ErrStackAlreadyExists{
+				stackName: stackConfig.StackName(),
+				parentErr: err,
+			}
+		}
+		return err
+	}
+	return nil
+}
+
 func (cf CloudFormation) deployChangeSet(in *cloudformation.CreateChangeSetInput) error {
 	set, err := cf.createChangeSet(in)
 	if err != nil {
diff --git a/internal/pkg/deploy/cloudformation/env.go b/internal/pkg/deploy/cloudformation/env.go
index c84337f2..077d961d 100644
--- a/internal/pkg/deploy/cloudformation/env.go
+++ b/internal/pkg/deploy/cloudformation/env.go
@@ -1,127 +1,50 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package cloudformation provides functionality to deploy archer resources with AWS CloudFormation.
 package cloudformation
 
 import (
-	"fmt"
-	"strconv"
-
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/arn"
-	"github.com/aws/aws-sdk-go/service/cloudformation"
-
-	"github.com/gobuffalo/packd"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack"
 )
 
-// envStackConfig is for providing all the values to set up an
-// environment stack and to interpret the outputs from it.
-type envStackConfig struct {
-	*deploy.CreateEnvironmentInput
-	box packd.Box
-}
-
-const (
-	// EnvParamIncludeLBKey is the CF Param Key Name for whether to include a LB
-	EnvParamIncludeLBKey = "IncludePublicLoadBalancer"
-	// EnvParamProjectNameKey is the CF Param Key Name for providing the project name
-	EnvParamProjectNameKey = "ProjectName"
-	// EnvParamEnvNameKey is the CF Param Key Name for providing the environment name
-	EnvParamEnvNameKey            = "EnvironmentName"
-	envParamToolsAccountPrincipal = "ToolsAccountPrincipalARN"
-	// EnvOutputECRKey is the CF Output Key Name for the ECR Repo Name
-	EnvOutputECRKey = "ECRRepositoryName"
-
-	ecrURLFormatString = "%s.dkr.ecr.%s.amazonaws.com/%s"
-
-	envTemplatePath = "environment/cf.yml"
-)
-
-// newEnvStackConfig sets up a struct which can provide values to CloudFormation for
-// spinning up an environment.
-func newEnvStackConfig(input *deploy.CreateEnvironmentInput, box packd.Box) *envStackConfig {
-	return &envStackConfig{
-		CreateEnvironmentInput: input,
-		box:                    box,
-	}
-}
-
-// Template returns the environment CloudFormation template.
-func (e *envStackConfig) Template() (string, error) {
-	template, err := e.box.FindString(envTemplatePath)
-	if err != nil {
-		return "", &ErrTemplateNotFound{templateLocation: envTemplatePath, parentErr: err}
-	}
-	return template, nil
-}
-
-// Parameters returns the parameters to be passed into a environment CloudFormation template.
-func (e *envStackConfig) Parameters() []*cloudformation.Parameter {
-	return []*cloudformation.Parameter{
-		{
-			ParameterKey:   aws.String(EnvParamIncludeLBKey),
-			ParameterValue: aws.String(strconv.FormatBool(e.PublicLoadBalancer)),
-		},
-		{
-			ParameterKey:   aws.String(EnvParamProjectNameKey),
-			ParameterValue: aws.String(e.Project),
-		},
-		{
-			ParameterKey:   aws.String(EnvParamEnvNameKey),
-			ParameterValue: aws.String(e.Name),
-		},
-		{
-			ParameterKey:   aws.String(envParamToolsAccountPrincipal),
-			ParameterValue: aws.String(e.ToolsAccountPrincipalARN),
-		},
-	}
-}
-
-// Tags returns the tags that should be applied to the environment CloudFormation stack.
-func (e *envStackConfig) Tags() []*cloudformation.Tag {
-	return []*cloudformation.Tag{
-		{
-			Key:   aws.String(projectTagKey),
-			Value: aws.String(e.Project),
-		},
-		{
-			Key:   aws.String(envTagKey),
-			Value: aws.String(e.Name),
-		},
-	}
+// DeployEnvironment creates the CloudFormation stack for an environment by creating and executing a change set.
+//
+// If the deployment succeeds, returns nil.
+// If the stack already exists, returns a ErrStackAlreadyExists.
+// If the change set to create the stack cannot be executed, returns a ErrNotExecutableChangeSet.
+// Otherwise, returns a wrapped error.
+func (cf CloudFormation) DeployEnvironment(env *deploy.CreateEnvironmentInput) error {
+	return cf.deploy(stack.NewEnvStackConfig(env, cf.box))
 }
 
-// StackName returns the name of the CloudFormation stack (based on the project and env names).
-func (e *envStackConfig) StackName() string {
-	return fmt.Sprintf("%s-%s", e.Project, e.Name)
+// StreamEnvironmentCreation streams resource update events while a deployment is taking place.
+// Once the CloudFormation stack operation halts, the update channel is closed and a
+// CreateEnvironmentResponse is sent to the second channel.
+func (cf CloudFormation) StreamEnvironmentCreation(env *deploy.CreateEnvironmentInput) (<-chan []deploy.ResourceEvent, <-chan deploy.CreateEnvironmentResponse) {
+	done := make(chan struct{})
+	events := make(chan []deploy.ResourceEvent)
+	resp := make(chan deploy.CreateEnvironmentResponse, 1)
+
+	stack := stack.NewEnvStackConfig(env, cf.box)
+	go cf.streamResourceEvents(done, events, stack.StackName())
+	go cf.streamEnvironmentResponse(done, resp, stack)
+	return events, resp
 }
 
-// ToEnv inspects an environment cloudformation stack and constructs an environment
-// struct out of it (including resources like ECR Repo)
-func (e *envStackConfig) ToEnv(stack *cloudformation.Stack) (*archer.Environment, error) {
-	stackARN, err := arn.Parse(*stack.StackId)
+// streamEnvironmentResponse sends a CreateEnvironmentResponse to the response channel once the stack creation halts.
+// The done channel is closed once this method exits to notify other streams that they should stop working.
+func (cf CloudFormation) streamEnvironmentResponse(done chan struct{}, resp chan deploy.CreateEnvironmentResponse, stack *stack.EnvStackConfig) {
+	defer close(done)
+	deployed, err := cf.waitForStackCreation(stack)
 	if err != nil {
-		return nil, fmt.Errorf("couldn't extract region and account from stack ID %s: %w", *stack.StackId, err)
+		resp <- deploy.CreateEnvironmentResponse{Err: err}
+		return
 	}
-
-	stackOutputs := make(map[string]string)
-	for _, output := range stack.Outputs {
-		stackOutputs[*output.OutputKey] = *output.OutputValue
-	}
-
-	createdEnv := archer.Environment{
-		Name:      e.Name,
-		Project:   e.Project,
-		Prod:      e.Prod,
-		Region:    stackARN.Region,
-		AccountID: stackARN.AccountID,
+	env, err := stack.ToEnv(deployed)
+	resp <- deploy.CreateEnvironmentResponse{
+		Env: env,
+		Err: err,
 	}
-
-	if stackOutputs[EnvOutputECRKey] != "" {
-		createdEnv.RegistryURL = fmt.Sprintf(ecrURLFormatString,
-			createdEnv.AccountID,
-			createdEnv.Region,
-			stackOutputs[EnvOutputECRKey])
-	}
-
-	return &createdEnv, nil
 }
diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go
new file mode 100644
index 00000000..8f0c6d48
--- /dev/null
+++ b/internal/pkg/deploy/cloudformation/stack/env.go
@@ -0,0 +1,123 @@
+package stack
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/arn"
+	"github.com/aws/aws-sdk-go/service/cloudformation"
+
+	"github.com/gobuffalo/packd"
+)
+
+// EnvStackConfig is for providing all the values to set up an
+// environment stack and to interpret the outputs from it.
+type EnvStackConfig struct {
+	*deploy.CreateEnvironmentInput
+	box packd.Box
+}
+
+const (
+	// EnvParamIncludeLBKey is the CF Param Key Name for whether to include a LB
+	EnvParamIncludeLBKey = "IncludePublicLoadBalancer"
+
+	// EnvParamProjectNameKey is the CF Param Key Name for providing the project name
+	EnvParamProjectNameKey = "ProjectName"
+
+	// EnvParamEnvNameKey is the CF Param Key Name for providing the environment name
+	EnvParamEnvNameKey = "EnvironmentName"
+
+	// EnvTemplatePath is the path where the cloudformation for the environment is written
+	EnvTemplatePath = "environment/cf.yml"
+
+	// EnvOutputECRKey is the CF Output Key Name for the ECR Repo Name
+	EnvOutputECRKey = "ECRRepositoryName"
+
+	envParamToolsAccountPrincipal = "ToolsAccountPrincipalARN"
+)
+
+// newEnvStackConfig sets up a struct which can provide values to CloudFormation for
+// spinning up an environment.
+func NewEnvStackConfig(input *deploy.CreateEnvironmentInput, box packd.Box) *EnvStackConfig {
+	return &EnvStackConfig{
+		CreateEnvironmentInput: input,
+		box:                    box,
+	}
+}
+
+// Template returns the environment CloudFormation template.
+func (e *EnvStackConfig) Template() (string, error) {
+	template, err := e.box.FindString(EnvTemplatePath)
+	if err != nil {
+		return "", &ErrTemplateNotFound{templateLocation: EnvTemplatePath, parentErr: err}
+	}
+	return template, nil
+}
+
+// Parameters returns the parameters to be passed into a environment CloudFormation template.
+func (e *EnvStackConfig) Parameters() []*cloudformation.Parameter {
+	return []*cloudformation.Parameter{
+		{
+			ParameterKey:   aws.String(EnvParamIncludeLBKey),
+			ParameterValue: aws.String(strconv.FormatBool(e.PublicLoadBalancer)),
+		},
+		{
+			ParameterKey:   aws.String(EnvParamProjectNameKey),
+			ParameterValue: aws.String(e.Project),
+		},
+		{
+			ParameterKey:   aws.String(EnvParamEnvNameKey),
+			ParameterValue: aws.String(e.Name),
+		},
+		{
+			ParameterKey:   aws.String(envParamToolsAccountPrincipal),
+			ParameterValue: aws.String(e.ToolsAccountPrincipalARN),
+		},
+	}
+}
+
+// Tags returns the tags that should be applied to the environment CloudFormation stack.
+func (e *EnvStackConfig) Tags() []*cloudformation.Tag {
+	return []*cloudformation.Tag{
+		{
+			Key:   aws.String(projectTagKey),
+			Value: aws.String(e.Project),
+		},
+		{
+			Key:   aws.String(envTagKey),
+			Value: aws.String(e.Name),
+		},
+	}
+}
+
+// StackName returns the name of the CloudFormation stack (based on the project and env names).
+func (e *EnvStackConfig) StackName() string {
+	return fmt.Sprintf("%s-%s", e.Project, e.Name)
+}
+
+// ToEnv inspects an environment cloudformation stack and constructs an environment
+// struct out of it (including resources like ECR Repo)
+func (e *EnvStackConfig) ToEnv(stack *cloudformation.Stack) (*archer.Environment, error) {
+	stackARN, err := arn.Parse(*stack.StackId)
+	if err != nil {
+		return nil, fmt.Errorf("couldn't extract region and account from stack ID %s: %w", *stack.StackId, err)
+	}
+
+	stackOutputs := make(map[string]string)
+	for _, output := range stack.Outputs {
+		stackOutputs[*output.OutputKey] = *output.OutputValue
+	}
+
+	createdEnv := archer.Environment{
+		Name:      e.Name,
+		Project:   e.Project,
+		Prod:      e.Prod,
+		Region:    stackARN.Region,
+		AccountID: stackARN.AccountID,
+	}
+
+	return &createdEnv, nil
+}


Test patch:
diff --git a/internal/pkg/deploy/cloudformation/cloudformation_test.go b/internal/pkg/deploy/cloudformation/cloudformation_test.go
index 90b4211a..3ab11131 100644
--- a/internal/pkg/deploy/cloudformation/cloudformation_test.go
+++ b/internal/pkg/deploy/cloudformation/cloudformation_test.go
@@ -9,8 +9,7 @@ import (
 	"regexp"
 	"testing"
 
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/service/cloudformation"
@@ -404,161 +403,6 @@ func TestWaitForStackCreation(t *testing.T) {
 	}
 }
 
-func TestStreamEnvironmentCreation(t *testing.T) {
-	testCases := map[string]struct {
-		in *deploy.CreateEnvironmentInput
-
-		mockWaitUntilStackCreateComplete func(t *testing.T, in *cloudformation.DescribeStacksInput) error
-		mockDescribeStacks               func(t *testing.T, in *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error)
-		mockDescribeStackEvents          func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error)
-
-		wantedEvents []deploy.ResourceEvent
-		wantedResult deploy.CreateEnvironmentResponse
-	}{
-		"error while creating stack": {
-			in: &deploy.CreateEnvironmentInput{
-				Project: "phonetool",
-				Name:    "test",
-			},
-
-			mockWaitUntilStackCreateComplete: func(t *testing.T, in *cloudformation.DescribeStacksInput) error {
-				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
-				return errors.New("wait until error")
-			},
-			mockDescribeStackEvents: func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error) {
-				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
-				return &cloudformation.DescribeStackEventsOutput{
-					StackEvents: []*cloudformation.StackEvent{
-						{
-							LogicalResourceId:    aws.String("vpc"),
-							ResourceType:         aws.String("AWS::EC2::VPC"),
-							ResourceStatus:       aws.String(cloudformation.ResourceStatusCreateInProgress),
-							ResourceStatusReason: aws.String("create initiated"),
-						},
-					},
-				}, nil
-			},
-
-			wantedEvents: []deploy.ResourceEvent{
-				{
-					Resource: deploy.Resource{
-						LogicalName: "vpc",
-						Type:        "AWS::EC2::VPC",
-					},
-					Status:       cloudformation.ResourceStatusCreateInProgress,
-					StatusReason: "create initiated",
-				},
-			},
-			wantedResult: deploy.CreateEnvironmentResponse{
-				Env: nil,
-				Err: errors.New("failed to create stack phonetool-test: wait until error"),
-			},
-		},
-		"swallows error while describing stack": {
-			in: &deploy.CreateEnvironmentInput{
-				Project: "phonetool",
-				Name:    "test",
-			},
-
-			mockWaitUntilStackCreateComplete: func(t *testing.T, in *cloudformation.DescribeStacksInput) error {
-				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
-				return errors.New("wait until error")
-			},
-			mockDescribeStackEvents: func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error) {
-				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
-				return nil, errors.New("describe stack events error")
-			},
-
-			wantedEvents: nil,
-			wantedResult: deploy.CreateEnvironmentResponse{
-				Env: nil,
-				Err: errors.New("failed to create stack phonetool-test: wait until error"),
-			},
-		},
-		"sends an environment on success": {
-			in: &deploy.CreateEnvironmentInput{
-				Project: "phonetool",
-				Name:    "test",
-			},
-
-			mockWaitUntilStackCreateComplete: func(t *testing.T, in *cloudformation.DescribeStacksInput) error {
-				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
-				return nil
-			},
-			mockDescribeStacks: func(t *testing.T, in *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error) {
-				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
-				return &cloudformation.DescribeStacksOutput{
-					Stacks: []*cloudformation.Stack{
-						{
-							StackId: aws.String(fmt.Sprintf("arn:aws:cloudformation:eu-west-3:902697171733:stack/%s", "phonetool-test")),
-						},
-					},
-				}, nil
-			},
-			mockDescribeStackEvents: func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error) {
-				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
-				return &cloudformation.DescribeStackEventsOutput{
-					StackEvents: []*cloudformation.StackEvent{
-						{
-							LogicalResourceId:    aws.String("vpc"),
-							ResourceType:         aws.String("AWS::EC2::VPC"),
-							ResourceStatus:       aws.String(cloudformation.ResourceStatusCreateInProgress),
-							ResourceStatusReason: aws.String("create initiated"),
-						},
-					},
-				}, nil
-			},
-
-			wantedEvents: []deploy.ResourceEvent{
-				{
-					Resource: deploy.Resource{
-						LogicalName: "vpc",
-						Type:        "AWS::EC2::VPC",
-					},
-					Status:       cloudformation.ResourceStatusCreateInProgress,
-					StatusReason: "create initiated",
-				},
-			},
-			wantedResult: deploy.CreateEnvironmentResponse{
-				Env: &archer.Environment{
-					Project:   "phonetool",
-					Name:      "test",
-					Region:    "eu-west-3",
-					AccountID: "902697171733",
-				},
-				Err: nil,
-			},
-		},
-	}
-
-	for name, tc := range testCases {
-		t.Run(name, func(t *testing.T) {
-			// GIVEN
-			cf := CloudFormation{
-				client: &mockCloudFormation{
-					t:                                t,
-					mockDescribeStackEvents:          tc.mockDescribeStackEvents,
-					mockDescribeStacks:               tc.mockDescribeStacks,
-					mockWaitUntilStackCreateComplete: tc.mockWaitUntilStackCreateComplete,
-				},
-				box: emptyEnvBox(),
-			}
-
-			// WHEN
-			events, resp := cf.StreamEnvironmentCreation(tc.in)
-
-			// THEN
-			require.Equal(t, tc.wantedEvents, <-events)
-			got := <-resp
-			if tc.wantedResult.Err != nil {
-				require.EqualError(t, got.Err, tc.wantedResult.Err.Error(), "expected %v got %v", tc.wantedResult.Err, got.Err)
-			} else {
-				require.Equal(t, tc.wantedResult, got)
-			}
-		})
-	}
-}
-
 func getMockWaitStackCreateCFClient(t *testing.T, stackName string, shouldThrowError, shouldReturnEmptyStacks bool) CloudFormation {
 	return CloudFormation{
 		client: &mockCloudFormation{
@@ -586,7 +430,7 @@ func getMockWaitStackCreateCFClient(t *testing.T, stackName string, shouldThrowE
 				}, nil
 			},
 		},
-		box: emptyEnvBox(),
+		box: emptyBox(),
 	}
 }
 
@@ -614,7 +458,7 @@ func emptyBox() packd.Box {
 func boxWithTemplateFile() packd.Box {
 	box := packd.NewMemoryBox()
 
-	box.AddString(envTemplatePath, mockTemplate)
+	box.AddString(stack.EnvTemplatePath, mockTemplate)
 
 	return box
 }
diff --git a/internal/pkg/deploy/cloudformation/env_test.go b/internal/pkg/deploy/cloudformation/env_test.go
index 6e591347..49e65193 100644
--- a/internal/pkg/deploy/cloudformation/env_test.go
+++ b/internal/pkg/deploy/cloudformation/env_test.go
@@ -1,165 +1,179 @@
 // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
+// Package cloudformation provides functionality to deploy archer resources with AWS CloudFormation.
 package cloudformation
 
 import (
+	"errors"
 	"fmt"
-	"strconv"
 	"testing"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/cloudformation"
+
 	"github.com/gobuffalo/packd"
 	"github.com/stretchr/testify/require"
 )
 
-func TestEnvTemplate(t *testing.T) {
+func TestStreamEnvironmentCreation(t *testing.T) {
 	testCases := map[string]struct {
-		box            packd.Box
-		expectedOutput string
-		want           error
-	}{
-		"should return error given template not found": {
-			box:  emptyEnvBox(),
-			want: fmt.Errorf("failed to find the cloudformation template at %s", envTemplatePath),
-		},
-		"should return template body when present": {
-			box:            envBoxWithTemplateFile(),
-			expectedOutput: mockTemplate,
-		},
-	}
+		in *deploy.CreateEnvironmentInput
 
-	for name, tc := range testCases {
-		t.Run(name, func(t *testing.T) {
-			envStack := newEnvStackConfig(mockDeployEnvironmentInput(), tc.box)
-			got, err := envStack.Template()
+		mockWaitUntilStackCreateComplete func(t *testing.T, in *cloudformation.DescribeStacksInput) error
+		mockDescribeStacks               func(t *testing.T, in *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error)
+		mockDescribeStackEvents          func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error)
 
-			if tc.want != nil {
-				require.EqualError(t, tc.want, err.Error())
-			} else {
-				require.NoError(t, err)
-				require.Equal(t, tc.expectedOutput, got)
-			}
-		})
-	}
-}
+		wantedEvents []deploy.ResourceEvent
+		wantedResult deploy.CreateEnvironmentResponse
+	}{
+		"error while creating stack": {
+			in: &deploy.CreateEnvironmentInput{
+				Project: "phonetool",
+				Name:    "test",
+			},
 
-func TestEnvParameters(t *testing.T) {
-	deploymentInput := mockDeployEnvironmentInput()
-	env := newEnvStackConfig(deploymentInput, emptyEnvBox())
-	expectedParams := []*cloudformation.Parameter{
-		{
-			ParameterKey:   aws.String(EnvParamIncludeLBKey),
-			ParameterValue: aws.String(strconv.FormatBool(deploymentInput.PublicLoadBalancer)),
-		},
-		{
-			ParameterKey:   aws.String(EnvParamProjectNameKey),
-			ParameterValue: aws.String(deploymentInput.Project),
-		},
-		{
-			ParameterKey:   aws.String(EnvParamEnvNameKey),
-			ParameterValue: aws.String(deploymentInput.Name),
-		},
-		{
-			ParameterKey:   aws.String(envParamToolsAccountPrincipal),
-			ParameterValue: aws.String(deploymentInput.ToolsAccountPrincipalARN),
-		},
-	}
-	require.ElementsMatch(t, expectedParams, env.Parameters())
-}
+			mockWaitUntilStackCreateComplete: func(t *testing.T, in *cloudformation.DescribeStacksInput) error {
+				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
+				return errors.New("wait until error")
+			},
+			mockDescribeStackEvents: func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error) {
+				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
+				return &cloudformation.DescribeStackEventsOutput{
+					StackEvents: []*cloudformation.StackEvent{
+						{
+							LogicalResourceId:    aws.String("vpc"),
+							ResourceType:         aws.String("AWS::EC2::VPC"),
+							ResourceStatus:       aws.String(cloudformation.ResourceStatusCreateInProgress),
+							ResourceStatusReason: aws.String("create initiated"),
+						},
+					},
+				}, nil
+			},
 
-func TestEnvTags(t *testing.T) {
-	deploymentInput := mockDeployEnvironmentInput()
-	env := newEnvStackConfig(deploymentInput, emptyEnvBox())
-	expectedTags := []*cloudformation.Tag{
-		{
-			Key:   aws.String(projectTagKey),
-			Value: aws.String(deploymentInput.Project),
-		},
-		{
-			Key:   aws.String(envTagKey),
-			Value: aws.String(deploymentInput.Name),
+			wantedEvents: []deploy.ResourceEvent{
+				{
+					Resource: deploy.Resource{
+						LogicalName: "vpc",
+						Type:        "AWS::EC2::VPC",
+					},
+					Status:       cloudformation.ResourceStatusCreateInProgress,
+					StatusReason: "create initiated",
+				},
+			},
+			wantedResult: deploy.CreateEnvironmentResponse{
+				Env: nil,
+				Err: errors.New("failed to create stack phonetool-test: wait until error"),
+			},
 		},
-	}
-	require.ElementsMatch(t, expectedTags, env.Tags())
-}
+		"swallows error while describing stack": {
+			in: &deploy.CreateEnvironmentInput{
+				Project: "phonetool",
+				Name:    "test",
+			},
 
-func TestStackName(t *testing.T) {
-	deploymentInput := mockDeployEnvironmentInput()
-	env := newEnvStackConfig(deploymentInput, emptyEnvBox())
-	require.Equal(t, fmt.Sprintf("%s-%s", deploymentInput.Project, deploymentInput.Name), env.StackName())
-}
+			mockWaitUntilStackCreateComplete: func(t *testing.T, in *cloudformation.DescribeStacksInput) error {
+				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
+				return errors.New("wait until error")
+			},
+			mockDescribeStackEvents: func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error) {
+				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
+				return nil, errors.New("describe stack events error")
+			},
 
-func TestToEnv(t *testing.T) {
-	mockDeployInput := mockDeployEnvironmentInput()
-	testCases := map[string]struct {
-		expectedEnv archer.Environment
-		mockStack   *cloudformation.Stack
-		want        error
-	}{
-		"should return error if Stack ID is invalid": {
-			want:      fmt.Errorf("couldn't extract region and account from stack ID : arn: invalid prefix"),
-			mockStack: mockEnvironmentStack("", ""),
+			wantedEvents: nil,
+			wantedResult: deploy.CreateEnvironmentResponse{
+				Env: nil,
+				Err: errors.New("failed to create stack phonetool-test: wait until error"),
+			},
 		},
-		"should return a well formed environment": {
-			mockStack: mockEnvironmentStack("arn:aws:cloudformation:eu-west-3:902697171733:stack/project-env", "project/env"),
-			expectedEnv: archer.Environment{
-				Name:        mockDeployInput.Name,
-				Project:     mockDeployInput.Project,
-				Prod:        mockDeployInput.Prod,
-				AccountID:   "902697171733",
-				Region:      "eu-west-3",
-				RegistryURL: "902697171733.dkr.ecr.eu-west-3.amazonaws.com/project/env",
+		"sends an environment on success": {
+			in: &deploy.CreateEnvironmentInput{
+				Project: "phonetool",
+				Name:    "test",
+			},
+
+			mockWaitUntilStackCreateComplete: func(t *testing.T, in *cloudformation.DescribeStacksInput) error {
+				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
+				return nil
+			},
+			mockDescribeStacks: func(t *testing.T, in *cloudformation.DescribeStacksInput) (*cloudformation.DescribeStacksOutput, error) {
+				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
+				return &cloudformation.DescribeStacksOutput{
+					Stacks: []*cloudformation.Stack{
+						{
+							StackId: aws.String(fmt.Sprintf("arn:aws:cloudformation:eu-west-3:902697171733:stack/%s", "phonetool-test")),
+						},
+					},
+				}, nil
+			},
+			mockDescribeStackEvents: func(t *testing.T, in *cloudformation.DescribeStackEventsInput) (*cloudformation.DescribeStackEventsOutput, error) {
+				require.Equal(t, "phonetool-test", *in.StackName, "stack names should be equal to each other")
+				return &cloudformation.DescribeStackEventsOutput{
+					StackEvents: []*cloudformation.StackEvent{
+						{
+							LogicalResourceId:    aws.String("vpc"),
+							ResourceType:         aws.String("AWS::EC2::VPC"),
+							ResourceStatus:       aws.String(cloudformation.ResourceStatusCreateInProgress),
+							ResourceStatusReason: aws.String("create initiated"),
+						},
+					},
+				}, nil
+			},
+
+			wantedEvents: []deploy.ResourceEvent{
+				{
+					Resource: deploy.Resource{
+						LogicalName: "vpc",
+						Type:        "AWS::EC2::VPC",
+					},
+					Status:       cloudformation.ResourceStatusCreateInProgress,
+					StatusReason: "create initiated",
+				},
+			},
+			wantedResult: deploy.CreateEnvironmentResponse{
+				Env: &archer.Environment{
+					Project:   "phonetool",
+					Name:      "test",
+					Region:    "eu-west-3",
+					AccountID: "902697171733",
+				},
+				Err: nil,
 			},
 		},
 	}
 
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
-			envStack := newEnvStackConfig(mockDeployInput, emptyBox())
-			got, err := envStack.ToEnv(tc.mockStack)
+			// GIVEN
+			cf := CloudFormation{
+				client: &mockCloudFormation{
+					t:                                t,
+					mockDescribeStackEvents:          tc.mockDescribeStackEvents,
+					mockDescribeStacks:               tc.mockDescribeStacks,
+					mockWaitUntilStackCreateComplete: tc.mockWaitUntilStackCreateComplete,
+				},
+				box: emptyEnvBox(),
+			}
 
-			if tc.want != nil {
-				require.EqualError(t, tc.want, err.Error())
+			// WHEN
+			events, resp := cf.StreamEnvironmentCreation(tc.in)
+
+			// THEN
+			require.Equal(t, tc.wantedEvents, <-events)
+			got := <-resp
+			if tc.wantedResult.Err != nil {
+				require.EqualError(t, got.Err, tc.wantedResult.Err.Error(), "expected %v got %v", tc.wantedResult.Err, got.Err)
 			} else {
-				require.NoError(t, err)
-				require.Equal(t, tc.expectedEnv, *got)
+				require.Equal(t, tc.wantedResult, got)
 			}
 		})
 	}
 }
-func mockEnvironmentStack(stackArn, ecrOutput string) *cloudformation.Stack {
-	return &cloudformation.Stack{
-		StackId: aws.String(stackArn),
-		Outputs: []*cloudformation.Output{
-			&cloudformation.Output{
-				OutputKey:   aws.String(EnvOutputECRKey),
-				OutputValue: aws.String(ecrOutput),
-			},
-		},
-	}
-}
 
-func mockDeployEnvironmentInput() *deploy.CreateEnvironmentInput {
-	return &deploy.CreateEnvironmentInput{
-		Name:                     "env",
-		Project:                  "project",
-		Prod:                     true,
-		PublicLoadBalancer:       true,
-		ToolsAccountPrincipalARN: "mockToolsAccountPrincipalARN",
-	}
-}
 func emptyEnvBox() packd.Box {
 	return packd.NewMemoryBox()
 }
-
-func envBoxWithTemplateFile() packd.Box {
-	box := packd.NewMemoryBox()
-
-	box.AddString(envTemplatePath, mockTemplate)
-
-	return box
-}
diff --git a/internal/pkg/deploy/cloudformation/stack/env_test.go b/internal/pkg/deploy/cloudformation/stack/env_test.go
new file mode 100644
index 00000000..e35a3f98
--- /dev/null
+++ b/internal/pkg/deploy/cloudformation/stack/env_test.go
@@ -0,0 +1,165 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package stack
+
+import (
+	"fmt"
+	"strconv"
+	"testing"
+
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/cloudformation"
+	"github.com/gobuffalo/packd"
+	"github.com/stretchr/testify/require"
+)
+
+func TestEnvTemplate(t *testing.T) {
+	testCases := map[string]struct {
+		box            packd.Box
+		expectedOutput string
+		want           error
+	}{
+		"should return error given template not found": {
+			box:  emptyEnvBox(),
+			want: fmt.Errorf("failed to find the cloudformation template at %s", EnvTemplatePath),
+		},
+		"should return template body when present": {
+			box:            envBoxWithTemplateFile(),
+			expectedOutput: mockTemplate,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			envStack := NewEnvStackConfig(mockDeployEnvironmentInput(), tc.box)
+			got, err := envStack.Template()
+
+			if tc.want != nil {
+				require.EqualError(t, tc.want, err.Error())
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tc.expectedOutput, got)
+			}
+		})
+	}
+}
+
+func TestEnvParameters(t *testing.T) {
+	deploymentInput := mockDeployEnvironmentInput()
+	env := NewEnvStackConfig(deploymentInput, emptyEnvBox())
+	expectedParams := []*cloudformation.Parameter{
+		{
+			ParameterKey:   aws.String(EnvParamIncludeLBKey),
+			ParameterValue: aws.String(strconv.FormatBool(deploymentInput.PublicLoadBalancer)),
+		},
+		{
+			ParameterKey:   aws.String(EnvParamProjectNameKey),
+			ParameterValue: aws.String(deploymentInput.Project),
+		},
+		{
+			ParameterKey:   aws.String(EnvParamEnvNameKey),
+			ParameterValue: aws.String(deploymentInput.Name),
+		},
+		{
+			ParameterKey:   aws.String(envParamToolsAccountPrincipal),
+			ParameterValue: aws.String(deploymentInput.ToolsAccountPrincipalARN),
+		},
+	}
+	require.ElementsMatch(t, expectedParams, env.Parameters())
+}
+
+func TestEnvTags(t *testing.T) {
+	deploymentInput := mockDeployEnvironmentInput()
+	env := NewEnvStackConfig(deploymentInput, emptyEnvBox())
+	expectedTags := []*cloudformation.Tag{
+		{
+			Key:   aws.String(projectTagKey),
+			Value: aws.String(deploymentInput.Project),
+		},
+		{
+			Key:   aws.String(envTagKey),
+			Value: aws.String(deploymentInput.Name),
+		},
+	}
+	require.ElementsMatch(t, expectedTags, env.Tags())
+}
+
+func TestStackName(t *testing.T) {
+	deploymentInput := mockDeployEnvironmentInput()
+	env := NewEnvStackConfig(deploymentInput, emptyEnvBox())
+	require.Equal(t, fmt.Sprintf("%s-%s", deploymentInput.Project, deploymentInput.Name), env.StackName())
+}
+
+func TestToEnv(t *testing.T) {
+	mockDeployInput := mockDeployEnvironmentInput()
+	testCases := map[string]struct {
+		expectedEnv archer.Environment
+		mockStack   *cloudformation.Stack
+		want        error
+	}{
+		"should return error if Stack ID is invalid": {
+			want:      fmt.Errorf("couldn't extract region and account from stack ID : arn: invalid prefix"),
+			mockStack: mockEnvironmentStack("", ""),
+		},
+		"should return a well formed environment": {
+			mockStack: mockEnvironmentStack("arn:aws:cloudformation:eu-west-3:902697171733:stack/project-env", "project/env"),
+			expectedEnv: archer.Environment{
+				Name:        mockDeployInput.Name,
+				Project:     mockDeployInput.Project,
+				Prod:        mockDeployInput.Prod,
+				AccountID:   "902697171733",
+				Region:      "eu-west-3",
+			},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			envStack := NewEnvStackConfig(mockDeployInput, emptyEnvBox())
+			got, err := envStack.ToEnv(tc.mockStack)
+
+			if tc.want != nil {
+				require.EqualError(t, tc.want, err.Error())
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tc.expectedEnv, *got)
+			}
+		})
+	}
+}
+func mockEnvironmentStack(stackArn, ecrOutput string) *cloudformation.Stack {
+	return &cloudformation.Stack{
+		StackId: aws.String(stackArn),
+		Outputs: []*cloudformation.Output{
+			&cloudformation.Output{
+				OutputKey:   aws.String(EnvOutputECRKey),
+				OutputValue: aws.String(ecrOutput),
+			},
+		},
+	}
+}
+
+func mockDeployEnvironmentInput() *deploy.CreateEnvironmentInput {
+	return &deploy.CreateEnvironmentInput{
+		Name:                     "env",
+		Project:                  "project",
+		Prod:                     true,
+		PublicLoadBalancer:       true,
+		ToolsAccountPrincipalARN: "mockToolsAccountPrincipalARN",
+	}
+}
+
+func emptyEnvBox() packd.Box {
+	return packd.NewMemoryBox()
+}
+
+func envBoxWithTemplateFile() packd.Box {
+	box := packd.NewMemoryBox()
+
+	box.AddString(EnvTemplatePath, mockTemplate)
+
+	return box
+}