Repository: aws/amazon-ecs-cli-v2
Pull request #288 (2019-11-07 00:31:08)

Add a ImageRepoURL field to CreateLBFargateAppInput


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.


efekarakus: @SoManyHs I'll fill that value once the interfaces in this PR https://github.com/aws/amazon-ecs-cli-v2/pull/289#pullrequestreview-312963184 are implemented :)

Patch:
diff --git a/internal/pkg/cli/app_deploy.go b/internal/pkg/cli/app_deploy.go
index eb19c575..89ae30c4 100644
--- a/internal/pkg/cli/app_deploy.go
+++ b/internal/pkg/cli/app_deploy.go
@@ -341,6 +341,10 @@ func (opts appDeployOpts) deployApp() error {
 
 func (opts appDeployOpts) getAppDeployTemplate() (string, error) {
 	buffer := &bytes.Buffer{}
+	sess, err := session.Default()
+	if err != nil {
+		return "", fmt.Errorf("create default session: %w", err)
+	}
 
 	appPackage := PackageAppOpts{
 		AppName:      opts.app,
@@ -348,7 +352,8 @@ func (opts appDeployOpts) getAppDeployTemplate() (string, error) {
 		Tag:          opts.imageTag,
 		stackWriter:  buffer,
 		paramsWriter: ioutil.Discard,
-		envStore:     opts.projectService,
+		store:        opts.projectService,
+		describer:    cloudformation.New(sess),
 		ws:           opts.workspaceService,
 		GlobalOpts:   opts.GlobalOpts,
 	}
diff --git a/internal/pkg/cli/app_package.go b/internal/pkg/cli/app_package.go
index f4c68d4b..f2456bff 100644
--- a/internal/pkg/cli/app_package.go
+++ b/internal/pkg/cli/app_package.go
@@ -14,7 +14,9 @@ import (
 	"strings"
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/aws/session"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation/stack"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store/ssm"
@@ -40,7 +42,8 @@ type PackageAppOpts struct {
 
 	// Interfaces to interact with dependencies.
 	ws           archer.Workspace
-	envStore     archer.EnvironmentStore
+	store        projectService
+	describer    projectResourcesGetter
 	stackWriter  io.Writer
 	paramsWriter io.Writer
 	fs           afero.Fs
@@ -94,6 +97,9 @@ func (opts *PackageAppOpts) Ask() error {
 		if err != nil {
 			return err
 		}
+		if len(names) == 0 {
+			return fmt.Errorf("there are no environments in project %s", opts.ProjectName())
+		}
 		env, err := opts.prompt.SelectOne(appPackageEnvNamePrompt, "", names)
 		if err != nil {
 			return fmt.Errorf("prompt environment name: %w", err)
@@ -121,7 +127,7 @@ func (opts *PackageAppOpts) Validate() error {
 		}
 	}
 	if opts.EnvName != "" {
-		if _, err := opts.envStore.GetEnvironment(opts.ProjectName(), opts.EnvName); err != nil {
+		if _, err := opts.store.GetEnvironment(opts.ProjectName(), opts.EnvName); err != nil {
 			return err
 		}
 	}
@@ -130,7 +136,7 @@ func (opts *PackageAppOpts) Validate() error {
 
 // Execute prints the CloudFormation template of the application for the environment.
 func (opts *PackageAppOpts) Execute() error {
-	env, err := opts.envStore.GetEnvironment(opts.ProjectName(), opts.EnvName)
+	env, err := opts.store.GetEnvironment(opts.ProjectName(), opts.EnvName)
 	if err != nil {
 		return err
 	}
@@ -175,12 +181,32 @@ func (opts *PackageAppOpts) getTemplates(env *archer.Environment) (*cfnTemplates
 	if err != nil {
 		return nil, err
 	}
+
+	proj, err := opts.store.GetProject(opts.ProjectName())
+	if err != nil {
+		return nil, err
+	}
+	resources, err := opts.describer.GetProjectResourcesByRegion(proj, env.Region)
+	if err != nil {
+		return nil, err
+	}
+
+	repoURL, ok := resources.RepositoryURLs[opts.AppName]
+	if !ok {
+		return nil, &errRepoNotFound{
+			appName:       opts.AppName,
+			envRegion:     env.Region,
+			projAccountID: proj.AccountID,
+		}
+	}
+
 	switch t := mft.(type) {
 	case *manifest.LBFargateManifest:
 		appStack := stack.NewLBFargateStack(&deploy.CreateLBFargateAppInput{
-			App:      mft.(*manifest.LBFargateManifest),
-			Env:      env,
-			ImageTag: opts.Tag,
+			App:          mft.(*manifest.LBFargateManifest),
+			Env:          env,
+			ImageRepoURL: repoURL,
+			ImageTag:     opts.Tag,
 		})
 		tpl, err := appStack.Template()
 		if err != nil {
@@ -230,7 +256,7 @@ func contains(s string, items []string) bool {
 }
 
 func (opts *PackageAppOpts) listEnvNames() ([]string, error) {
-	envs, err := opts.envStore.ListEnvironments(opts.ProjectName())
+	envs, err := opts.store.ListEnvironments(opts.ProjectName())
 	if err != nil {
 		return nil, fmt.Errorf("list environments for project %s: %w", opts.ProjectName(), err)
 	}
@@ -241,6 +267,26 @@ func (opts *PackageAppOpts) listEnvNames() ([]string, error) {
 	return names, nil
 }
 
+type errRepoNotFound struct {
+	appName       string
+	envRegion     string
+	projAccountID string
+}
+
+func (e *errRepoNotFound) Error() string {
+	return fmt.Sprintf("ECR repository not found for application %s in region %s and account %s", e.appName, e.envRegion, e.projAccountID)
+}
+
+func (e *errRepoNotFound) Is(target error) bool {
+	t, ok := target.(*errRepoNotFound)
+	if !ok {
+		return false
+	}
+	return e.appName == t.appName &&
+		e.envRegion == t.envRegion &&
+		e.projAccountID == t.projAccountID
+}
+
 // BuildAppPackageCmd builds the command for printing an application's CloudFormation template.
 func BuildAppPackageCmd() *cobra.Command {
 	opts := NewPackageAppOpts()
@@ -267,7 +313,13 @@ func BuildAppPackageCmd() *cobra.Command {
 			if err != nil {
 				return fmt.Errorf("couldn't connect to application datastore: %w", err)
 			}
-			opts.envStore = store
+			opts.store = store
+
+			sess, err := session.Default()
+			if err != nil {
+				return fmt.Errorf("error retrieving default session: %w", err)
+			}
+			opts.describer = cloudformation.New(sess)
 			return opts.Validate()
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
diff --git a/internal/pkg/cli/deploy.go b/internal/pkg/cli/deploy.go
index 641256c7..8a89eb54 100644
--- a/internal/pkg/cli/deploy.go
+++ b/internal/pkg/cli/deploy.go
@@ -19,6 +19,11 @@ type projectDeployer interface {
 	AddEnvToProject(project *archer.Project, env *archer.Environment) error
 }
 
+type projectResourcesGetter interface {
+	GetProjectResourcesByRegion(project *archer.Project, region string) (*archer.ProjectRegionalResources, error)
+	GetRegionalProjectResources(project *archer.Project) ([]*archer.ProjectRegionalResources, error)
+}
+
 type deployer interface {
 	environmentDeployer
 	projectDeployer
diff --git a/internal/pkg/cli/mocks/mock_deploy.go b/internal/pkg/cli/mocks/mock_deploy.go
index 810c8978..e01d7e22 100644
--- a/internal/pkg/cli/mocks/mock_deploy.go
+++ b/internal/pkg/cli/mocks/mock_deploy.go
@@ -128,6 +128,59 @@ func (mr *MockprojectDeployerMockRecorder) AddEnvToProject(project, env interfac
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEnvToProject", reflect.TypeOf((*MockprojectDeployer)(nil).AddEnvToProject), project, env)
 }
 
+// MockprojectResourcesGetter is a mock of projectResourcesGetter interface
+type MockprojectResourcesGetter struct {
+	ctrl     *gomock.Controller
+	recorder *MockprojectResourcesGetterMockRecorder
+}
+
+// MockprojectResourcesGetterMockRecorder is the mock recorder for MockprojectResourcesGetter
+type MockprojectResourcesGetterMockRecorder struct {
+	mock *MockprojectResourcesGetter
+}
+
+// NewMockprojectResourcesGetter creates a new mock instance
+func NewMockprojectResourcesGetter(ctrl *gomock.Controller) *MockprojectResourcesGetter {
+	mock := &MockprojectResourcesGetter{ctrl: ctrl}
+	mock.recorder = &MockprojectResourcesGetterMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockprojectResourcesGetter) EXPECT() *MockprojectResourcesGetterMockRecorder {
+	return m.recorder
+}
+
+// GetProjectResourcesByRegion mocks base method
+func (m *MockprojectResourcesGetter) GetProjectResourcesByRegion(project *archer.Project, region string) (*archer.ProjectRegionalResources, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetProjectResourcesByRegion", project, region)
+	ret0, _ := ret[0].(*archer.ProjectRegionalResources)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetProjectResourcesByRegion indicates an expected call of GetProjectResourcesByRegion
+func (mr *MockprojectResourcesGetterMockRecorder) GetProjectResourcesByRegion(project, region interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProjectResourcesByRegion", reflect.TypeOf((*MockprojectResourcesGetter)(nil).GetProjectResourcesByRegion), project, region)
+}
+
+// GetRegionalProjectResources mocks base method
+func (m *MockprojectResourcesGetter) GetRegionalProjectResources(project *archer.Project) ([]*archer.ProjectRegionalResources, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRegionalProjectResources", project)
+	ret0, _ := ret[0].([]*archer.ProjectRegionalResources)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetRegionalProjectResources indicates an expected call of GetRegionalProjectResources
+func (mr *MockprojectResourcesGetterMockRecorder) GetRegionalProjectResources(project interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegionalProjectResources", reflect.TypeOf((*MockprojectResourcesGetter)(nil).GetRegionalProjectResources), project)
+}
+
 // Mockdeployer is a mock of deployer interface
 type Mockdeployer struct {
 	ctrl     *gomock.Controller
diff --git a/internal/pkg/deploy/cloudformation/stack/lb_fargate_app.go b/internal/pkg/deploy/cloudformation/stack/lb_fargate_app.go
index 242efc69..a8251d2b 100644
--- a/internal/pkg/deploy/cloudformation/stack/lb_fargate_app.go
+++ b/internal/pkg/deploy/cloudformation/stack/lb_fargate_app.go
@@ -35,10 +35,6 @@ const (
 	lbFargateTaskCountKey           = "TaskCount"
 )
 
-const (
-	ecrURLFormatString = "%s.dkr.ecr.%s.amazonaws.com/%s"
-)
-
 // LBFargateStackConfig represents the configuration needed to create a CloudFormation stack from a
 // load balanced Fargate application.
 type LBFargateStackConfig struct {
@@ -181,8 +177,7 @@ type lbFargateTemplateParams struct {
 }
 
 func (c *LBFargateStackConfig) toTemplateParams() *lbFargateTemplateParams {
-	imgLoc := fmt.Sprintf("%s/%s:%s", c.Env.Project, c.App.Name, c.ImageTag)
-	url := fmt.Sprintf(ecrURLFormatString, c.Env.AccountID, c.Env.Region, imgLoc)
+	url := fmt.Sprintf("%s:%s", c.ImageRepoURL, c.ImageTag)
 	return &lbFargateTemplateParams{
 		CreateLBFargateAppInput: &deploy.CreateLBFargateAppInput{
 			App: &manifest.LBFargateManifest{
diff --git a/internal/pkg/deploy/deploy.go b/internal/pkg/deploy/deploy.go
index 9c916773..4e0ae921 100644
--- a/internal/pkg/deploy/deploy.go
+++ b/internal/pkg/deploy/deploy.go
@@ -207,9 +207,10 @@ type AssociatedEnvironment struct {
 
 // CreateLBFargateAppInput holds the fields required to deploy a load-balanced AWS Fargate application.
 type CreateLBFargateAppInput struct {
-	App      *manifest.LBFargateManifest
-	Env      *archer.Environment
-	ImageTag string
+	App          *manifest.LBFargateManifest
+	Env          *archer.Environment
+	ImageRepoURL string
+	ImageTag     string
 }
 
 // Resource represents an AWS resource.


Test patch:
diff --git a/internal/pkg/cli/app_package_test.go b/internal/pkg/cli/app_package_test.go
index 9ebae785..50615c67 100644
--- a/internal/pkg/cli/app_package_test.go
+++ b/internal/pkg/cli/app_package_test.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	climocks "github.com/aws/amazon-ecs-cli-v2/internal/pkg/cli/mocks"
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy/cloudformation"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/manifest"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/store"
 	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/workspace"
@@ -25,9 +26,9 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 		inAppName string
 		inEnvName string
 
-		expectWS       func(m *mocks.MockWorkspace)
-		expectEnvStore func(m *mocks.MockEnvironmentStore)
-		expectPrompt   func(m *climocks.Mockprompter)
+		expectWS     func(m *mocks.MockWorkspace)
+		expectStore  func(m *climocks.MockprojectService)
+		expectPrompt func(m *climocks.Mockprompter)
 
 		wantedAppName string
 		wantedEnvName string
@@ -37,7 +38,7 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Return(nil, errors.New("some error"))
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().ListEnvironments(gomock.Any()).Times(0)
 			},
 			expectPrompt: func(m *climocks.Mockprompter) {
@@ -50,7 +51,7 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Return([]string{}, nil)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().ListEnvironments(gomock.Any()).Times(0)
 			},
 			expectPrompt: func(m *climocks.Mockprompter) {
@@ -64,7 +65,7 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Times(0)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().ListEnvironments(gomock.Any()).Return(nil, errors.New("some ssm error"))
 			},
 			expectPrompt: func(m *climocks.Mockprompter) {
@@ -74,11 +75,26 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			wantedAppName: "frontend",
 			wantedErrorS:  "list environments for project : some ssm error",
 		},
+		"empty environments error": {
+			inAppName: "frontend",
+			expectWS: func(m *mocks.MockWorkspace) {
+				m.EXPECT().AppNames().Times(0)
+			},
+			expectStore: func(m *climocks.MockprojectService) {
+				m.EXPECT().ListEnvironments(gomock.Any()).Return(nil, nil)
+			},
+			expectPrompt: func(m *climocks.Mockprompter) {
+				m.EXPECT().SelectOne(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
+			},
+
+			wantedAppName: "frontend",
+			wantedErrorS:  "there are no environments in project ",
+		},
 		"prompt for all options": {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Return([]string{"frontend", "backend"}, nil)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().ListEnvironments(gomock.Any()).Return([]*archer.Environment{
 					{
 						Name: "test",
@@ -102,7 +118,7 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Return([]string{"frontend", "backend"}, nil)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().ListEnvironments(gomock.Any()).Times(0)
 			},
 			expectPrompt: func(m *climocks.Mockprompter) {
@@ -118,7 +134,7 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Times(0)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().ListEnvironments(gomock.Any()).Return([]*archer.Environment{
 					{
 						Name: "test",
@@ -142,7 +158,7 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Times(0)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().ListEnvironments(gomock.Any()).Times(0)
 			},
 			expectPrompt: func(m *climocks.Mockprompter) {
@@ -161,18 +177,18 @@ func TestPackageAppOpts_Ask(t *testing.T) {
 			defer ctrl.Finish()
 
 			mockWorkspace := mocks.NewMockWorkspace(ctrl)
-			mockEnvStore := mocks.NewMockEnvironmentStore(ctrl)
+			mockStore := climocks.NewMockprojectService(ctrl)
 			mockPrompt := climocks.NewMockprompter(ctrl)
 
 			tc.expectWS(mockWorkspace)
-			tc.expectEnvStore(mockEnvStore)
+			tc.expectStore(mockStore)
 			tc.expectPrompt(mockPrompt)
 
 			opts := &PackageAppOpts{
 				AppName:    tc.inAppName,
 				EnvName:    tc.inEnvName,
 				ws:         mockWorkspace,
-				envStore:   mockEnvStore,
+				store:      mockStore,
 				prompt:     mockPrompt,
 				GlobalOpts: &GlobalOpts{},
 			}
@@ -200,8 +216,8 @@ func TestPackageAppOpts_Validate(t *testing.T) {
 		inAppName     string
 		inTag         string
 
-		expectWS       func(m *mocks.MockWorkspace)
-		expectEnvStore func(m *mocks.MockEnvironmentStore)
+		expectWS    func(m *mocks.MockWorkspace)
+		expectStore func(m *climocks.MockprojectService)
 
 		wantedErrorS string
 	}{
@@ -209,7 +225,7 @@ func TestPackageAppOpts_Validate(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Times(0)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).Times(0)
 			},
 
@@ -220,7 +236,7 @@ func TestPackageAppOpts_Validate(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Times(0)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).Times(0)
 			},
 			wantedErrorS: "image tag cannot be empty, please provide the `--tag` flag",
@@ -233,7 +249,7 @@ func TestPackageAppOpts_Validate(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Return(nil, errors.New("some error"))
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).Times(0)
 			},
 
@@ -247,7 +263,7 @@ func TestPackageAppOpts_Validate(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Return([]string{"backend"}, nil)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).Times(0)
 			},
 
@@ -261,7 +277,7 @@ func TestPackageAppOpts_Validate(t *testing.T) {
 			expectWS: func(m *mocks.MockWorkspace) {
 				m.EXPECT().AppNames().Times(0)
 			},
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment("phonetool", "test").Return(nil, &store.ErrNoSuchEnvironment{
 					ProjectName:     "phonetool",
 					EnvironmentName: "test",
@@ -282,17 +298,17 @@ func TestPackageAppOpts_Validate(t *testing.T) {
 			defer ctrl.Finish()
 
 			mockWorkspace := mocks.NewMockWorkspace(ctrl)
-			mockEnvStore := mocks.NewMockEnvironmentStore(ctrl)
+			mockStore := climocks.NewMockprojectService(ctrl)
 			tc.expectWS(mockWorkspace)
-			tc.expectEnvStore(mockEnvStore)
+			tc.expectStore(mockStore)
 
 			opts := &PackageAppOpts{
 				AppName: tc.inAppName,
 				EnvName: tc.inEnvName,
 				Tag:     tc.inTag,
 
-				ws:       mockWorkspace,
-				envStore: mockEnvStore,
+				ws:    mockWorkspace,
+				store: mockStore,
 
 				GlobalOpts: &GlobalOpts{projectName: tc.inProjectName},
 			}
@@ -318,8 +334,9 @@ func TestPackageAppOpts_Execute(t *testing.T) {
 		inTagName     string
 		inOutputDir   string
 
-		expectEnvStore  func(m *mocks.MockEnvironmentStore)
+		expectStore     func(m *climocks.MockprojectService)
 		expectWorkspace func(m *mocks.MockWorkspace)
+		expectDeployer  func(m *climocks.MockprojectResourcesGetter)
 		expectFS        func(t *testing.T, mockFS *afero.Afero)
 
 		wantedErr error
@@ -328,7 +345,7 @@ func TestPackageAppOpts_Execute(t *testing.T) {
 			inProjectName: "phonetool",
 			inEnvName:     "test",
 
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment("phonetool", "test").Return(nil, &store.ErrNoSuchEnvironment{
 					ProjectName:     "phonetool",
 					EnvironmentName: "test",
@@ -337,6 +354,7 @@ func TestPackageAppOpts_Execute(t *testing.T) {
 			expectWorkspace: func(m *mocks.MockWorkspace) {
 				m.EXPECT().ReadManifestFile(gomock.Any()).Times(0)
 			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {},
 
 			wantedErr: &store.ErrNoSuchEnvironment{
 				ProjectName:     "phonetool",
@@ -348,7 +366,7 @@ func TestPackageAppOpts_Execute(t *testing.T) {
 			inEnvName:     "test",
 			inAppName:     "frontend",
 
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
 					Project: "phonetool",
 					Name:    "test",
@@ -360,6 +378,7 @@ func TestPackageAppOpts_Execute(t *testing.T) {
 					ManifestName: "frontend-app.yml",
 				})
 			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {},
 
 			wantedErr: &workspace.ErrManifestNotFound{
 				ManifestName: "frontend-app.yml",
@@ -370,7 +389,7 @@ func TestPackageAppOpts_Execute(t *testing.T) {
 			inEnvName:     "test",
 			inAppName:     "frontend",
 
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
 					Project: "phonetool",
 					Name:    "test",
@@ -380,22 +399,114 @@ func TestPackageAppOpts_Execute(t *testing.T) {
 				m.EXPECT().ManifestFileName("frontend").Return("frontend-app.yml")
 				m.EXPECT().ReadManifestFile("frontend-app.yml").Return([]byte("somecontent"), nil)
 			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {},
 
 			wantedErr: &manifest.ErrUnmarshalAppManifest{},
 		},
+		"error while getting project from store": {
+			inProjectName: "phonetool",
+			inEnvName:     "test",
+			inAppName:     "frontend",
+
+			expectStore: func(m *climocks.MockprojectService) {
+				m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
+					Project: "phonetool",
+					Name:    "test",
+				}, nil)
+				m.EXPECT().GetProject("phonetool").Return(nil, &store.ErrNoSuchProject{ProjectName: "phonetool"})
+			},
+			expectWorkspace: func(m *mocks.MockWorkspace) {
+				m.EXPECT().ManifestFileName("frontend").Return("frontend-app.yml")
+				m.EXPECT().ReadManifestFile("frontend-app.yml").Return([]byte(`name: frontend
+type: Load Balanced Web App`), nil)
+			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {},
+
+			wantedErr: &store.ErrNoSuchProject{ProjectName: "phonetool"},
+		},
+		"error while getting regional resources from describer": {
+			inProjectName: "phonetool",
+			inEnvName:     "test",
+			inAppName:     "frontend",
+
+			expectStore: func(m *climocks.MockprojectService) {
+				m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
+					Project:   "phonetool",
+					Name:      "test",
+					Region:    "us-west-2",
+					AccountID: "1111",
+				}, nil)
+				m.EXPECT().GetProject("phonetool").Return(&archer.Project{
+					Name:      "phonetool",
+					AccountID: "1234",
+				}, nil)
+			},
+			expectWorkspace: func(m *mocks.MockWorkspace) {
+				m.EXPECT().ManifestFileName("frontend").Return("frontend-app.yml")
+				m.EXPECT().ReadManifestFile("frontend-app.yml").Return([]byte(`name: frontend
+type: Load Balanced Web App`), nil)
+			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {
+				m.EXPECT().GetProjectResourcesByRegion(&archer.Project{
+					Name:      "phonetool",
+					AccountID: "1234",
+				}, "us-west-2").Return(nil, &cloudformation.ErrStackSetOutOfDate{})
+			},
+			wantedErr: &cloudformation.ErrStackSetOutOfDate{},
+		},
+		"error if the repository does not exist": {
+			inProjectName: "phonetool",
+			inEnvName:     "test",
+			inAppName:     "frontend",
+
+			expectStore: func(m *climocks.MockprojectService) {
+				m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
+					Project:   "phonetool",
+					Name:      "test",
+					Region:    "us-west-2",
+					AccountID: "1111",
+				}, nil)
+				m.EXPECT().GetProject("phonetool").Return(&archer.Project{
+					Name:      "phonetool",
+					AccountID: "1234",
+				}, nil)
+			},
+			expectWorkspace: func(m *mocks.MockWorkspace) {
+				m.EXPECT().ManifestFileName("frontend").Return("frontend-app.yml")
+				m.EXPECT().ReadManifestFile("frontend-app.yml").Return([]byte(`name: frontend
+type: Load Balanced Web App`), nil)
+			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {
+				m.EXPECT().GetProjectResourcesByRegion(&archer.Project{
+					Name:      "phonetool",
+					AccountID: "1234",
+				}, "us-west-2").Return(&archer.ProjectRegionalResources{
+					RepositoryURLs: map[string]string{},
+				}, nil)
+			},
+			wantedErr: &errRepoNotFound{
+				appName:       "frontend",
+				envRegion:     "us-west-2",
+				projAccountID: "1234",
+			},
+		},
 		"print CFN template": {
 			inProjectName: "phonetool",
 			inEnvName:     "test",
 			inAppName:     "frontend",
 			inTagName:     "latest",
 
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
 					Project:   "phonetool",
 					Name:      "test",
 					AccountID: "1111",
 					Region:    "us-west-2",
 				}, nil)
+				m.EXPECT().GetProject("phonetool").Return(&archer.Project{
+					Name:      "phonetool",
+					AccountID: "1234",
+				}, nil)
 			},
 			expectWorkspace: func(m *mocks.MockWorkspace) {
 				m.EXPECT().ManifestFileName("frontend").Return("frontend-app.yml")
@@ -410,6 +521,13 @@ cpu: 256
 memory: 512
 count: 1`), nil)
 			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {
+				m.EXPECT().GetProjectResourcesByRegion(gomock.Any(), gomock.Any()).Return(&archer.ProjectRegionalResources{
+					RepositoryURLs: map[string]string{
+						"frontend": "some url",
+					},
+				}, nil)
+			},
 		},
 		"with output directory": {
 			inProjectName: "phonetool",
@@ -418,13 +536,17 @@ count: 1`), nil)
 			inTagName:     "latest",
 			inOutputDir:   "./infrastructure",
 
-			expectEnvStore: func(m *mocks.MockEnvironmentStore) {
+			expectStore: func(m *climocks.MockprojectService) {
 				m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
 					Project:   "phonetool",
 					Name:      "test",
 					AccountID: "1111",
 					Region:    "us-west-2",
 				}, nil)
+				m.EXPECT().GetProject("phonetool").Return(&archer.Project{
+					Name:      "phonetool",
+					AccountID: "1234",
+				}, nil)
 			},
 			expectWorkspace: func(m *mocks.MockWorkspace) {
 				m.EXPECT().ManifestFileName("frontend").Return("frontend-app.yml")
@@ -439,6 +561,13 @@ cpu: 256
 memory: 512
 count: 1`), nil)
 			},
+			expectDeployer: func(m *climocks.MockprojectResourcesGetter) {
+				m.EXPECT().GetProjectResourcesByRegion(gomock.Any(), gomock.Any()).Return(&archer.ProjectRegionalResources{
+					RepositoryURLs: map[string]string{
+						"frontend": "some url",
+					},
+				}, nil)
+			},
 			expectFS: func(t *testing.T, mockFS *afero.Afero) {
 				stackPath := filepath.Join("infrastructure", "frontend.stack.yml")
 				stackFileExists, _ := mockFS.Exists(stackPath)
@@ -457,10 +586,12 @@ count: 1`), nil)
 			ctrl := gomock.NewController(t)
 			defer ctrl.Finish()
 
-			mockEnvStore := mocks.NewMockEnvironmentStore(ctrl)
+			mockStore := climocks.NewMockprojectService(ctrl)
 			mockWorkspace := mocks.NewMockWorkspace(ctrl)
-			tc.expectEnvStore(mockEnvStore)
+			mockDeployer := climocks.NewMockprojectResourcesGetter(ctrl)
+			tc.expectStore(mockStore)
 			tc.expectWorkspace(mockWorkspace)
+			tc.expectDeployer(mockDeployer)
 
 			templateBuf := &strings.Builder{}
 			paramsBuf := &strings.Builder{}
@@ -471,8 +602,9 @@ count: 1`), nil)
 				Tag:       tc.inTagName,
 				OutputDir: tc.inOutputDir,
 
-				envStore:     mockEnvStore,
+				store:        mockStore,
 				ws:           mockWorkspace,
+				describer:    mockDeployer,
 				stackWriter:  templateBuf,
 				paramsWriter: paramsBuf,
 				fs:           mockFS,
diff --git a/internal/pkg/deploy/cloudformation/stack/lb_fargate_app_test.go b/internal/pkg/deploy/cloudformation/stack/lb_fargate_app_test.go
index e2a51fa9..f9464fc2 100644
--- a/internal/pkg/deploy/cloudformation/stack/lb_fargate_app_test.go
+++ b/internal/pkg/deploy/cloudformation/stack/lb_fargate_app_test.go
@@ -95,7 +95,8 @@ func TestLBFargateStackConfig_Template(t *testing.T) {
 					AccountID: "12345",
 					Prod:      false,
 				},
-				ImageTag: "manual-bf3678c",
+				ImageRepoURL: "12345.dkr.ecr.us-west-2.amazonaws.com/phonetool/frontend",
+				ImageTag:     "manual-bf3678c",
 			},
 			mockBox: func(box *packd.MemoryBox) {
 				box.AddString(lbFargateAppTemplatePath, `Parameters:
@@ -158,7 +159,8 @@ func TestLBFargateStackConfig_Parameters(t *testing.T) {
 				AccountID: "12345",
 				Prod:      false,
 			},
-			ImageTag: "manual-bf3678c",
+			ImageRepoURL: "12345.dkr.ecr.us-west-2.amazonaws.com/phonetool/frontend",
+			ImageTag:     "manual-bf3678c",
 		},
 	}
 
@@ -239,7 +241,8 @@ func TestLBFargateStackConfig_SerializedParameters(t *testing.T) {
 					AccountID: "12345",
 					Prod:      false,
 				},
-				ImageTag: "manual-bf3678c",
+				ImageRepoURL: "12345.dkr.ecr.us-west-2.amazonaws.com/phonetool/frontend",
+				ImageTag:     "manual-bf3678c",
 			},
 			mockBox: func(box *packd.MemoryBox) {
 				box.AddString(lbFargateAppParamsPath, `{