From 707cbc71a44f809530ccaffc6be5fc5b34431705 Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Sun, 2 Jul 2017 17:37:50 -0700 Subject: [PATCH] Redo templates --- server/apply_executor.go | 135 ++++++------------ server/github_comment_renderer.go | 89 ++++++++++-- server/github_status.go | 2 +- server/plan_executor.go | 230 ++++++++++++------------------ server/project_config.go | 14 +- server/server.go | 24 ++-- terraform/terraform_client.go | 20 ++- 7 files changed, 246 insertions(+), 268 deletions(-) diff --git a/server/apply_executor.go b/server/apply_executor.go index b72f7dae5..b1d5f5de1 100644 --- a/server/apply_executor.go +++ b/server/apply_executor.go @@ -3,7 +3,6 @@ package server import ( "fmt" "os" - "strings" "github.com/pkg/errors" @@ -63,14 +62,8 @@ func (n NoPlansFailure) Template() *CompiledTemplate { return NoPlansFailureTmpl } +// todo: why pass githbub.client here, just use the one on the struct func (a *ApplyExecutor) execute(ctx *CommandContext, github *github.Client) { - if a.concurrentRunLocker.TryLock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { - ctx.Log.Info("run was locked by a concurrent run") - github.CreateComment(ctx.BaseRepo, ctx.Pull, "This environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again") - return - } - defer a.concurrentRunLocker.Unlock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) - a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Pending, ApplyStep) res := a.setupAndApply(ctx) res.Command = Apply @@ -78,19 +71,26 @@ func (a *ApplyExecutor) execute(ctx *CommandContext, github *github.Client) { github.CreateComment(ctx.BaseRepo, ctx.Pull, comment) } -func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { +func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) CommandResponse { + if a.concurrentRunLocker.TryLock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { + return a.failureResponse(ctx, + fmt.Sprintf("The %s environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again.", ctx.Command.environment)) + } + defer a.concurrentRunLocker.Unlock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) + if a.requireApproval { - approved, res := a.isApproved(ctx) + approved, err := a.github.PullIsApproved(ctx.BaseRepo, ctx.Pull) + if err != nil { + return a.errorResponse(ctx, errors.Wrap(err, "checking if pull request was approved")) + } if !approved { - return res + return a.failureResponse(ctx, "Pull request must be approved before running apply.") } } repoDir, err := a.workspace.GetWorkspace(ctx) if err != nil { - ctx.Log.Err(err.Error()) - a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Error, ApplyStep) - return ExecutionResult{SetupError: GeneralError{errors.New("Workspace missing, please plan again")}} + return a.failureResponse(ctx, "No workspace found. Did you run plan?") } // plans are stored at project roots by their environment names. We just need to find them @@ -110,37 +110,29 @@ func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { return nil }) if len(plans) == 0 { - failure := "found 0 plans for that environment" - ctx.Log.Warn(failure) - a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, ApplyStep) - return ExecutionResult{SetupFailure: NoPlansFailure{}} + return a.failureResponse(ctx, "No plans found for that environment.") } - applyOutputs := []PathResult{} + results := []ProjectResult{} for _, plan := range plans { - output := a.apply(ctx, repoDir, plan) - output.Path = plan.LocalPath - applyOutputs = append(applyOutputs, output) - + result := a.apply(ctx, repoDir, plan) + result.Path = plan.LocalPath + results = append(results, result) } - a.githubStatus.UpdatePathResult(ctx, applyOutputs) - return ExecutionResult{PathResults: applyOutputs} + a.githubStatus.UpdatePathResult(ctx, results) + return CommandResponse{ProjectResults: results} } -func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.Plan) PathResult { +func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.Plan) ProjectResult { tfEnv := ctx.Command.environment lockAttempt, err := a.lockingClient.TryLock(plan.Project, tfEnv, ctx.Pull, ctx.User) if err != nil { - return PathResult{ - Status: Error, - Result: GeneralError{errors.Wrap(err, "trying acquire lock")}, - } + return ProjectResult{Error: errors.Wrap(err, "acquiring lock")} } if lockAttempt.LockAcquired != true && lockAttempt.CurrLock.Pull.Num != ctx.Pull.Num { - return PathResult{ - Status: Error, - Result: GeneralError{fmt.Errorf("failed to acquire lock: lock held by pull request #%d", lockAttempt.CurrLock.Pull.Num)}, - } + return ProjectResult{Failure: fmt.Sprintf( + "This project is currently locked by #%d. The locking plan must be applied or discarded before future plans can execute.", + lockAttempt.CurrLock.Pull.Num)} } // check if config file is found, if not we continue the run @@ -151,12 +143,7 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P ctx.Log.Info("Config file found in %s", projectAbsolutePath) config, err := a.configReader.Read(projectAbsolutePath) if err != nil { - msg := fmt.Sprintf("Error reading config file: %v", err) - ctx.Log.Err(msg) - return PathResult{ - Status: Error, - Result: GeneralError{errors.New(msg)}, - } + return ProjectResult{Error: err} } // add terraform arguments from project config @@ -173,26 +160,16 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P // run terraform init and environment outputs, err := a.terraform.RunInitAndEnv(projectAbsolutePath, tfEnv, config.GetExtraArguments("init")) if err != nil { - msg := fmt.Sprintf("terraform init and environment commands failed. %s %v", outputs, err) - ctx.Log.Err(msg) - return PathResult{ - Status: Error, - Result: GeneralError{errors.New(msg)}, - } + return ProjectResult{Error: err} } ctx.Log.Info("terraform init and environment commands ran successfully %s", outputs) } // if there are pre plan commands then run them - if len(config.PrePlan.Commands) > 0 { + if len(config.PreApply.Commands) > 0 { preRunOutput, err := a.preRun.Start(config.PreApply.Commands, projectAbsolutePath, ctx.Command.environment, config.TerraformVersion) if err != nil { - msg := fmt.Sprintf("pre run failed: %v", err) - ctx.Log.Err(msg) - return PathResult{ - Status: Error, - Result: GeneralError{errors.New(msg)}, - } + return ProjectResult{Error: err} } ctx.Log.Info("Pre run output: \n%s", preRunOutput) } @@ -202,58 +179,40 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P a.awsConfig.SessionName = ctx.User.Username awsSession, err := a.awsConfig.CreateSession() if err != nil { - ctx.Log.Err(err.Error()) - return PathResult{ - Status: Error, - Result: GeneralError{err}, - } + return ProjectResult{Error: err} } credVals, err := awsSession.Config.Credentials.Get() if err != nil { - msg := fmt.Sprintf("failed to get assumed role credentials: %v", err) - ctx.Log.Err(msg) - return PathResult{ - Status: Error, - Result: GeneralError{errors.New(msg)}, - } + err = errors.Wrap(err, "getting assumed role credentials") + ctx.Log.Err(err.Error()) + return ProjectResult{Error: err} } ctx.Log.Info("running apply from %q", plan.Project.Path) tfApplyCmd := []string{"apply", "-no-color", plan.LocalPath} // append terraform arguments from config file tfApplyCmd = append(tfApplyCmd, terraformApplyExtraArgs...) - terraformApplyCmdArgs, output, err := a.terraform.RunCommand(projectAbsolutePath, tfApplyCmd, []string{ + output, err := a.terraform.RunCommand(projectAbsolutePath, tfApplyCmd, []string{ fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", credVals.AccessKeyID), fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", credVals.SecretAccessKey), fmt.Sprintf("AWS_SESSION_TOKEN=%s", credVals.SessionToken), }) if err != nil { - ctx.Log.Err("failed to apply: %v %s", err, output) - return PathResult{ - Status: Failure, - Result: ApplyFailure{Command: strings.Join(terraformApplyCmdArgs, " "), Output: output, ErrorMessage: err.Error()}, - } + return ProjectResult{Error: fmt.Errorf("%s\n%s", err.Error(), output)} } - return PathResult{ - Status: Success, - Result: ApplySuccess{output}, - } + return ProjectResult{ApplySuccess: output} } -func (a *ApplyExecutor) isApproved(ctx *CommandContext) (bool, ExecutionResult) { - ok, err := a.github.PullIsApproved(ctx.BaseRepo, ctx.Pull) - if err != nil { - msg := fmt.Sprintf("failed to determine if pull request was approved: %v", err) - ctx.Log.Err(msg) - a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Error, ApplyStep) - return false, ExecutionResult{SetupError: GeneralError{errors.New(msg)}} - } - if !ok { - ctx.Log.Info("pull request was not approved") - a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, ApplyStep) - return false, ExecutionResult{SetupFailure: PullNotApprovedFailure{}} - } - return true, ExecutionResult{} +func (a *ApplyExecutor) failureResponse(ctx *CommandContext, msg string) CommandResponse { + ctx.Log.Warn(msg) + a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, ApplyStep) + return CommandResponse{Failure: msg} +} + +func (a *ApplyExecutor) errorResponse(ctx *CommandContext, err error) CommandResponse { + ctx.Log.Err(err.Error()) + a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Error, ApplyStep) + return CommandResponse{Error: err} } diff --git a/server/github_comment_renderer.go b/server/github_comment_renderer.go index 6aa17c16a..e45486b30 100644 --- a/server/github_comment_renderer.go +++ b/server/github_comment_renderer.go @@ -20,7 +20,7 @@ type CompiledTemplate struct { // PathResultRendered is used as an intermediary data container. We render the individual path results // into Render and then pass around this struct to be rendered into the main templates type PathResultRendered struct { - PathResult + ProjectResult Rendered string } @@ -31,17 +31,18 @@ var ( text: "**{{.Command}} Failed**:\n{{.Output}}\n" + logTmpl, } SinglePath *CompiledTemplate = &CompiledTemplate{ - text: "{{ range $result := .Results }}{{$result.Rendered}}{{end}}\n" + logTmpl, + // we know we'll only have one result + text: "{{ range $result := .Results }}{{$result}}{{end}}\n" + logTmpl, } MultiPath *CompiledTemplate = &CompiledTemplate{ text: "Ran {{.Command}} in {{ len .Results }} directories:\n" + "{{ range $path, $result := .Results }}" + - " * `{{$path}}`\n" + //todo: add result status + " * `{{$path}}`\n" + "{{end}}\n" + "{{ range $path, $result := .Results }}" + - "Terraform {{$.Command}} for `{{$path}}`:\n" + - "{{$result.Rendered}}\n\n" + - "---\n{{end}}" + + "##{{$path}}/\n" + + "{{$result}}\n" + + "---{{end}}" + logTmpl, } PlanSuccessTmpl *CompiledTemplate = &CompiledTemplate{ @@ -100,9 +101,24 @@ var ( GeneralErrorTmpl *CompiledTemplate = &CompiledTemplate{ text: "{{.Error}}", } + ErrTmpl *CompiledTemplate = &CompiledTemplate{ + text: "**{{.Command}} Error**\n" + + "```\n" + + "{{.Error}}\n" + + "```\n", + } + ErrWithLogTmpl *CompiledTemplate = &CompiledTemplate{ + text: ErrTmpl.text + logTmpl, + } + FailureTmpl *CompiledTemplate = &CompiledTemplate{ + text: "**{{.Command}} Failed**: {{.Failure}}\n", + } + FailureWithLogTmpl *CompiledTemplate = &CompiledTemplate{ + text: FailureTmpl.text + logTmpl, + } ) -var logTmpl = "{{if .Verbose}}\nAtlantis Log:\n```\n{{.Log}}```{{end}}\n" +var logTmpl = "{{if .Verbose}}\n
Log\n

\n\n```\n{{.Log}}```\n

{{end}}\n" func init() { // compile the templates @@ -121,13 +137,36 @@ func init() { NoPlansFailureTmpl, ErrorTmpl, GeneralErrorTmpl, + ErrTmpl, + ErrWithLogTmpl, + FailureTmpl, + FailureWithLogTmpl, } { t.Template = template.Must(template.New("").Parse(t.text)) } } -func (g *GithubCommentRenderer) render(res ExecutionResult, log string, verbose bool) string { +func (g *GithubCommentRenderer) render(res CommandResponse, log string, verbose bool) string { commandStr := strings.Title(res.Command.String()) + if res.Error != nil { + return g.renderTemplate(ErrWithLogTmpl.Template, struct{ + Command string + Error string + Verbose bool + Log string + }{commandStr, res.Error.Error(), verbose, log}) + } + if res.Failure != "" { + return g.renderTemplate(FailureWithLogTmpl.Template, struct{ + Command string + Failure string + Verbose bool + Log string + }{commandStr, res.Failure, verbose, log}) + } + return g.renderProjectResults(res.ProjectResults, commandStr, log, verbose) + + if res.SetupError != nil { renderedError := g.renderTemplate(res.SetupError.Template().Template, res.SetupError) return g.renderTemplate(ErrorTmpl.Template, struct { @@ -144,19 +183,41 @@ func (g *GithubCommentRenderer) render(res ExecutionResult, log string, verbose }{commandStr, renderedFailure, log, verbose}) } else { hasErrors := false - for _, res := range res.PathResults { + for _, res := range res.ProjectResults { if res.Status == Error { hasErrors = true } } - return g.renderPathOutputs(res.PathResults, commandStr, log, hasErrors || verbose) + return g.renderProjectResults(res.ProjectResults, commandStr, log, hasErrors || verbose) } } -func (g *GithubCommentRenderer) renderPathOutputs(pathResults []PathResult, command string, log string, verbose bool) string { - renderedOutputs := map[string]PathResultRendered{} +func (g *GithubCommentRenderer) renderProjectResults(pathResults []ProjectResult, command string, log string, verbose bool) string { + renderedOutputs := make(map[string]string) for _, result := range pathResults { - renderedOutputs[result.Path] = PathResultRendered{result, g.renderTemplate(result.Result.Template().Template, result.Result)} + if result.Error != nil { + renderedOutputs[result.Path] = g.renderTemplate(ErrTmpl.Template, struct{ + Command string + Output string + }{ + Command: command, + Output: result.Error.Error(), + }) + } else if result.Failure != "" { + renderedOutputs[result.Path] = g.renderTemplate(FailureTmpl.Template, struct{ + Command string + Failure string + }{ + Command: command, + Failure: result.Failure, + }) + } else if result.PlanSuccess != nil { + renderedOutputs[result.Path] = g.renderTemplate(PlanSuccessTmpl.Template, *result.PlanSuccess) + } else if result.ApplySuccess != "" { + renderedOutputs[result.Path] = g.renderTemplate(ApplySuccessTmpl.Template, struct{Output string}{result.ApplySuccess}) + } else { + renderedOutputs[result.Path] = "Found no template. This is a bug!" + } } var tmpl *template.Template @@ -166,7 +227,7 @@ func (g *GithubCommentRenderer) renderPathOutputs(pathResults []PathResult, comm tmpl = MultiPath.Template } return g.renderTemplate(tmpl, struct { - Results map[string]PathResultRendered + Results map[string]string Log string Verbose bool Command string diff --git a/server/github_status.go b/server/github_status.go index e4d87cb20..163a092c3 100644 --- a/server/github_status.go +++ b/server/github_status.go @@ -44,7 +44,7 @@ func (g *GithubStatus) Update(repo models.Repo, pull models.PullRequest, status return g.client.UpdateStatus(repo, pull, status.String(), description, statusContext) } -func (g *GithubStatus) UpdatePathResult(ctx *CommandContext, pathResults []PathResult) error { +func (g *GithubStatus) UpdatePathResult(ctx *CommandContext, pathResults []ProjectResult) error { var statuses []Status for _, p := range pathResults { statuses = append(statuses, p.Status) diff --git a/server/plan_executor.go b/server/plan_executor.go index dbccf0ed9..76752a55d 100644 --- a/server/plan_executor.go +++ b/server/plan_executor.go @@ -76,149 +76,108 @@ func (e EnvironmentFailure) Template() *CompiledTemplate { } func (p *PlanExecutor) execute(ctx *CommandContext, github *github.Client) { - if p.concurrentRunLocker.TryLock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { - ctx.Log.Info("run was locked by a concurrent run") - github.CreateComment(ctx.BaseRepo, ctx.Pull, "This environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again") - return - } - defer p.concurrentRunLocker.Unlock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) + p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Pending, PlanStep) res := p.setupAndPlan(ctx) res.Command = Plan comment := p.githubCommentRenderer.render(res, ctx.Log.History.String(), ctx.Command.verbose) github.CreateComment(ctx.BaseRepo, ctx.Pull, comment) } -func (p *PlanExecutor) setupAndPlan(ctx *CommandContext) ExecutionResult { - p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Pending, PlanStep) +func (p *PlanExecutor) setupAndPlan(ctx *CommandContext) CommandResponse { + if p.concurrentRunLocker.TryLock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { + return p.failureResponse(ctx, + fmt.Sprintf("The %s environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again.", ctx.Command.environment)) + } + defer p.concurrentRunLocker.Unlock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) // figure out what projects have been modified so we know where to run plan ctx.Log.Info("listing modified files from pull request") modifiedFiles, err := p.github.GetModifiedFiles(ctx.BaseRepo, ctx.Pull) if err != nil { - return p.setupError(ctx, errors.Wrap(err, "getting modified files")) + return p.errorResponse(ctx, errors.Wrap(err, "getting modified files")) } modifiedTerraformFiles := p.filterToTerraform(modifiedFiles) if len(modifiedTerraformFiles) == 0 { - ctx.Log.Info("no modified terraform files found, exiting") - p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, PlanStep) - return ExecutionResult{SetupError: GeneralError{errors.New("Plan Failed: no modified terraform files found")}} + return p.failureResponse(ctx, "No Terraform files were modified.") } ctx.Log.Debug("Found %d modified terraform files: %v", len(modifiedTerraformFiles), modifiedTerraformFiles) - projects := p.ModifiedProjects(ctx.BaseRepo.FullName, modifiedTerraformFiles) - if len(projects) == 0 { - ctx.Log.Info("no Terraform projects were modified") - p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, PlanStep) - return ExecutionResult{SetupError: GeneralError{errors.New("Plan Failed: we determined that no terraform projects were modified")}} - } cloneDir, err := p.workspace.Clone(ctx) if err != nil { - return ExecutionResult{SetupError: GeneralError{fmt.Errorf("Plan Failed: setting up workspace: %s", err)}} + return p.errorResponse(ctx, err) } - tfEnv := ctx.Command.environment - planOutputs := []PathResult{} + results := []ProjectResult{} for _, project := range projects { - // check if config file is found, if not we continue the run - var config ProjectConfig - absolutePath := filepath.Join(cloneDir, project.Path) - var terraformPlanExtraArgs []string - if p.configReader.Exists(absolutePath) { - ctx.Log.Info("Config file found in %s", absolutePath) - config, err = p.configReader.Read(absolutePath) - if err != nil { - errMsg := fmt.Sprintf("Error reading config file: %v", err) - ctx.Log.Err(errMsg) - return ExecutionResult{SetupError: GeneralError{errors.New(errMsg)}} - } - - // add terraform arguments from project config - terraformPlanExtraArgs = config.GetExtraArguments(ctx.Command.commandType.String()) - } - - // check if terraform version is >= 0.9.0 - terraformVersion := p.terraform.Version() - if config.TerraformVersion != nil { - terraformVersion = config.TerraformVersion - } - constraints, _ := version.NewConstraint(">= 0.9.0") - if constraints.Check(terraformVersion) { - // run terraform init and environment - outputs, err := p.terraform.RunInitAndEnv(absolutePath, tfEnv, config.GetExtraArguments("init")) - if err != nil { - errMsg := fmt.Sprintf("terraform init and environment commands failed. %s %v", outputs, err) - ctx.Log.Err(errMsg) - return ExecutionResult{SetupError: GeneralError{errors.New(errMsg)}} - } - ctx.Log.Info("terraform init and environment commands ran successfully %s", outputs) - } else { - // run terraform get for 0.8.8 and below - terraformGetCmd := append([]string{"get", "-no-color"}, config.GetExtraArguments("get")...) - _, output, err := p.terraform.RunCommand(absolutePath, terraformGetCmd, nil) - if err != nil { - errMsg := fmt.Sprintf("terraform get failed. %s %v", output, err) - ctx.Log.Err(errMsg) - return ExecutionResult{SetupError: GeneralError{errors.New(errMsg)}} - } - ctx.Log.Info("terraform get ran successfully %s", output) - } - - // if there are pre plan commands then run them - if len(config.PrePlan.Commands) > 0 { - preRunOutput, err := p.preRun.Start(config.PrePlan.Commands, absolutePath, tfEnv, terraformVersion) - if err != nil { - errMsg := fmt.Sprintf("pre run failed: %v", err) - ctx.Log.Err(errMsg) - return ExecutionResult{SetupError: GeneralError{errors.New(errMsg)}} - } - ctx.Log.Info("Pre run output: \n%s", preRunOutput) - } - - generatePlanResponse := p.plan(ctx, cloneDir, project, terraformPlanExtraArgs) - generatePlanResponse.Path = project.Path - planOutputs = append(planOutputs, generatePlanResponse) + result := p.plan(ctx, cloneDir, project) + result.Path = project.Path + results = append(results, result) } - p.githubStatus.UpdatePathResult(ctx, planOutputs) - return ExecutionResult{PathResults: planOutputs} + p.githubStatus.UpdatePathResult(ctx, results) + return CommandResponse{ProjectResults: results} } // plan runs the steps necessary to run `terraform plan`. If there is an error, the error message will be encapsulated in error // and the GeneratePlanResponse struct will also contain the full log including the error -func (p *PlanExecutor) plan( - ctx *CommandContext, - repoDir string, - project models.Project, - terraformArgs []string) PathResult { - ctx.Log.Info("generating plan for path %q", project.Path) +func (p *PlanExecutor) plan(ctx *CommandContext, repoDir string, project models.Project) ProjectResult { + ctx.Log.Info("generating plan at %q", project.Path) tfEnv := ctx.Command.environment lockAttempt, err := p.lockingClient.TryLock(project, tfEnv, ctx.Pull, ctx.User) if err != nil { - return PathResult{ - Status: Failure, - Result: GeneralError{fmt.Errorf("failed to lock state: %v", err)}, - } + return ProjectResult{Error: errors.Wrap(err, "acquiring lock")} } - - // the run is locked unless the locking run is the same pull id as this run if lockAttempt.LockAcquired == false && lockAttempt.CurrLock.Pull.Num != ctx.Pull.Num { - return PathResult{ - Status: Failure, - Result: RunLockedFailure{lockAttempt.CurrLock.Pull.Num}, - } + return ProjectResult{Failure: fmt.Sprintf( + "This project is currently locked by #%d. The locking plan must be applied or discarded before future plans can execute.", + lockAttempt.CurrLock.Pull.Num)} } - // Run terraform plan - ctx.Log.Info("running terraform plan in directory %q", project.Path) - planFile := filepath.Join(repoDir, project.Path, fmt.Sprintf("%s.tfplan", tfEnv)) - tfPlanCmd := []string{"plan", "-refresh", "-no-color", "-out", planFile} - // append terraform arguments from config file - tfPlanCmd = append(tfPlanCmd, terraformArgs...) - // check if env/{environment}.tfvars exist - tfEnvFileName := filepath.Join("env", tfEnv+".tfvars") - if _, err := os.Stat(filepath.Join(repoDir, project.Path, tfEnvFileName)); err == nil { - tfPlanCmd = append(tfPlanCmd, "-var-file", tfEnvFileName) + // check if config file is found, if not we continue the run + var config ProjectConfig + absolutePath := filepath.Join(repoDir, project.Path) + var planExtraArgs []string + if p.configReader.Exists(absolutePath) { + config, err = p.configReader.Read(absolutePath) + if err != nil { + return ProjectResult{Error: err} + } + + // add terraform arguments from project config + planExtraArgs = config.GetExtraArguments(ctx.Command.commandType.String()) + } + + // check if terraform version is >= 0.9.0 + terraformVersion := p.terraform.Version() + if config.TerraformVersion != nil { + terraformVersion = config.TerraformVersion + } + constraints, _ := version.NewConstraint(">= 0.9.0") + if constraints.Check(terraformVersion) { + // run terraform init and environment + outputs, err := p.terraform.RunInitAndEnv(absolutePath, tfEnv, config.GetExtraArguments("init")) + if err != nil { + return ProjectResult{Error: err} + } + ctx.Log.Info("terraform init and environment commands ran successfully %s", outputs) + } else { + // run terraform get for 0.8.8 and below + terraformGetCmd := append([]string{"get", "-no-color"}, config.GetExtraArguments("get")...) + output, err := p.terraform.RunCommand(absolutePath, terraformGetCmd, nil) + if err != nil { + return ProjectResult{Error: err} + } + ctx.Log.Info("terraform get ran successfully %s", output) + } + + // if there are pre plan commands then run them + if len(config.PrePlan.Commands) > 0 { + preRunOutput, err := p.preRun.Start(config.PrePlan.Commands, absolutePath, tfEnv, terraformVersion) + if err != nil { + return ProjectResult{Error: errors.Wrap(err, "running pre plan commands")} + } + ctx.Log.Info("Pre run output: \n%s", preRunOutput) } // set pull request creator as the session name @@ -226,50 +185,43 @@ func (p *PlanExecutor) plan( awsSession, err := p.awsConfig.CreateSession() if err != nil { ctx.Log.Err(err.Error()) - return PathResult{ - Status: Error, - Result: GeneralError{err}, - } + return ProjectResult{Error: err} } credVals, err := awsSession.Config.Credentials.Get() if err != nil { - err = fmt.Errorf("failed to get assumed role credentials: %v", err) + err = errors.Wrap(err, "getting assumed role credentials") ctx.Log.Err(err.Error()) - return PathResult{ - Status: Error, - Result: GeneralError{err}, - } + return ProjectResult{Error: err} } - terraformPlanCmdArgs, output, err := p.terraform.RunCommand(filepath.Join(repoDir, project.Path), tfPlanCmd, []string{ + + // Run terraform plan + ctx.Log.Info("running terraform plan in directory %q", project.Path) + planFile := filepath.Join(repoDir, project.Path, fmt.Sprintf("%s.tfplan", tfEnv)) + tfPlanCmd := []string{"plan", "-refresh", "-no-color", "-out", planFile} + // append terraform arguments from config file + tfPlanCmd = append(tfPlanCmd, planExtraArgs...) + // check if env/{environment}.tfvars exist + tfEnvFileName := filepath.Join("env", tfEnv+".tfvars") + if _, err := os.Stat(filepath.Join(repoDir, project.Path, tfEnvFileName)); err == nil { + tfPlanCmd = append(tfPlanCmd, "-var-file", tfEnvFileName) + } + output, err := p.terraform.RunCommand(filepath.Join(repoDir, project.Path), tfPlanCmd, []string{ fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", credVals.AccessKeyID), fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", credVals.SecretAccessKey), fmt.Sprintf("AWS_SESSION_TOKEN=%s", credVals.SessionToken), }) if err != nil { - if err.Error() != "exit status 1" { - // if it's not an exit 1 then the details about the failure won't be in the output but in the error itself - output = err.Error() - } - err := TerraformFailure{ - Command: strings.Join(terraformPlanCmdArgs, " "), - Output: output, - } - ctx.Log.Err("error running terraform plan: %v", output) - ctx.Log.Info("unlocking state since plan failed") + // plan failed so unlock the state if _, err := p.lockingClient.Unlock(lockAttempt.LockKey); err != nil { ctx.Log.Err("error unlocking state: %v", err) } - return PathResult{ - Status: Failure, - Result: err, - } + return ProjectResult{Error: fmt.Errorf("%s\n%s", err.Error(), output)} } - return PathResult{ - Status: Success, - Result: PlanSuccess{ + return ProjectResult{ + PlanSuccess: &PlanSuccess{ TerraformOutput: output, LockURL: p.LockURL(lockAttempt.LockKey), }, @@ -317,8 +269,14 @@ func (p *PlanExecutor) getProjectPath(modifiedFilePath string) string { return dir } -func (p *PlanExecutor) setupError(ctx *CommandContext, err error) ExecutionResult { +func (p *PlanExecutor) failureResponse(ctx *CommandContext, msg string) CommandResponse { + ctx.Log.Warn(msg) + p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, PlanStep) + return CommandResponse{Failure: msg} +} + +func (p *PlanExecutor) errorResponse(ctx *CommandContext, err error) CommandResponse { ctx.Log.Err(err.Error()) p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Error, PlanStep) - return ExecutionResult{SetupError: GeneralError{err}} + return CommandResponse{Error: err} } diff --git a/server/project_config.go b/server/project_config.go index f4f682d34..9baaa9320 100644 --- a/server/project_config.go +++ b/server/project_config.go @@ -1,7 +1,6 @@ package server import ( - "fmt" "io/ioutil" "os" "path/filepath" @@ -51,13 +50,14 @@ func (c *ConfigReader) Exists(execPath string) bool { func (c *ConfigReader) Read(execPath string) (ProjectConfig, error) { var pc ProjectConfig - raw, err := ioutil.ReadFile(filepath.Join(execPath, ProjectConfigFile)) + filename := filepath.Join(execPath, ProjectConfigFile) + raw, err := ioutil.ReadFile(filename) if err != nil { - return pc, fmt.Errorf("Couldn't read config file %q: %v", execPath, err) + return pc, errors.Wrapf(err, "reading %s", ProjectConfigFile) } var pcYaml ProjectConfigYaml if err := yaml.Unmarshal(raw, &pcYaml); err != nil { - return pc, fmt.Errorf("Couldn't decode yaml in config file %q: %v", execPath, err) + return pc, errors.Wrapf(err, "parsing %s", ProjectConfigFile) } var v *version.Version @@ -67,14 +67,12 @@ func (c *ConfigReader) Read(execPath string) (ProjectConfig, error) { return pc, errors.Wrap(err, "parsing terraform_version") } } - pc = ProjectConfig{ + return ProjectConfig{ TerraformVersion: v, ExtraArguments: pcYaml.ExtraArguments, PreApply: pcYaml.PreApply, PrePlan: pcYaml.PrePlan, - } - - return pc, nil + }, nil } func (c *ProjectConfig) GetExtraArguments(command string) []string { diff --git a/server/server.go b/server/server.go index e0e5e03d5..e80b1981a 100644 --- a/server/server.go +++ b/server/server.go @@ -73,17 +73,23 @@ type CommandContext struct { } // todo: These structs have nothing to do with the server. Move to a different file/package #refactor -type ExecutionResult struct { - SetupError Templater - SetupFailure Templater - PathResults []PathResult - Command CommandType +type CommandResponse struct { + Error error + Failure string + SetupError Templater + SetupFailure Templater + ProjectResults []ProjectResult + Command CommandType } -type PathResult struct { - Path string - Status Status - Result Templater +type ProjectResult struct { + Path string + Status Status + Result Templater + Error error + Failure string + PlanSuccess *PlanSuccess + ApplySuccess string } type Templater interface { diff --git a/terraform/terraform_client.go b/terraform/terraform_client.go index 5a40eba36..20fbe407c 100644 --- a/terraform/terraform_client.go +++ b/terraform/terraform_client.go @@ -7,6 +7,7 @@ import ( version "github.com/hashicorp/go-version" "github.com/pkg/errors" + "strings" ) type Client struct { @@ -40,7 +41,7 @@ func NewClient() (*Client, error) { }, nil } -func (c *Client) RunCommand(path string, tfCmd []string, tfEnvVars []string) ([]string, string, error) { +func (c *Client) RunCommand(path string, tfCmd []string, tfEnvVars []string) (string, error) { return c.RunCommandWithVersion(path, tfCmd, tfEnvVars, c.defaultVersion) } @@ -48,7 +49,7 @@ func (c *Client) Version() *version.Version { return c.defaultVersion } -func (c *Client) RunCommandWithVersion(path string, tfCmd []string, tfEnvVars []string, v *version.Version) ([]string, string, error) { +func (c *Client) RunCommandWithVersion(path string, tfCmd []string, tfEnvVars []string, v *version.Version) (string, error) { tfExecutable := "terraform" // if version is the same as the default, don't need to prepend the version name to the executable if !v.Equal(c.defaultVersion) { @@ -60,31 +61,26 @@ func (c *Client) RunCommandWithVersion(path string, tfCmd []string, tfEnvVars [] terraformCmd.Env = tfEnvVars } out, err := terraformCmd.CombinedOutput() - output := string(out) - if err != nil { - return terraformCmd.Args, output, err - } - - return terraformCmd.Args, output, nil + return string(out), errors.Wrapf(err, "running %s", strings.Join(terraformCmd.Args, " ")) } func (c *Client) RunInitAndEnv(path string, env string, extraArgs []string) ([]string, error) { var outputs []string // run terraform init - _, output, err := c.RunCommand(path, append([]string{"init", "-no-color"}, extraArgs...), []string{}) + output, err := c.RunCommand(path, append([]string{"init", "-no-color"}, extraArgs...), []string{}) if err != nil { return nil, errors.Wrapf(err, "running terraform init: %s", output) } outputs = append(outputs, output) // run terraform env new and select - _, output, err = c.RunCommand(path, []string{"env", "select", "-no-color", env}, []string{}) + output, err = c.RunCommand(path, []string{"env", "select", "-no-color", env}, []string{}) if err != nil { // if terraform env select fails we will run terraform env new // to create a new environment - _, output, err = c.RunCommand(path, []string{"env", "new", "-no-color", env}, []string{}) + output, err = c.RunCommand(path, []string{"env", "new", "-no-color", env}, []string{}) if err != nil { - return nil, errors.Wrapf(err, "running terraform environment command: %s", output) + return nil, errors.Wrapf(err, "running terraform env new: %s", output) } } return append(outputs, output), nil