Skip to content

Commit

Permalink
Add pre_init and pre_get hooks. Fixes hootsuite#160
Browse files Browse the repository at this point in the history
  • Loading branch information
lkysow authored and Luke Kysow committed Oct 23, 2017
1 parent 9626b5f commit a3e503c
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 80 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,10 @@ 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 **before** `init`, `get`, `plan` and `apply` with `pre_init`, `pre_get`, `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`
- the commmands that we support adding extra args to are `init`, `get`, `plan` and `apply`
- what version of Terraform to use (see [Terraform Versions](#terraform-versions))

The schema of the `atlantis.yaml` project config file is
Expand All @@ -174,6 +175,14 @@ The schema of the `atlantis.yaml` project config file is
# atlantis.yaml
---
terraform_version: 0.8.8 # optional version
# pre_init commands are run when the Terraform version is >= 0.9.0
pre_init:
commands:
- "curl http://example.com"
# pre_get commands are run when the Terraform version is < 0.9.0
pre_get:
commands:
- "curl http://example.com"
pre_plan:
commands:
- "curl http://example.com"
Expand Down
4 changes: 2 additions & 2 deletions server/events/apply_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P
}
ctx.Log.Info("apply succeeded")

if len(config.PostApply.Commands) > 0 {
_, err := a.Run.Execute(ctx.Log, config.PostApply.Commands, absolutePath, env, terraformVersion, "post_apply")
if len(config.PostApply) > 0 {
_, err := a.Run.Execute(ctx.Log, config.PostApply, absolutePath, env, terraformVersion, "post_apply")
if err != nil {
return ProjectResult{Error: errors.Wrap(err, "running post apply commands")}
}
Expand Down
4 changes: 2 additions & 2 deletions server/events/plan_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ 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 {
if len(config.PostPlan) > 0 {
absolutePath := filepath.Join(repoDir, project.Path)
_, err := p.Run.Execute(ctx.Log, config.PostPlan.Commands, absolutePath, tfEnv, terraformVersion, "post_plan")
_, err := p.Run.Execute(ctx.Log, config.PostPlan, absolutePath, tfEnv, terraformVersion, "post_plan")
if err != nil {
return ProjectResult{Error: errors.Wrap(err, "running post plan commands")}
}
Expand Down
114 changes: 70 additions & 44 deletions server/events/project_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,97 @@ import (
"os"
"path/filepath"

version "github.com/hashicorp/go-version"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
"gopkg.in/yaml.v2"
)

const ProjectConfigFile = "atlantis.yaml"

type PrePlan struct {
Commands []string `yaml:"commands"`
}

type PostPlan struct {
Commands []string `yaml:"commands"`
}
//go:generate pegomock generate --use-experimental-model-gen --package mocks -o mocks/mock_project_config_reader.go ProjectConfigReader

type PreApply struct {
Commands []string `yaml:"commands"`
// ProjectConfigReader implements reading project config.
type ProjectConfigReader interface {
// Exists returns true if a project config file exists at projectPath.
Exists(projectPath string) bool
// Read attempts to read the project config file for the project at projectPath.
// NOTE: projectPath is not the path to the actual config file.
// Returns the parsed ProjectConfig or error if unable to read.
Read(projectPath string) (ProjectConfig, error)
}

type PostApply struct {
// Hook represents the commands that can be run at a certain stage.
type Hook struct {
Commands []string `yaml:"commands"`
}

//go:generate pegomock generate --use-experimental-model-gen --package mocks -o mocks/mock_project_config_reader.go ProjectConfigReader
type ProjectConfigReader interface {
Exists(execPath string) bool
Read(execPath string) (ProjectConfig, error)
}

type ProjectConfigManager struct{}

type ProjectConfigYaml struct {
PrePlan PrePlan `yaml:"pre_plan"`
PostPlan PostPlan `yaml:"post_plan"`
PreApply PreApply `yaml:"pre_apply"`
PostApply PostApply `yaml:"post_apply"`
// projectConfigYAML is used to parse the YAML.
type projectConfigYAML struct {
PreInit Hook `yaml:"pre_init"`
PreGet Hook `yaml:"pre_get"`
PrePlan Hook `yaml:"pre_plan"`
PostPlan Hook `yaml:"post_plan"`
PreApply Hook `yaml:"pre_apply"`
PostApply Hook `yaml:"post_apply"`
TerraformVersion string `yaml:"terraform_version"`
ExtraArguments []CommandExtraArguments `yaml:"extra_arguments"`
ExtraArguments []commandExtraArguments `yaml:"extra_arguments"`
}

// ProjectConfig is a more usable version of projectConfigYAML that we can
// return to our callers. It holds the config for a project.
type ProjectConfig struct {
PrePlan PrePlan
PostPlan PostPlan
PreApply PreApply
PostApply PostApply
// TerraformVersion is the version specified in the config file or nil if version wasn't specified
// PreInit is a slice of command strings to run prior to terraform init.
PreInit []string
// PreGet is a slice of command strings to run prior to terraform get.
PreGet []string
// PrePlan is a slice of command strings to run prior to terraform plan.
PrePlan []string
// PostPlan is a slice of command strings to run after terraform plan.
PostPlan []string
// PreApply is a slice of command strings to run prior to terraform apply.
PreApply []string
// PostApply is a slice of command strings to run after terraform apply.
PostApply []string
// TerraformVersion is the version specified in the config file or nil
// if version wasn't specified.
TerraformVersion *version.Version
ExtraArguments []CommandExtraArguments
// extraArguments is the extra args that we should tack on to certain
// terraform commands. It shouldn't be used directly and instead callers
// should use the GetExtraArguments method on ProjectConfig.
extraArguments []commandExtraArguments
}

type CommandExtraArguments struct {
Name string `yaml:"command_name"`
// commandExtraArguments is used to parse the config file. These are the args
// that should be tacked on to certain terraform commands.
type commandExtraArguments struct {
// Name is the name of the command we should add the args to.
Name string `yaml:"command_name"`
// Arguments is the list of args we should append.
Arguments []string `yaml:"arguments"`
}

func (c *ProjectConfigManager) Exists(execPath string) bool {
// Check if config file exists
_, err := os.Stat(filepath.Join(execPath, ProjectConfigFile))
// ProjectConfigManager deals with project config files that users can
// use to specify additional behaviour around how Atlantis executes for a project.
type ProjectConfigManager struct{}

// Exists returns true if an atlantis config file exists for the project at
// projectPath. projectPath is an absolute path to the project.
func (c *ProjectConfigManager) Exists(projectPath string) bool {
_, err := os.Stat(filepath.Join(projectPath, ProjectConfigFile))
return err == nil
}

// Read attempts to read the project config file for the project at projectPath.
// NOTE: projectPath is not the path to the actual config file.
// Returns the parsed ProjectConfig or error if unable to read.
func (c *ProjectConfigManager) Read(execPath string) (ProjectConfig, error) {
var pc ProjectConfig
filename := filepath.Join(execPath, ProjectConfigFile)
raw, err := ioutil.ReadFile(filename)
if err != nil {
return pc, errors.Wrapf(err, "reading %s", ProjectConfigFile)
}
var pcYaml ProjectConfigYaml
var pcYaml projectConfigYAML
if err := yaml.Unmarshal(raw, &pcYaml); err != nil {
return pc, errors.Wrapf(err, "parsing %s", ProjectConfigFile)
}
Expand All @@ -87,16 +109,20 @@ func (c *ProjectConfigManager) Read(execPath string) (ProjectConfig, error) {
}
return ProjectConfig{
TerraformVersion: v,
ExtraArguments: pcYaml.ExtraArguments,
PostApply: pcYaml.PostApply,
PreApply: pcYaml.PreApply,
PrePlan: pcYaml.PrePlan,
PostPlan: pcYaml.PostPlan,
extraArguments: pcYaml.ExtraArguments,
PreInit: pcYaml.PreInit.Commands,
PreGet: pcYaml.PreGet.Commands,
PostApply: pcYaml.PostApply.Commands,
PreApply: pcYaml.PreApply.Commands,
PrePlan: pcYaml.PrePlan.Commands,
PostPlan: pcYaml.PostPlan.Commands,
}, nil
}

// GetExtraArguments returns the arguments that were specified to be appended
// to command in the project config file.
func (c *ProjectConfig) GetExtraArguments(command string) []string {
for _, value := range c.ExtraArguments {
for _, value := range c.extraArguments {
if value.Name == command {
return value.Arguments
}
Expand Down
71 changes: 51 additions & 20 deletions server/events/project_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,81 @@ var tempConfigFile = "/tmp/" + events.ProjectConfigFile
var projectConfigFileStr = `
---
terraform_version: "0.0.1"
post_apply:
pre_init:
commands:
- "echo"
- "date"
pre_apply:
- "echo"
- "pre_init"
pre_get:
commands:
- "echo"
- "date"
- "echo"
- "pre_get"
pre_plan:
commands:
- "echo"
- "date"
- "echo"
- "pre_plan"
post_plan:
commands:
- "echo"
- "post_plan"
pre_apply:
commands:
- "echo"
- "pre_apply"
post_apply:
commands:
- "echo"
- "post_apply"
extra_arguments:
- command_name: "plan"
arguments: ["-var", "hello=world"]
- command_name: "init"
arguments: ["arg", "init"]
- command_name: "get"
arguments: ["arg", "get"]
- command_name: "plan"
arguments: ["arg", "plan"]
- command_name: "apply"
arguments: ["arg", "apply"]
`

func TestConfigFileExists_invalid_path(t *testing.T) {
var c events.ProjectConfigManager
var c events.ProjectConfigManager

func TestExists_InvalidPath(t *testing.T) {
t.Log("given a path to a directory that doesn't exist Exists should return false")
Equals(t, c.Exists("/invalid/path"), false)
}

func TestConfigFileExists_valid_path(t *testing.T) {
var c events.ProjectConfigManager
func TestExists_ValidPath(t *testing.T) {
t.Log("given a path to a directory with an atlantis config file, Exists returns true")
writeAtlantisConfigFile([]byte(projectConfigFileStr))
defer os.Remove(tempConfigFile)
Equals(t, c.Exists("/tmp"), true)
}

func TestConfigFileRead_invalid_config(t *testing.T) {
var c events.ProjectConfigManager
func TestRead_InvalidConfig(t *testing.T) {
t.Log("when the config file has invalid yaml, we expect an error")
str := []byte(`---invalid`)
writeAtlantisConfigFile(str)
defer os.Remove(tempConfigFile)
_, err := c.Read("/tmp")
Assert(t, err != nil, "expect an error")
}

func TestConfigFileRead_valid_config(t *testing.T) {
var c events.ProjectConfigManager
func TestRead_ValidConfig(t *testing.T) {
t.Log("when the config file has valid yaml, it should be parsed")
writeAtlantisConfigFile([]byte(projectConfigFileStr))
defer os.Remove(tempConfigFile)
_, err := c.Read("/tmp")
Assert(t, err == nil, "should be valid yaml")
config, err := c.Read("/tmp")
Ok(t, err)
Equals(t, []string{"echo", "pre_init"}, config.PreInit)
Equals(t, []string{"echo", "pre_get"}, config.PreGet)
Equals(t, []string{"echo", "pre_plan"}, config.PrePlan)
Equals(t, []string{"echo", "post_plan"}, config.PostPlan)
Equals(t, []string{"echo", "pre_apply"}, config.PreApply)
Equals(t, []string{"echo", "post_apply"}, config.PostApply)
Equals(t, []string{"arg", "init"}, config.GetExtraArguments("init"))
Equals(t, []string{"arg", "get"}, config.GetExtraArguments("get"))
Equals(t, []string{"arg", "plan"}, config.GetExtraArguments("plan"))
Equals(t, []string{"arg", "apply"}, config.GetExtraArguments("apply"))
Equals(t, 0, len(config.GetExtraArguments("not-specified")))
}

func writeAtlantisConfigFile(s []byte) error {
Expand Down
16 changes: 14 additions & 2 deletions server/events/project_pre_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,24 @@ func (p *ProjectPreExecute) Execute(ctx *CommandContext, repoDir string, project
constraints, _ := version.NewConstraint(">= 0.9.0")
if constraints.Check(terraformVersion) {
ctx.Log.Info("determined that we are running terraform with version >= 0.9.0. Running version %s", terraformVersion)
if len(config.PreInit) > 0 {
_, err := p.Run.Execute(ctx.Log, config.PreInit, absolutePath, tfEnv, terraformVersion, "pre_init")
if err != nil {
return PreExecuteResult{ProjectResult: ProjectResult{Error: errors.Wrapf(err, "running %s commands", "pre_init")}}
}
}
_, err := p.Terraform.RunInitAndEnv(ctx.Log, absolutePath, tfEnv, config.GetExtraArguments("init"), terraformVersion)
if err != nil {
return PreExecuteResult{ProjectResult: ProjectResult{Error: err}}
}
} else {
ctx.Log.Info("determined that we are running terraform with version < 0.9.0. Running version %s", terraformVersion)
if len(config.PreGet) > 0 {
_, err := p.Run.Execute(ctx.Log, config.PreGet, absolutePath, tfEnv, terraformVersion, "pre_get")
if err != nil {
return PreExecuteResult{ProjectResult: ProjectResult{Error: errors.Wrapf(err, "running %s commands", "pre_get")}}
}
}
terraformGetCmd := append([]string{"get", "-no-color"}, config.GetExtraArguments("get")...)
_, err := p.Terraform.RunCommandWithVersion(ctx.Log, absolutePath, terraformGetCmd, terraformVersion, tfEnv)
if err != nil {
Expand All @@ -75,9 +87,9 @@ func (p *ProjectPreExecute) Execute(ctx *CommandContext, repoDir string, project
stage := fmt.Sprintf("pre_%s", strings.ToLower(ctx.Command.Name.String()))
var commands []string
if ctx.Command.Name == Plan {
commands = config.PrePlan.Commands
commands = config.PrePlan
} else {
commands = config.PreApply.Commands
commands = config.PreApply
}
if len(commands) > 0 {
_, err := p.Run.Execute(ctx.Log, commands, absolutePath, tfEnv, terraformVersion, stage)
Expand Down
Loading

0 comments on commit a3e503c

Please sign in to comment.