diff --git a/internal/pkg/cli/env_init.go b/internal/pkg/cli/env_init.go index 4e7f0b4a4dc..9ded7b24295 100644 --- a/internal/pkg/cli/env_init.go +++ b/internal/pkg/cli/env_init.go @@ -176,6 +176,7 @@ type initEnvOpts struct { selApp appSelector appCFN appResourcesGetter manifestWriter environmentManifestWriter + envLister wsEnvironmentsLister sess *session.Session // Session pointing to environment's AWS account and region. @@ -224,6 +225,7 @@ func newInitEnvOpts(vars initEnvVars) (*initEnvOpts, error) { selApp: selector.NewAppEnvSelector(prompt.New(), store), appCFN: deploycfn.New(defaultSession, deploycfn.WithProgressTracker(os.Stderr)), manifestWriter: ws, + envLister: ws, wsAppName: tryReadingAppName(), templateVersion: version.LatestTemplateVersion(), @@ -661,6 +663,15 @@ func (o *initEnvOpts) askAZs() ([]string, error) { func (o *initEnvOpts) validateDuplicateEnv() error { _, err := o.store.GetEnvironment(o.appName, o.name) if err == nil { + // Skip error if environment already exists in workspace + envs, err := o.envLister.ListEnvironments() + if err != nil { + return err + } + if contains(o.name, envs) { + return nil + } + dir := filepath.Join("copilot", "environments", o.name) log.Infof(`It seems like you are trying to init an environment that already exists. To generate a manifest for the environment: diff --git a/internal/pkg/cli/env_init_test.go b/internal/pkg/cli/env_init_test.go index 5aefa86de6f..55719053d12 100644 --- a/internal/pkg/cli/env_init_test.go +++ b/internal/pkg/cli/env_init_test.go @@ -38,6 +38,7 @@ type initEnvMocks struct { ec2Client *mocks.Mockec2Client selApp *mocks.MockappSelector store *mocks.Mockstore + envLister *mocks.MockwsEnvironmentsLister wsAppName string } @@ -111,9 +112,21 @@ func TestInitEnvOpts_Validate(t *testing.T) { m.wsAppName = "phonetool" m.store.EXPECT().GetApplication("phonetool").Return(nil, nil) m.store.EXPECT().GetEnvironment("phonetool", "test-pdx").Return(nil, nil) + m.envLister.EXPECT().ListEnvironments().Return([]string{}, nil) }, wantedErrMsg: "environment test-pdx already exists", }, + "should skip error if environment already exists in current workspace": { + inEnvName: "test-pdx", + inAppName: "phonetool", + + setupMocks: func(m *initEnvMocks) { + m.wsAppName = "phonetool" + m.store.EXPECT().GetApplication("phonetool").Return(nil, nil) + m.store.EXPECT().GetEnvironment("phonetool", "test-pdx").Return(nil, nil) + m.envLister.EXPECT().ListEnvironments().Return([]string{"test-pdx"}, nil) + }, + }, "cannot specify both vpc resources importing flags and configuring flags": { inEnvName: "test-pdx", inAppName: "phonetool", @@ -270,7 +283,8 @@ func TestInitEnvOpts_Validate(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() m := &initEnvMocks{ - store: mocks.NewMockstore(ctrl), + store: mocks.NewMockstore(ctrl), + envLister: mocks.NewMockwsEnvironmentsLister(ctrl), } if tc.setupMocks != nil { tc.setupMocks(m) @@ -302,6 +316,7 @@ func TestInitEnvOpts_Validate(t *testing.T) { }, store: m.store, wsAppName: m.wsAppName, + envLister: m.envLister, } // WHEN @@ -380,10 +395,30 @@ func TestInitEnvOpts_Ask(t *testing.T) { Get(envInitNamePrompt, envInitNameHelpPrompt, gomock.Any(), gomock.Any()). Return("test", nil), m.store.EXPECT().GetEnvironment(mockApp, mockEnv).Return(nil, nil), + m.envLister.EXPECT().ListEnvironments().Return([]string{}, nil), ) }, wantedError: errors.New("environment test already exists"), }, + "should skip error if environment already exists in workspace": { + inAppName: mockApp, + inProfile: mockProfile, + inDefault: true, + setupMocks: func(m initEnvMocks) { + gomock.InOrder( + m.prompt.EXPECT(). + Get(envInitNamePrompt, envInitNameHelpPrompt, gomock.Any(), gomock.Any()). + Return("test", nil), + m.store.EXPECT().GetEnvironment(mockApp, mockEnv).Return(nil, nil), + m.envLister.EXPECT().ListEnvironments().Return([]string{mockEnv}, nil), + m.sessProvider.EXPECT().FromProfile(mockProfile).Return(&session.Session{ + Config: &aws.Config{ + Region: aws.String("us-west-2"), + }, + }, nil).AnyTimes(), + ) + }, + }, "should create a session from a named profile if flag is provided": { inAppName: mockApp, inEnv: mockEnv, @@ -920,6 +955,7 @@ func TestInitEnvOpts_Ask(t *testing.T) { ec2Client: mocks.NewMockec2Client(ctrl), selApp: mocks.NewMockappSelector(ctrl), store: mocks.NewMockstore(ctrl), + envLister: mocks.NewMockwsEnvironmentsLister(ctrl), } tc.setupMocks(mocks) @@ -948,6 +984,7 @@ func TestInitEnvOpts_Ask(t *testing.T) { prompt: mocks.prompt, selApp: mocks.selApp, store: mocks.store, + envLister: mocks.envLister, } // WHEN diff --git a/internal/pkg/cli/init.go b/internal/pkg/cli/init.go index dc44683193b..7549354d60d 100644 --- a/internal/pkg/cli/init.go +++ b/internal/pkg/cli/init.go @@ -210,6 +210,7 @@ func newInitOpts(vars initVars) (*initOpts, error) { } sel := selector.NewLocalWorkloadSelector(prompt, configStore, ws, selector.OnlyInitializedWorkloads) initEnvCmd.manifestWriter = ws + initEnvCmd.envLister = ws deployEnvCmd.ws = ws deployEnvCmd.newEnvDeployer = func() (envDeployer, error) { return newEnvDeployer(deployEnvCmd, ws) @@ -218,14 +219,19 @@ func newInitOpts(vars initVars) (*initOpts, error) { deploySvcCmd.sel = sel deployJobCmd.ws = ws deployJobCmd.sel = sel - if initWkCmd, ok := o.initWlCmd.(*initSvcOpts); ok { - initWkCmd.init = &initialize.WorkloadInitializer{Store: configStore, Ws: ws, Prog: spin, Deployer: deployer} + if initSvcCmd, ok := o.initWlCmd.(*initSvcOpts); ok { + initSvcCmd.init = &initialize.WorkloadInitializer{Store: configStore, Ws: ws, Prog: spin, Deployer: deployer} } - if initWkCmd, ok := o.initWlCmd.(*initJobOpts); ok { - initWkCmd.init = &initialize.WorkloadInitializer{Store: configStore, Ws: ws, Prog: spin, Deployer: deployer} + if initJobCmd, ok := o.initWlCmd.(*initJobOpts); ok { + initJobCmd.init = &initialize.WorkloadInitializer{Store: configStore, Ws: ws, Prog: spin, Deployer: deployer} } return nil } + ws, err := workspace.Use(fs) + var errWorkspaceNotFound *workspace.ErrWorkspaceNotFound + if err != nil && !errors.As(err, &errWorkspaceNotFound) { + return nil, err + } return &initOpts{ initVars: vars, @@ -292,6 +298,11 @@ func newInitOpts(vars initVars) (*initOpts, error) { return envDescriber, nil }, } + if ws != nil { + opts.mftReader = ws + opts.wsAppName = initAppCmd.name + opts.wsPendingCreation = false + } o.initWlCmd = &opts o.schedule = &opts.schedule // Surfaced via pointer for logging o.initWkldVars = &opts.initWkldVars @@ -334,6 +345,13 @@ func newInitOpts(vars initVars) (*initOpts, error) { } return envDescriber, nil } + if ws != nil { + opts.svcLister = ws + opts.mftReader = ws + opts.wsAppName = initAppCmd.name + opts.wsRoot = ws.ProjectRoot() + opts.wsPendingCreation = false + } o.initWlCmd = &opts o.port = &opts.port // Surfaced via pointer for logging. o.initWkldVars = &opts.initWkldVars @@ -398,6 +416,7 @@ func (o *initOpts) deploy() error { } return o.deploySvc() } + func (o *initOpts) loadApp() error { if err := o.initAppCmd.Ask(); err != nil { return fmt.Errorf("ask app init: %w", err) @@ -419,7 +438,6 @@ func (o *initOpts) loadWkld() error { if err := o.initWlCmd.Ask(); err != nil { return fmt.Errorf("ask %s: %w", o.wkldType, err) } - return nil } @@ -431,7 +449,6 @@ func (o *initOpts) loadWkldCmd() error { if err := o.setupWorkloadInit(o, wkldType); err != nil { return err } - return nil } diff --git a/internal/pkg/cli/init_test.go b/internal/pkg/cli/init_test.go index 7aede8d6623..d367c87f67e 100644 --- a/internal/pkg/cli/init_test.go +++ b/internal/pkg/cli/init_test.go @@ -6,9 +6,10 @@ package cli import ( "errors" "fmt" + "testing" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/copilot-cli/internal/pkg/config" - "testing" awscfn "github.com/aws/copilot-cli/internal/pkg/aws/cloudformation" "github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo" diff --git a/internal/pkg/cli/svc_init.go b/internal/pkg/cli/svc_init.go index 0c079b25177..b27c98f953e 100644 --- a/internal/pkg/cli/svc_init.go +++ b/internal/pkg/cli/svc_init.go @@ -150,6 +150,7 @@ type initSvcOpts struct { sourceSel staticSourceSelector topicSel topicSelector mftReader manifestReader + svcLister wlLister // Outputs stored on successful actions. manifestPath string @@ -218,6 +219,7 @@ func newInitSvcOpts(vars initSvcVars) (*initSvcOpts, error) { topicSel: snsSel, sourceSel: sourceSel, mftReader: ws, + svcLister: ws, newAppVersionGetter: func(appName string) (versionGetter, error) { return describe.NewAppDescriber(appName) }, @@ -486,6 +488,16 @@ func (o *initSvcOpts) validateSvc() error { func (o *initSvcOpts) validateDuplicateSvc() error { _, err := o.store.GetService(o.appName, o.name) if err == nil { + // Skip error if service already exists in workspace + if !o.wsPendingCreation { + svcs, err := o.svcLister.ListWorkloads() + if err != nil { + return err + } + if contains(o.name, svcs) { + return nil + } + } log.Errorf(`It seems like you are trying to init a service that already exists. To recreate the service, please run: 1. %s. Note: The manifest file will not be deleted and will be used in Step 2. diff --git a/internal/pkg/cli/svc_init_test.go b/internal/pkg/cli/svc_init_test.go index 081e7f9be83..af76608a4e3 100644 --- a/internal/pkg/cli/svc_init_test.go +++ b/internal/pkg/cli/svc_init_test.go @@ -35,6 +35,7 @@ type initSvcMocks struct { mockDockerEngine *mocks.MockdockerEngine mockMftReader *mocks.MockmanifestReader mockStore *mocks.Mockstore + mockSvcLister *mocks.MockwlLister mockCachedWSRoot string } @@ -346,9 +347,25 @@ func TestSvcInitOpts_Ask(t *testing.T) { m.mockPrompt.EXPECT().Get(gomock.Eq("What do you want to name this service?"), gomock.Any(), gomock.Any(), gomock.Any()). Return(wantedSvcName, nil) m.mockStore.EXPECT().GetService(mockAppName, wantedSvcName).Return(&config.Workload{}, nil) + m.mockSvcLister.EXPECT().ListWorkloads().Return([]string{}, nil) }, wantedErr: fmt.Errorf("service frontend already exists"), }, + "skip error if service already exists within workspace": { + inSvcType: wantedSvcType, + inSvcName: "", + inSvcPort: wantedSvcPort, + inDockerfilePath: wantedDockerfilePath, + + setupMocks: func(m *initSvcMocks) { + m.mockPrompt.EXPECT().Get(gomock.Eq("What do you want to name this service?"), gomock.Any(), gomock.Any(), gomock.Any()). + Return(wantedSvcName, nil) + m.mockStore.EXPECT().GetService(mockAppName, wantedSvcName).Return(&config.Workload{}, nil) + m.mockSvcLister.EXPECT().ListWorkloads().Return([]string{wantedSvcName}, nil) + m.mockMftReader.EXPECT().ReadWorkloadManifest(wantedSvcName).Return([]byte(` +type: Load Balanced Web Service`), nil) + }, + }, "returns an error if fail to validate service existence": { inSvcType: wantedSvcType, inSvcName: "", @@ -843,6 +860,7 @@ type: Request-Driven Web Service`), nil) mockDockerEngine: mocks.NewMockdockerEngine(ctrl), mockMftReader: mocks.NewMockmanifestReader(ctrl), mockStore: mocks.NewMockstore(ctrl), + mockSvcLister: mocks.NewMockwlLister(ctrl), } if tc.setupMocks != nil { tc.setupMocks(&m) @@ -870,6 +888,7 @@ type: Request-Driven Web Service`), nil) df: m.mockDockerfile, prompt: m.mockPrompt, mftReader: m.mockMftReader, + svcLister: m.mockSvcLister, sel: m.mockSel, topicSel: m.mocktopicSel, sourceSel: m.mockSourceSel, @@ -1341,7 +1360,6 @@ network: if tc.mockStore != nil { tc.mockStore(mockStore) } - if tc.mockSvcInit != nil { tc.mockSvcInit(mockSvcInitializer) }