Skip to content

Commit

Permalink
Merge pull request #540 from lorengordon/hook-init-once
Browse files Browse the repository at this point in the history
Adds options attribute to track state of the terraform command
  • Loading branch information
brikis98 authored Aug 15, 2018
2 parents 7e5e256 + b15ffe3 commit a791566
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 76 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1670,6 +1670,11 @@ that will be called either before or after execution of the `terraform` command.
The `commands` array specifies the `terraform` commands that will trigger the execution of the hook.
Because Terragrunt uses `terraform init` in two different ways, use the special command `init-from-module`
to execute hooks only when retrieving the terraform source. The command `init` will execute hooks only when
executing Terragrunt's [Auto-Init](#auto-init) capability, which includes configuring the backend, retrieving
provider plugins, and remote modules specified within the root module.
It is possible to use the [interpolation syntax](#interpolation-syntax) defined above in the `execute` array of the hooks.
The `run_on_error` parameter allows you to specify whether you still want this hook to execute if an error has been encountered with the earlier execution of another hook OR with the execution of the `terraform` command iteself.
Expand Down Expand Up @@ -1703,6 +1708,11 @@ terragrunt = {
execute = ["echo", "Baz"]
run_on_error = true
}
after_hook "init_from_module" {
commands = ["init-from-module"]
execute = ["cp", "${get_parent_tfvars_dir()}/foo.tf", "."]
}
}
}
```
Expand Down
21 changes: 2 additions & 19 deletions cli/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func parseTerragruntOptionsFromArgs(args []string, writer, errWriter io.Writer)
opts.AutoInit = !parseBooleanArg(args, OPT_TERRAGRUNT_NO_AUTO_INIT, os.Getenv("TERRAGRUNT_AUTO_INIT") == "false")
opts.NonInteractive = parseBooleanArg(args, OPT_NON_INTERACTIVE, os.Getenv("TF_INPUT") == "false" || os.Getenv("TF_INPUT") == "0")
opts.TerraformCliArgs = filterTerragruntArgs(args)
opts.TerraformCommand = util.FirstArg(opts.TerraformCliArgs)
opts.WorkingDir = filepath.ToSlash(workingDir)
opts.DownloadDir = filepath.ToSlash(downloadDir)
opts.Logger = util.CreateLoggerWithWriter(errWriter, "")
Expand All @@ -113,7 +114,7 @@ func parseTerragruntOptionsFromArgs(args []string, writer, errWriter io.Writer)

func filterTerraformExtraArgs(terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) []string {
out := []string{}
cmd := firstArg(terragruntOptions.TerraformCliArgs)
cmd := util.FirstArg(terragruntOptions.TerraformCliArgs)

for _, arg := range terragruntConfig.Terraform.ExtraArgs {
for _, arg_cmd := range arg.Commands {
Expand Down Expand Up @@ -208,24 +209,6 @@ func parseStringArg(args []string, argName string, defaultValue string) (string,
return defaultValue, nil
}

// A convenience method that returns the first item (0th index) in the given list or an empty string if this is an
// empty list
func firstArg(args []string) string {
if len(args) > 0 {
return args[0]
}
return ""
}

// A convenience method that returns the second item (1st index) in the given list or an empty string if this is a
// list that has less than 2 items in it
func secondArg(args []string) string {
if len(args) > 1 {
return args[1]
}
return ""
}

// Custom error types

type ArgMissingValue string
Expand Down
18 changes: 12 additions & 6 deletions cli/cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const CMD_OUTPUT_ALL = "output-all"
const CMD_VALIDATE_ALL = "validate-all"

const CMD_INIT = "init"
const CMD_INIT_FROM_MODULE = "init-from-module"

// CMD_SPIN_UP is deprecated.
const CMD_SPIN_UP = "spin-up"
Expand Down Expand Up @@ -290,7 +291,7 @@ func shouldRunHook(hook config.Hook, terragruntOptions *options.TerragruntOption
//for the len(previousExecErrors) == 0 check that used to be here
multiError := errors.NewMultiError(previousExecErrors...)

return util.ListContainsElement(hook.Commands, terragruntOptions.TerraformCliArgs[0]) && (multiError == nil || hook.RunOnError)
return util.ListContainsElement(hook.Commands, terragruntOptions.TerraformCommand) && (multiError == nil || hook.RunOnError)
}

// Assume an IAM role, if one is specified, by making API calls to Amazon STS and setting the environment variables
Expand Down Expand Up @@ -322,7 +323,7 @@ func runTerragruntWithConfig(terragruntOptions *options.TerragruntOptions, terra
terragruntOptions.InsertTerraformCliArgs(filterTerraformExtraArgs(terragruntOptions, terragruntConfig)...)
}

if firstArg(terragruntOptions.TerraformCliArgs) == CMD_INIT {
if util.FirstArg(terragruntOptions.TerraformCliArgs) == CMD_INIT {
if err := prepareInitCommand(terragruntOptions, terragruntConfig, allowSourceDownload); err != nil {
return err
}
Expand Down Expand Up @@ -468,7 +469,7 @@ func prepareNonInitCommand(terragruntOptions *options.TerragruntOptions, terragr

// Determines if 'terraform init' needs to be executed
func needsInit(terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) (bool, error) {
if util.ListContainsElement(TERRAFORM_COMMANDS_THAT_DO_NOT_NEED_INIT, firstArg(terragruntOptions.TerraformCliArgs)) {
if util.ListContainsElement(TERRAFORM_COMMANDS_THAT_DO_NOT_NEED_INIT, util.FirstArg(terragruntOptions.TerraformCliArgs)) {
return false, nil
}

Expand Down Expand Up @@ -506,14 +507,15 @@ func providersNeedInit(terragruntOptions *options.TerragruntOptions) bool {
func runTerraformInit(terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig, terraformSource *TerraformSource) error {

// Prevent Auto-Init if the user has disabled it
if firstArg(terragruntOptions.TerraformCliArgs) != CMD_INIT && !terragruntOptions.AutoInit {
if util.FirstArg(terragruntOptions.TerraformCliArgs) != CMD_INIT && !terragruntOptions.AutoInit {
return errors.WithStackTrace(InitNeededButDisabled("Cannot continue because init is needed, but Auto-Init is disabled. You must run 'terragrunt init' manually."))
}

// Need to clone the terragruntOptions, so the TerraformCliArgs can be configured to run the init command
initOptions := terragruntOptions.Clone(terragruntOptions.TerragruntConfigPath)
initOptions.TerraformCliArgs = []string{CMD_INIT}
initOptions.WorkingDir = terragruntOptions.WorkingDir
initOptions.TerraformCommand = CMD_INIT

// Don't pollute stdout with the stdout from Aoto Init
initOptions.Writer = initOptions.ErrWriter
Expand All @@ -533,6 +535,9 @@ func runTerraformInit(terragruntOptions *options.TerragruntOptions, terragruntCo
initOptions.AppendTerraformCliArgs("-get-plugins=false")
initOptions.AppendTerraformCliArgs("-backend=false")

// Set the TerraformCommand attribute to match hooks on `init-from-module`
initOptions.TerraformCommand = CMD_INIT_FROM_MODULE

v0_10_0, err := version.NewVersion("v0.10.0")
if err != nil {
return err
Expand All @@ -545,6 +550,7 @@ func runTerraformInit(terragruntOptions *options.TerragruntOptions, terragruntCo
// Terraform versions >= 0.10.0 specify the module source using the -from-module option
initOptions.AppendTerraformCliArgs("-from-module="+terraformSource.CanonicalSourceURL.String(), "-no-color")
}

initOptions.AppendTerraformCliArgs(terraformSource.DownloadDir)
}

Expand Down Expand Up @@ -618,7 +624,7 @@ func remoteStateNeedsInit(remoteState *remote.RemoteState, terragruntOptions *op

// We only configure remote state for the commands that use the tfstate files. We do not configure it for
// commands such as "get" or "version".
if remoteState != nil && util.ListContainsElement(TERRAFORM_COMMANDS_THAT_USE_STATE, firstArg(terragruntOptions.TerraformCliArgs)) {
if remoteState != nil && util.ListContainsElement(TERRAFORM_COMMANDS_THAT_USE_STATE, util.FirstArg(terragruntOptions.TerraformCliArgs)) {
return remoteState.NeedsInit(terragruntOptions)
}
return false, nil
Expand Down Expand Up @@ -703,7 +709,7 @@ func validateAll(terragruntOptions *options.TerragruntOptions) error {

// checkProtectedModule checks if module is protected via the "prevent_destroy" flag
func checkProtectedModule(terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) error {
if firstArg(terragruntOptions.TerraformCliArgs) != "destroy" {
if util.FirstArg(terragruntOptions.TerraformCliArgs) != "destroy" {
return nil
}
if terragruntConfig.PreventDestroy {
Expand Down
2 changes: 2 additions & 0 deletions configstack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/gruntwork-io/terragrunt/config"
"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/util"
"sort"
)

Expand Down Expand Up @@ -114,6 +115,7 @@ func FindStackInSubfolders(terragruntOptions *options.TerragruntOptions) (*Stack
func (stack *Stack) setTerraformCommand(command []string) {
for _, module := range stack.Modules {
module.TerragruntOptions.TerraformCliArgs = append(command, module.TerragruntOptions.TerraformCliArgs...)
module.TerragruntOptions.TerraformCommand = util.FirstArg(command)
}
}

Expand Down
5 changes: 5 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type TerragruntOptions struct {
// Location of the terraform binary
TerraformPath string

// Current Terraform command being executed by Terragrunt
TerraformCommand string

// Version of terraform (obtained by running 'terraform version')
TerraformVersion *version.Version

Expand Down Expand Up @@ -97,6 +100,7 @@ func NewTerragruntOptions(terragruntConfigPath string) (*TerragruntOptions, erro
return &TerragruntOptions{
TerragruntConfigPath: terragruntConfigPath,
TerraformPath: "terraform",
TerraformCommand: "",
AutoInit: true,
NonInteractive: false,
TerraformCliArgs: []string{},
Expand Down Expand Up @@ -154,6 +158,7 @@ func (terragruntOptions *TerragruntOptions) Clone(terragruntConfigPath string) *
return &TerragruntOptions{
TerragruntConfigPath: terragruntConfigPath,
TerraformPath: terragruntOptions.TerraformPath,
TerraformCommand: terragruntOptions.TerraformCommand,
TerraformVersion: terragruntOptions.TerraformVersion,
AutoInit: terragruntOptions.AutoInit,
NonInteractive: terragruntOptions.NonInteractive,
Expand Down
3 changes: 3 additions & 0 deletions test/fixture-hooks/init-once/backend.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
terraform {
backend "s3" {}
}
7 changes: 7 additions & 0 deletions test/fixture-hooks/init-once/base-module/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
data "template_file" "example" {
template = "hello, world"
}

output "example" {
value = "${data.template_file.example.rendered}"
}
7 changes: 7 additions & 0 deletions test/fixture-hooks/init-once/no-source-no-backend/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
data "template_file" "example" {
template = "hello, world"
}

output "example" {
value = "${data.template_file.example.rendered}"
}
17 changes: 17 additions & 0 deletions test/fixture-hooks/init-once/no-source-no-backend/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
terragrunt = {
terraform {
# Should NOT execute. With no source, init-from-module should never execute.
# If AFTER_INIT_FROM_MODULE_ONLY_ONCE is present in output, the test failed
after_hook "after_init_from_module" {
commands = ["init-from-module"]
execute = ["echo","AFTER_INIT_FROM_MODULE_ONLY_ONCE"]
}

# SHOULD execute
# If AFTER_INIT_ONLY_ONCE is not present exactly once in output, the test failed
after_hook "after_init" {
commands = ["init"]
execute = ["echo","AFTER_INIT_ONLY_ONCE"]
}
}
}
11 changes: 11 additions & 0 deletions test/fixture-hooks/init-once/no-source-with-backend/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
terraform {
backend "s3" {}
}

data "template_file" "example" {
template = "hello, world"
}

output "example" {
value = "${data.template_file.example.rendered}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
terragrunt = {
remote_state {
backend = "s3"
config {
encrypt = true
bucket = "__FILL_IN_BUCKET_NAME__"
key = "terraform.tfstate"
region = "__FILL_IN_REGION__"
}
}

terraform {
# Should NOT execute. With no source, init-from-module should never execute.
# If AFTER_INIT_FROM_MODULE_ONLY_ONCE is present in output, the test failed
after_hook "after_init_from_module" {
commands = ["init-from-module"]
execute = ["echo","AFTER_INIT_FROM_MODULE_ONLY_ONCE"]
}

# SHOULD execute.
# If AFTER_INIT_ONLY_ONCE is not echoed exactly once, the test failed
after_hook "after_init" {
commands = ["init"]
execute = ["echo","AFTER_INIT_ONLY_ONCE"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
terragrunt = {
terraform {
source = "../base-module"

# SHOULD execute.
# If AFTER_INIT_FROM_MODULE_ONLY_ONCE is not echoed exactly once, the test failed
after_hook "after_init_from_module" {
commands = ["init-from-module"]
execute = ["echo","AFTER_INIT_FROM_MODULE_ONLY_ONCE"]
}

# SHOULD execute.
# If AFTER_INIT_ONLY_ONCE is not echoed exactly once, the test failed
after_hook "after_init" {
commands = ["init"]
execute = ["echo","AFTER_INIT_ONLY_ONCE"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
terragrunt = {
remote_state {
backend = "s3"
config {
encrypt = true
bucket = "__FILL_IN_BUCKET_NAME__"
key = "terraform.tfstate"
region = "__FILL_IN_REGION__"
}
}

terraform {
source = "../base-module"

after_hook "backend" {
commands = ["init-from-module"]
execute = ["cp", "${get_tfvars_dir()}/../backend.tf", "."]
}

# SHOULD execute.
# If AFTER_INIT_FROM_MODULE_ONLY_ONCE is not echoed exactly once, the test failed
after_hook "after_init_from_module" {
commands = ["init-from-module"]
execute = ["echo","AFTER_INIT_FROM_MODULE_ONLY_ONCE"]
}

# SHOULD execute.
# If AFTER_INIT_ONLY_ONCE is not echoed exactly once, the test failed
after_hook "after_init" {
commands = ["init"]
execute = ["echo","AFTER_INIT_ONLY_ONCE"]
}
}
}
Loading

0 comments on commit a791566

Please sign in to comment.