Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixed preapply commands and added postapply/postplan #102

Merged
merged 2 commits into from
Aug 10, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** `apply` with `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))

Expand All @@ -159,13 +160,16 @@ pre_plan:
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`, `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`
Expand Down
89 changes: 89 additions & 0 deletions postrun/post_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Package postrun handles running commands after the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of redundant code here with prerun package doing the same thing. We should restructure this so we can use the code for both prerun and postrun pacakges.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. I'll get rid of the copy paste replace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anubhavmishra The code is basically the same, I could move duplicate functions with different names in a more general package "run" or we could just rename the package and functions to "run". What were you thinking?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep! run might make more sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay @anubhavmishra , settled on a more general package and opted to pass in the stage name(i.e "pre_plan", "post_plan", etc)

// regular Atlantis commands.
package postrun

import (
"bufio"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"

version "github.com/hashicorp/go-version"
"github.com/hootsuite/atlantis/logging"
"github.com/pkg/errors"
)

const inlineShebang = "#!/bin/sh -e"

type PostRun struct{}

// Execute runs the commands by writing them as a script to disk
// and then executing the script.
func (p *PostRun) Execute(
log *logging.SimpleLogger,
commands []string,
path string,
environment string,
terraformVersion *version.Version) (string, error) {
// we create a script from the commands provided
if len(commands) == 0 {
return "", errors.New("postrun commands cannot be empty")
}

s, err := createScript(commands)
if err != nil {
return "", err
}
defer os.Remove(s)

log.Info("running postrun commands: %v", commands)

// set environment variable for the run.
// this is to support scripts to use the ENVIRONMENT, ATLANTIS_TERRAFORM_VERSION
// and WORKSPACE variables in their scripts
os.Setenv("ENVIRONMENT", environment)
os.Setenv("ATLANTIS_TERRAFORM_VERSION", terraformVersion.String())
os.Setenv("WORKSPACE", path)
return execute(s)
}

func createScript(cmds []string) (string, error) {
tmp, err := ioutil.TempFile("/tmp", "atlantis-temp-script")
if err != nil {
return "", errors.Wrap(err, "preparing post run shell script")
}

scriptName := tmp.Name()

// Write our contents to it
writer := bufio.NewWriter(tmp)
writer.WriteString(fmt.Sprintf("%s\n", inlineShebang))
cmdsJoined := strings.Join(cmds, "\n")
if _, err := writer.WriteString(cmdsJoined); err != nil {
return "", errors.Wrap(err, "preparing post run")
}

if err := writer.Flush(); err != nil {
return "", errors.Wrap(err, "flushing contents to file")
}
tmp.Close()

if err := os.Chmod(scriptName, 0755); err != nil {
return "", errors.Wrap(err, "making post run script executable")
}

return scriptName, nil
}

func execute(script string) (string, error) {
localCmd := exec.Command("sh", "-c", script)
out, err := localCmd.CombinedOutput()
output := string(out)
if err != nil {
return output, errors.Wrapf(err, "running script %s: %s", script, output)
}

return output, nil
}
43 changes: 43 additions & 0 deletions postrun/post_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package postrun

import (
"log"
"os"
"testing"

version "github.com/hashicorp/go-version"
"github.com/hootsuite/atlantis/logging"
. "github.com/hootsuite/atlantis/testing_util"
)

var logger = logging.NewSimpleLogger("", log.New(os.Stderr, "", log.LstdFlags), false, logging.Debug)
var postRun = &PostRun{}

func TestPostRunCreateScript_valid(t *testing.T) {
cmds := []string{"echo", "date"}
scriptName, err := createScript(cmds)
Assert(t, scriptName != "", "there should be a script name")
Assert(t, err == nil, "there should not be an error")
}

func TestPostRunExecuteScript_invalid(t *testing.T) {
cmds := []string{"invalid", "command"}
scriptName, _ := createScript(cmds)
_, err := execute(scriptName)
Assert(t, err != nil, "there should be an error")
}

func TestPostRunExecuteScript_valid(t *testing.T) {
cmds := []string{"echo", "date"}
scriptName, _ := createScript(cmds)
output, err := execute(scriptName)
Assert(t, err == nil, "there should not be an error")
Assert(t, output != "", "there should be output")
}

func TestPostRun_valid(t *testing.T) {
cmds := []string{"echo", "date"}
version, _ := version.NewVersion("0.8.8")
_, err := postRun.Execute(logger, cmds, "/tmp/atlantis", "staging", version)
Ok(t, err)
}
19 changes: 15 additions & 4 deletions server/apply_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/hootsuite/atlantis/github"
"github.com/hootsuite/atlantis/locking"
"github.com/hootsuite/atlantis/models"
"github.com/hootsuite/atlantis/postrun"
"github.com/hootsuite/atlantis/prerun"
"github.com/hootsuite/atlantis/terraform"
)
Expand All @@ -26,6 +27,7 @@ type ApplyExecutor struct {
lockingClient *locking.Client
requireApproval bool
preRun *prerun.PreRun
postRun *postrun.PostRun
configReader *ConfigReader
concurrentRunLocker *ConcurrentRunLocker
workspace *Workspace
Expand Down Expand Up @@ -117,7 +119,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}
}
Expand All @@ -132,6 +134,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()
Expand All @@ -156,7 +159,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}
Expand All @@ -165,9 +168,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.preRun.Execute(ctx.Log, config.PreApply.Commands, absolutePath, tfEnv, terraformVersion)
if err != nil {
return ProjectResult{Error: err}
return ProjectResult{Error: errors.Wrap(err, "running pre apply commands")}
}
}

Expand All @@ -178,6 +181,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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are adding post_apply might as well add post_plan too. Some people might want to use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing. for whatever reason i just figure pre_apply was the same thing as post_plan, but you are right, the steps are different. Let me add it.

_, err := a.postRun.Execute(ctx.Log, config.PostApply.Commands, absolutePath, tfEnv, terraformVersion)
if err != nil {
return ProjectResult{Error: errors.Wrap(err, "running post apply commands")}
}
}

return ProjectResult{ApplySuccess: output}
}

Expand Down
4 changes: 2 additions & 2 deletions server/plan_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 9 additions & 2 deletions server/project_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,24 @@ 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"`
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
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
Expand Down Expand Up @@ -70,6 +76,7 @@ func (c *ConfigReader) Read(execPath string) (ProjectConfig, error) {
return ProjectConfig{
TerraformVersion: v,
ExtraArguments: pcYaml.ExtraArguments,
PostApply: pcYaml.PostApply,
PreApply: pcYaml.PreApply,
PrePlan: pcYaml.PrePlan,
}, nil
Expand Down
4 changes: 4 additions & 0 deletions server/project_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ var tempConfigFile = "/tmp/" + ProjectConfigFile
var projectConfigFileStr = `
---
terraform_version: "0.0.1"
post_apply:
commands:
- "echo"
- "date"
pre_apply:
commands:
- "echo"
Expand Down