diff --git a/README.md b/README.md index 8e6aa085..4906a0a0 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ Now when Atlantis executes it will use the `terraform{version}` executable. ## Project-Specific Customization An `atlantis.yaml` config file in your project root (which is not necessarily the repo root) can be used to customize - what commands Atlantis runs **before** `plan` and `apply` with `pre_plan` and `pre_apply` +- what commands Atlantis runs **after** `plan` and `apply` with `post_plan` and `post_apply` - additional arguments to be supplied to specific terraform commands with `extra_arguments` - what version of Terraform to use (see [Terraform Versions](#terraform-versions)) @@ -156,16 +157,22 @@ terraform_version: 0.8.8 # optional version pre_plan: commands: - "curl http://example.com" +post_plan: + commands: + - "curl http://example.com" pre_apply: commands: - "curl http://example.com" +post_apply: + commands: + - "curl http://example.com" extra_arguments: - command: plan arguments: - "-tfvars=myvars.tfvars" ``` -When running the `pre_plan` and `pre_apply` commands the following environment variables are available +When running the `pre_plan`, `post_plan`, `pre_apply`, and `post_apply` commands the following environment variables are available - `ENVIRONMENT`: if an environment argument is supplied to `atlantis plan` or `atlantis apply` this will be the value of that argument. Else it will be `default` - `ATLANTIS_TERRAFORM_VERSION`: local version of `terraform` or the version from `terraform_version` if specified, ex. `0.10.0` diff --git a/prerun/pre_run.go b/run/run.go similarity index 73% rename from prerun/pre_run.go rename to run/run.go index a23deae5..fbd3da2b 100644 --- a/prerun/pre_run.go +++ b/run/run.go @@ -1,6 +1,6 @@ -// Package prerun handles running commands prior to the +// Package run handles running commands prior and following the // regular Atlantis commands. -package prerun +package run import ( "bufio" @@ -17,28 +17,29 @@ import ( const inlineShebang = "#!/bin/sh -e" -type PreRun struct{} +type Run struct{} // Execute runs the commands by writing them as a script to disk // and then executing the script. -func (p *PreRun) Execute( +func (p *Run) Execute( log *logging.SimpleLogger, commands []string, path string, environment string, - terraformVersion *version.Version) (string, error) { + terraformVersion *version.Version, + stage string) (string, error) { // we create a script from the commands provided if len(commands) == 0 { - return "", errors.New("prerun commands cannot be empty") + return "", errors.Errorf("%s commands cannot be empty", stage) } - s, err := createScript(commands) + s, err := createScript(commands, stage) if err != nil { return "", err } defer os.Remove(s) - log.Info("running prerun commands: %v", commands) + log.Info("running %s commands: %v", stage, commands) // set environment variable for the run. // this is to support scripts to use the ENVIRONMENT, ATLANTIS_TERRAFORM_VERSION @@ -49,10 +50,10 @@ func (p *PreRun) Execute( return execute(s) } -func createScript(cmds []string) (string, error) { +func createScript(cmds []string, stage string) (string, error) { tmp, err := ioutil.TempFile("/tmp", "atlantis-temp-script") if err != nil { - return "", errors.Wrap(err, "preparing pre run shell script") + return "", errors.Wrapf(err, "preparing %s shell script", stage) } scriptName := tmp.Name() @@ -62,7 +63,7 @@ func createScript(cmds []string) (string, error) { writer.WriteString(fmt.Sprintf("%s\n", inlineShebang)) cmdsJoined := strings.Join(cmds, "\n") if _, err := writer.WriteString(cmdsJoined); err != nil { - return "", errors.Wrap(err, "preparing pre run") + return "", errors.Wrapf(err, "preparing %s", stage) } if err := writer.Flush(); err != nil { @@ -71,7 +72,7 @@ func createScript(cmds []string) (string, error) { tmp.Close() if err := os.Chmod(scriptName, 0755); err != nil { - return "", errors.Wrap(err, "making pre run script executable") + return "", errors.Wrapf(err, "making %s script executable", stage) } return scriptName, nil diff --git a/prerun/pre_run_test.go b/run/run_test.go similarity index 64% rename from prerun/pre_run_test.go rename to run/run_test.go index 9b6c0ea1..57ba9f89 100644 --- a/prerun/pre_run_test.go +++ b/run/run_test.go @@ -1,4 +1,4 @@ -package prerun +package run import ( "log" @@ -11,33 +11,33 @@ import ( ) var logger = logging.NewSimpleLogger("", log.New(os.Stderr, "", log.LstdFlags), false, logging.Debug) -var preRun = &PreRun{} +var run = &Run{} -func TestPreRunCreateScript_valid(t *testing.T) { +func TestRunCreateScript_valid(t *testing.T) { cmds := []string{"echo", "date"} - scriptName, err := createScript(cmds) + scriptName, err := createScript(cmds, "post_apply") Assert(t, scriptName != "", "there should be a script name") Assert(t, err == nil, "there should not be an error") } -func TestPreRunExecuteScript_invalid(t *testing.T) { +func TestRunExecuteScript_invalid(t *testing.T) { cmds := []string{"invalid", "command"} - scriptName, _ := createScript(cmds) + scriptName, _ := createScript(cmds, "post_apply") _, err := execute(scriptName) Assert(t, err != nil, "there should be an error") } -func TestPreRunExecuteScript_valid(t *testing.T) { +func TestRunExecuteScript_valid(t *testing.T) { cmds := []string{"echo", "date"} - scriptName, _ := createScript(cmds) + scriptName, _ := createScript(cmds, "post_apply") output, err := execute(scriptName) Assert(t, err == nil, "there should not be an error") Assert(t, output != "", "there should be output") } -func TestPreRun_valid(t *testing.T) { +func TestRun_valid(t *testing.T) { cmds := []string{"echo", "date"} version, _ := version.NewVersion("0.8.8") - _, err := preRun.Execute(logger, cmds, "/tmp/atlantis", "staging", version) + _, err := run.Execute(logger, cmds, "/tmp/atlantis", "staging", version, "post_apply") Ok(t, err) } diff --git a/server/apply_executor.go b/server/apply_executor.go index ba932c53..67c02f1a 100644 --- a/server/apply_executor.go +++ b/server/apply_executor.go @@ -13,7 +13,7 @@ import ( "github.com/hootsuite/atlantis/github" "github.com/hootsuite/atlantis/locking" "github.com/hootsuite/atlantis/models" - "github.com/hootsuite/atlantis/prerun" + "github.com/hootsuite/atlantis/run" "github.com/hootsuite/atlantis/terraform" ) @@ -25,7 +25,7 @@ type ApplyExecutor struct { githubCommentRenderer *GithubCommentRenderer lockingClient *locking.Client requireApproval bool - preRun *prerun.PreRun + run *run.Run configReader *ConfigReader concurrentRunLocker *ConcurrentRunLocker workspace *Workspace @@ -117,7 +117,7 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P var applyExtraArgs []string var config ProjectConfig if a.configReader.Exists(absolutePath) { - config, err := a.configReader.Read(absolutePath) + config, err = a.configReader.Read(absolutePath) if err != nil { return ProjectResult{Error: err} } @@ -132,6 +132,7 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P if a.awsConfig != nil { awsSession, err := a.awsConfig.CreateSession(ctx.User.Username) if err != nil { + ctx.Log.Err(err.Error()) return ProjectResult{Error: err} } creds, err := awsSession.Config.Credentials.Get() @@ -156,7 +157,7 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P } constraints, _ := version.NewConstraint(">= 0.9.0") if constraints.Check(terraformVersion) { - ctx.Log.Info("determined that we are running terraform with version >= 0.9.0") + ctx.Log.Info("determined that we are running terraform with version >= 0.9.0. Running version %s", terraformVersion) _, err := a.terraform.RunInitAndEnv(ctx.Log, absolutePath, tfEnv, config.GetExtraArguments("init"), credsEnvVars, terraformVersion) if err != nil { return ProjectResult{Error: err} @@ -165,9 +166,9 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P // if there are pre apply commands then run them if len(config.PreApply.Commands) > 0 { - _, err := a.preRun.Execute(ctx.Log, config.PreApply.Commands, absolutePath, ctx.Command.Environment, config.TerraformVersion) + _, err := a.run.Execute(ctx.Log, config.PreApply.Commands, absolutePath, tfEnv, terraformVersion, "pre_apply") if err != nil { - return ProjectResult{Error: err} + return ProjectResult{Error: errors.Wrap(err, "running pre apply commands")} } } @@ -178,6 +179,14 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P } ctx.Log.Info("apply succeeded") + // if there are post apply commands then run them + if len(config.PostApply.Commands) > 0 { + _, err := a.run.Execute(ctx.Log, config.PostApply.Commands, absolutePath, tfEnv, terraformVersion, "post_apply") + if err != nil { + return ProjectResult{Error: errors.Wrap(err, "running post apply commands")} + } + } + return ProjectResult{ApplySuccess: output} } diff --git a/server/plan_executor.go b/server/plan_executor.go index e9e834c1..2d217cd8 100644 --- a/server/plan_executor.go +++ b/server/plan_executor.go @@ -12,7 +12,7 @@ import ( "github.com/hootsuite/atlantis/github" "github.com/hootsuite/atlantis/locking" "github.com/hootsuite/atlantis/models" - "github.com/hootsuite/atlantis/prerun" + "github.com/hootsuite/atlantis/run" "github.com/hootsuite/atlantis/terraform" "github.com/pkg/errors" ) @@ -29,7 +29,7 @@ type PlanExecutor struct { lockingClient *locking.Client // LockURL is a function that given a lock id will return a url for lock view LockURL func(id string) (url string) - preRun *prerun.PreRun + run *run.Run configReader *ConfigReader concurrentRunLocker *ConcurrentRunLocker workspace *Workspace @@ -151,13 +151,13 @@ func (p *PlanExecutor) plan(ctx *CommandContext, repoDir string, project models. } constraints, _ := version.NewConstraint(">= 0.9.0") if constraints.Check(terraformVersion) { - ctx.Log.Info("determined that we are running terraform with version >= 0.9.0") + ctx.Log.Info("determined that we are running terraform with version >= 0.9.0. Running version %s", terraformVersion) _, err := p.terraform.RunInitAndEnv(ctx.Log, absolutePath, tfEnv, config.GetExtraArguments("init"), credsEnvVars, terraformVersion) if err != nil { return ProjectResult{Error: err} } } else { - ctx.Log.Info("determined that we are running terraform with version < 0.9.0") + ctx.Log.Info("determined that we are running terraform with version < 0.9.0. Running version %s", terraformVersion) terraformGetCmd := append([]string{"get", "-no-color"}, config.GetExtraArguments("get")...) _, err := p.terraform.RunCommandWithVersion(ctx.Log, absolutePath, terraformGetCmd, nil, terraformVersion) if err != nil { @@ -167,7 +167,7 @@ func (p *PlanExecutor) plan(ctx *CommandContext, repoDir string, project models. // if there are pre plan commands then run them if len(config.PrePlan.Commands) > 0 { - _, err := p.preRun.Execute(ctx.Log, config.PrePlan.Commands, absolutePath, tfEnv, terraformVersion) + _, err := p.run.Execute(ctx.Log, config.PrePlan.Commands, absolutePath, tfEnv, terraformVersion, "pre_plan") if err != nil { return ProjectResult{Error: errors.Wrap(err, "running pre plan commands")} } @@ -192,6 +192,14 @@ func (p *PlanExecutor) plan(ctx *CommandContext, repoDir string, project models. } ctx.Log.Info("plan succeeded") + // if there are post plan commands then run them + if len(config.PostPlan.Commands) > 0 { + _, err := p.run.Execute(ctx.Log, config.PostPlan.Commands, absolutePath, tfEnv, terraformVersion, "post_plan") + if err != nil { + return ProjectResult{Error: errors.Wrap(err, "running post plan commands")} + } + } + return ProjectResult{ PlanSuccess: &PlanSuccess{ TerraformOutput: output, diff --git a/server/project_config.go b/server/project_config.go index 9baaa932..dce6345b 100644 --- a/server/project_config.go +++ b/server/project_config.go @@ -16,22 +16,34 @@ type PrePlan struct { Commands []string `yaml:"commands"` } +type PostPlan struct { + Commands []string `yaml:"commands"` +} + type PreApply struct { Commands []string `yaml:"commands"` } +type PostApply struct { + Commands []string `yaml:"commands"` +} + type ConfigReader struct{} type ProjectConfigYaml struct { PrePlan PrePlan `yaml:"pre_plan"` + PostPlan PostPlan `yaml:"post_plan"` PreApply PreApply `yaml:"pre_apply"` + PostApply PostApply `yaml:"post_apply"` TerraformVersion string `yaml:"terraform_version"` ExtraArguments []CommandExtraArguments `yaml:"extra_arguments"` } type ProjectConfig struct { - PrePlan PrePlan - PreApply PreApply + PrePlan PrePlan + PostPlan PostPlan + PreApply PreApply + PostApply PostApply // TerraformVersion is the version specified in the config file or nil if version wasn't specified TerraformVersion *version.Version ExtraArguments []CommandExtraArguments @@ -70,8 +82,10 @@ func (c *ConfigReader) Read(execPath string) (ProjectConfig, error) { return ProjectConfig{ TerraformVersion: v, ExtraArguments: pcYaml.ExtraArguments, + PostApply: pcYaml.PostApply, PreApply: pcYaml.PreApply, PrePlan: pcYaml.PrePlan, + PostPlan: pcYaml.PostPlan, }, nil } diff --git a/server/project_config_test.go b/server/project_config_test.go index 6573724f..792b7b43 100644 --- a/server/project_config_test.go +++ b/server/project_config_test.go @@ -12,6 +12,10 @@ var tempConfigFile = "/tmp/" + ProjectConfigFile var projectConfigFileStr = ` --- terraform_version: "0.0.1" +post_apply: + commands: + - "echo" + - "date" pre_apply: commands: - "echo" diff --git a/server/server.go b/server/server.go index f94eb192..b9c0dd56 100644 --- a/server/server.go +++ b/server/server.go @@ -19,7 +19,7 @@ import ( "github.com/hootsuite/atlantis/locking/boltdb" "github.com/hootsuite/atlantis/logging" "github.com/hootsuite/atlantis/models" - "github.com/hootsuite/atlantis/prerun" + "github.com/hootsuite/atlantis/run" "github.com/hootsuite/atlantis/terraform" homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" @@ -103,7 +103,7 @@ func NewServer(config ServerConfig) (*Server, error) { return nil, err } lockingClient := locking.NewClient(boltdb) - preRun := &prerun.PreRun{} + run := &run.Run{} configReader := &ConfigReader{} concurrentRunLocker := NewConcurrentRunLocker() workspace := &Workspace{ @@ -117,7 +117,7 @@ func NewServer(config ServerConfig) (*Server, error) { githubCommentRenderer: githubComments, lockingClient: lockingClient, requireApproval: config.RequireApproval, - preRun: preRun, + run: run, configReader: configReader, concurrentRunLocker: concurrentRunLocker, workspace: workspace, @@ -129,7 +129,7 @@ func NewServer(config ServerConfig) (*Server, error) { terraform: terraformClient, githubCommentRenderer: githubComments, lockingClient: lockingClient, - preRun: preRun, + run: run, configReader: configReader, concurrentRunLocker: concurrentRunLocker, workspace: workspace,