Files
atlantis/server/apply_executor.go

187 lines
6.4 KiB
Go

package server
import (
"fmt"
"os"
"github.com/pkg/errors"
"path/filepath"
version "github.com/hashicorp/go-version"
"github.com/hootsuite/atlantis/aws"
"github.com/hootsuite/atlantis/github"
"github.com/hootsuite/atlantis/locking"
"github.com/hootsuite/atlantis/models"
"github.com/hootsuite/atlantis/prerun"
"github.com/hootsuite/atlantis/terraform"
)
type ApplyExecutor struct {
github *github.Client
githubStatus *GithubStatus
awsConfig *aws.Config
terraform *terraform.Client
githubCommentRenderer *GithubCommentRenderer
lockingClient *locking.Client
requireApproval bool
preRun *prerun.PreRun
configReader *ConfigReader
concurrentRunLocker *ConcurrentRunLocker
workspace *Workspace
}
func (a *ApplyExecutor) execute(ctx *CommandContext) {
a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Pending, ApplyStep)
res := a.setupAndApply(ctx)
res.Command = Apply
comment := a.githubCommentRenderer.render(res, ctx.Log.History.String(), ctx.Command.Verbose)
a.github.CreateComment(ctx.BaseRepo, ctx.Pull, comment)
}
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, 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 a.failureResponse(ctx, "Pull request must be approved before running apply.")
}
}
repoDir, err := a.workspace.GetWorkspace(ctx)
if err != nil {
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
var plans []models.Plan
filepath.Walk(repoDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// if the plan is for the right env,
if !info.IsDir() && info.Name() == ctx.Command.Environment+".tfplan" {
rel, _ := filepath.Rel(repoDir, filepath.Dir(path))
plans = append(plans, models.Plan{
Project: models.NewProject(ctx.BaseRepo.FullName, rel),
LocalPath: path,
})
}
return nil
})
if len(plans) == 0 {
return a.failureResponse(ctx, "No plans found for that environment.")
}
results := []ProjectResult{}
for _, plan := range plans {
result := a.apply(ctx, repoDir, plan)
result.Path = plan.LocalPath
results = append(results, result)
}
a.githubStatus.UpdatePathResult(ctx, results)
return CommandResponse{ProjectResults: results}
}
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 ProjectResult{Error: errors.Wrap(err, "acquiring lock")}
}
if lockAttempt.LockAcquired != true && lockAttempt.CurrLock.Pull.Num != ctx.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
projectAbsolutePath := filepath.Dir(plan.LocalPath)
var terraformApplyExtraArgs []string
var config ProjectConfig
if a.configReader.Exists(projectAbsolutePath) {
ctx.Log.Info("Config file found in %s", projectAbsolutePath)
config, err := a.configReader.Read(projectAbsolutePath)
if err != nil {
return ProjectResult{Error: err}
}
// add terraform arguments from project config
terraformApplyExtraArgs = config.GetExtraArguments(ctx.Command.Name.String())
}
// check if terraform version is >= 0.9.0
terraformVersion := a.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 := a.terraform.RunInitAndEnv(projectAbsolutePath, tfEnv, config.GetExtraArguments("init"))
if err != nil {
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.PreApply.Commands) > 0 {
preRunOutput, err := a.preRun.Start(config.PreApply.Commands, projectAbsolutePath, ctx.Command.Environment, config.TerraformVersion)
if err != nil {
return ProjectResult{Error: err}
}
ctx.Log.Info("Pre run output: \n%s", preRunOutput)
}
// need to get auth data from assumed role
// todo: de-duplicate calls to assumeRole
a.awsConfig.SessionName = ctx.User.Username
awsSession, err := a.awsConfig.CreateSession()
if err != nil {
return ProjectResult{Error: err}
}
credVals, err := awsSession.Config.Credentials.Get()
if err != nil {
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...)
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 {
return ProjectResult{Error: fmt.Errorf("%s\n%s", err.Error(), output)}
}
return ProjectResult{ApplySuccess: output}
}
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}
}