Skip to content

Commit

Permalink
fix(cli): make env init idempotent (#763)
Browse files Browse the repository at this point in the history
* fix(cli): make env init idempotent

* chore: refactoring and integ test changes
  • Loading branch information
iamhopaul123 authored Mar 20, 2020
1 parent e84f14d commit de49678
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 206 deletions.
72 changes: 46 additions & 26 deletions internal/pkg/cli/env_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
fmtDeployEnvFailed = "Failed to accept changes for the %s environment."
fmtDNSDelegationStart = "Sharing DNS permissions for this project to account %s."
fmtDNSDelegationFailed = "Failed to grant DNS permissions to account %s."
fmtDNSDelegationComplete = "Shared DNS permissions for this project to account %s."
fmtStreamEnvStart = "Creating the infrastructure for the %s environment."
fmtStreamEnvFailed = "Failed to create the infrastructure for the %s environment."
fmtStreamEnvComplete = "Created the infrastructure for the %s environment."
Expand Down Expand Up @@ -136,16 +137,45 @@ func (o *initEnvOpts) Execute() error {
// Ensure the project actually exists before we do a deployment.
return err
}
caller, err := o.identity.Get()
if err != nil {
return fmt.Errorf("get identity: %w", err)
if project.RequiresDNSDelegation() {
if err := o.delegateDNSFromProject(project); err != nil {
return fmt.Errorf("granting DNS permissions: %w", err)
}
}

if err = o.initProfileClients(o); err != nil {
return err
}

// 1. Start creating the CloudFormation stack for the environment.
if err := o.deployEnv(project); err != nil {
return err
}

// 2. Get the environment
env, err := o.envDeployer.GetEnvironment(o.ProjectName(), o.EnvName)
if err != nil {
return fmt.Errorf("get environment struct for %s: %w", o.EnvName, err)
}

// 3. Add the stack set instance to the project stackset.
if err := o.addToStackset(project, env); err != nil {
return err
}

// 4. Store the environment in SSM.
if err := o.envCreator.CreateEnvironment(env); err != nil {
return fmt.Errorf("store environment: %w", err)
}
log.Successf("Created environment %s in region %s under project %s.\n",
color.HighlightUserInput(env.Name), color.HighlightResource(env.Region), color.HighlightResource(env.Project))
return nil
}

func (o *initEnvOpts) deployEnv(project *archer.Project) error {
caller, err := o.identity.Get()
if err != nil {
return fmt.Errorf("get identity: %w", err)
}
deployEnvInput := &deploy.CreateEnvironmentInput{
Name: o.EnvName,
Project: o.ProjectName(),
Expand All @@ -155,28 +185,21 @@ func (o *initEnvOpts) Execute() error {
ProjectDNSName: project.Domain,
}

if project.RequiresDNSDelegation() {
if err := o.delegateDNSFromProject(project); err != nil {
return fmt.Errorf("granting DNS permissions: %w", err)
}
}

o.prog.Start(fmt.Sprintf(fmtDeployEnvStart, color.HighlightUserInput(o.EnvName)))

if err := o.envDeployer.DeployEnvironment(deployEnvInput); err != nil {
var existsErr *cloudformation.ErrStackAlreadyExists
if errors.As(err, &existsErr) {
// Do nothing if the stack already exists.
o.prog.Stop("")
log.Successf("Environment %s already exists under project %s! Do nothing.\n",
log.Successf("CloudFormation stack for env %s already exists under project %s! Do nothing.\n",
color.HighlightUserInput(o.EnvName), color.HighlightResource(o.ProjectName()))
return nil
}
o.prog.Stop(log.Serrorf(fmtDeployEnvFailed, color.HighlightUserInput(o.EnvName)))
return err
}

// 2. Display updates while the deployment is happening.
// Display updates while the deployment is happening.
o.prog.Start(fmt.Sprintf(fmtStreamEnvStart, color.HighlightUserInput(o.EnvName)))
stackEvents, responses := o.envDeployer.StreamEnvironmentCreation(deployEnvInput)
for stackEvent := range stackEvents {
Expand All @@ -189,20 +212,17 @@ func (o *initEnvOpts) Execute() error {
}
o.prog.Stop(log.Ssuccessf(fmtStreamEnvComplete, color.HighlightUserInput(o.EnvName)))

// 3. Add the stack set instance to the project stackset.
o.prog.Start(fmt.Sprintf(fmtAddEnvToProjectStart, color.HighlightResource(resp.Env.AccountID), color.HighlightResource(resp.Env.Region), color.HighlightUserInput(o.ProjectName())))
if err := o.projDeployer.AddEnvToProject(project, resp.Env); err != nil {
o.prog.Stop(log.Serrorf(fmtAddEnvToProjectFailed, color.HighlightResource(resp.Env.AccountID), color.HighlightResource(resp.Env.Region), color.HighlightUserInput(o.ProjectName())))
return fmt.Errorf("deploy env %s to project %s: %w", resp.Env.Name, project.Name, err)
}
o.prog.Stop(log.Ssuccessf(fmtAddEnvToProjectComplete, color.HighlightResource(resp.Env.AccountID), color.HighlightResource(resp.Env.Region), color.HighlightUserInput(o.ProjectName())))
return nil
}

// 4. Store the environment in SSM.
if err := o.envCreator.CreateEnvironment(resp.Env); err != nil {
return fmt.Errorf("store environment: %w", err)
func (o *initEnvOpts) addToStackset(project *archer.Project, env *archer.Environment) error {
o.prog.Start(fmt.Sprintf(fmtAddEnvToProjectStart, color.HighlightResource(env.AccountID), color.HighlightResource(env.Region), color.HighlightUserInput(o.ProjectName())))
if err := o.projDeployer.AddEnvToProject(project, env); err != nil {
o.prog.Stop(log.Serrorf(fmtAddEnvToProjectFailed, color.HighlightResource(env.AccountID), color.HighlightResource(env.Region), color.HighlightUserInput(o.ProjectName())))
return fmt.Errorf("deploy env %s to project %s: %w", env.Name, project.Name, err)
}
log.Successf("Created environment %s in region %s under project %s.\n",
color.HighlightUserInput(resp.Env.Name), color.HighlightResource(resp.Env.Region), color.HighlightResource(resp.Env.Project))
o.prog.Stop(log.Ssuccessf(fmtAddEnvToProjectComplete, color.HighlightResource(env.AccountID), color.HighlightResource(env.Region), color.HighlightUserInput(o.ProjectName())))

return nil
}

Expand All @@ -218,11 +238,11 @@ func (o *initEnvOpts) delegateDNSFromProject(project *archer.Project) error {
}

o.prog.Start(fmt.Sprintf(fmtDNSDelegationStart, color.HighlightUserInput(envAccount.Account)))

if err := o.projDeployer.DelegateDNSPermissions(project, envAccount.Account); err != nil {
o.prog.Stop(log.Serrorf(fmtDNSDelegationFailed, color.HighlightUserInput(envAccount.Account)))
return err
}
o.prog.Stop(log.Ssuccessf(fmtDNSDelegationComplete, color.HighlightUserInput(envAccount.Account)))
return nil
}

Expand Down
148 changes: 125 additions & 23 deletions internal/pkg/cli/env_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,29 +184,6 @@ func TestInitEnvOpts_Execute(t *testing.T) {
},
wantedErrorS: "get identity: some identity error",
},
"stops if environment stack already exists": {
inProjectName: "phonetool",
inEnvName: "test",

expectProjectGetter: func(m *mocks.MockProjectGetter) {
m.EXPECT().GetProject("phonetool").Return(&archer.Project{Name: "phonetool"}, nil)
},
expectIdentity: func(m *climocks.MockidentityService) {
m.EXPECT().Get().Return(identity.Caller{RootUserARN: "some arn"}, nil)
},
expectProgress: func(m *climocks.Mockprogress) {
m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
m.EXPECT().Stop("")
},
expectDeployer: func(m *climocks.Mockdeployer) {
m.EXPECT().DeployEnvironment(&deploy.CreateEnvironmentInput{
Name: "test",
Project: "phonetool",
PublicLoadBalancer: true,
ToolsAccountPrincipalARN: "some arn",
}).Return(&cloudformation.ErrStackAlreadyExists{})
},
},
"errors if environment change set cannot be accepted": {
inProjectName: "phonetool",
inEnvName: "test",
Expand Down Expand Up @@ -274,6 +251,45 @@ func TestInitEnvOpts_Execute(t *testing.T) {
},
wantedErrorS: "some stream error",
},
"failed to get environment stack": {
inProjectName: "phonetool",
inEnvName: "test",

expectProjectGetter: func(m *mocks.MockProjectGetter) {
m.EXPECT().GetProject("phonetool").Return(&archer.Project{Name: "phonetool"}, nil)
},
expectIdentity: func(m *climocks.MockidentityService) {
m.EXPECT().Get().Return(identity.Caller{RootUserARN: "some arn"}, nil)
},
expectProgress: func(m *climocks.Mockprogress) {
m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
m.EXPECT().Start(fmt.Sprintf(fmtStreamEnvStart, "test"))
m.EXPECT().Stop(log.Ssuccessf(fmtStreamEnvComplete, "test"))
},
expectDeployer: func(m *climocks.Mockdeployer) {
m.EXPECT().DeployEnvironment(gomock.Any()).Return(nil)
events := make(chan []deploy.ResourceEvent, 1)
responses := make(chan deploy.CreateEnvironmentResponse, 1)
m.EXPECT().StreamEnvironmentCreation(gomock.Any()).Return(events, responses)
env := &archer.Environment{
Project: "phonetool",
Name: "test",
AccountID: "1234",
Region: "mars-1",
}
responses <- deploy.CreateEnvironmentResponse{
Env: env,
Err: nil,
}
close(events)
close(responses)
m.EXPECT().GetEnvironment("phonetool", "test").Return(nil, errors.New("some error"))
},
expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {
m.EXPECT().CreateEnvironment(gomock.Any()).Times(0)
},
wantedErrorS: "get environment struct for test: some error",
},
"failed to create stack set instance": {
inProjectName: "phonetool",
inEnvName: "test",
Expand Down Expand Up @@ -308,6 +324,12 @@ func TestInitEnvOpts_Execute(t *testing.T) {
}
close(events)
close(responses)
m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
AccountID: "1234",
Region: "mars-1",
Name: "test",
Project: "phonetool",
}, nil)
m.EXPECT().AddEnvToProject(&archer.Project{Name: "phonetool"}, env).Return(errors.New("some cfn error"))
},
expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {
Expand Down Expand Up @@ -348,6 +370,12 @@ func TestInitEnvOpts_Execute(t *testing.T) {
}
close(events)
close(responses)
m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
AccountID: "1234",
Region: "mars-1",
Name: "test",
Project: "phonetool",
}, nil)
m.EXPECT().AddEnvToProject(gomock.Any(), gomock.Any()).Return(nil)
},
expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {
Expand Down Expand Up @@ -393,6 +421,12 @@ func TestInitEnvOpts_Execute(t *testing.T) {
}
close(events)
close(responses)
m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
AccountID: "1234",
Region: "mars-1",
Name: "test",
Project: "phonetool",
}, nil)
m.EXPECT().AddEnvToProject(gomock.Any(), gomock.Any()).Return(nil)
},
expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {
Expand All @@ -404,6 +438,66 @@ func TestInitEnvOpts_Execute(t *testing.T) {
}).Return(nil)
},
},
"skips creating stack if environment stack already exists": {
inProjectName: "phonetool",
inEnvName: "test",

expectProjectGetter: func(m *mocks.MockProjectGetter) {
m.EXPECT().GetProject("phonetool").Return(&archer.Project{Name: "phonetool"}, nil)
},
expectIdentity: func(m *climocks.MockidentityService) {
m.EXPECT().Get().Return(identity.Caller{RootUserARN: "some arn"}, nil)
},
expectProgress: func(m *climocks.Mockprogress) {
m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
m.EXPECT().Stop("")
m.EXPECT().Start(fmt.Sprintf(fmtAddEnvToProjectStart, "1234", "mars-1", "phonetool"))
m.EXPECT().Stop(log.Ssuccessf(fmtAddEnvToProjectComplete, "1234", "mars-1", "phonetool"))
},
expectDeployer: func(m *climocks.Mockdeployer) {
m.EXPECT().DeployEnvironment(&deploy.CreateEnvironmentInput{
Name: "test",
Project: "phonetool",
PublicLoadBalancer: true,
ToolsAccountPrincipalARN: "some arn",
}).Return(&cloudformation.ErrStackAlreadyExists{})
m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
AccountID: "1234",
Region: "mars-1",
Name: "test",
Project: "phonetool",
}, nil)
m.EXPECT().AddEnvToProject(gomock.Any(), gomock.Any()).Return(nil)
},
expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {
m.EXPECT().CreateEnvironment(&archer.Environment{
Project: "phonetool",
Name: "test",
AccountID: "1234",
Region: "mars-1",
}).Return(nil)
},
},
"failed to delegate DNS (project has Domain and env and project are different)": {
inProjectName: "phonetool",
inEnvName: "test",

expectProjectGetter: func(m *mocks.MockProjectGetter) {
m.EXPECT().GetProject("phonetool").Return(&archer.Project{Name: "phonetool", AccountID: "1234", Domain: "amazon.com"}, nil)
},
expectIdentity: func(m *climocks.MockidentityService) {
m.EXPECT().Get().Return(identity.Caller{RootUserARN: "some arn", Account: "4567"}, nil).Times(1)
},
expectProgress: func(m *climocks.Mockprogress) {
m.EXPECT().Start(fmt.Sprintf(fmtDNSDelegationStart, "4567"))
m.EXPECT().Stop(log.Serrorf(fmtDNSDelegationFailed, "4567"))
},
expectDeployer: func(m *climocks.Mockdeployer) {
m.EXPECT().DelegateDNSPermissions(gomock.Any(), "4567").Return(errors.New("some error"))
},
expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {},
wantedErrorS: "granting DNS permissions: some error",
},
"success with DNS Delegation (project has Domain and env and project are different)": {
inProjectName: "phonetool",
inEnvName: "test",
Expand All @@ -416,6 +510,7 @@ func TestInitEnvOpts_Execute(t *testing.T) {
},
expectProgress: func(m *climocks.Mockprogress) {
m.EXPECT().Start(fmt.Sprintf(fmtDNSDelegationStart, "4567"))
m.EXPECT().Stop(log.Ssuccessf(fmtDNSDelegationComplete, "4567"))
m.EXPECT().Start(fmt.Sprintf(fmtDeployEnvStart, "test"))
m.EXPECT().Start(fmt.Sprintf(fmtStreamEnvStart, "test"))
m.EXPECT().Stop(log.Ssuccessf(fmtStreamEnvComplete, "test"))
Expand All @@ -439,6 +534,12 @@ func TestInitEnvOpts_Execute(t *testing.T) {
}
close(events)
close(responses)
m.EXPECT().GetEnvironment("phonetool", "test").Return(&archer.Environment{
AccountID: "1234",
Region: "mars-1",
Name: "test",
Project: "phonetool",
}, nil)
m.EXPECT().AddEnvToProject(gomock.Any(), gomock.Any()).Return(nil)
},
expectEnvCreator: func(m *mocks.MockEnvironmentCreator) {
Expand Down Expand Up @@ -528,6 +629,7 @@ func TestInitEnvOpts_delegateDNSFromProject(t *testing.T) {
},
expectProgress: func(m *climocks.Mockprogress) {
m.EXPECT().Start(fmt.Sprintf(fmtDNSDelegationStart, "4567"))
m.EXPECT().Stop(log.Ssuccessf(fmtDNSDelegationComplete, "4567"))
},
expectDeployer: func(m *climocks.Mockdeployer) {
m.EXPECT().DelegateDNSPermissions(gomock.Any(), "4567").Return(nil)
Expand Down
1 change: 1 addition & 0 deletions internal/pkg/cli/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ type environmentDeployer interface {
DeployEnvironment(env *deploy.CreateEnvironmentInput) error
StreamEnvironmentCreation(env *deploy.CreateEnvironmentInput) (<-chan []deploy.ResourceEvent, <-chan deploy.CreateEnvironmentResponse)
DeleteEnvironment(projName, envName string) error
GetEnvironment(projectName, envName string) (*archer.Environment, error)
}

type pipelineDeployer interface {
Expand Down
30 changes: 30 additions & 0 deletions internal/pkg/cli/mocks/mock_interfaces.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit de49678

Please sign in to comment.