diff --git a/cli/args.go b/cli/args.go index 96c7db9ca5..5589d655fc 100644 --- a/cli/args.go +++ b/cli/args.go @@ -120,6 +120,8 @@ func parseTerragruntOptionsFromArgs(terragruntVersion string, args []string, wri strictInclude := parseBooleanArg(args, OPT_TERRAGRUNT_STRICT_INCLUDE, false) + debugMode := parseBooleanArg(args, OPT_TERRAGRUNT_DEBUG, false) + opts, err := options.NewTerragruntOptions(filepath.ToSlash(terragruntConfigPath)) if err != nil { return nil, err @@ -162,6 +164,7 @@ func parseTerragruntOptionsFromArgs(terragruntVersion string, args []string, wri opts.ExcludeDirs = excludeDirs opts.IncludeDirs = includeDirs opts.StrictInclude = strictInclude + opts.DebugMode = debugMode opts.Parallelism = parallelism opts.Check = parseBooleanArg(args, OPT_TERRAGRUNT_CHECK, os.Getenv("TERRAGRUNT_CHECK") == "true") opts.HclFile = filepath.ToSlash(terragruntHclFilePath) @@ -350,6 +353,22 @@ func parseMultiStringArg(args []string, argName string, defaultValue []string) ( return stringArgs, nil } +// deleteTFVarsFile will delete the autogenerated tfvars file from disk, unless in debug mode. This should be done at +// the end of each terragrunt call, to prevent unintended leakage of secrets from the filesystem. +func deleteTFVarsFile(terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) error { + if terragruntOptions.DebugMode { + terragruntOptions.Logger.Printf("Detected DEBUG mode: will skip removing tfvars file %s in working dir (%s)", TerragruntTFVarsFileName, terragruntOptions.WorkingDir) + return nil + } + + terragruntOptions.Logger.Printf("Removing tfvars file %s in working dir (%s)", TerragruntTFVarsFileName, terragruntOptions.WorkingDir) + fileName := filepath.Join(terragruntOptions.WorkingDir, TerragruntTFVarsFileName) + if util.FileExists(fileName) { + return os.Remove(fileName) + } + return nil +} + // writeTFVarsFile will create a tfvars file that can be used to invoke the terraform module with the inputs generated // in terragrunt. func writeTFVarsFile(terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) error { @@ -372,6 +391,7 @@ func writeTFVarsFile(terragruntOptions *options.TerragruntOptions, terragruntCon // If the file already exists, log a warning indicating that we will overwrite it. if util.FileExists(fileName) { util.Debugf( + terragruntOptions.Logger, "WARNING: File with name \"%s\" already exists in terraform working directory. This file will be replaced with terragrunt generated vars", TerragruntTFVarsFileName, ) diff --git a/cli/cli_app.go b/cli/cli_app.go index b661ff27c5..0487558749 100644 --- a/cli/cli_app.go +++ b/cli/cli_app.go @@ -42,6 +42,7 @@ const OPT_TERRAGRUNT_STRICT_INCLUDE = "terragrunt-strict-include" const OPT_TERRAGRUNT_PARALLELISM = "terragrunt-parallelism" const OPT_TERRAGRUNT_CHECK = "terragrunt-check" const OPT_TERRAGRUNT_HCLFMT_FILE = "terragrunt-hclfmt-file" +const OPT_TERRAGRUNT_DEBUG = "terragrunt-debug" var ALL_TERRAGRUNT_BOOLEAN_OPTS = []string{ OPT_NON_INTERACTIVE, @@ -54,6 +55,7 @@ var ALL_TERRAGRUNT_BOOLEAN_OPTS = []string{ OPT_TERRAGRUNT_NO_AUTO_RETRY, OPT_TERRAGRUNT_CHECK, OPT_TERRAGRUNT_STRICT_INCLUDE, + OPT_TERRAGRUNT_DEBUG, } var ALL_TERRAGRUNT_STRING_OPTS = []string{ OPT_TERRAGRUNT_CONFIG, @@ -172,6 +174,7 @@ GLOBAL OPTIONS: terragrunt-parallelism *-all commands parallelism set to at most N modules terragrunt-exclude-dir Unix-style glob of directories to exclude when running *-all commands terragrunt-include-dir Unix-style glob of directories to include when running *-all commands + terragrunt-debug Enable debug mode: keep the tfvars file that is generated instead of cleaning it up terragrunt-check Enable check mode in the hclfmt command. terragrunt-hclfmt-file The path to a single terragrunt.hcl file that the hclfmt command should run on. @@ -370,6 +373,7 @@ func RunTerragrunt(terragruntOptions *options.TerragruntOptions) error { // Generate the tfvars file here, after all the terragrunt generated terraform files are created, so that the module // variables check includes variables defined in `generate` blocks. + defer deleteTFVarsFile(terragruntOptions, terragruntConfig) if err := writeTFVarsFile(terragruntOptions, terragruntConfig); err != nil { return err } diff --git a/docs/_docs/02_features/debugging.md b/docs/_docs/02_features/debugging.md index 10f7419624..4848507e7f 100644 --- a/docs/_docs/02_features/debugging.md +++ b/docs/_docs/02_features/debugging.md @@ -16,11 +16,16 @@ Terragrunt and Terraform usually play well together in helping you write DRY, re-usable infrastructure. But how do we figure out what went wrong in the rare case that they _don't_ play well? -Whenever you run a Terragrunt command, two things happen: +Whenever you run a Terragrunt command, it will convert the inputs into [a tfvars json +file](https://www.terraform.io/docs/configuration/variables.html#variable-definitions-tfvars-files) named +`terragrunt-generated.auto.tfvars.json`. This file will be autoloaded by terraform when it is invoked. However, normally +this file is cleaned up at the end of each execution, which makes it hard to understand what inputs were passed into +terraform. -- Terragrunt will convert the inputs into [a tfvars json - file](https://www.terraform.io/docs/configuration/variables.html#variable-definitions-tfvars-files) named - `terragrunt-generated.auto.tfvars.json`. This file will be autoloaded by terraform when it is invoked. +To help with debugging, Terragrunt offers a CLI flag `--terragrunt-debug` which can be used to run in debug mode. +Whenever you run a Terragrunt command in debug mode, two things happen: + +- Terragrunt will keep around the generated `terragrunt-generated.auto.tfvars.json` file. - Print instructions on how to invoke terraform against the generated file to reproduce exactly the same terraform output as you saw when invoking `terragrunt`. This will help you to determine where the problem's root cause lies. @@ -88,7 +93,7 @@ You perform a `terragrunt apply`, and find that `outputs.task_ids` has 7 elements, but you know that the cluster only has 4 VMs in it! What's happening? Let's figure it out. Run this: - $ terragrunt apply + $ terragrunt apply --terragrunt-debug After applying, you will see this output on standard error diff --git a/docs/_docs/02_features/inputs.md b/docs/_docs/02_features/inputs.md index c0c607ac87..362508a31d 100644 --- a/docs/_docs/02_features/inputs.md +++ b/docs/_docs/02_features/inputs.md @@ -48,6 +48,8 @@ Note that Terragrunt will respect any `TF_VAR_xxx` variables you’ve manually s } } +Since these inputs may include unencrypted secrets, Terragrunt will clean up this file at the end of each terraform +call. To keep this file around even after terragrunt exits, pass in the `--terragrunt-debug` CLI arg. ### Variable precedence diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 5081594991..4bf1c014fe 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -30,6 +30,7 @@ Terragrunt forwards all arguments and options to Terraform. The only exceptions - [terragrunt-ignore-dependency-order](#terragrunt-ignore-dependency-order) - [terragrunt-ignore-external-dependencies](#terragrunt-ignore-external-dependencies) - [terragrunt-include-external-dependencies](#terragrunt-include-external-dependencies) +- [terragrunt-debug](#terragrunt-debug) - [terragrunt-check](#terragrunt-check) - [terragrunt-hclfmt-file](#terragrunt-hclfmt-file) @@ -208,6 +209,13 @@ dependency is a dependency that is outside the current terragrunt working direct included directories with `terragrunt-include-dir`. +## terragrunt-debug + +**CLI Arg**: `--terragrunt-debug` + +When passed in, run in terragrunt in debug mode, where terragrunt will keep around autogenerated tfvars file instead of +cleaning them up at the end of each run. + ## terragrunt-check diff --git a/options/options.go b/options/options.go index f33e3b08a2..9e906c967a 100644 --- a/options/options.go +++ b/options/options.go @@ -123,6 +123,9 @@ type TerragruntOptions struct { // If set to true, do not include dependencies when processing IncludeDirs (unless they are in the included dirs) StrictInclude bool + // If set to true, don't clean up the tfvars json file used to pass inputs to terraform. + DebugMode bool + // Parallelism limits the number of commands to run concurrently during *-all commands Parallelism int diff --git a/test/integration_test.go b/test/integration_test.go index 6d4266616c..8c1078eda2 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -1459,7 +1459,7 @@ func TestGeneratedInputsExternalEnvVar(t *testing.T) { tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_INPUTS) rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_INPUTS) - runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) + runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-debug --terragrunt-working-dir %s", rootPath)) pathFromRoot, err := util.PathInDirectory(cli.TerragruntTFVarsFileName, rootPath) assert.NoError(t, err) @@ -1472,7 +1472,7 @@ func TestGeneratedInputsExternalEnvVar(t *testing.T) { stderr := bytes.Buffer{} require.NoError( t, - runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr), + runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-debug --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr), ) outputs := map[string]TerraformOutput{} @@ -1494,14 +1494,15 @@ func TestGeneratedInputsExternalEnvVar(t *testing.T) { // Finally, verify that unsetting the env var will reconstruct the json file with the var included os.Unsetenv("TF_VAR_string") - runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) + runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-debug --terragrunt-working-dir %s", rootPath)) getOutputAndVerifyInputs(validateInputs) newTfvarsJsonContents, err := ioutil.ReadFile(tfvarsPath) require.NoError(t, err) require.NoError(t, json.Unmarshal(newTfvarsJsonContents, &data)) assert.Equal(t, data["string"], "string") } -func TestGeneratedInputs(t *testing.T) { + +func TestGeneratedInputsFileDiscarded(t *testing.T) { t.Parallel() cleanupTerraformFolder(t, TEST_FIXTURE_INPUTS) @@ -1510,6 +1511,21 @@ func TestGeneratedInputs(t *testing.T) { runTerragrunt(t, fmt.Sprintf("terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath)) + // Assert that the tfvars file was deleted. + pathFromRoot, err := util.PathInDirectory(cli.TerragruntTFVarsFileName, rootPath) + assert.NoError(t, err) + assert.Equal(t, "", pathFromRoot) +} + +func TestGeneratedInputs(t *testing.T) { + t.Parallel() + + cleanupTerraformFolder(t, TEST_FIXTURE_INPUTS) + tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_INPUTS) + rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_INPUTS) + + runTerragrunt(t, fmt.Sprintf("terragrunt plan --terragrunt-non-interactive --terragrunt-debug --terragrunt-working-dir %s", rootPath)) + pathFromRoot, err := util.PathInDirectory(cli.TerragruntTFVarsFileName, rootPath) assert.NoError(t, err) assert.NotEqual(t, "", pathFromRoot) @@ -1530,7 +1546,7 @@ func TestGeneratedInputs(t *testing.T) { stderr := bytes.Buffer{} require.NoError( t, - runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr), + runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-debug --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr), ) outputs := map[string]TerraformOutput{}