<|start_filename|>internal/pkg/describe/mocks/mock_service.go<|end_filename|>
// Code generated by MockGen. DO NOT EDIT.
// Source: ./internal/pkg/describe/service.go

// Package mocks is a generated GoMock package.
package mocks

import (
	reflect "reflect"

	apprunner "github.com/aws/copilot-cli/internal/pkg/aws/apprunner"
	ecs "github.com/aws/copilot-cli/internal/pkg/aws/ecs"
	config "github.com/aws/copilot-cli/internal/pkg/config"
	stack "github.com/aws/copilot-cli/internal/pkg/describe/stack"
	gomock "github.com/golang/mock/gomock"
)

// MockecsClient is a mock of ecsClient interface.
type MockecsClient struct {
	ctrl     *gomock.Controller
	recorder *MockecsClientMockRecorder
}

// MockecsClientMockRecorder is the mock recorder for MockecsClient.
type MockecsClientMockRecorder struct {
	mock *MockecsClient
}

// NewMockecsClient creates a new mock instance.
func NewMockecsClient(ctrl *gomock.Controller) *MockecsClient {
	mock := &MockecsClient{ctrl: ctrl}
	mock.recorder = &MockecsClientMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockecsClient) EXPECT() *MockecsClientMockRecorder {
	return m.recorder
}

// TaskDefinition mocks base method.
func (m *MockecsClient) TaskDefinition(app, env, svc string) (*ecs.TaskDefinition, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "TaskDefinition", app, env, svc)
	ret0, _ := ret[0].(*ecs.TaskDefinition)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// TaskDefinition indicates an expected call of TaskDefinition.
func (mr *MockecsClientMockRecorder) TaskDefinition(app, env, svc interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskDefinition", reflect.TypeOf((*MockecsClient)(nil).TaskDefinition), app, env, svc)
}

// MockapprunnerClient is a mock of apprunnerClient interface.
type MockapprunnerClient struct {
	ctrl     *gomock.Controller
	recorder *MockapprunnerClientMockRecorder
}

// MockapprunnerClientMockRecorder is the mock recorder for MockapprunnerClient.
type MockapprunnerClientMockRecorder struct {
	mock *MockapprunnerClient
}

// NewMockapprunnerClient creates a new mock instance.
func NewMockapprunnerClient(ctrl *gomock.Controller) *MockapprunnerClient {
	mock := &MockapprunnerClient{ctrl: ctrl}
	mock.recorder = &MockapprunnerClientMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockapprunnerClient) EXPECT() *MockapprunnerClientMockRecorder {
	return m.recorder
}

// DescribeService mocks base method.
func (m *MockapprunnerClient) DescribeService(svcArn string) (*apprunner.Service, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "DescribeService", svcArn)
	ret0, _ := ret[0].(*apprunner.Service)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// DescribeService indicates an expected call of DescribeService.
func (mr *MockapprunnerClientMockRecorder) DescribeService(svcArn interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeService", reflect.TypeOf((*MockapprunnerClient)(nil).DescribeService), svcArn)
}

// MockworkloadStackDescriber is a mock of workloadStackDescriber interface.
type MockworkloadStackDescriber struct {
	ctrl     *gomock.Controller
	recorder *MockworkloadStackDescriberMockRecorder
}

// MockworkloadStackDescriberMockRecorder is the mock recorder for MockworkloadStackDescriber.
type MockworkloadStackDescriberMockRecorder struct {
	mock *MockworkloadStackDescriber
}

// NewMockworkloadStackDescriber creates a new mock instance.
func NewMockworkloadStackDescriber(ctrl *gomock.Controller) *MockworkloadStackDescriber {
	mock := &MockworkloadStackDescriber{ctrl: ctrl}
	mock.recorder = &MockworkloadStackDescriberMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockworkloadStackDescriber) EXPECT() *MockworkloadStackDescriberMockRecorder {
	return m.recorder
}

// Outputs mocks base method.
func (m *MockworkloadStackDescriber) Outputs() (map[string]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Outputs")
	ret0, _ := ret[0].(map[string]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Outputs indicates an expected call of Outputs.
func (mr *MockworkloadStackDescriberMockRecorder) Outputs() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Outputs", reflect.TypeOf((*MockworkloadStackDescriber)(nil).Outputs))
}

// Params mocks base method.
func (m *MockworkloadStackDescriber) Params() (map[string]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Params")
	ret0, _ := ret[0].(map[string]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Params indicates an expected call of Params.
func (mr *MockworkloadStackDescriberMockRecorder) Params() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Params", reflect.TypeOf((*MockworkloadStackDescriber)(nil).Params))
}

// ServiceStackResources mocks base method.
func (m *MockworkloadStackDescriber) ServiceStackResources() ([]*stack.Resource, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ServiceStackResources")
	ret0, _ := ret[0].([]*stack.Resource)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ServiceStackResources indicates an expected call of ServiceStackResources.
func (mr *MockworkloadStackDescriberMockRecorder) ServiceStackResources() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceStackResources", reflect.TypeOf((*MockworkloadStackDescriber)(nil).ServiceStackResources))
}

// MockecsDescriber is a mock of ecsDescriber interface.
type MockecsDescriber struct {
	ctrl     *gomock.Controller
	recorder *MockecsDescriberMockRecorder
}

// MockecsDescriberMockRecorder is the mock recorder for MockecsDescriber.
type MockecsDescriberMockRecorder struct {
	mock *MockecsDescriber
}

// NewMockecsDescriber creates a new mock instance.
func NewMockecsDescriber(ctrl *gomock.Controller) *MockecsDescriber {
	mock := &MockecsDescriber{ctrl: ctrl}
	mock.recorder = &MockecsDescriberMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockecsDescriber) EXPECT() *MockecsDescriberMockRecorder {
	return m.recorder
}

// EnvVars mocks base method.
func (m *MockecsDescriber) EnvVars() ([]*ecs.ContainerEnvVar, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "EnvVars")
	ret0, _ := ret[0].([]*ecs.ContainerEnvVar)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// EnvVars indicates an expected call of EnvVars.
func (mr *MockecsDescriberMockRecorder) EnvVars() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvVars", reflect.TypeOf((*MockecsDescriber)(nil).EnvVars))
}

// Outputs mocks base method.
func (m *MockecsDescriber) Outputs() (map[string]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Outputs")
	ret0, _ := ret[0].(map[string]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Outputs indicates an expected call of Outputs.
func (mr *MockecsDescriberMockRecorder) Outputs() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Outputs", reflect.TypeOf((*MockecsDescriber)(nil).Outputs))
}

// Params mocks base method.
func (m *MockecsDescriber) Params() (map[string]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Params")
	ret0, _ := ret[0].(map[string]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Params indicates an expected call of Params.
func (mr *MockecsDescriberMockRecorder) Params() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Params", reflect.TypeOf((*MockecsDescriber)(nil).Params))
}

// Platform mocks base method.
func (m *MockecsDescriber) Platform() (*ecs.ContainerPlatform, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Platform")
	ret0, _ := ret[0].(*ecs.ContainerPlatform)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Platform indicates an expected call of Platform.
func (mr *MockecsDescriberMockRecorder) Platform() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Platform", reflect.TypeOf((*MockecsDescriber)(nil).Platform))
}

// Secrets mocks base method.
func (m *MockecsDescriber) Secrets() ([]*ecs.ContainerSecret, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Secrets")
	ret0, _ := ret[0].([]*ecs.ContainerSecret)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Secrets indicates an expected call of Secrets.
func (mr *MockecsDescriberMockRecorder) Secrets() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Secrets", reflect.TypeOf((*MockecsDescriber)(nil).Secrets))
}

// ServiceStackResources mocks base method.
func (m *MockecsDescriber) ServiceStackResources() ([]*stack.Resource, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ServiceStackResources")
	ret0, _ := ret[0].([]*stack.Resource)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ServiceStackResources indicates an expected call of ServiceStackResources.
func (mr *MockecsDescriberMockRecorder) ServiceStackResources() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceStackResources", reflect.TypeOf((*MockecsDescriber)(nil).ServiceStackResources))
}

// MockapprunnerDescriber is a mock of apprunnerDescriber interface.
type MockapprunnerDescriber struct {
	ctrl     *gomock.Controller
	recorder *MockapprunnerDescriberMockRecorder
}

// MockapprunnerDescriberMockRecorder is the mock recorder for MockapprunnerDescriber.
type MockapprunnerDescriberMockRecorder struct {
	mock *MockapprunnerDescriber
}

// NewMockapprunnerDescriber creates a new mock instance.
func NewMockapprunnerDescriber(ctrl *gomock.Controller) *MockapprunnerDescriber {
	mock := &MockapprunnerDescriber{ctrl: ctrl}
	mock.recorder = &MockapprunnerDescriberMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockapprunnerDescriber) EXPECT() *MockapprunnerDescriberMockRecorder {
	return m.recorder
}

// Outputs mocks base method.
func (m *MockapprunnerDescriber) Outputs() (map[string]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Outputs")
	ret0, _ := ret[0].(map[string]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Outputs indicates an expected call of Outputs.
func (mr *MockapprunnerDescriberMockRecorder) Outputs() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Outputs", reflect.TypeOf((*MockapprunnerDescriber)(nil).Outputs))
}

// Params mocks base method.
func (m *MockapprunnerDescriber) Params() (map[string]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Params")
	ret0, _ := ret[0].(map[string]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Params indicates an expected call of Params.
func (mr *MockapprunnerDescriberMockRecorder) Params() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Params", reflect.TypeOf((*MockapprunnerDescriber)(nil).Params))
}

// Service mocks base method.
func (m *MockapprunnerDescriber) Service() (*apprunner.Service, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Service")
	ret0, _ := ret[0].(*apprunner.Service)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Service indicates an expected call of Service.
func (mr *MockapprunnerDescriberMockRecorder) Service() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Service", reflect.TypeOf((*MockapprunnerDescriber)(nil).Service))
}

// ServiceARN mocks base method.
func (m *MockapprunnerDescriber) ServiceARN() (string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ServiceARN")
	ret0, _ := ret[0].(string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ServiceARN indicates an expected call of ServiceARN.
func (mr *MockapprunnerDescriberMockRecorder) ServiceARN() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceARN", reflect.TypeOf((*MockapprunnerDescriber)(nil).ServiceARN))
}

// ServiceStackResources mocks base method.
func (m *MockapprunnerDescriber) ServiceStackResources() ([]*stack.Resource, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ServiceStackResources")
	ret0, _ := ret[0].([]*stack.Resource)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ServiceStackResources indicates an expected call of ServiceStackResources.
func (mr *MockapprunnerDescriberMockRecorder) ServiceStackResources() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceStackResources", reflect.TypeOf((*MockapprunnerDescriber)(nil).ServiceStackResources))
}

// ServiceURL mocks base method.
func (m *MockapprunnerDescriber) ServiceURL() (string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ServiceURL")
	ret0, _ := ret[0].(string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ServiceURL indicates an expected call of ServiceURL.
func (mr *MockapprunnerDescriberMockRecorder) ServiceURL() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceURL", reflect.TypeOf((*MockapprunnerDescriber)(nil).ServiceURL))
}

// MockConfigStoreSvc is a mock of ConfigStoreSvc interface.
type MockConfigStoreSvc struct {
	ctrl     *gomock.Controller
	recorder *MockConfigStoreSvcMockRecorder
}

// MockConfigStoreSvcMockRecorder is the mock recorder for MockConfigStoreSvc.
type MockConfigStoreSvcMockRecorder struct {
	mock *MockConfigStoreSvc
}

// NewMockConfigStoreSvc creates a new mock instance.
func NewMockConfigStoreSvc(ctrl *gomock.Controller) *MockConfigStoreSvc {
	mock := &MockConfigStoreSvc{ctrl: ctrl}
	mock.recorder = &MockConfigStoreSvcMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConfigStoreSvc) EXPECT() *MockConfigStoreSvcMockRecorder {
	return m.recorder
}

// GetEnvironment mocks base method.
func (m *MockConfigStoreSvc) GetEnvironment(appName, environmentName string) (*config.Environment, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "GetEnvironment", appName, environmentName)
	ret0, _ := ret[0].(*config.Environment)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// GetEnvironment indicates an expected call of GetEnvironment.
func (mr *MockConfigStoreSvcMockRecorder) GetEnvironment(appName, environmentName interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironment", reflect.TypeOf((*MockConfigStoreSvc)(nil).GetEnvironment), appName, environmentName)
}

// GetWorkload mocks base method.
func (m *MockConfigStoreSvc) GetWorkload(appName, name string) (*config.Workload, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "GetWorkload", appName, name)
	ret0, _ := ret[0].(*config.Workload)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// GetWorkload indicates an expected call of GetWorkload.
func (mr *MockConfigStoreSvcMockRecorder) GetWorkload(appName, name interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkload", reflect.TypeOf((*MockConfigStoreSvc)(nil).GetWorkload), appName, name)
}

// ListEnvironments mocks base method.
func (m *MockConfigStoreSvc) ListEnvironments(appName string) ([]*config.Environment, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ListEnvironments", appName)
	ret0, _ := ret[0].([]*config.Environment)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ListEnvironments indicates an expected call of ListEnvironments.
func (mr *MockConfigStoreSvcMockRecorder) ListEnvironments(appName interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnvironments", reflect.TypeOf((*MockConfigStoreSvc)(nil).ListEnvironments), appName)
}

// ListServices mocks base method.
func (m *MockConfigStoreSvc) ListServices(appName string) ([]*config.Workload, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ListServices", appName)
	ret0, _ := ret[0].([]*config.Workload)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ListServices indicates an expected call of ListServices.
func (mr *MockConfigStoreSvcMockRecorder) ListServices(appName interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServices", reflect.TypeOf((*MockConfigStoreSvc)(nil).ListServices), appName)
}

// MockDeployedEnvServicesLister is a mock of DeployedEnvServicesLister interface.
type MockDeployedEnvServicesLister struct {
	ctrl     *gomock.Controller
	recorder *MockDeployedEnvServicesListerMockRecorder
}

// MockDeployedEnvServicesListerMockRecorder is the mock recorder for MockDeployedEnvServicesLister.
type MockDeployedEnvServicesListerMockRecorder struct {
	mock *MockDeployedEnvServicesLister
}

// NewMockDeployedEnvServicesLister creates a new mock instance.
func NewMockDeployedEnvServicesLister(ctrl *gomock.Controller) *MockDeployedEnvServicesLister {
	mock := &MockDeployedEnvServicesLister{ctrl: ctrl}
	mock.recorder = &MockDeployedEnvServicesListerMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDeployedEnvServicesLister) EXPECT() *MockDeployedEnvServicesListerMockRecorder {
	return m.recorder
}

// ListDeployedServices mocks base method.
func (m *MockDeployedEnvServicesLister) ListDeployedServices(appName, envName string) ([]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ListDeployedServices", appName, envName)
	ret0, _ := ret[0].([]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ListDeployedServices indicates an expected call of ListDeployedServices.
func (mr *MockDeployedEnvServicesListerMockRecorder) ListDeployedServices(appName, envName interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDeployedServices", reflect.TypeOf((*MockDeployedEnvServicesLister)(nil).ListDeployedServices), appName, envName)
}

// ListEnvironmentsDeployedTo mocks base method.
func (m *MockDeployedEnvServicesLister) ListEnvironmentsDeployedTo(appName, svcName string) ([]string, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ListEnvironmentsDeployedTo", appName, svcName)
	ret0, _ := ret[0].([]string)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// ListEnvironmentsDeployedTo indicates an expected call of ListEnvironmentsDeployedTo.
func (mr *MockDeployedEnvServicesListerMockRecorder) ListEnvironmentsDeployedTo(appName, svcName interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnvironmentsDeployedTo", reflect.TypeOf((*MockDeployedEnvServicesLister)(nil).ListEnvironmentsDeployedTo), appName, svcName)
}


<|start_filename|>internal/pkg/cli/job_deploy_test.go<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package cli

import (
	"errors"
	"fmt"
	"path/filepath"
	"testing"

	"github.com/aws/copilot-cli/internal/pkg/addon"
	"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
	"github.com/aws/copilot-cli/internal/pkg/docker/dockerengine"
	"github.com/spf13/afero"

	"github.com/aws/copilot-cli/internal/pkg/cli/mocks"
	"github.com/aws/copilot-cli/internal/pkg/config"
	"github.com/aws/copilot-cli/internal/pkg/manifest"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"
)

type deployJobMocks struct {
	mockWs                 *mocks.MockwsJobDirReader
	mockimageBuilderPusher *mocks.MockimageBuilderPusher
	mockInterpolator       *mocks.Mockinterpolator
	mockS3Svc              *mocks.Mockuploader
	mockAddons             *mocks.Mocktemplater
}

func TestJobDeployOpts_Validate(t *testing.T) {
	testCases := map[string]struct {
		inAppName string
		inEnvName string
		inJobName string

		mockWs    func(m *mocks.MockwsJobDirReader)
		mockStore func(m *mocks.Mockstore)

		wantedError error
	}{
		"no existing applications": {
			mockWs:    func(m *mocks.MockwsJobDirReader) {},
			mockStore: func(m *mocks.Mockstore) {},

			wantedError: errNoAppInWorkspace,
		},
		"with workspace error": {
			inAppName: "phonetool",
			inJobName: "resizer",
			mockWs: func(m *mocks.MockwsJobDirReader) {
				m.EXPECT().ListJobs().Return(nil, errors.New("some error"))
			},
			mockStore: func(m *mocks.Mockstore) {},

			wantedError: errors.New("list jobs in the workspace: some error"),
		},
		"with job not in workspace": {
			inAppName: "phonetool",
			inJobName: "resizer",
			mockWs: func(m *mocks.MockwsJobDirReader) {
				m.EXPECT().ListJobs().Return([]string{}, nil)
			},
			mockStore: func(m *mocks.Mockstore) {},

			wantedError: errors.New("job resizer not found in the workspace"),
		},
		"with unknown environment": {
			inAppName: "phonetool",
			inEnvName: "test",
			mockWs:    func(m *mocks.MockwsJobDirReader) {},
			mockStore: func(m *mocks.Mockstore) {
				m.EXPECT().GetEnvironment("phonetool", "test").
					Return(nil, errors.New("unknown env"))
			},

			wantedError: errors.New("get environment test configuration: unknown env"),
		},
		"successful validation": {
			inAppName: "phonetool",
			inJobName: "resizer",
			inEnvName: "test",
			mockWs: func(m *mocks.MockwsJobDirReader) {
				m.EXPECT().ListJobs().Return([]string{"resizer"}, nil)
			},
			mockStore: func(m *mocks.Mockstore) {
				m.EXPECT().GetEnvironment("phonetool", "test").
					Return(&config.Environment{Name: "test"}, nil)
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockWs := mocks.NewMockwsJobDirReader(ctrl)
			mockStore := mocks.NewMockstore(ctrl)
			tc.mockWs(mockWs)
			tc.mockStore(mockStore)
			opts := deployJobOpts{
				deployWkldVars: deployWkldVars{
					appName: tc.inAppName,
					name:    tc.inJobName,
					envName: tc.inEnvName,
				},
				ws:    mockWs,
				store: mockStore,
			}

			// WHEN
			err := opts.Validate()

			// THEN
			if tc.wantedError != nil {
				require.EqualError(t, err, tc.wantedError.Error())
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestJobDeployOpts_Ask(t *testing.T) {
	testCases := map[string]struct {
		inAppName  string
		inEnvName  string
		inJobName  string
		inImageTag string

		wantedCalls func(m *mocks.MockwsSelector)

		wantedJobName  string
		wantedEnvName  string
		wantedImageTag string
		wantedError    error
	}{
		"prompts for environment name and job names": {
			inAppName:  "phonetool",
			inImageTag: "latest",
			wantedCalls: func(m *mocks.MockwsSelector) {
				m.EXPECT().Job("Select a job from your workspace", "").Return("resizer", nil)
				m.EXPECT().Environment("Select an environment", "", "phonetool").Return("prod-iad", nil)
			},

			wantedJobName:  "resizer",
			wantedEnvName:  "prod-iad",
			wantedImageTag: "latest",
		},
		"don't call selector if flags are provided": {
			inAppName:  "phonetool",
			inEnvName:  "prod-iad",
			inJobName:  "resizer",
			inImageTag: "latest",
			wantedCalls: func(m *mocks.MockwsSelector) {
				m.EXPECT().Job(gomock.Any(), gomock.Any()).Times(0)
				m.EXPECT().Environment(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
			},

			wantedJobName:  "resizer",
			wantedEnvName:  "prod-iad",
			wantedImageTag: "latest",
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			mockSel := mocks.NewMockwsSelector(ctrl)

			tc.wantedCalls(mockSel)
			opts := deployJobOpts{
				deployWkldVars: deployWkldVars{
					appName:  tc.inAppName,
					name:     tc.inJobName,
					envName:  tc.inEnvName,
					imageTag: tc.inImageTag,
				},
				sel: mockSel,
			}

			// WHEN
			err := opts.Ask()

			// THEN
			if tc.wantedError == nil {
				require.NoError(t, err)
				require.Equal(t, tc.wantedJobName, opts.name)
				require.Equal(t, tc.wantedEnvName, opts.envName)
				require.Equal(t, tc.wantedImageTag, opts.imageTag)
			} else {
				require.EqualError(t, err, tc.wantedError.Error())
			}
		})
	}
}

func TestJobDeployOpts_configureContainerImage(t *testing.T) {
	mockError := errors.New("mockError")
	mockManifest := []byte(`name: mailer
type: 'Scheduled Job'
image:
  build:
    dockerfile: path/to/Dockerfile
    context: path
on:
  schedule: "@daily"`)
	mockMftNoBuild := []byte(`name: mailer
type: 'Scheduled Job'
image:
  location: foo/bar
on:
  schedule: "@daily"`)
	mockMftBuildString := []byte(`name: mailer
type: 'Scheduled Job'
image:
  build: path/to/Dockerfile
on:
  schedule: "@daily"`)
	mockMftNoContext := []byte(`name: mailer
type: 'Scheduled Job'
image:
  build:
    dockerfile: path/to/Dockerfile
on:
  schedule: "@daily"`)

	tests := map[string]struct {
		inputJob   string
		setupMocks func(mocks deployJobMocks)

		wantErr      error
		wantedDigest string
	}{
		"should return error if ws ReadFile returns error": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest("mailer").Return(nil, mockError),
				)
			},
			wantErr: fmt.Errorf("read job %s manifest: %w", "mailer", mockError),
		},
		"should return error if interpolation fail": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest(gomock.Any()).Return(mockManifest, nil),
					m.mockInterpolator.EXPECT().Interpolate(string(mockManifest)).Return("", mockError),
				)
			},
			wantErr: fmt.Errorf("interpolate environment variables for mailer manifest: %w", mockError),
		},
		"should return error if workspace methods fail": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest(gomock.Any()).Return(mockManifest, nil),
					m.mockInterpolator.EXPECT().Interpolate(string(mockManifest)).Return(string(mockManifest), nil),
					m.mockWs.EXPECT().Path().Return("", mockError),
				)
			},
			wantErr: fmt.Errorf("get workspace path: %w", mockError),
		},
		"success without building and pushing": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest("mailer").Return(mockMftNoBuild, nil),
					m.mockInterpolator.EXPECT().Interpolate(string(mockMftNoBuild)).Return(string(mockMftNoBuild), nil),
					m.mockimageBuilderPusher.EXPECT().BuildAndPush(gomock.Any(), gomock.Any()).Times(0),
				)
			},
		},
		"should return error if fail to build and push": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest("mailer").Return(mockManifest, nil),
					m.mockInterpolator.EXPECT().Interpolate(string(mockManifest)).Return(string(mockManifest), nil),
					m.mockWs.EXPECT().Path().Return("/ws/root/copilot", nil),
					m.mockimageBuilderPusher.EXPECT().BuildAndPush(gomock.Any(), gomock.Any()).Return("", mockError),
				)
			},
			wantErr: fmt.Errorf("build and push image: mockError"),
		},
		"success": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest("mailer").Return(mockManifest, nil),
					m.mockInterpolator.EXPECT().Interpolate(string(mockManifest)).Return(string(mockManifest), nil),
					m.mockWs.EXPECT().Path().Return("/ws/root", nil),
					m.mockimageBuilderPusher.EXPECT().BuildAndPush(gomock.Any(), &dockerengine.BuildArguments{
						Dockerfile: filepath.Join("/ws", "root", "path", "to", "Dockerfile"),
						Context:    filepath.Join("/ws", "root", "path"),
					}).Return("sha256:741d3e95eefa2c3b594f970a938ed6e497b50b3541a5fdc28af3ad8959e76b49", nil),
				)
			},
			wantedDigest: "sha256:741d3e95eefa2c3b594f970a938ed6e497b50b3541a5fdc28af3ad8959e76b49",
		},
		"using simple buildstring (backwards compatible)": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest("mailer").Return(mockMftBuildString, nil),
					m.mockInterpolator.EXPECT().Interpolate(string(mockMftBuildString)).Return(string(mockMftBuildString), nil),
					m.mockWs.EXPECT().Path().Return("/ws/root", nil),
					m.mockimageBuilderPusher.EXPECT().BuildAndPush(gomock.Any(), &dockerengine.BuildArguments{
						Dockerfile: filepath.Join("/ws", "root", "path", "to", "Dockerfile"),
						Context:    filepath.Join("/ws", "root", "path", "to"),
					}).Return("sha256:741d3e95eefa2c3b594f970a938ed6e497b50b3541a5fdc28af3ad8959e76b49", nil),
				)
			},
			wantedDigest: "sha256:741d3e95eefa2c3b594f970a938ed6e497b50b3541a5fdc28af3ad8959e76b49",
		},
		"without context field in overrides": {
			inputJob: "mailer",
			setupMocks: func(m deployJobMocks) {
				gomock.InOrder(
					m.mockWs.EXPECT().ReadWorkloadManifest("mailer").Return(mockMftNoContext, nil),
					m.mockInterpolator.EXPECT().Interpolate(string(mockMftNoContext)).Return(string(mockMftNoContext), nil),
					m.mockWs.EXPECT().Path().Return("/ws/root", nil),
					m.mockimageBuilderPusher.EXPECT().BuildAndPush(gomock.Any(), &dockerengine.BuildArguments{
						Dockerfile: filepath.Join("/ws", "root", "path", "to", "Dockerfile"),
						Context:    filepath.Join("/ws", "root", "path", "to"),
					}).Return("sha256:741d3e95eefa2c3b594f970a938ed6e497b50b3541a5fdc28af3ad8959e76b49", nil),
				)
			},
			wantedDigest: "sha256:741d3e95eefa2c3b594f970a938ed6e497b50b3541a5fdc28af3ad8959e76b49",
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockWorkspace := mocks.NewMockwsJobDirReader(ctrl)
			mockimageBuilderPusher := mocks.NewMockimageBuilderPusher(ctrl)
			mockInterpolator := mocks.NewMockinterpolator(ctrl)
			mocks := deployJobMocks{
				mockWs:                 mockWorkspace,
				mockimageBuilderPusher: mockimageBuilderPusher,
				mockInterpolator:       mockInterpolator,
			}
			test.setupMocks(mocks)
			opts := deployJobOpts{
				deployWkldVars: deployWkldVars{
					name: test.inputJob,
				},
				unmarshal:          manifest.UnmarshalWorkload,
				imageBuilderPusher: mockimageBuilderPusher,
				ws:                 mockWorkspace,
				newInterpolator: func(app, env string) interpolator {
					return mockInterpolator
				},
			}

			gotErr := opts.configureContainerImage()

			if test.wantErr != nil {
				require.EqualError(t, gotErr, test.wantErr.Error())
			} else {
				require.NoError(t, gotErr)
				require.Equal(t, test.wantedDigest, opts.imageDigest)
			}
		})
	}
}

func TestJobDeployOpts_pushToS3Bucket(t *testing.T) {
	const (
		mockJobName         = "mockJob"
		mockEnvFile         = "foo.env"
		mockS3Bucket        = "mockBucket"
		mockAddonsS3URL     = "https://mockS3DomainName/mockPath"
		mockBadEnvFileS3URL = "badURL"
		mockEnvFileS3URL    = "https://stackset-demo-infrastruc-pipelinebuiltartifactbuc-11dj7ctf52wyf.s3.us-west-2.amazonaws.com/manual/1638391936/env"
		mockEnvFileS3ARN    = "arn:aws:s3:::stackset-demo-infrastruc-pipelinebuiltartifactbuc-11dj7ctf52wyf/manual/1638391936/env"
	)
	mockEnvFilePath := fmt.Sprintf("%s/%s/%s", "manual", "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", mockEnvFile)
	mockError := errors.New("some error")
	tests := map[string]struct {
		inEnvFile     string
		inEnvironment *config.Environment
		inApp         *config.Application

		mock func(m *deployJobMocks)

		wantAddonsURL  string
		wantEnvFileARN string
		wantErr        error
	}{
		"error if fail to put env file to s3 bucket": {
			inEnvFile: mockEnvFile,
			mock: func(m *deployJobMocks) {
				m.mockS3Svc.EXPECT().Upload(mockS3Bucket, mockEnvFilePath, gomock.Any()).
					Return("", mockError)
			},
			wantErr: fmt.Errorf("put env file foo.env artifact to bucket mockBucket: some error"),
		},
		"error if fail to parse s3 url": {
			inEnvFile: mockEnvFile,
			mock: func(m *deployJobMocks) {
				m.mockS3Svc.EXPECT().Upload(mockS3Bucket, mockEnvFilePath, gomock.Any()).
					Return(mockBadEnvFileS3URL, nil)

			},
			wantErr: fmt.Errorf("parse s3 url: cannot parse S3 URL badURL into bucket name and key"),
		},
		"error if fail to find the partition": {
			inEnvFile: mockEnvFile,
			inEnvironment: &config.Environment{
				Region: "sun-south-0",
			},
			mock: func(m *deployJobMocks) {
				m.mockS3Svc.EXPECT().Upload(mockS3Bucket, mockEnvFilePath, gomock.Any()).
					Return(mockEnvFileS3URL, nil)
			},
			wantErr: fmt.Errorf("find the partition for region sun-south-0"),
		},
		"should push addons template to S3 bucket": {
			inEnvFile: mockEnvFile,
			inEnvironment: &config.Environment{
				Name:   "mockEnv",
				Region: "us-west-2",
			},
			inApp: &config.Application{
				Name: "mockApp",
			},
			mock: func(m *deployJobMocks) {
				m.mockS3Svc.EXPECT().Upload(mockS3Bucket, mockEnvFilePath, gomock.Any()).
					Return(mockEnvFileS3URL, nil)
				m.mockAddons.EXPECT().Template().Return("some data", nil)
				m.mockS3Svc.EXPECT().Upload(mockS3Bucket, "mockJob.addons.stack.yml", gomock.Any()).
					Return(mockAddonsS3URL, nil)
			},

			wantAddonsURL:  mockAddonsS3URL,
			wantEnvFileARN: mockEnvFileS3ARN,
		},
		"should return error if fail to upload to S3 bucket": {
			inEnvironment: &config.Environment{
				Name:   "mockEnv",
				Region: "us-west-2",
			},
			inApp: &config.Application{
				Name: "mockApp",
			},
			mock: func(m *deployJobMocks) {
				m.mockAddons.EXPECT().Template().Return("some data", nil)
				m.mockS3Svc.EXPECT().Upload(mockS3Bucket, "mockJob.addons.stack.yml", gomock.Any()).
					Return("", mockError)
			},

			wantErr: fmt.Errorf("put addons artifact to bucket mockBucket: some error"),
		},
		"should return empty url if the service doesn't have any addons and env files": {
			mock: func(m *deployJobMocks) {
				m.mockAddons.EXPECT().Template().Return("", &addon.ErrAddonsNotFound{
					WlName: "mockJob",
				})
			},
		},
		"should fail if addons cannot be retrieved from workspace": {
			mock: func(m *deployJobMocks) {
				m.mockAddons.EXPECT().Template().Return("", mockError)
			},
			wantErr: fmt.Errorf("retrieve addons template: %w", mockError),
		},
	}

	for name, tc := range tests {
		fs := afero.NewMemMapFs()
		afero.WriteFile(fs, mockEnvFile, []byte("foobar"), 0644)
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			m := &deployJobMocks{
				mockWs:     mocks.NewMockwsJobDirReader(ctrl),
				mockS3Svc:  mocks.NewMockuploader(ctrl),
				mockAddons: mocks.NewMocktemplater(ctrl),
			}
			tc.mock(m)

			opts := deployJobOpts{
				deployWkldVars: deployWkldVars{
					name: mockJobName,
				},
				addons:            m.mockAddons,
				s3:                m.mockS3Svc,
				ws:                m.mockWs,
				fs:                &afero.Afero{Fs: fs},
				appliedManifest:   &mockWorkloadMft{tc.inEnvFile},
				workspacePath:     ".",
				targetEnvironment: tc.inEnvironment,
				targetApp:         tc.inApp,
				appEnvResources: &stack.AppRegionalResources{
					S3Bucket: mockS3Bucket,
				},
			}

			gotErr := opts.pushArtifactsToS3()

			if gotErr != nil {
				require.EqualError(t, gotErr, tc.wantErr.Error())
			} else {
				require.Equal(t, tc.wantAddonsURL, opts.addonsURL)
				require.Equal(t, tc.wantEnvFileARN, opts.envFileARN)
			}
		})
	}
}


<|start_filename|>internal/pkg/describe/uri.go<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package describe

import (
	"encoding/json"
	"fmt"

	"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
	"github.com/aws/copilot-cli/internal/pkg/manifest"
	"github.com/dustin/go-humanize/english"
)

var (
	fmtSvcDiscoveryEndpointWithPort = "%s.%s:%s" // Format string of the form {svc}.{endpoint}:{port}
)

// ReachableService represents a service describe that has an endpoint.
type ReachableService interface {
	URI(env string) (string, error)
}

// NewReachableService returns a ReachableService based on the type of the service.
func NewReachableService(app, svc string, store ConfigStoreSvc) (ReachableService, error) {
	cfg, err := store.GetWorkload(app, svc)
	if err != nil {
		return nil, err
	}
	in := NewServiceConfig{
		App:         app,
		Svc:         svc,
		ConfigStore: store,
	}
	switch cfg.Type {
	case manifest.LoadBalancedWebServiceType:
		return NewLBWebServiceDescriber(in)
	case manifest.RequestDrivenWebServiceType:
		return NewRDWebServiceDescriber(in)
	case manifest.BackendServiceType:
		return NewBackendServiceDescriber(in)
	default:
		return nil, fmt.Errorf("service %s is of type %s which cannot be reached over the network", svc, cfg.Type)
	}
}

// URI returns the LBWebServiceURI to identify this service uniquely given an environment name.
func (d *LBWebServiceDescriber) URI(envName string) (string, error) {
	err := d.initClients(envName)
	if err != nil {
		return "", err
	}

	envParams, err := d.envDescriber[envName].Params()
	if err != nil {
		return "", fmt.Errorf("get stack parameters for environment %s: %w", envName, err)
	}
	envOutputs, err := d.envDescriber[envName].Outputs()
	if err != nil {
		return "", fmt.Errorf("get stack outputs for environment %s: %w", envName, err)
	}
	svcParams, err := d.ecsServiceDescribers[envName].Params()
	if err != nil {
		return "", fmt.Errorf("get stack parameters for service %s: %w", d.svc, err)
	}

	uri := &LBWebServiceURI{
		DNSNames: []string{envOutputs[envOutputPublicLoadBalancerDNSName]},
		Path:     svcParams[stack.LBWebServiceRulePathParamKey],
	}
	_, isHTTPS := envOutputs[envOutputSubdomain]
	if isHTTPS {
		dnsName := fmt.Sprintf("%s.%s", d.svc, envOutputs[envOutputSubdomain])
		uri.DNSNames = []string{dnsName}
		uri.HTTPS = true
	}
	aliases := envParams[stack.EnvParamAliasesKey]
	if aliases != "" {
		value := make(map[string][]string)
		if err := json.Unmarshal([]byte(aliases), &value); err != nil {
			return "", err
		}
		if value[d.svc] != nil {
			uri.DNSNames = value[d.svc]
		}
	}
	d.svcParams = svcParams
	return uri.String(), nil
}

// URI returns the service discovery namespace and is used to make
// BackendServiceDescriber have the same signature as WebServiceDescriber.
func (d *BackendServiceDescriber) URI(envName string) (string, error) {
	if err := d.initClients(envName); err != nil {
		return "", err
	}
	svcStackParams, err := d.ecsServiceDescribers[envName].Params()
	if err != nil {
		return "", fmt.Errorf("get stack parameters for environment %s: %w", envName, err)
	}
	port := svcStackParams[stack.LBWebServiceContainerPortParamKey]
	if port == stack.NoExposedContainerPort {
		return BlankServiceDiscoveryURI, nil
	}
	endpoint, err := d.envStackDescriber[envName].ServiceDiscoveryEndpoint()
	if err != nil {
		return "nil", fmt.Errorf("retrieve service discovery endpoint for environment %s: %w", envName, err)
	}
	s := serviceDiscovery{
		Service:  d.svc,
		Port:     port,
		Endpoint: endpoint,
	}
	return s.String(), nil
}

// URI returns the WebServiceURI to identify this service uniquely given an environment name.
func (d *RDWebServiceDescriber) URI(envName string) (string, error) {
	err := d.initClients(envName)
	if err != nil {
		return "", err
	}

	serviceURL, err := d.envSvcDescribers[envName].ServiceURL()
	if err != nil {
		return "", fmt.Errorf("get outputs for service %s: %w", d.svc, err)
	}

	return serviceURL, nil
}

// LBWebServiceURI represents the unique identifier to access a load balanced web service.
type LBWebServiceURI struct {
	HTTPS    bool
	DNSNames []string // The environment's subdomain if the service is served on HTTPS. Otherwise, the public load balancer's DNS.
	Path     string   // Empty if the service is served on HTTPS. Otherwise, the pattern used to match the service.
}

func (u *LBWebServiceURI) String() string {
	var uris []string
	for _, dnsName := range u.DNSNames {
		protocol := "http://"
		if u.HTTPS {
			protocol = "https://"
		}
		path := ""
		if u.Path != "/" {
			path = fmt.Sprintf("/%s", u.Path)
		}
		uris = append(uris, fmt.Sprintf("%s%s%s", protocol, dnsName, path))
	}
	return english.OxfordWordSeries(uris, "or")
}

type serviceDiscovery struct {
	Service  string
	Endpoint string
	Port     string
}

func (s *serviceDiscovery) String() string {
	return fmt.Sprintf(fmtSvcDiscoveryEndpointWithPort, s.Service, s.Endpoint, s.Port)
}


<|start_filename|>internal/pkg/describe/uri_test.go<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package describe

import (
	"errors"
	"fmt"
	"testing"

	"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
	"github.com/aws/copilot-cli/internal/pkg/describe/mocks"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"
)

func TestLBWebServiceDescriber_URI(t *testing.T) {
	const (
		testApp          = "phonetool"
		testEnv          = "test"
		testSvc          = "jobs"
		testEnvSubdomain = "test.phonetool.com"
		testEnvLBDNSName = "abc.us-west-1.elb.amazonaws.com"
		testSvcPath      = "/"
	)
	mockErr := errors.New("some error")
	testCases := map[string]struct {
		setupMocks func(mocks lbWebSvcDescriberMocks)

		wantedURI   string
		wantedError error
	}{
		"fail to get parameters of environment stack": {
			setupMocks: func(m lbWebSvcDescriberMocks) {
				gomock.InOrder(
					m.envDescriber.EXPECT().Params().Return(nil, mockErr),
				)
			},
			wantedError: fmt.Errorf("get stack parameters for environment test: some error"),
		},
		"fail to get outputs of environment stack": {
			setupMocks: func(m lbWebSvcDescriberMocks) {
				gomock.InOrder(
					m.envDescriber.EXPECT().Params().Return(map[string]string{}, nil),
					m.envDescriber.EXPECT().Outputs().Return(nil, mockErr),
				)
			},
			wantedError: fmt.Errorf("get stack outputs for environment test: some error"),
		},
		"fail to get parameters of service stack": {
			setupMocks: func(m lbWebSvcDescriberMocks) {
				gomock.InOrder(
					m.envDescriber.EXPECT().Params().Return(map[string]string{}, nil),
					m.envDescriber.EXPECT().Outputs().Return(map[string]string{
						envOutputPublicLoadBalancerDNSName: testEnvLBDNSName,
						envOutputSubdomain:                 testEnvSubdomain,
					}, nil),
					m.ecsDescriber.EXPECT().Params().Return(nil, mockErr),
				)
			},
			wantedError: fmt.Errorf("get stack parameters for service jobs: some error"),
		},
		"https web service": {
			setupMocks: func(m lbWebSvcDescriberMocks) {
				gomock.InOrder(
					m.envDescriber.EXPECT().Params().Return(map[string]string{}, nil),
					m.envDescriber.EXPECT().Outputs().Return(map[string]string{
						envOutputPublicLoadBalancerDNSName: testEnvLBDNSName,
						envOutputSubdomain:                 testEnvSubdomain,
					}, nil),
					m.ecsDescriber.EXPECT().Params().Return(map[string]string{
						stack.LBWebServiceRulePathParamKey: testSvcPath,
					}, nil),
				)
			},

			wantedURI: "https://jobs.test.phonetool.com",
		},
		"http web service": {
			setupMocks: func(m lbWebSvcDescriberMocks) {
				gomock.InOrder(
					m.envDescriber.EXPECT().Params().Return(map[string]string{}, nil),
					m.envDescriber.EXPECT().Outputs().Return(map[string]string{
						envOutputPublicLoadBalancerDNSName: testEnvLBDNSName,
					}, nil),
					m.ecsDescriber.EXPECT().Params().Return(map[string]string{
						stack.LBWebServiceRulePathParamKey: "*",
					}, nil),
				)
			},

			wantedURI: "http://abc.us-west-1.elb.amazonaws.com/*",
		},
		"with alias": {
			setupMocks: func(m lbWebSvcDescriberMocks) {
				gomock.InOrder(
					m.envDescriber.EXPECT().Params().Return(map[string]string{
						stack.EnvParamAliasesKey: `{"jobs": ["example.com", "v1.example.com"]}`,
					}, nil),
					m.envDescriber.EXPECT().Outputs().Return(map[string]string{
						envOutputPublicLoadBalancerDNSName: testEnvLBDNSName,
						envOutputSubdomain:                 testEnvSubdomain,
					}, nil),
					m.ecsDescriber.EXPECT().Params().Return(map[string]string{
						stack.LBWebServiceRulePathParamKey: testSvcPath,
					}, nil),
				)
			},

			wantedURI: "https://example.com or https://v1.example.com",
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockSvcDescriber := mocks.NewMockecsDescriber(ctrl)
			mockEnvDescriber := mocks.NewMockenvDescriber(ctrl)
			mocks := lbWebSvcDescriberMocks{
				ecsDescriber: mockSvcDescriber,
				envDescriber: mockEnvDescriber,
			}

			tc.setupMocks(mocks)

			d := &LBWebServiceDescriber{
				app:         testApp,
				svc:         testSvc,
				initClients: func(string) error { return nil },

				ecsServiceDescribers: map[string]ecsDescriber{
					"test": mockSvcDescriber,
				},
				envDescriber: map[string]envDescriber{
					"test": mockEnvDescriber,
				},
			}

			// WHEN
			actual, err := d.URI(testEnv)

			// THEN
			if tc.wantedError != nil {
				require.EqualError(t, err, tc.wantedError.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedURI, actual)
			}
		})
	}
}

func TestBackendServiceDescriber_URI(t *testing.T) {
	t.Run("should return a blank service discovery URI if there is no port exposed", func(t *testing.T) {
		// GIVEN
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		m := mocks.NewMockecsDescriber(ctrl)
		m.EXPECT().Params().Return(map[string]string{
			stack.LBWebServiceContainerPortParamKey: stack.NoExposedContainerPort, // No port is set for the backend service.
		}, nil)

		d := &BackendServiceDescriber{
			initClients: func(string) error { return nil },

			ecsServiceDescribers: map[string]ecsDescriber{
				"test": m,
			},
		}

		// WHEN
		actual, err := d.URI("test")

		// THEN
		require.NoError(t, err)
		require.Equal(t, BlankServiceDiscoveryURI, actual)
	})
	t.Run("should return service discovery endpoint if port is exposed", func(t *testing.T) {
		// GIVEN
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		mockSvcStack := mocks.NewMockecsDescriber(ctrl)
		mockSvcStack.EXPECT().Params().Return(map[string]string{
			stack.LBWebServiceContainerPortParamKey: "8080",
		}, nil)
		mockEnvStack := mocks.NewMockenvDescriber(ctrl)
		mockEnvStack.EXPECT().ServiceDiscoveryEndpoint().Return("test.app.local", nil)

		d := &BackendServiceDescriber{
			svc:         "hello",
			initClients: func(string) error { return nil },

			ecsServiceDescribers: map[string]ecsDescriber{
				"test": mockSvcStack,
			},
			envStackDescriber: map[string]envDescriber{
				"test": mockEnvStack,
			},
		}

		// WHEN
		actual, err := d.URI("test")

		// THEN
		require.NoError(t, err)
		require.Equal(t, "hello.test.app.local:8080", actual)
	})
}

func TestRDWebServiceDescriber_URI(t *testing.T) {
	const (
		testApp    = "phonetool"
		testEnv    = "test"
		testSvc    = "frontend"
		testSvcURL = "https://6znxd4ra33.public.us-east-1.apprunner.amazonaws.com"
	)
	mockErr := errors.New("some error")
	testCases := map[string]struct {
		setupMocks func(mocks apprunnerSvcDescriberMocks)

		wantedURI   string
		wantedError error
	}{
		"fail to get outputs of service stack": {
			setupMocks: func(m apprunnerSvcDescriberMocks) {
				gomock.InOrder(
					m.ecsSvcDescriber.EXPECT().ServiceURL().Return("", mockErr),
				)
			},
			wantedError: fmt.Errorf("get outputs for service frontend: some error"),
		},
		"succeed in getting outputs of service stack": {
			setupMocks: func(m apprunnerSvcDescriberMocks) {
				gomock.InOrder(
					m.ecsSvcDescriber.EXPECT().ServiceURL().Return(testSvcURL, nil),
				)
			},

			wantedURI: "https://6znxd4ra33.public.us-east-1.apprunner.amazonaws.com",
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockSvcDescriber := mocks.NewMockapprunnerDescriber(ctrl)
			mocks := apprunnerSvcDescriberMocks{
				ecsSvcDescriber: mockSvcDescriber,
			}

			tc.setupMocks(mocks)

			d := &RDWebServiceDescriber{
				app:         testApp,
				svc:         testSvc,
				initClients: func(string) error { return nil },

				envSvcDescribers: map[string]apprunnerDescriber{
					"test": mockSvcDescriber,
				},
			}

			// WHEN
			actual, err := d.URI(testEnv)

			// THEN
			if tc.wantedError != nil {
				require.EqualError(t, err, tc.wantedError.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedURI, actual)
			}
		})
	}
}

func TestLBWebServiceURI_String(t *testing.T) {
	testCases := map[string]struct {
		dnsNames []string
		path     string
		https    bool

		wanted string
	}{
		"http": {
			dnsNames: []string{"abc.us-west-1.elb.amazonaws.com"},
			path:     "svc",

			wanted: "http://abc.us-west-1.elb.amazonaws.com/svc",
		},
		"http with / path": {
			dnsNames: []string{"jobs.test.phonetool.com"},
			path:     "/",

			wanted: "http://jobs.test.phonetool.com",
		},
		"https": {
			dnsNames: []string{"jobs.test.phonetool.com"},
			path:     "svc",
			https:    true,

			wanted: "https://jobs.test.phonetool.com/svc",
		},
		"https with / path": {
			dnsNames: []string{"jobs.test.phonetool.com"},
			path:     "/",
			https:    true,

			wanted: "https://jobs.test.phonetool.com",
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			uri := &LBWebServiceURI{
				DNSNames: tc.dnsNames,
				Path:     tc.path,
				HTTPS:    tc.https,
			}

			require.Equal(t, tc.wanted, uri.String())
		})
	}
}


<|start_filename|>internal/pkg/cli/env_upgrade_test.go<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package cli

import (
	"errors"
	"testing"

	"github.com/aws/copilot-cli/internal/pkg/cli/mocks"
	"github.com/aws/copilot-cli/internal/pkg/config"
	"github.com/aws/copilot-cli/internal/pkg/deploy"
	"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
	"github.com/aws/copilot-cli/internal/pkg/manifest"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"
)

func TestEnvUpgradeOpts_Validate(t *testing.T) {
	testCases := map[string]struct {
		given     func(ctrl *gomock.Controller) *envUpgradeOpts
		wantedErr error
	}{
		"should not error if the environment exists and a name is provided": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				m := mocks.NewMockstore(ctrl)
				m.EXPECT().GetEnvironment("phonetool", "test").Return(nil, nil)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
					},
					store: m,
				}
			},
		},
		"should throw a config.ErrNoSuchEnvironment if the environment is not found": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				m := mocks.NewMockstore(ctrl)
				m.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).Return(nil, &config.ErrNoSuchEnvironment{
					ApplicationName: "phonetool",
					EnvironmentName: "test",
				})

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
					},
					store: m,
				}
			},
			wantedErr: &config.ErrNoSuchEnvironment{
				ApplicationName: "phonetool",
				EnvironmentName: "test",
			},
		},
		"should throw a wrapped error on unexpected config failure": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				m := mocks.NewMockstore(ctrl)
				m.EXPECT().GetEnvironment(gomock.Any(), gomock.Any()).Return(nil, errors.New("some error"))

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
					},
					store: m,
				}
			},
			wantedErr: errors.New("get environment test configuration from application phonetool: some error"),
		},
		"should not allow --all and --name": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
						all:     true,
					},
				}
			},
			wantedErr: errors.New("cannot specify both --all and --name flags"),
		},
		"should shorcircuit if only --all is provided": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						all:     true,
					},
				}
			},
			wantedErr: nil,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			opts := tc.given(ctrl)

			err := opts.Validate()

			if tc.wantedErr != nil {
				require.EqualError(t, err, tc.wantedErr.Error())
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestEnvUpgradeOpts_Ask(t *testing.T) {
	testCases := map[string]struct {
		given func(ctrl *gomock.Controller) *envUpgradeOpts

		wantedAppName string
		wantedEnvName string
		wantedErr     error
	}{
		"should prompt for application if not set": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				m := mocks.NewMockappEnvSelector(ctrl)
				m.EXPECT().Application("In which application is your environment?", "").Return("phonetool", nil)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						name: "test",
					},
					sel: m,
				}
			},
			wantedAppName: "phonetool",
			wantedEnvName: "test",
		},
		"should not prompt for environment if --all is set": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				m := mocks.NewMockappEnvSelector(ctrl)
				m.EXPECT().Application(gomock.Any(), gomock.Any()).Times(0)
				m.EXPECT().Environment(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
						all:     true,
					},
					sel: m,
				}
			},
			wantedAppName: "phonetool",
			wantedEnvName: "test",
		},
		"should prompt for environment if --all and --name is not provided": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				m := mocks.NewMockappEnvSelector(ctrl)
				m.EXPECT().Environment(
					"Which environment do you want to upgrade?",
					`Upgrades the AWS CloudFormation template for your environment
to support the latest Copilot features.`,
					"phonetool").
					Return("test", nil)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
					},
					sel: m,
				}
			},
			wantedAppName: "phonetool",
			wantedEnvName: "test",
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			opts := tc.given(ctrl)

			err := opts.Ask()

			if tc.wantedErr != nil {
				require.EqualError(t, err, tc.wantedErr.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedAppName, opts.appName)
				require.Equal(t, tc.wantedEnvName, opts.name)
			}
		})
	}
}

func TestEnvUpgradeOpts_Execute(t *testing.T) {
	testCases := map[string]struct {
		given     func(ctrl *gomock.Controller) *envUpgradeOpts
		wantedErr error
	}{
		"should skip upgrading if the environment version is already at least latest": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				mockStore := mocks.NewMockstore(ctrl)
				mockStore.EXPECT().ListEnvironments("phonetool").Return([]*config.Environment{
					{
						Name:   "test",
						Region: "us-west-2",
					},
					{
						Name:   "prod",
						Region: "us-east-1",
					},
				}, nil)
				mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil)
				mockAppCFN := mocks.NewMockappResourcesGetter(ctrl)
				mockAppCFN.EXPECT().GetAppResourcesByRegion(&config.Application{Name: "phonetool"}, "us-west-2").
					Return(&stack.AppRegionalResources{
						S3Bucket: "mockBucket",
					}, nil)
				mockAppCFN.EXPECT().GetAppResourcesByRegion(&config.Application{Name: "phonetool"}, "us-east-1").
					Return(&stack.AppRegionalResources{
						S3Bucket: "mockBucket",
					}, nil)
				mockUploader := mocks.NewMockcustomResourcesUploader(ctrl)
				mockUploader.EXPECT().UploadEnvironmentCustomResources(gomock.Any()).Return(nil, nil).Times(2)
				mockEnvTpl := mocks.NewMockversionGetter(ctrl)
				mockEnvTpl.EXPECT().Version().Return(deploy.LatestEnvTemplateVersion, nil).Times(2)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						all:     true,
					},
					store: mockStore,
					newEnvVersionGetter: func(_, _ string) (versionGetter, error) {
						return mockEnvTpl, nil
					},
					uploader: mockUploader,
					appCFN:   mockAppCFN,
					newS3: func(region string) (uploader, error) {
						return mocks.NewMockuploader(ctrl), nil
					},
				}
			},
		},
		"should upgrade non-legacy environments with UpgradeEnvironment call": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				mockEnvTpl := mocks.NewMockversionGetter(ctrl)
				mockEnvTpl.EXPECT().Version().Return("v0.1.0", nil) // Legacy versions are v0.0.0

				mockProg := mocks.NewMockprogress(ctrl)
				mockProg.EXPECT().Start(gomock.Any())
				mockProg.EXPECT().Stop(gomock.Any())

				mockStore := mocks.NewMockstore(ctrl)
				mockStore.EXPECT().GetEnvironment("phonetool", "test").
					Return(&config.Environment{
						App:              "phonetool",
						Name:             "test",
						Region:           "us-west-2",
						ExecutionRoleARN: "execARN",
						CustomConfig: &config.CustomizeEnv{
							ImportVPC: &config.ImportVPC{
								ID: "abc",
							},
						},
					}, nil)
				mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil)
				mockAppCFN := mocks.NewMockappResourcesGetter(ctrl)
				mockAppCFN.EXPECT().GetAppResourcesByRegion(&config.Application{Name: "phonetool"}, "us-west-2").
					Return(&stack.AppRegionalResources{
						S3Bucket: "mockBucket",
					}, nil)
				mockUploader := mocks.NewMockcustomResourcesUploader(ctrl)
				mockUploader.EXPECT().UploadEnvironmentCustomResources(gomock.Any()).Return(map[string]string{"mockCustomResource": "mockURL"}, nil)

				mockUpgrader := mocks.NewMockenvTemplateUpgrader(ctrl)
				mockUpgrader.EXPECT().UpgradeEnvironment(&deploy.CreateEnvironmentInput{
					Version: deploy.LatestEnvTemplateVersion,
					App: deploy.AppInformation{
						Name: "phonetool",
					},
					Name: "test",
					ImportVPCConfig: &config.ImportVPC{
						ID: "abc",
					},
					CFNServiceRoleARN:   "execARN",
					CustomResourcesURLs: map[string]string{"mockCustomResource": "mockURL"},
				}).Return(nil)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
					},
					store: mockStore,
					prog:  mockProg,
					newEnvVersionGetter: func(_, _ string) (versionGetter, error) {
						return mockEnvTpl, nil
					},
					newTemplateUpgrader: func(conf *config.Environment) (envTemplateUpgrader, error) {
						return mockUpgrader, nil
					},
					uploader: mockUploader,
					appCFN:   mockAppCFN,
					newS3: func(region string) (uploader, error) {
						return mocks.NewMockuploader(ctrl), nil
					},
				}
			},
		},
		"should upgrade default legacy environments without any VPC configuration": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				mockEnvTpl := mocks.NewMockversionGetter(ctrl)
				mockEnvTpl.EXPECT().Version().Return(deploy.LegacyEnvTemplateVersion, nil)

				mockProg := mocks.NewMockprogress(ctrl)
				mockProg.EXPECT().Start(gomock.Any())
				mockProg.EXPECT().Stop(gomock.Any())

				mockStore := mocks.NewMockstore(ctrl)
				mockStore.EXPECT().GetEnvironment("phonetool", "test").
					Return(&config.Environment{
						App:              "phonetool",
						Name:             "test",
						Region:           "us-west-2",
						ExecutionRoleARN: "execARN",
					}, nil)
				mockStore.EXPECT().ListServices("phonetool").Return([]*config.Workload{
					{
						App:  "phonetool",
						Name: "frontend",
						Type: manifest.LoadBalancedWebServiceType,
					},
					{
						App:  "phonetool",
						Name: "backend",
						Type: manifest.BackendServiceType,
					},
				}, nil)
				mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil)
				mockAppCFN := mocks.NewMockappResourcesGetter(ctrl)
				mockAppCFN.EXPECT().GetAppResourcesByRegion(&config.Application{Name: "phonetool"}, "us-west-2").
					Return(&stack.AppRegionalResources{
						S3Bucket: "mockBucket",
					}, nil)
				mockUploader := mocks.NewMockcustomResourcesUploader(ctrl)
				mockUploader.EXPECT().UploadEnvironmentCustomResources(gomock.Any()).Return(map[string]string{"mockCustomResource": "mockURL"}, nil)

				mockTemplater := mocks.NewMocktemplater(ctrl)
				mockTemplater.EXPECT().Template().Return("template", nil)

				mockUpgrader := mocks.NewMockenvTemplateUpgrader(ctrl)
				mockUpgrader.EXPECT().EnvironmentTemplate("phonetool", "test").Return("template", nil)
				mockUpgrader.EXPECT().UpgradeLegacyEnvironment(&deploy.CreateEnvironmentInput{
					Version: deploy.LatestEnvTemplateVersion,
					App: deploy.AppInformation{
						Name: "phonetool",
					},
					Name:                "test",
					CFNServiceRoleARN:   "execARN",
					CustomResourcesURLs: map[string]string{"mockCustomResource": "mockURL"},
				}, "frontend").Return(nil)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
					},
					store:              mockStore,
					legacyEnvTemplater: mockTemplater,
					prog:               mockProg,
					newEnvVersionGetter: func(_, _ string) (versionGetter, error) {
						return mockEnvTpl, nil
					},
					newTemplateUpgrader: func(conf *config.Environment) (envTemplateUpgrader, error) {
						return mockUpgrader, nil
					},
					uploader: mockUploader,
					appCFN:   mockAppCFN,
					newS3: func(region string) (uploader, error) {
						return mocks.NewMockuploader(ctrl), nil
					},
				}
			},
		},
		"should upgrade legacy environments with imported VPC": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				mockEnvTpl := mocks.NewMockversionGetter(ctrl)
				mockEnvTpl.EXPECT().Version().Return(deploy.LegacyEnvTemplateVersion, nil)

				mockProg := mocks.NewMockprogress(ctrl)
				mockProg.EXPECT().Start(gomock.Any())
				mockProg.EXPECT().Stop(gomock.Any())

				mockStore := mocks.NewMockstore(ctrl)
				mockStore.EXPECT().GetEnvironment("phonetool", "test").
					Return(&config.Environment{
						App:              "phonetool",
						Name:             "test",
						Region:           "us-west-2",
						ExecutionRoleARN: "execARN",
						CustomConfig: &config.CustomizeEnv{
							ImportVPC: &config.ImportVPC{
								ID: "abc",
							},
						},
					}, nil)
				mockStore.EXPECT().ListServices("phonetool").Return([]*config.Workload{}, nil)
				mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil)
				mockAppCFN := mocks.NewMockappResourcesGetter(ctrl)
				mockAppCFN.EXPECT().GetAppResourcesByRegion(&config.Application{Name: "phonetool"}, "us-west-2").
					Return(&stack.AppRegionalResources{
						S3Bucket: "mockBucket",
					}, nil)
				mockUploader := mocks.NewMockcustomResourcesUploader(ctrl)
				mockUploader.EXPECT().UploadEnvironmentCustomResources(gomock.Any()).Return(nil, nil)

				mockTemplater := mocks.NewMocktemplater(ctrl)
				mockTemplater.EXPECT().Template().Return("template", nil)

				mockUpgrader := mocks.NewMockenvTemplateUpgrader(ctrl)
				mockUpgrader.EXPECT().EnvironmentTemplate("phonetool", "test").Return("modified template", nil)
				mockUpgrader.EXPECT().UpgradeLegacyEnvironment(&deploy.CreateEnvironmentInput{
					Version: deploy.LatestEnvTemplateVersion,
					App: deploy.AppInformation{
						Name: "phonetool",
					},
					Name:              "test",
					CFNServiceRoleARN: "execARN",
					ImportVPCConfig: &config.ImportVPC{
						ID: "abc",
					},
				}).Return(nil)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
					},
					store:              mockStore,
					legacyEnvTemplater: mockTemplater,
					prog:               mockProg,
					newEnvVersionGetter: func(_, _ string) (versionGetter, error) {
						return mockEnvTpl, nil
					},
					newTemplateUpgrader: func(conf *config.Environment) (envTemplateUpgrader, error) {
						return mockUpgrader, nil
					},
					uploader: mockUploader,
					appCFN:   mockAppCFN,
					newS3: func(region string) (uploader, error) {
						return mocks.NewMockuploader(ctrl), nil
					},
				}
			},
		},
		"should throw an error if trying to upgrade a legacy environment with modified VPC but no SSM information": {
			given: func(ctrl *gomock.Controller) *envUpgradeOpts {
				mockEnvTpl := mocks.NewMockversionGetter(ctrl)
				mockEnvTpl.EXPECT().Version().Return(deploy.LegacyEnvTemplateVersion, nil)

				mockProg := mocks.NewMockprogress(ctrl)
				mockProg.EXPECT().Start(gomock.Any())
				mockProg.EXPECT().Stop(gomock.Any())

				mockStore := mocks.NewMockstore(ctrl)
				mockStore.EXPECT().GetEnvironment("phonetool", "test").
					Return(&config.Environment{
						App:              "phonetool",
						Name:             "test",
						Region:           "us-west-2",
						ExecutionRoleARN: "execARN",
					}, nil)
				mockStore.EXPECT().ListServices("phonetool").Return([]*config.Workload{}, nil)
				mockStore.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil)
				mockAppCFN := mocks.NewMockappResourcesGetter(ctrl)
				mockAppCFN.EXPECT().GetAppResourcesByRegion(&config.Application{Name: "phonetool"}, "us-west-2").
					Return(&stack.AppRegionalResources{
						S3Bucket: "mockBucket",
					}, nil)
				mockUploader := mocks.NewMockcustomResourcesUploader(ctrl)
				mockUploader.EXPECT().UploadEnvironmentCustomResources(gomock.Any()).Return(nil, nil)

				mockTemplater := mocks.NewMocktemplater(ctrl)
				mockTemplater.EXPECT().Template().Return("template", nil)

				mockUpgrader := mocks.NewMockenvTemplateUpgrader(ctrl)
				mockUpgrader.EXPECT().EnvironmentTemplate(gomock.Any(), gomock.Any()).Return("modified template", nil)
				mockUpgrader.EXPECT().UpgradeLegacyEnvironment(gomock.Any()).Times(0)

				return &envUpgradeOpts{
					envUpgradeVars: envUpgradeVars{
						appName: "phonetool",
						name:    "test",
					},
					store:              mockStore,
					legacyEnvTemplater: mockTemplater,
					prog:               mockProg,
					newEnvVersionGetter: func(_, _ string) (versionGetter, error) {
						return mockEnvTpl, nil
					},
					newTemplateUpgrader: func(conf *config.Environment) (envTemplateUpgrader, error) {
						return mockUpgrader, nil
					},
					uploader: mockUploader,
					appCFN:   mockAppCFN,
					newS3: func(region string) (uploader, error) {
						return mocks.NewMockuploader(ctrl), nil
					},
				}
			},
			wantedErr: errors.New("cannot upgrade environment due to missing vpc configuration"),
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			opts := tc.given(ctrl)

			err := opts.Execute()

			if tc.wantedErr != nil {
				require.EqualError(t, err, tc.wantedErr.Error())
			} else {
				require.NoError(t, err)
			}
		})
	}
}


<|start_filename|>internal/pkg/aws/ec2/ec2_test.go<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package ec2

import (
	"errors"
	"fmt"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/copilot-cli/internal/pkg/aws/ec2/mocks"
	"github.com/aws/copilot-cli/internal/pkg/deploy"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"
)

var (
	inAppEnvFilters = []Filter{
		{
			Name:   fmt.Sprintf(TagFilterName, deploy.AppTagKey),
			Values: []string{"my-app"},
		},
		{
			Name:   fmt.Sprintf(TagFilterName, deploy.EnvTagKey),
			Values: []string{"my-env"},
		},
	}

	subnet1 = &ec2.Subnet{
		SubnetId:            aws.String("subnet-1"),
		MapPublicIpOnLaunch: aws.Bool(false),
	}
	subnet2 = &ec2.Subnet{
		SubnetId:            aws.String("subnet-2"),
		MapPublicIpOnLaunch: aws.Bool(true),
	}
	subnet3 = &ec2.Subnet{
		SubnetId:            aws.String("subnet-3"),
		MapPublicIpOnLaunch: aws.Bool(true),
	}
)

func TestEC2_extractResource(t *testing.T) {
	testCases := map[string]struct {
		displayString  string
		wantedError    error
		wantedResource *Resource
	}{
		"returns error if string is empty": {
			displayString: "",
			wantedError:   fmt.Errorf("extract resource ID from string: "),
		},
		"returns just the VPC ID if no name present": {
			displayString: "vpc-imagr8vpcstring",
			wantedError:   nil,
			wantedResource: &Resource{
				ID: "vpc-imagr8vpcstring",
			},
		},
		"returns both the VPC ID and name if both present": {
			displayString: "vpc-imagr8vpcstring (copilot-app-name-env)",
			wantedError:   nil,
			wantedResource: &Resource{
				ID:   "vpc-imagr8vpcstring",
				Name: "copilot-app-name-env",
			},
		},
	}
	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			resource, err := extractResource(tc.displayString)
			if tc.wantedError != nil {
				require.EqualError(t, tc.wantedError, err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedResource, resource)
			}
		})
	}
}

func TestEC2_ListVPC(t *testing.T) {
	testCases := map[string]struct {
		mockEC2Client func(m *mocks.Mockapi)

		wantedError error
		wantedVPC   []VPC
	}{
		"fail to describe vpcs": {
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeVpcs(gomock.Any()).Return(nil, errors.New("some error"))
			},
			wantedError: fmt.Errorf("describe VPCs: some error"),
		},
		"success": {
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeVpcs(&ec2.DescribeVpcsInput{}).Return(&ec2.DescribeVpcsOutput{
					Vpcs: []*ec2.Vpc{
						{
							VpcId: aws.String("mockVPCID1"),
						},
					},
					NextToken: aws.String("mockNextToken"),
				}, nil)
				m.EXPECT().DescribeVpcs(&ec2.DescribeVpcsInput{
					NextToken: aws.String("mockNextToken"),
				}).Return(&ec2.DescribeVpcsOutput{
					Vpcs: []*ec2.Vpc{
						{
							VpcId: aws.String("mockVPCID2"),
							Tags: []*ec2.Tag{
								{
									Key:   aws.String("Name"),
									Value: aws.String("mockVPC2Name"),
								},
							},
						},
					},
				}, nil)
			},
			wantedVPC: []VPC{
				{
					Resource: Resource{
						ID: "mockVPCID1",
					},
				},
				{
					Resource: Resource{
						ID:   "mockVPCID2",
						Name: "mockVPC2Name",
					},
				},
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)

			mockAPI := mocks.NewMockapi(ctrl)
			tc.mockEC2Client(mockAPI)

			ec2Client := EC2{
				client: mockAPI,
			}

			vpcs, err := ec2Client.ListVPCs()
			if tc.wantedError != nil {
				require.EqualError(t, tc.wantedError, err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedVPC, vpcs)
			}
		})
	}
}

func TestEC2_ListAZs(t *testing.T) {
	testCases := map[string]struct {
		mockClient func(m *mocks.Mockapi)

		wantedErr string
		wantedAZs []AZ
	}{
		"return wrapped error on unexpected call error": {
			mockClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeAvailabilityZones(gomock.Any()).Return(nil, errors.New("some error"))
			},
			wantedErr: "describe availability zones: some error",
		},
		"returns AZs that are available and opted-in": {
			mockClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeAvailabilityZones(&ec2.DescribeAvailabilityZonesInput{
					Filters: []*ec2.Filter{
						{
							Name:   aws.String("zone-type"),
							Values: aws.StringSlice([]string{"availability-zone"}),
						},
						{
							Name:   aws.String("state"),
							Values: aws.StringSlice([]string{"available"}),
						},
					},
				}).Return(&ec2.DescribeAvailabilityZonesOutput{
					AvailabilityZones: []*ec2.AvailabilityZone{
						{
							GroupName:          aws.String("us-west-2"),
							NetworkBorderGroup: aws.String("us-west-2"),
							OptInStatus:        aws.String("opt-in-not-required"),
							RegionName:         aws.String("us-west-2"),
							State:              aws.String("available"),
							ZoneId:             aws.String("usw2-az1"),
							ZoneName:           aws.String("us-west-2a"),
							ZoneType:           aws.String("availability-zone"),
						},
						{
							GroupName:          aws.String("us-west-2"),
							NetworkBorderGroup: aws.String("us-west-2"),
							OptInStatus:        aws.String("opt-in-not-required"),
							RegionName:         aws.String("us-west-2"),
							State:              aws.String("available"),
							ZoneId:             aws.String("usw2-az2"),
							ZoneName:           aws.String("us-west-2b"),
							ZoneType:           aws.String("availability-zone"),
						},
					},
				}, nil)
			},
			wantedAZs: []AZ{
				{
					ID:   "usw2-az1",
					Name: "us-west-2a",
				},
				{
					ID:   "usw2-az2",
					Name: "us-west-2b",
				},
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			m := mocks.NewMockapi(ctrl)
			tc.mockClient(m)
			ec2 := EC2{client: m}

			// WHEN
			azs, err := ec2.ListAZs()

			// THEN
			if tc.wantedErr != "" {
				require.EqualError(t, err, tc.wantedErr)
			} else {
				require.NoError(t, err)
				require.ElementsMatch(t, tc.wantedAZs, azs)
			}
		})
	}
}

func TestEC2_ListVPCSubnets(t *testing.T) {
	const (
		mockVPCID     = "mockVPC"
		mockNextToken = "mockNextToken"
	)
	mockfilter := []*ec2.Filter{
		{
			Name:   aws.String("vpc-id"),
			Values: aws.StringSlice([]string{mockVPCID}),
		},
	}
	mockError := errors.New("some error")

	testCases := map[string]struct {
		mockEC2Client func(m *mocks.Mockapi)

		wantedError          error
		wantedPublicSubnets  []Subnet
		wantedPrivateSubnets []Subnet
	}{
		"fail to describe route tables": {
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeRouteTables(gomock.Any()).Return(nil, mockError)
			},
			wantedError: fmt.Errorf("describe route tables: some error"),
		},
		"fail to describe subnets": {
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeRouteTables(&ec2.DescribeRouteTablesInput{
					Filters: mockfilter,
				}).Return(&ec2.DescribeRouteTablesOutput{}, nil)
				m.EXPECT().DescribeSubnets(gomock.Any()).Return(nil, mockError)
			},
			wantedError: fmt.Errorf("describe subnets: some error"),
		},
		"success": {
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeRouteTables(&ec2.DescribeRouteTablesInput{
					Filters: mockfilter,
				}).Return(&ec2.DescribeRouteTablesOutput{
					RouteTables: []*ec2.RouteTable{
						{
							Associations: []*ec2.RouteTableAssociation{
								{
									SubnetId: aws.String("subnet1"),
								},
							},
							Routes: []*ec2.Route{
								{
									GatewayId: aws.String("local"),
								},
							},
						},
					},
					NextToken: aws.String(mockNextToken),
				}, nil)
				m.EXPECT().DescribeRouteTables(&ec2.DescribeRouteTablesInput{
					Filters:   mockfilter,
					NextToken: aws.String(mockNextToken),
				}).Return(&ec2.DescribeRouteTablesOutput{
					RouteTables: []*ec2.RouteTable{
						{
							Associations: []*ec2.RouteTableAssociation{
								{
									SubnetId: aws.String("subnet2"),
								},
								{
									SubnetId: aws.String("subnet3"),
								},
							},
							Routes: []*ec2.Route{
								{
									GatewayId: aws.String("igw-0333791c413f9e2d8"),
								},
							},
						},
					},
				}, nil)
				m.EXPECT().DescribeSubnets(&ec2.DescribeSubnetsInput{
					Filters: mockfilter,
				}).Return(&ec2.DescribeSubnetsOutput{
					Subnets: []*ec2.Subnet{
						{
							SubnetId:  aws.String("subnet1"),
							CidrBlock: aws.String("10.0.0.0/24"),
						},
						{
							SubnetId:  aws.String("subnet2"),
							CidrBlock: aws.String("10.0.1.0/24"),
						},
						{
							SubnetId: aws.String("subnet3"),
							Tags: []*ec2.Tag{
								{
									Key:   aws.String("Name"),
									Value: aws.String("mySubnet"),
								},
							},
							CidrBlock: aws.String("10.0.2.0/24"),
						},
					},
				}, nil)
			},
			wantedPublicSubnets: []Subnet{
				{
					Resource: Resource{
						ID: "subnet2",
					},
					CIDRBlock: "10.0.1.0/24",
				},
				{
					Resource: Resource{
						ID:   "subnet3",
						Name: "mySubnet",
					},
					CIDRBlock: "10.0.2.0/24",
				},
			},
			wantedPrivateSubnets: []Subnet{
				{
					Resource: Resource{
						ID: "subnet1",
					},
					CIDRBlock: "10.0.0.0/24",
				},
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)

			mockAPI := mocks.NewMockapi(ctrl)
			tc.mockEC2Client(mockAPI)

			ec2Client := EC2{
				client: mockAPI,
			}

			subnets, err := ec2Client.ListVPCSubnets(mockVPCID)
			if tc.wantedError != nil {
				require.EqualError(t, tc.wantedError, err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedPublicSubnets, subnets.Public, "public subnets must equal")
				require.Equal(t, tc.wantedPrivateSubnets, subnets.Private, "private subnets must equal")
			}
		})
	}
}

func TestEC2_PublicIP(t *testing.T) {
	testCases := map[string]struct {
		inENI         string
		mockEC2Client func(m *mocks.Mockapi)

		wantedIP  string
		wantedErr error
	}{
		"failed to describe network interfaces": {
			inENI: "eni-1",
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
					NetworkInterfaceIds: aws.StringSlice([]string{"eni-1"}),
				}).Return(nil, errors.New("some error"))
			},
			wantedErr: errors.New("describe network interface with ENI eni-1: some error"),
		},
		"no association information found": {
			inENI: "eni-1",
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
					NetworkInterfaceIds: aws.StringSlice([]string{"eni-1"}),
				}).Return(&ec2.DescribeNetworkInterfacesOutput{
					NetworkInterfaces: []*ec2.NetworkInterface{
						{},
					},
				}, nil)
			},
			wantedErr: errors.New("no association information found for ENI eni-1"),
		},
		"successfully get public ip": {
			inENI: "eni-1",
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
					NetworkInterfaceIds: aws.StringSlice([]string{"eni-1"}),
				}).Return(&ec2.DescribeNetworkInterfacesOutput{
					NetworkInterfaces: []*ec2.NetworkInterface{
						{
							Association: &ec2.NetworkInterfaceAssociation{
								PublicIp: aws.String("1.2.3"),
							},
						},
					},
				}, nil)
			},
			wantedIP: "1.2.3",
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			mockAPI := mocks.NewMockapi(ctrl)
			tc.mockEC2Client(mockAPI)

			ec2Client := EC2{
				client: mockAPI,
			}

			out, err := ec2Client.PublicIP(tc.inENI)
			if tc.wantedErr != nil {
				require.EqualError(t, tc.wantedErr, err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedIP, out)
			}
		})
	}
}

func TestEC2_SubnetIDs(t *testing.T) {
	mockNextToken := aws.String("mockNextToken")
	testCases := map[string]struct {
		inFilter []Filter

		mockEC2Client func(m *mocks.Mockapi)

		wantedError error
		wantedARNs  []string
	}{
		"failed to get subnets": {
			inFilter: inAppEnvFilters,
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeSubnets(&ec2.DescribeSubnetsInput{
					Filters: toEC2Filter(inAppEnvFilters),
				}).Return(nil, errors.New("error describing subnets"))
			},
			wantedError: fmt.Errorf("describe subnets: error describing subnets"),
		},
		"successfully get subnets": {
			inFilter: inAppEnvFilters,
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeSubnets(&ec2.DescribeSubnetsInput{
					Filters: toEC2Filter(inAppEnvFilters),
				}).Return(&ec2.DescribeSubnetsOutput{
					Subnets: []*ec2.Subnet{
						subnet1, subnet2,
					},
				}, nil)
			},
			wantedARNs: []string{"subnet-1", "subnet-2"},
		},
		"successfully get subnets with pagination": {
			inFilter: inAppEnvFilters,
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeSubnets(&ec2.DescribeSubnetsInput{
					Filters: toEC2Filter(inAppEnvFilters),
				}).Return(&ec2.DescribeSubnetsOutput{
					Subnets: []*ec2.Subnet{
						subnet1, subnet2,
					},
					NextToken: mockNextToken,
				}, nil)
				m.EXPECT().DescribeSubnets(&ec2.DescribeSubnetsInput{
					Filters:   toEC2Filter(inAppEnvFilters),
					NextToken: mockNextToken,
				}).Return(&ec2.DescribeSubnetsOutput{
					Subnets: []*ec2.Subnet{
						subnet3,
					},
				}, nil)
			},
			wantedARNs: []string{"subnet-1", "subnet-2", "subnet-3"},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)

			mockAPI := mocks.NewMockapi(ctrl)
			tc.mockEC2Client(mockAPI)

			ec2Client := EC2{
				client: mockAPI,
			}

			arns, err := ec2Client.SubnetIDs(tc.inFilter...)
			if tc.wantedError != nil {
				require.EqualError(t, tc.wantedError, err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedARNs, arns)
			}
		})
	}
}

func TestEC2_SecurityGroups(t *testing.T) {
	testCases := map[string]struct {
		inFilter []Filter

		mockEC2Client func(m *mocks.Mockapi)

		wantedError error
		wantedARNs  []string
	}{
		"failed to get security groups": {
			inFilter: inAppEnvFilters,
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{
					Filters: toEC2Filter(inAppEnvFilters),
				}).Return(nil, errors.New("error getting security groups"))
			},

			wantedError: errors.New("describe security groups: error getting security groups"),
		},
		"get security groups success": {
			inFilter: inAppEnvFilters,
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{
					Filters: toEC2Filter(inAppEnvFilters),
				}).Return(&ec2.DescribeSecurityGroupsOutput{
					SecurityGroups: []*ec2.SecurityGroup{
						{
							GroupId: aws.String("sg-1"),
						},
						{
							GroupId: aws.String("sg-2"),
						},
					},
				}, nil)
			},

			wantedARNs: []string{"sg-1", "sg-2"},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			mockAPI := mocks.NewMockapi(ctrl)
			tc.mockEC2Client(mockAPI)

			ec2Client := EC2{
				client: mockAPI,
			}

			arns, err := ec2Client.SecurityGroups(inAppEnvFilters...)
			if tc.wantedError != nil {
				require.EqualError(t, tc.wantedError, err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedARNs, arns)
			}
		})
	}
}

func TestEC2_HasDNSSupport(t *testing.T) {
	testCases := map[string]struct {
		vpcID string

		mockEC2Client func(m *mocks.Mockapi)

		wantedError   error
		wantedSupport bool
	}{
		"fail to descibe VPC attribute": {
			vpcID: "mockVPCID",
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeVpcAttribute(gomock.Any()).Return(nil, errors.New("some error"))
			},
			wantedError: fmt.Errorf("describe enableDnsSupport attribute for VPC mockVPCID: some error"),
		},
		"success": {
			vpcID: "mockVPCID",
			mockEC2Client: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeVpcAttribute(&ec2.DescribeVpcAttributeInput{
					VpcId:     aws.String("mockVPCID"),
					Attribute: aws.String(ec2.VpcAttributeNameEnableDnsSupport),
				}).Return(&ec2.DescribeVpcAttributeOutput{
					EnableDnsSupport: &ec2.AttributeBooleanValue{
						Value: aws.Bool(true),
					},
				}, nil)
			},
			wantedSupport: true,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			mockAPI := mocks.NewMockapi(ctrl)
			tc.mockEC2Client(mockAPI)

			ec2Client := EC2{
				client: mockAPI,
			}

			support, err := ec2Client.HasDNSSupport(tc.vpcID)
			if tc.wantedError != nil {
				require.EqualError(t, tc.wantedError, err.Error())
			} else {
				require.NoError(t, err)
				require.Equal(t, tc.wantedSupport, support)
			}
		})
	}
}


<|start_filename|>internal/pkg/config/env.go<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package config

import (
	"encoding/json"
	"fmt"
	"sort"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/ssm"
)

// Environment represents a deployment environment in an application.
type Environment struct {
	App              string        `json:"app"`                    // Name of the app this environment belongs to.
	Name             string        `json:"name"`                   // Name of the environment, must be unique within a App.
	Region           string        `json:"region"`                 // Name of the region this environment is stored in.
	AccountID        string        `json:"accountID"`              // Account ID of the account this environment is stored in.
	Prod             bool          `json:"prod"`                   // Whether or not this environment is a production environment.
	RegistryURL      string        `json:"registryURL"`            // URL For ECR Registry for this environment.
	ExecutionRoleARN string        `json:"executionRoleARN"`       // ARN used by CloudFormation to make modification to the environment stack.
	ManagerRoleARN   string        `json:"managerRoleARN"`         // ARN for the manager role assumed to manipulate the environment and its services.
	CustomConfig     *CustomizeEnv `json:"customConfig,omitempty"` // Custom environment configuration by users.
}

// CustomizeEnv represents the custom environment config.
type CustomizeEnv struct {
	ImportVPC *ImportVPC `json:"importVPC,omitempty"`
	VPCConfig *AdjustVPC `json:"adjustVPC,omitempty"`
}

// NewCustomizeEnv returns a new CustomizeEnv struct.
func NewCustomizeEnv(importVPC *ImportVPC, adjustVPC *AdjustVPC) *CustomizeEnv {
	if importVPC == nil && adjustVPC == nil {
		return nil
	}
	return &CustomizeEnv{
		ImportVPC: importVPC,
		VPCConfig: adjustVPC,
	}
}

// ImportVPC holds the fields to import VPC resources.
type ImportVPC struct {
	ID               string   `json:"id"` // ID for the VPC.
	PublicSubnetIDs  []string `json:"publicSubnetIDs"`
	PrivateSubnetIDs []string `json:"privateSubnetIDs"`
}

// AdjustVPC holds the fields to adjust default VPC resources.
type AdjustVPC struct {
	CIDR               string   `json:"cidr"` // CIDR range for the VPC.
	AZs                []string `json:"availabilityZoneNames"`
	PublicSubnetCIDRs  []string `json:"publicSubnetCIDRs"`
	PrivateSubnetCIDRs []string `json:"privateSubnetCIDRs"`
}

// CreateEnvironment instantiates a new environment within an existing App. Skip if
// the environment already exists in the App.
func (s *Store) CreateEnvironment(environment *Environment) error {
	if _, err := s.GetApplication(environment.App); err != nil {
		return err
	}

	environmentPath := fmt.Sprintf(fmtEnvParamPath, environment.App, environment.Name)
	data, err := marshal(environment)
	if err != nil {
		return fmt.Errorf("serializing environment %s: %w", environment.Name, err)
	}

	_, err = s.ssmClient.PutParameter(&ssm.PutParameterInput{
		Name:        aws.String(environmentPath),
		Description: aws.String(fmt.Sprintf("The %s deployment stage", environment.Name)),
		Type:        aws.String(ssm.ParameterTypeString),
		Value:       aws.String(data),
	})
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case ssm.ErrCodeParameterAlreadyExists:
				return nil
			}
		}
		return fmt.Errorf("create environment %s in application %s: %w", environment.Name, environment.App, err)
	}
	return nil
}

// GetEnvironment gets an environment belonging to a particular application by name. If no environment is found
// it returns ErrNoSuchEnvironment.
func (s *Store) GetEnvironment(appName string, environmentName string) (*Environment, error) {
	environmentPath := fmt.Sprintf(fmtEnvParamPath, appName, environmentName)
	environmentParam, err := s.ssmClient.GetParameter(&ssm.GetParameterInput{
		Name: aws.String(environmentPath),
	})

	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case ssm.ErrCodeParameterNotFound:
				return nil, &ErrNoSuchEnvironment{
					ApplicationName: appName,
					EnvironmentName: environmentName,
				}
			}
		}
		return nil, fmt.Errorf("get environment %s in application %s: %w", environmentName, appName, err)
	}

	var env Environment
	err = json.Unmarshal([]byte(*environmentParam.Parameter.Value), &env)
	if err != nil {
		return nil, fmt.Errorf("read configuration for environment %s in application %s: %w", environmentName, appName, err)
	}
	return &env, nil
}

// ListEnvironments returns all environments belonging to a particular application.
func (s *Store) ListEnvironments(appName string) ([]*Environment, error) {
	var environments []*Environment

	environmentsPath := fmt.Sprintf(rootEnvParamPath, appName)
	serializedEnvs, err := s.listParams(environmentsPath)
	if err != nil {
		return nil, fmt.Errorf("list environments for application %s: %w", appName, err)
	}
	for _, serializedEnv := range serializedEnvs {
		var env Environment
		if err := json.Unmarshal([]byte(*serializedEnv), &env); err != nil {
			return nil, fmt.Errorf("read environment configuration for application %s: %w", appName, err)
		}

		environments = append(environments, &env)
	}
	// non-prod env before prod env. sort by alphabetically if same
	sort.SliceStable(environments, func(i, j int) bool { return environments[i].Name < environments[j].Name })
	sort.SliceStable(environments, func(i, j int) bool { return !environments[i].Prod })
	return environments, nil
}

// DeleteEnvironment removes an environment from SSM.
// If the environment does not exist in the store or is successfully deleted then returns nil. Otherwise, returns an error.
func (s *Store) DeleteEnvironment(appName, environmentName string) error {
	paramName := fmt.Sprintf(fmtEnvParamPath, appName, environmentName)
	_, err := s.ssmClient.DeleteParameter(&ssm.DeleteParameterInput{
		Name: aws.String(paramName),
	})

	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case ssm.ErrCodeParameterNotFound:
				return nil
			}
		}
		return fmt.Errorf("delete environment %s from application %s: %w", environmentName, appName, err)
	}
	return nil
}


<|start_filename|>cf-custom-resources/lib/nlb-cert-manager.js<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

const AWS = require('aws-sdk');
const CRYPTO = require("crypto");
const ATTEMPTS_VALIDATION_OPTIONS_READY = 10;
const ATTEMPTS_RECORD_SETS_CHANGE = 10;
const DELAY_RECORD_SETS_CHANGE_IN_S = 30;
const ATTEMPTS_CERTIFICATE_VALIDATED = 19;
const ATTEMPTS_CERTIFICATE_NOT_IN_USE = 12;
const DELAY_CERTIFICATE_VALIDATED_IN_S = 30;

let envHostedZoneID, appName, envName, serviceName, certificateDomain, domainTypes, rootDNSRole, domainName;
let defaultSleep = function (ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
};
let sleep = defaultSleep;
let random = Math.random;

const appRoute53Context = () => {
    let client;
    return () => {
        if (!client) {
            client = new AWS.Route53({
                credentials: new AWS.ChainableTemporaryCredentials({
                    params: { RoleArn: rootDNSRole, },
                    masterCredentials: new AWS.EnvironmentCredentials("AWS"),
                }),
            });
        }
        return client;
    };
}

const envRoute53Context = () => {
    let client;
    return () => {
        if (!client) {
            client = new AWS.Route53();
        }
        return client;
    };
}

const acmContext = () => {
    let client;
    return () => {
        if (!client) {
            client = new AWS.ACM();
        }
        return client;
    };
}

const resourceGroupsTaggingAPIContext = () => {
    let client;
    return () => {
        if (!client) {
            client = new AWS.ResourceGroupsTaggingAPI();
        }
        return client;
    };
}

const clients = {
    app: {
        route53: appRoute53Context(),
    },
    root: {
        route53: appRoute53Context(),
    },
    env: {
        route53:envRoute53Context(),
    },
    acm: acmContext(),
    resourceGroupsTaggingAPI: resourceGroupsTaggingAPIContext(),
}

const appHostedZoneIDContext = () => {
    let id;
    return async () => {
        if (!id) {
            id = await hostedZoneIDByName(`${appName}.${domainName}`);
        }
        return id
    };
}

const rootHostedZoneIDContext = () => {
    let id;
    return async () => {
        if (!id) {
            id = await hostedZoneIDByName(`${domainName}`);
        }
        return id
    };
}

let hostedZoneID = {
    app: appHostedZoneIDContext(),
    root: rootHostedZoneIDContext(),
}

/**
 * Upload a CloudFormation response object to S3.
 *
 * @param {object} event the Lambda event payload received by the handler function
 * @param {object} context the Lambda context received by the handler function
 * @param {string} responseStatus the response status, either 'SUCCESS' or 'FAILED'
 * @param {string} physicalResourceId CloudFormation physical resource ID
 * @param {object} [responseData] arbitrary response data object
 * @param {string} [reason] reason for failure, if any, to convey to the user
 * @returns {Promise} Promise that is resolved on success, or rejected on connection error or HTTP error response
 */
function report (
    event,
    context,
    responseStatus,
    physicalResourceId,
    responseData,
    reason
) {
    return new Promise((resolve, reject) => {
        const https = require("https");
        const { URL } = require("url");

        let reasonWithLogInfo = `${reason} (Log: ${context.logGroupName}/${context.logStreamName})`;
        let responseBody = JSON.stringify({
            Status: responseStatus,
            Reason: reasonWithLogInfo,
            PhysicalResourceId: physicalResourceId || context.logStreamName,
            StackId: event.StackId,
            RequestId: event.RequestId,
            LogicalResourceId: event.LogicalResourceId,
            Data: responseData,
        });

        const parsedUrl = new URL(event.ResponseURL);
        const options = {
            hostname: parsedUrl.hostname,
            port: 443,
            path: parsedUrl.pathname + parsedUrl.search,
            method: "PUT",
            headers: {
                "Content-Type": "",
                "Content-Length": responseBody.length,
            },
        };

        https
            .request(options)
            .on("error", reject)
            .on("response", (res) => {
                res.resume();
                if (res.statusCode >= 400) {
                    reject(new Error(`Error ${res.statusCode}: ${res.statusMessage}`));
                } else {
                    resolve();
                }
            })
            .end(responseBody, "utf8");
    });
}

exports.handler = async function (event, context) {
    // Destruct resource properties into local variables.
    const props = event.ResourceProperties;
    let {LoadBalancerDNS: loadBalancerDNS,
        LoadBalancerHostedZoneID: loadBalancerHostedZoneID,
    } = props;
    const aliases = new Set(props.Aliases);

    // Initialize global variables.
    envHostedZoneID = props.EnvHostedZoneId;
    envName = props.EnvName;
    appName = props.AppName;
    serviceName = props.ServiceName;
    domainName = props.DomainName;
    rootDNSRole = props.RootDNSRole;
    certificateDomain = `${serviceName}-nlb.${envName}.${appName}.${domainName}`;
    domainTypes = {
        EnvDomainZone: {
            regex: new RegExp(`^([^\.]+\.)?${envName}.${appName}.${domainName}`),
            domain: `${envName}.${appName}.${domainName}`,
        },
        AppDomainZone: {
            regex: new RegExp(`^([^\.]+\.)?${appName}.${domainName}`),
            domain: `${appName}.${domainName}`,
        },
        RootDomainZone: {
            regex: new RegExp(`^([^\.]+\.)?${domainName}`),
            domain: `${domainName}`,
        },
    };

    let aliasesSorted = [...aliases].sort().join(",");
    let physicalResourceID = event.PhysicalResourceId; // The certificate ARN. By default, keep old physical resource ID unchanged.
    let handler = async function() {
        switch (event.RequestType) {
            case "Update":
                let oldAliases = new Set(event.OldResourceProperties.Aliases);
                let oldAliasesSorted = [...oldAliases].sort().join(",");
                if (oldAliasesSorted === aliasesSorted) {
                    break;
                }
                // Fallthrough to "Create". When the aliases are different, the same actions are taken for both "Update" and "Create".
            case "Create":
                await validateAliases(aliases, loadBalancerDNS);
                const certificateARN = await requestCertificate({
                    aliases: aliases,
                    idempotencyToken: CRYPTO
                        .createHash("md5")
                        .update(`/${serviceName}/${aliasesSorted}`)
                        .digest("hex")});
                physicalResourceID = certificateARN; // Update the physical resource ID if a new certificate is created.
                const options = await waitForValidationOptionsToBeReady(certificateARN, aliases);
                await activate(options, certificateARN, loadBalancerDNS, loadBalancerHostedZoneID);
                break;
            case "Delete":
                if (!physicalResourceID || !physicalResourceID.startsWith("arn:")) {
                    // This means no certificate has been created, nor any records. Exit without doing anything.
                    break;
                }
                let unusedOptions = await unusedValidationOptions(physicalResourceID, loadBalancerDNS);
                await deactivate(unusedOptions, loadBalancerDNS, loadBalancerHostedZoneID);
                await deleteCertificate(physicalResourceID);
                break;
            default:
                throw new Error(`Unsupported request type ${event.RequestType}`);
        }
    };

    try {
        await Promise.race([exports.deadlineExpired(), handler(),]);
        await report(event, context, "SUCCESS", physicalResourceID);
    } catch (err) {
        console.log(`Caught error for service ${serviceName}: ${err.message}`);
        await report(event, context, "FAILED", physicalResourceID, null, err.message);
    }
};

/**
 * Delete the certificate.
 *
 * @param certARN The ARN of the certificate to delete.
 * @returns {Promise<void>}
 */
async function deleteCertificate(certARN) {
    // NOTE: wait for certificate to be not in-used.
    let attempt;
    for (attempt = 0; attempt < ATTEMPTS_CERTIFICATE_NOT_IN_USE; attempt++) {
        let certificate;
        try {
            ({ Certificate:certificate } = await clients.acm().describeCertificate({
                CertificateArn: certARN,
            }).promise());
        } catch (err) {
            console.log(err);
            if (err.name === "ResourceNotFoundException") {
                return;
            }
            throw err;
        }

        if (!certificate.InUseBy || certificate.InUseBy.length <= 0) {
            break;
        }
        await sleep(30000);
    }

    if (attempt >= ATTEMPTS_CERTIFICATE_NOT_IN_USE) {
        throw new Error(
            `Certificate still in use after checking for ${ATTEMPTS_CERTIFICATE_NOT_IN_USE} attempts.`
        );
    }

    await clients.acm().deleteCertificate({ CertificateArn: certARN }).promise().catch(err => {
        if (err.name !== "ResourceNotFoundException") {
            throw err;
        }
    });
}

/**
 * Validate that the aliases are not in use.
 *
 * @param {Set<String>} aliases for the service.
 * @param {String} loadBalancerDNS the DNS of the service's load balancer.
 * @throws error if at least one of the aliases is not valid.
 */
async function validateAliases(aliases, loadBalancerDNS) {
    let promises = [];

    for (let alias of aliases) {
        let {hostedZoneID, route53Client } = await domainResources(alias);
        const promise = route53Client.listResourceRecordSets({
            HostedZoneId: hostedZoneID,
            MaxItems: "1",
            StartRecordName: alias,
        }).promise().then(({ ResourceRecordSets: recordSet }) => {
            if (!targetRecordExists(alias, recordSet)) {
                return;
            }
            let aliasTarget = recordSet[0].AliasTarget;
            if (aliasTarget && aliasTarget.DNSName.toLowerCase() === `${loadBalancerDNS.toLowerCase()}.`) {
                return; // The record is an alias record and is in use by myself, hence valid.
            }
            if (aliasTarget) {
                throw new Error(`Alias ${alias} is already in use by ${aliasTarget.DNSName}. This could be another load balancer of a different service.`);
            }
            throw new Error(`Alias ${alias} is already in use`);
        })
        promises.push(promise);
    }
    await Promise.all(promises);
}

/**
 * Requests a public certificate from AWS Certificate Manager, using DNS validation.
 *
 * @param {Object} requestCertificateInput is the input to requestCertificate, containing the alias and idempotencyToken.
 * @return {String} The ARN of the requested certificate.
 */
async function requestCertificate({ aliases, idempotencyToken }) {
    const { CertificateArn } = await clients.acm().requestCertificate({
        DomainName: certificateDomain,
        IdempotencyToken: idempotencyToken,
        SubjectAlternativeNames: aliases.size === 0? null: [...aliases],
        Tags: [
            {
                Key: "copilot-application",
                Value: appName,
            },
            {
                Key: "copilot-environment",
                Value: envName,
            },
            {
                Key: "copilot-service",
                Value: serviceName,
            }
        ],
        ValidationMethod: "DNS"
    }).promise();
    return CertificateArn;
}

/**
 * Wait until the validation options are ready
 *
 * @param certificateARN
 * @param {Set<String>} aliases for the service.
 */
async function waitForValidationOptionsToBeReady(certificateARN, aliases) {
    let expectedCount = aliases.size + 1; // Expect one validation option for each alias and the cert domain.

    let attempt; // TODO: This wait loops could be further abstracted.
    for (attempt = 0; attempt < ATTEMPTS_VALIDATION_OPTIONS_READY; attempt++) {
        let readyCount = 0;
        const { Certificate } = await clients.acm().describeCertificate({
            CertificateArn: certificateARN,
        }).promise();
        const options = Certificate.DomainValidationOptions || [];
        options.forEach(option => {
            if (option.ResourceRecord && (aliases.has(option.DomainName) || option.DomainName.toLowerCase() === certificateDomain.toLowerCase())) {
                readyCount++;
            }
        })
        if (readyCount === expectedCount) {
            return options;
        }

        // Exponential backoff with jitter based on 200ms base
        // component of backoff fixed to ensure minimum total wait time on
        // slow targets.
        const base = Math.pow(2, attempt);
        await sleep(random() * base * 50 + base * 150);
    }
    throw new Error(`resource validation records are not ready after ${attempt} tries`);
}

/**
 * Validate the certificate and insert the alias records
 *
 * @param {Array<Object>} validationOptions
 * @param {String} certificateARN
 * @param {String} loadBalancerDNS
 * @param {String} loadBalancerHostedZone
 */
async function activate(validationOptions, certificateARN, loadBalancerDNS, loadBalancerHostedZone) {
    let promises = [];
    for (let option of validationOptions) {
        promises.push(activateOption(option, loadBalancerDNS, loadBalancerHostedZone));
    }
    await Promise.all(promises);
    await clients.acm().waitFor("certificateValidated", {
        // Wait up to 9 minutes and 30 seconds
        $waiter: {
            delay: DELAY_CERTIFICATE_VALIDATED_IN_S,
            maxAttempts: ATTEMPTS_CERTIFICATE_VALIDATED,
        },
        CertificateArn: certificateARN,
    }).promise();
}

/**
 * Upsert the validation record for the alias, as well as adding the A record if the alias is not the default certificate domain.
 *
 * @param {Object} option
 * @param {String} loadBalancerDNS
 * @param {String} loadBalancerHostedZone
 */
async function activateOption(option, loadBalancerDNS, loadBalancerHostedZone) {
    let changes = [{
        Action: "UPSERT",
        ResourceRecordSet: {
            Name: option.ResourceRecord.Name,
            Type: option.ResourceRecord.Type,
            TTL: 60,
            ResourceRecords: [
                {
                    Value: option.ResourceRecord.Value,
                },
            ],
        }
    }];

    if (option.DomainName !== certificateDomain) {
        changes.push({
            Action: "UPSERT", // It is validated that if the alias is in use, it is in use by the service itself.
            ResourceRecordSet: {
                Name: option.DomainName,
                Type: "A",
                AliasTarget: {
                    DNSName: loadBalancerDNS,
                    EvaluateTargetHealth: true,
                    HostedZoneId: loadBalancerHostedZone,
                }
            }
        });
    }

    let {hostedZoneID, route53Client} = await domainResources(option.DomainName);
    let { ChangeInfo } = await route53Client.changeResourceRecordSets({
        ChangeBatch: {
            Comment: "Validate the certificate and create A record for the alias",
            Changes: changes,
        },
        HostedZoneId: hostedZoneID,
    }).promise();

    await route53Client.waitFor('resourceRecordSetsChanged', {
        // Wait up to 5 minutes
        $waiter: {
            delay: DELAY_RECORD_SETS_CHANGE_IN_S,
            maxAttempts: ATTEMPTS_RECORD_SETS_CHANGE,
        },
        Id: ChangeInfo.Id,
    }).promise();
}

/**
 * Retrieve validation options that will be unused by any service.
 *
 * @param {String} ownedCertARN The ARN of the certificate that this custom resource manages.
 * @param {String} loadBalancerDNS The DNS of the load balancer used by this service.
 * @returns {Promise<Set<Object>>}
 */
async function unusedValidationOptions(ownedCertARN, loadBalancerDNS) {
    // Look for validation options that will be no longer needed by this service.
    const certificates = await serviceCertificates();
    const { certOwned: certPendingDeletion, otherCerts: certInUse } = categorizeCertificates(certificates, ownedCertARN);
    if (!certPendingDeletion) {
        // Cannot find the certificate that is pending deletion; perhaps it is deleted already. Exit peacefully.
        return new Set();
    }
    let optionsPendingDeletion = await unusedOptionsByService(certPendingDeletion, certInUse);

    // For each of the options pending deletion, validate if it is in use by other services. If it is, Copilot
    // will not delete it.
    let promises = [];
    for (const option of optionsPendingDeletion) {
        const domainName = option["DomainName"];
        // NOTE: The client is initialized outside of the `inUseByOtherServices` function because AWS-SDK mocks cannot
        // mock its API calls if it is initialized in a callback.
        let route53Client;
        try {
            ({route53Client} = await domainResources(domainName));
        } catch (err) {
            // NOTE: The UnrecognizedDomainTypeError is swallowed here because it is preferably handled inside
            // `inUseByOtherServices`.
            if (!err instanceof UnrecognizedDomainTypeError) {
                throw err;
            }
        }
        const promise = inUseByOtherServices(loadBalancerDNS, domainName, route53Client).then((isUsed) => {
            if (isUsed) {
                optionsPendingDeletion.delete(option);
            }
        });
        promises.push(promise);
    }
    await Promise.all(promises);
    return optionsPendingDeletion;
}

async function deactivate(unusedOptions, loadBalancerDNS, loadBalancerHostedZoneID) {
    let promises = [];
    for (let option of unusedOptions) {
        promises.push(deactivateOption(option, loadBalancerDNS, loadBalancerHostedZoneID));
    }
    await Promise.all(promises);
}

async function deactivateOption(option, loadBalancerDNS, loadBalancerHostedZoneID) {
    let changes = [{
        Action: "DELETE",
        ResourceRecordSet: {
            Name: option.ResourceRecord.Name,
            Type: option.ResourceRecord.Type,
            TTL: 60,
            ResourceRecords: [
                {
                    Value: option.ResourceRecord.Value,
                }
            ],
        }
    }];

    if (option.DomainName !== certificateDomain) {
        changes.push({
            Action: "DELETE", // It is validated that if the alias is in use, it is in use by the service itself.
            ResourceRecordSet: {
                Name: option.DomainName,
                Type: "A",
                AliasTarget: {
                    DNSName: loadBalancerDNS,
                    EvaluateTargetHealth: true,
                    HostedZoneId: loadBalancerHostedZoneID,
                }
            }
        });
    }

    let {hostedZoneID, route53Client} = await domainResources(option.DomainName);
    let changeResourceRecordSetsInput = {
        ChangeBatch: {
            Comment: `Delete the validation record and A record for ${option.DomainName}`,
            Changes: changes,
        },
        HostedZoneId: hostedZoneID,
    }
    let changeInfo;
    try {
        ({ ChangeInfo: changeInfo } = await route53Client.changeResourceRecordSets(changeResourceRecordSetsInput).promise());
    } catch (e) {
        let recordSetNotFoundErrMessageRegex = /Tried to delete resource record set \[name='.*', type='A'] but it was not found/;
        if (e.message.search(recordSetNotFoundErrMessageRegex) !== -1) {
            return; // If we attempt to `DELETE` a record that doesn't exist, the job is already done, skip waiting.
        }
        throw new Error(`delete record ${option.ResourceRecord.Name}: ` + e.message);
    }

    await route53Client.waitFor('resourceRecordSetsChanged', {
        // Wait up to 5 minutes
        $waiter: {
            delay: DELAY_RECORD_SETS_CHANGE_IN_S,
            maxAttempts: ATTEMPTS_RECORD_SETS_CHANGE,
        },
        Id: changeInfo.Id,
    }).promise();
}

/**
 * Retrieve all certificates used for the service and cache the results.
 * @returns {Array<Object>} An array of descriptions for the certificates used by the service.
 */
async function serviceCertificates() {
    let { ResourceTagMappingList } = await clients.resourceGroupsTaggingAPI().getResources({
        TagFilters: [
            {
                Key: "copilot-application",
                Values: [appName],
            },
            {
                Key: "copilot-environment",
                Values: [envName],
            },
            {
                Key: "copilot-service",
                Values: [serviceName],
            }
        ],
        ResourceTypeFilters: ["acm:certificate"]
    }).promise();

    let certificates = [];
    let promises = [];
    for (const {ResourceARN: arn} of ResourceTagMappingList) {
        let promise = clients.acm().describeCertificate({
            CertificateArn: arn
        }).promise().then(( { Certificate } ) => {
            certificates.push(Certificate);
        });
        promises.push(promise);
    }
    await Promise.all(promises);
    return certificates;
}

/**
 * Retrieve the validation options that are pending deletion. An option is pending deletion if it is only used to
 * validate a certificate that is pending deletion.
 * @param {Object} certPendingDeletion The certificate that is pending deletion.
 * @param {Array<Object>} certsInUse
 * @returns {Promise<Set<Object>>} options that are pending deletion.
 */
async function unusedOptionsByService(certPendingDeletion, certsInUse) {
    let optionsPendingDeletion = new Map();
    for (const option of certPendingDeletion["DomainValidationOptions"]) {
        if (option["ResourceRecord"]) {
            optionsPendingDeletion.set(JSON.stringify(option["ResourceRecord"]), option);
        }
    }
    for (const { DomainValidationOptions: validationOptions } of certsInUse) {
        for (const option of validationOptions) {
            if (option["ResourceRecord"]) {
                optionsPendingDeletion.delete(JSON.stringify(option["ResourceRecord"]));
            }
        }
    }
    let options = new Set();
    for (const opt of optionsPendingDeletion.values()) {
        options.add(opt);
    }
    return options;
}

/**
 * Validate if the domain name is currently in use by other services.
 * @param loadBalancerDNS The DNS of the Network Load Balancer used by this service. The domain name in considered in use
 * by this service, not other services, if it is an alias target pointing to this service's load balancer DNS.
 * @param domainName 
 * @param route53Client The Route53 client to use for the domain name. This client can be a app-level client, or an
 * env-level client, depending on the pattern of the domain name. It is initialized outside of the function because
 * AWS-SDK mocks cannot mock the API call if the client is initialized in a callback.
 * @returns {Promise<boolean>} True if it is considered in use; otherwise false.
 */
async function inUseByOtherServices(loadBalancerDNS, domainName, route53Client) {
    let hostedZoneID;
    try {
        ({hostedZoneID} = await domainResources(domainName));
    } catch (err) {
        if (err instanceof UnrecognizedDomainTypeError) {
            console.log(`Found ${domainName} in subject alternative names. ` +
"It does not match any of these patterns: '.<env>.<app>.<domain>'， '.<app>.<domain>' or '.<domain>'. " +
"This is unexpected. We don't error out as it may not cause any issue.");
            return true; // This option has unrecognized pattern, we can't check if it is in use, so we assume it is in use.
        }
        throw err;
    }
    const { ResourceRecordSets: recordSet } = await route53Client.listResourceRecordSets({
        HostedZoneId: hostedZoneID,
        MaxItems: "1",
        StartRecordName: domainName,
    }).promise();
    if (!targetRecordExists(domainName, recordSet)) {
        return false; // If there is no record using this domain, it is not in use.
    }
    const inUseByMySelf = recordSet[0].AliasTarget && recordSet[0].AliasTarget.DNSName.toLowerCase() === `${loadBalancerDNS.toLowerCase()}.`
    return !inUseByMySelf
}

/**
 * Categorize a list of certificates into the certificate that corresponds to this particular custom resource, and other certificates.
 * @param {Array<Object>} certificates
 * @param {String} ownedCertARN The ARN of the certificate that this custom resource manages.
 * @returns {Object},Array{Object}
 */
function categorizeCertificates(certificates, ownedCertARN) {
    let certOwned;
    let otherCerts = [];
    for (const cert of certificates) {
        if (cert["CertificateArn"].toLowerCase() === ownedCertARN.toLowerCase()) {
            certOwned = cert;
        } else {
            otherCerts.push(cert);
        }
    }
    return { certOwned, otherCerts };
}

/**
 * Validate if the exact record exits in the set of records.
 * @param targetDomainName The domain name that the target record should have
 * @param recordSet
 * @returns {boolean}
 */
function targetRecordExists(targetDomainName, recordSet) {
    if (!recordSet || recordSet.length === 0) {
        return false;
    }
    return recordSet[0].Name === `${targetDomainName}.`;
}

async function hostedZoneIDByName(domain) {
    const { HostedZones } = await clients.app.route53()
        .listHostedZonesByName({
            DNSName: domain,
            MaxItems: "1",
        }).promise();
    if (!HostedZones || HostedZones.length === 0) {
        throw new Error( `Couldn't find any Hosted Zone with DNS name ${domainName}.`);
    }
    return HostedZones[0].Id.split("/").pop();
}

async function domainResources (alias) {
    if (domainTypes.EnvDomainZone.regex.test(alias)) {
        return {
            domain: domainTypes.EnvDomainZone.domain,
            route53Client: clients.env.route53(),
            hostedZoneID: envHostedZoneID,
        };
    }
    if (domainTypes.AppDomainZone.regex.test(alias)) {
        return {
            domain: domainTypes.AppDomainZone.domain,
            route53Client: clients.app.route53(),
            hostedZoneID: await hostedZoneID.app(),
        };
    }
    if (domainTypes.RootDomainZone.regex.test(alias)) {
        return {
            domain: domainTypes.RootDomainZone.domain,
            route53Client: clients.root.route53(),
            hostedZoneID: await hostedZoneID.root(),
        };
    }
    throw new UnrecognizedDomainTypeError(`unrecognized domain type for ${alias}`);
}

function setEqual(setA, setB) {
    if (setA.size !== setB.size) {
        return false;
    }

    for (let elem of setA) {
        if (!setB.has(elem)) {
            return false;
        }
    }
    return true;
}

function UnrecognizedDomainTypeError(message = "") {
    this.message = message;
}
UnrecognizedDomainTypeError.prototype = Object.create(Error.prototype, {
    constructor: {
        value: Error,
        enumerable: false,
        writable: true,
        configurable: true
    }
});


exports.deadlineExpired = function () {
    return new Promise(function (resolve, reject) {
        setTimeout(
            reject,
            14 * 60 * 1000 + 30 * 1000 /* 14.5 minutes*/,
            new Error(`Lambda took longer than 14.5 minutes to update custom domain`)
        );
    });
};

exports.withSleep = function (s) {
    sleep = s;
};
exports.reset = function () {
    sleep = defaultSleep;
};
exports.withDeadlineExpired = function (d) {
    exports.deadlineExpired = d;
};
exports.attemptsValidationOptionsReady = ATTEMPTS_VALIDATION_OPTIONS_READY;


<|start_filename|>internal/pkg/cli/env_upgrade.go<|end_filename|>
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package cli

import (
	"errors"
	"fmt"

	"github.com/aws/copilot-cli/internal/pkg/aws/s3"
	"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
	"github.com/aws/copilot-cli/internal/pkg/config"
	"github.com/aws/copilot-cli/internal/pkg/deploy"
	"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation"
	"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack"
	"github.com/aws/copilot-cli/internal/pkg/describe"
	"github.com/aws/copilot-cli/internal/pkg/manifest"
	"github.com/aws/copilot-cli/internal/pkg/template"
	"github.com/aws/copilot-cli/internal/pkg/term/color"
	"github.com/aws/copilot-cli/internal/pkg/term/log"
	termprogress "github.com/aws/copilot-cli/internal/pkg/term/progress"
	"github.com/aws/copilot-cli/internal/pkg/term/prompt"
	"github.com/aws/copilot-cli/internal/pkg/term/selector"
	"github.com/spf13/cobra"
	"golang.org/x/mod/semver"
)

const (
	envUpgradeAppPrompt = "In which application is your environment?"

	envUpgradeEnvPrompt = "Which environment do you want to upgrade?"
	envUpgradeEnvHelp   = `Upgrades the AWS CloudFormation template for your environment
to support the latest Copilot features.`

	fmtEnvUpgradeStart    = "Upgrading environment %s from version %s to version %s."
	fmtEnvUpgradeFailed   = "Failed to upgrade environment %s's template to version %s.\n"
	fmtEnvUpgradeComplete = "Upgraded environment %s's template to version %s.\n"
)

// envUpgradeVars holds flag values.
type envUpgradeVars struct {
	appName string // Required. Name of the application.
	name    string // Required. Name of the environment.
	all     bool   // True means all environments should be upgraded.
}

// envUpgradeOpts represents the env upgrade command and holds the necessary data
// and clients to execute the command.
type envUpgradeOpts struct {
	envUpgradeVars

	store              store
	sel                appEnvSelector
	legacyEnvTemplater templater
	prog               progress
	appCFN             appResourcesGetter
	uploader           customResourcesUploader

	// Constructors for clients that can be initialized only at runtime.
	// These functions are overridden in tests to provide mocks.
	newEnvVersionGetter func(app, env string) (versionGetter, error)
	newTemplateUpgrader func(conf *config.Environment) (envTemplateUpgrader, error)
	newS3               func(region string) (uploader, error)
}

func newEnvUpgradeOpts(vars envUpgradeVars) (*envUpgradeOpts, error) {
	store, err := config.NewStore()
	if err != nil {
		return nil, fmt.Errorf("connect to config store: %v", err)
	}
	defaultSession, err := sessions.NewProvider().Default()
	if err != nil {
		return nil, err
	}
	return &envUpgradeOpts{
		envUpgradeVars: vars,

		store: store,
		sel:   selector.NewSelect(prompt.New(), store),
		legacyEnvTemplater: stack.NewEnvStackConfig(&deploy.CreateEnvironmentInput{
			Version: deploy.LegacyEnvTemplateVersion,
			App: deploy.AppInformation{
				Name: vars.appName,
			},
		}),
		prog:     termprogress.NewSpinner(log.DiagnosticWriter),
		uploader: template.New(),
		appCFN:   cloudformation.New(defaultSession),

		newEnvVersionGetter: func(app, env string) (versionGetter, error) {
			d, err := describe.NewEnvDescriber(describe.NewEnvDescriberConfig{
				App:         app,
				Env:         env,
				ConfigStore: store,
			})
			if err != nil {
				return nil, fmt.Errorf("new env describer for environment %s in app %s: %v", env, app, err)
			}
			return d, nil
		},
		newTemplateUpgrader: func(conf *config.Environment) (envTemplateUpgrader, error) {
			sess, err := sessions.NewProvider().FromRole(conf.ManagerRoleARN, conf.Region)
			if err != nil {
				return nil, fmt.Errorf("create session from role %s and region %s: %v", conf.ManagerRoleARN, conf.Region, err)
			}
			return cloudformation.New(sess), nil
		},
		newS3: func(region string) (uploader, error) {
			sess, err := sessions.NewProvider().DefaultWithRegion(region)
			if err != nil {
				return nil, fmt.Errorf("create session with region %s: %v", region, err)
			}
			return s3.New(sess), nil
		},
	}, nil
}

// Validate returns an error if the values passed by flags are invalid.
func (o *envUpgradeOpts) Validate() error {
	if o.all && o.name != "" {
		return fmt.Errorf("cannot specify both --%s and --%s flags", allFlag, nameFlag)
	}
	if o.all {
		return nil
	}
	if o.name != "" {
		if _, err := o.store.GetEnvironment(o.appName, o.name); err != nil {
			var errEnvDoesNotExist *config.ErrNoSuchEnvironment
			if errors.As(err, &errEnvDoesNotExist) {
				return err
			}
			return fmt.Errorf("get environment %s configuration from application %s: %v", o.name, o.appName, err)
		}
	}
	return nil
}

// Ask prompts for any required flags that are not set by the user.
func (o *envUpgradeOpts) Ask() error {
	if o.appName == "" {
		app, err := o.sel.Application(envUpgradeAppPrompt, "")
		if err != nil {
			return fmt.Errorf("select application: %v", err)
		}
		o.appName = app
	}
	if !o.all && o.name == "" {
		env, err := o.sel.Environment(envUpgradeEnvPrompt, envUpgradeEnvHelp, o.appName)
		if err != nil {
			return fmt.Errorf("select environment: %v", err)
		}
		o.name = env
	}
	return nil
}

// Execute updates the cloudformation stack of an environment to the latest version.
// If the environment stack is busy updating, it spins and waits until the stack can be updated.
func (o *envUpgradeOpts) Execute() error {
	envs, err := o.listEnvsToUpgrade()
	if err != nil {
		return err
	}
	app, err := o.store.GetApplication(o.appName)
	if err != nil {
		return fmt.Errorf("get application %s: %w", o.appName, err)
	}
	for _, env := range envs {
		resources, err := o.appCFN.GetAppResourcesByRegion(app, env.Region)
		if err != nil {
			return fmt.Errorf("get app resources: %w", err)
		}
		s3Client, err := o.newS3(env.Region)
		if err != nil {
			return err
		}
		urls, err := o.uploader.UploadEnvironmentCustomResources(s3.CompressAndUploadFunc(func(key string, objects ...s3.NamedBinary) (string, error) {
			return s3Client.ZipAndUpload(resources.S3Bucket, key, objects...)
		}))
		if err != nil {
			return fmt.Errorf("upload custom resources to bucket %s: %w", resources.S3Bucket, err)
		}
		if err := o.upgrade(env, urls); err != nil {
			return err
		}
	}
	return nil
}

// RecommendActions is a no-op for this command.
func (o *envUpgradeOpts) RecommendActions() error {
	return nil
}

func (o *envUpgradeOpts) listEnvsToUpgrade() ([]*config.Environment, error) {
	if !o.all {
		env, err := o.store.GetEnvironment(o.appName, o.name)
		if err != nil {
			return nil, fmt.Errorf("get environment %s in app %s: %w", o.appName, o.name, err)
		}
		return []*config.Environment{env}, nil
	}

	envs, err := o.store.ListEnvironments(o.appName)
	if err != nil {
		return nil, fmt.Errorf("list environments in app %s: %w", o.appName, err)
	}
	return envs, nil
}

func (o *envUpgradeOpts) upgrade(env *config.Environment, customResourcesURLs map[string]string) (err error) {
	version, err := o.envVersion(env.Name)
	if err != nil {
		return err
	}
	if !shouldUpgradeEnv(env.Name, version) {
		return nil
	}

	o.prog.Start(fmt.Sprintf(fmtEnvUpgradeStart, color.HighlightUserInput(env.Name), color.Emphasize(version), color.Emphasize(deploy.LatestEnvTemplateVersion)))
	defer func() {
		if err != nil {
			o.prog.Stop(log.Serrorf(fmtEnvUpgradeFailed, color.HighlightUserInput(env.Name), color.Emphasize(deploy.LatestEnvTemplateVersion)))
			return
		}
		o.prog.Stop(log.Ssuccessf(fmtEnvUpgradeComplete, color.HighlightUserInput(env.Name), color.Emphasize(deploy.LatestEnvTemplateVersion)))
	}()
	upgrader, err := o.newTemplateUpgrader(env)
	if err != nil {
		return err
	}
	if version == deploy.LegacyEnvTemplateVersion {
		return o.upgradeLegacyEnvironment(upgrader, env, customResourcesURLs, version, deploy.LatestEnvTemplateVersion)
	}
	return o.upgradeEnvironment(upgrader, env, customResourcesURLs, version, deploy.LatestEnvTemplateVersion)
}

func (o *envUpgradeOpts) envVersion(name string) (string, error) {
	envTpl, err := o.newEnvVersionGetter(o.appName, name)
	if err != nil {
		return "", err
	}
	version, err := envTpl.Version()
	if err != nil {
		return "", fmt.Errorf("get template version of environment %s in app %s: %v", name, o.appName, err)
	}
	return version, err
}

func shouldUpgradeEnv(env, version string) bool {
	diff := semver.Compare(version, deploy.LatestEnvTemplateVersion)
	if diff < 0 {
		// Newer version available.
		return true
	}

	msg := fmt.Sprintf("Environment %s is already on the latest version %s, skip upgrade.", env, deploy.LatestEnvTemplateVersion)
	if diff > 0 {
		// It's possible that a teammate used a different version of the CLI to upgrade the environment
		// to a newer version. And the current user is on an older version of the CLI.
		// In this situation we notify them they should update the CLI.
		msg = fmt.Sprintf(`Skip upgrading environment %s to version %s since it's on version %s. 
Are you using the latest version of AWS Copilot?`, env, deploy.LatestEnvTemplateVersion, version)
	}
	log.Debugln(msg)
	return false
}

func (o *envUpgradeOpts) upgradeEnvironment(upgrader envUpgrader, conf *config.Environment,
	customResourcesURLs map[string]string, fromVersion, toVersion string) error {
	var importedVPC *config.ImportVPC
	var adjustedVPC *config.AdjustVPC
	if conf.CustomConfig != nil {
		importedVPC = conf.CustomConfig.ImportVPC
		adjustedVPC = conf.CustomConfig.VPCConfig
	}

	if err := upgrader.UpgradeEnvironment(&deploy.CreateEnvironmentInput{
		Version: toVersion,
		App: deploy.AppInformation{
			Name: conf.App,
		},
		Name:                conf.Name,
		CustomResourcesURLs: customResourcesURLs,
		ImportVPCConfig:     importedVPC,
		AdjustVPCConfig:     adjustedVPC,
		CFNServiceRoleARN:   conf.ExecutionRoleARN,
	}); err != nil {
		return fmt.Errorf("upgrade environment %s from version %s to version %s: %v", conf.Name, fromVersion, toVersion, err)
	}
	return nil
}

func (o *envUpgradeOpts) upgradeLegacyEnvironment(upgrader legacyEnvUpgrader, conf *config.Environment,
	customResourcesURLs map[string]string, fromVersion, toVersion string) error {
	isDefaultEnv, err := o.isDefaultLegacyTemplate(upgrader, conf.App, conf.Name)
	if err != nil {
		return err
	}
	albWorkloads, err := o.listLBWebServices()
	if err != nil {
		return err
	}
	if isDefaultEnv {
		if err := upgrader.UpgradeLegacyEnvironment(&deploy.CreateEnvironmentInput{
			Version: toVersion,
			App: deploy.AppInformation{
				Name: conf.App,
			},
			Name:                conf.Name,
			CustomResourcesURLs: customResourcesURLs,
			CFNServiceRoleARN:   conf.ExecutionRoleARN,
		}, albWorkloads...); err != nil {
			return fmt.Errorf("upgrade environment %s from version %s to version %s: %v", conf.Name, fromVersion, toVersion, err)
		}
		return nil
	}
	return o.upgradeLegacyEnvironmentWithVPCOverrides(upgrader, conf, fromVersion, toVersion, albWorkloads)
}

func (o *envUpgradeOpts) isDefaultLegacyTemplate(cfn envTemplater, appName, envName string) (bool, error) {
	defaultLegacyEnvTemplate, err := o.legacyEnvTemplater.Template()
	if err != nil {
		return false, fmt.Errorf("generate default legacy environment template: %v", err)
	}
	actualTemplate, err := cfn.EnvironmentTemplate(appName, envName)
	if err != nil {
		return false, fmt.Errorf("get environment %s template body: %v", envName, err)
	}
	return defaultLegacyEnvTemplate == actualTemplate, nil
}

func (o *envUpgradeOpts) listLBWebServices() ([]string, error) {
	services, err := o.store.ListServices(o.appName)
	if err != nil {
		return nil, fmt.Errorf("list services in application %s: %v", o.appName, err)
	}
	var lbWebServiceNames []string
	for _, svc := range services {
		if svc.Type != manifest.LoadBalancedWebServiceType {
			continue
		}
		lbWebServiceNames = append(lbWebServiceNames, svc.Name)
	}
	return lbWebServiceNames, nil
}

func (o *envUpgradeOpts) upgradeLegacyEnvironmentWithVPCOverrides(upgrader legacyEnvUpgrader, conf *config.Environment,
	fromVersion, toVersion string, albWorkloads []string) error {
	if conf.CustomConfig != nil {
		if err := upgrader.UpgradeLegacyEnvironment(&deploy.CreateEnvironmentInput{
			Version: toVersion,
			App: deploy.AppInformation{
				Name: conf.App,
			},
			Name:              conf.Name,
			ImportVPCConfig:   conf.CustomConfig.ImportVPC,
			AdjustVPCConfig:   conf.CustomConfig.VPCConfig,
			CFNServiceRoleARN: conf.ExecutionRoleARN,
		}, albWorkloads...); err != nil {
			return fmt.Errorf("upgrade environment %s from version %s to version %s: %v", conf.Name, fromVersion, toVersion, err)
		}
		return nil
	}
	// Prior to #1433, we did not store the custom VPC config in SSM.
	// In this situation we unfortunately have to ask the users to enter the VPC configuration into SSM or re-create the
	// environment in case they run into this issue.
	log.Warningln(`
Looks like you've an environment with a customized VPC configuration.
Copilot could not upgrade your environment's CloudFormation template.
To learn more about how to fix it: https://github.com/aws/copilot-cli/issues/1601`)
	return errors.New("cannot upgrade environment due to missing vpc configuration")
}

// buildEnvUpgradeCmd builds the command to update environment(s) to the latest version of
// the environment template.
func buildEnvUpgradeCmd() *cobra.Command {
	vars := envUpgradeVars{}
	cmd := &cobra.Command{
		Use:    "upgrade",
		Short:  "Upgrades the template of an environment to the latest version.",
		Hidden: true,
		RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
			opts, err := newEnvUpgradeOpts(vars)
			if err != nil {
				return err
			}
			return run(opts)
		}),
	}
	cmd.Flags().StringVarP(&vars.name, nameFlag, nameFlagShort, "", envFlagDescription)
	cmd.Flags().StringVarP(&vars.appName, appFlag, appFlagShort, tryReadingAppName(), appFlagDescription)
	cmd.Flags().BoolVar(&vars.all, allFlag, false, upgradeAllEnvsDescription)
	return cmd
}
