diff --git a/cli/commands/terraform/creds/providers/externalcmd/provider.go b/cli/commands/terraform/creds/providers/externalcmd/provider.go index 07e894de1a..60040070ca 100644 --- a/cli/commands/terraform/creds/providers/externalcmd/provider.go +++ b/cli/commands/terraform/creds/providers/externalcmd/provider.go @@ -46,7 +46,7 @@ func (provider *Provider) GetCredentials(ctx context.Context) (*providers.Creden args = parts[1:] } - output, err := shell.RunShellCommandWithOutput(ctx, provider.terragruntOptions, "", true, false, command, args...) + output, err := shell.RunShellCommandWithOutput(ctx, provider.terragruntOptions, "", true, true, false, command, args...) if err != nil { return nil, err } diff --git a/cli/commands/terraform/hook.go b/cli/commands/terraform/hook.go index ede1e704b5..b4d94d729a 100644 --- a/cli/commands/terraform/hook.go +++ b/cli/commands/terraform/hook.go @@ -67,6 +67,11 @@ func processErrorHooks(ctx context.Context, hooks []config.ErrorHook, terragrunt suppressStdout = true } + var suppressStderr bool + if curHook.SuppressStderr != nil && *curHook.SuppressStderr { + suppressStderr = true + } + actionToExecute := curHook.Execute[0] actionParams := curHook.Execute[1:] terragruntOptions = terragruntOptionsWithHookEnvs(terragruntOptions, curHook.Name) @@ -76,6 +81,7 @@ func processErrorHooks(ctx context.Context, hooks []config.ErrorHook, terragrunt terragruntOptions, workingDir, suppressStdout, + suppressStderr, false, actionToExecute, actionParams..., ) @@ -148,6 +154,11 @@ func runHook(ctx context.Context, terragruntOptions *options.TerragruntOptions, suppressStdout = true } + var suppressStderr bool + if curHook.SuppressStderr != nil && *curHook.SuppressStderr { + suppressStderr = true + } + actionToExecute := curHook.Execute[0] actionParams := curHook.Execute[1:] terragruntOptions = terragruntOptionsWithHookEnvs(terragruntOptions, curHook.Name) @@ -162,6 +173,7 @@ func runHook(ctx context.Context, terragruntOptions *options.TerragruntOptions, terragruntOptions, workingDir, suppressStdout, + suppressStderr, false, actionToExecute, actionParams..., ) diff --git a/config/config.go b/config/config.go index bc2d4496d7..eca5108e4d 100644 --- a/config/config.go +++ b/config/config.go @@ -437,6 +437,7 @@ type Hook struct { Execute []string `hcl:"execute,attr" cty:"execute"` RunOnError *bool `hcl:"run_on_error,attr" cty:"run_on_error"` SuppressStdout *bool `hcl:"suppress_stdout,attr" cty:"suppress_stdout"` + SuppressStderr *bool `hcl:"suppress_stderr,attr" cty:"suppress_stderr"` WorkingDir *string `hcl:"working_dir,attr" cty:"working_dir"` } @@ -446,6 +447,7 @@ type ErrorHook struct { Execute []string `hcl:"execute,attr" cty:"execute"` OnErrors []string `hcl:"on_errors,attr" cty:"on_errors"` SuppressStdout *bool `hcl:"suppress_stdout,attr" cty:"suppress_stdout"` + SuppressStderr *bool `hcl:"suppress_stderr,attr" cty:"suppress_stderr"` WorkingDir *string `hcl:"working_dir,attr" cty:"working_dir"` } diff --git a/config/config_helpers.go b/config/config_helpers.go index 5217f85728..049cdd5308 100644 --- a/config/config_helpers.go +++ b/config/config_helpers.go @@ -340,6 +340,7 @@ func RunCommand(ctx *ParsingContext, args []string) (string, error) { } suppressOutput := false + suppressErrorOutput := false currentPath := filepath.Dir(ctx.TerragruntOptions.TerragruntConfigPath) cachePath := currentPath @@ -348,6 +349,7 @@ func RunCommand(ctx *ParsingContext, args []string) (string, error) { switch args[0] { case "--terragrunt-quiet": suppressOutput = true + suppressErrorOutput = true args = append(args[:0], args[1:]...) case "--terragrunt-global-cache": @@ -374,7 +376,7 @@ func RunCommand(ctx *ParsingContext, args []string) (string, error) { return cachedValue, nil } - cmdOutput, err := shell.RunShellCommandWithOutput(ctx, ctx.TerragruntOptions, currentPath, suppressOutput, false, args[0], args[1:]...) + cmdOutput, err := shell.RunShellCommandWithOutput(ctx, ctx.TerragruntOptions, currentPath, suppressOutput, suppressErrorOutput, false, args[0], args[1:]...) if err != nil { return "", errors.New(err) } diff --git a/docs/_docs/04_reference/04-config-blocks-and-attributes.md b/docs/_docs/04_reference/04-config-blocks-and-attributes.md index 2bea194978..b5943b71a5 100644 --- a/docs/_docs/04_reference/04-config-blocks-and-attributes.md +++ b/docs/_docs/04_reference/04-config-blocks-and-attributes.md @@ -149,6 +149,7 @@ The `terraform` block supports the following arguments: - `run_on_error` (optional) : If set to true, this hook will run even if a previous hook hit an error, or in the case of "after" hooks, if the OpenTofu/Terraform command hit an error. Default is false. - `suppress_stdout` (optional) : If set to true, the stdout output of the executed commands will be suppressed. This can be useful when there are scripts relying on OpenTofu/Terraform's output and any other output would break their parsing. + - `suppress_stderr` (optional) : If set to true, the stderr output of the executed commands will be suppressed. This can be useful when there are scripts relying on OpenTofu/Terraform's output and any other output would break their parsing. - `after_hook` (block): Nested blocks used to specify command hooks that should be run after `tofu`/`terraform` is called. Hooks run from the terragrunt configuration directory (the directory where `terragrunt.hcl` lives). Supports the same diff --git a/engine/engine.go b/engine/engine.go index dc97157ab3..c1fb3981a2 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -61,6 +61,7 @@ type ExecutionOptions struct { CmdStderr io.Writer WorkingDir string SuppressStdout bool + SuppressStderr bool AllocatePseudoTty bool Command string Args []string diff --git a/shell/git.go b/shell/git.go index 02cfae6ec3..427718a4d4 100644 --- a/shell/git.go +++ b/shell/git.go @@ -41,7 +41,7 @@ func GitTopLevelDir(ctx context.Context, terragruntOptions *options.TerragruntOp opts.Writer = &stdout opts.ErrWriter = &stderr - cmd, err := RunShellCommandWithOutput(ctx, opts, path, true, false, "git", "rev-parse", "--show-toplevel") + cmd, err := RunShellCommandWithOutput(ctx, opts, path, true, true, false, "git", "rev-parse", "--show-toplevel") if err != nil { return "", err } @@ -72,7 +72,7 @@ func GitRepoTags(ctx context.Context, opts *options.TerragruntOptions, gitRepo * gitOpts.Writer = &stdout gitOpts.ErrWriter = &stderr - output, err := RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, true, false, "git", "ls-remote", "--tags", repoPath) + output, err := RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, true, true, false, "git", "ls-remote", "--tags", repoPath) if err != nil { return nil, errors.New(err) } diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go index 7aa065801f..8e88301820 100644 --- a/shell/run_shell_cmd.go +++ b/shell/run_shell_cmd.go @@ -69,7 +69,7 @@ func RunTerraformCommandWithOutput(ctx context.Context, opts *options.Terragrunt return nil, err } - output, err := RunShellCommandWithOutput(ctx, opts, "", false, needsPTY, opts.TerraformPath, args...) + output, err := RunShellCommandWithOutput(ctx, opts, "", false, false, needsPTY, opts.TerraformPath, args...) if err != nil && util.ListContainsElement(args, terraform.FlagNameDetailedExitCode) { code, _ := util.GetExitCode(err) @@ -87,7 +87,7 @@ func RunTerraformCommandWithOutput(ctx context.Context, opts *options.Terragrunt // RunShellCommand runs the given shell command. func RunShellCommand(ctx context.Context, opts *options.TerragruntOptions, command string, args ...string) error { - _, err := RunShellCommandWithOutput(ctx, opts, "", false, false, command, args...) + _, err := RunShellCommandWithOutput(ctx, opts, "", false, false, false, command, args...) return err } @@ -102,6 +102,7 @@ func RunShellCommandWithOutput( opts *options.TerragruntOptions, workingDir string, suppressStdout bool, + suppressStderr bool, needsPTY bool, command string, args ...string, @@ -182,6 +183,12 @@ func RunShellCommandWithOutput( cmdStdout = io.MultiWriter(&output.Stdout) } + if suppressStderr { + opts.Logger.Debugf("Command error output will be suppressed.") + + cmdStderr = io.MultiWriter(&output.Stderr) + } + if command == opts.TerraformPath { // If the engine is enabled and the command is IaC executable, use the engine to run the command. if opts.Engine != nil && opts.EngineEnabled { @@ -193,6 +200,7 @@ func RunShellCommandWithOutput( CmdStderr: cmdStderr, WorkingDir: commandDir, SuppressStdout: suppressStdout, + SuppressStderr: suppressStderr, AllocatePseudoTty: needsPTY, Command: command, Args: args, diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go index c3ce3fcf8e..64a12ca01b 100644 --- a/shell/run_shell_cmd_output_test.go +++ b/shell/run_shell_cmd_output_test.go @@ -56,7 +56,7 @@ var ( func testCommandOutputOrder(t *testing.T, withPtty bool, fullOutput []string, stdout []string, stderr []string) { t.Helper() - testCommandOutput(t, noop[*options.TerragruntOptions], assertOutputs(t, fullOutput, stdout, stderr), withPtty) + testCommandOutput(t, noop[*options.TerragruntOptions], assertOutputs(t, fullOutput, stdout, stderr), withPtty, withPtty) } func TestCommandOutputPrefix(t *testing.T) { @@ -78,10 +78,10 @@ func TestCommandOutputPrefix(t *testing.T) { prefixedOutput, Stdout, Stderr, - ), true) + ), true, true) } -func testCommandOutput(t *testing.T, withOptions func(*options.TerragruntOptions), assertResults func(string, *util.CmdOutput), allocateStdout bool) { +func testCommandOutput(t *testing.T, withOptions func(*options.TerragruntOptions), assertResults func(string, *util.CmdOutput), allocateStdout bool, allocateStderr bool) { t.Helper() terragruntOptions, err := options.NewTerragruntOptionsForTest("") @@ -97,7 +97,7 @@ func testCommandOutput(t *testing.T, withOptions func(*options.TerragruntOptions withOptions(terragruntOptions) - out, err := shell.RunShellCommandWithOutput(context.Background(), terragruntOptions, "", !allocateStdout, false, "testdata/test_outputs.sh", "same") + out, err := shell.RunShellCommandWithOutput(context.Background(), terragruntOptions, "", !allocateStdout, !allocateStderr, false, "testdata/test_outputs.sh", "same") assert.NotNil(t, out, "Should get output") require.NoError(t, err, "Should have no error") diff --git a/shell/run_shell_cmd_unix_test.go b/shell/run_shell_cmd_unix_test.go index fcdabb4dce..31d0cd8ef6 100644 --- a/shell/run_shell_cmd_unix_test.go +++ b/shell/run_shell_cmd_unix_test.go @@ -33,7 +33,7 @@ func TestRunShellCommandWithOutputInterrupt(t *testing.T) { cmdPath := "testdata/test_sigint_wait.sh" go func() { - _, err := shell.RunShellCommandWithOutput(ctx, terragruntOptions, "", false, false, cmdPath, strconv.Itoa(expectedWait)) + _, err := shell.RunShellCommandWithOutput(ctx, terragruntOptions, "", false, false, false, cmdPath, strconv.Itoa(expectedWait)) errCh <- err }() diff --git a/test/fixtures/hooks/init-once/with-source-no-backend-no-suppress-hook-stderr/terragrunt.hcl b/test/fixtures/hooks/init-once/with-source-no-backend-no-suppress-hook-stderr/terragrunt.hcl new file mode 100644 index 0000000000..e8183f530f --- /dev/null +++ b/test/fixtures/hooks/init-once/with-source-no-backend-no-suppress-hook-stderr/terragrunt.hcl @@ -0,0 +1,17 @@ +terraform { + source = "../base-module" + + # SHOULD execute. + after_hook "after_init_from_module" { + commands = ["init-from-module"] + execute = ["${get_parent_terragrunt_dir()}/util.sh","ERROR MESSAGE"] + suppress_stderr = false + } + + # SHOULD execute. + after_hook "after_init" { + commands = ["init"] + execute = ["${get_parent_terragrunt_dir()}/util.sh","ERROR MESSAGE"] + suppress_stderr = false + } +} diff --git a/test/fixtures/hooks/init-once/with-source-no-backend-no-suppress-hook-stderr/util.sh b/test/fixtures/hooks/init-once/with-source-no-backend-no-suppress-hook-stderr/util.sh new file mode 100755 index 0000000000..ab82255a33 --- /dev/null +++ b/test/fixtures/hooks/init-once/with-source-no-backend-no-suppress-hook-stderr/util.sh @@ -0,0 +1,2 @@ +#!/bin/bash +>&2 echo "$1" \ No newline at end of file diff --git a/test/fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stderr/terragrunt.hcl b/test/fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stderr/terragrunt.hcl new file mode 100644 index 0000000000..18c11ab3c9 --- /dev/null +++ b/test/fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stderr/terragrunt.hcl @@ -0,0 +1,17 @@ +terraform { + source = "../base-module" + + # SHOULD execute. + after_hook "after_init_from_module" { + commands = ["init-from-module"] + execute = ["${get_parent_terragrunt_dir()}/util.sh","ERROR MESSAGE"] + suppress_stderr = true + } + + # SHOULD execute. + after_hook "after_init" { + commands = ["init"] + execute = ["${get_parent_terragrunt_dir()}/util.sh","ERROR MESSAGE"] + suppress_stderr = true + } +} diff --git a/test/fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stderr/util.sh b/test/fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stderr/util.sh new file mode 100755 index 0000000000..ab82255a33 --- /dev/null +++ b/test/fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stderr/util.sh @@ -0,0 +1,2 @@ +#!/bin/bash +>&2 echo "$1" \ No newline at end of file diff --git a/test/integration_hooks_test.go b/test/integration_hooks_test.go index 6c5c60c735..762f08a2e2 100644 --- a/test/integration_hooks_test.go +++ b/test/integration_hooks_test.go @@ -17,22 +17,24 @@ import ( ) const ( - testFixtureHooksBeforeOnlyPath = "fixtures/hooks/before-only" - testFixtureHooksAllPath = "fixtures/hooks/all" - testFixtureHooksAfterOnlyPath = "fixtures/hooks/after-only" - testFixtureHooksBeforeAndAfterPath = "fixtures/hooks/before-and-after" - testFixtureHooksBeforeAfterAndErrorMergePath = "fixtures/hooks/before-after-and-error-merge" - testFixtureHooksSkipOnErrorPath = "fixtures/hooks/skip-on-error" - testFixtureErrorHooksPath = "fixtures/hooks/error-hooks" - testFixtureHooksOneArgActionPath = "fixtures/hooks/one-arg-action" - testFixtureHooksEmptyStringCommandPath = "fixtures/hooks/bad-arg-action/empty-string-command" - testFixtureHooksEmptyCommandListPath = "fixtures/hooks/bad-arg-action/empty-command-list" - testFixtureHooksInterpolationsPath = "fixtures/hooks/interpolations" - testFixtureHooksInitOnceNoSourceNoBackend = "fixtures/hooks/init-once/no-source-no-backend" - testFixtureHooksInitOnceNoSourceWithBackend = "fixtures/hooks/init-once/no-source-with-backend" - testFixtureHooksInitOnceWithSourceNoBackend = "fixtures/hooks/init-once/with-source-no-backend" - testFixtureHooksInitOnceWithSourceNoBackendSuppressHookStdout = "fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stdout" - testFixtureHooksInitOnceWithSourceWithBackend = "fixtures/hooks/init-once/with-source-with-backend" + testFixtureHooksBeforeOnlyPath = "fixtures/hooks/before-only" + testFixtureHooksAllPath = "fixtures/hooks/all" + testFixtureHooksAfterOnlyPath = "fixtures/hooks/after-only" + testFixtureHooksBeforeAndAfterPath = "fixtures/hooks/before-and-after" + testFixtureHooksBeforeAfterAndErrorMergePath = "fixtures/hooks/before-after-and-error-merge" + testFixtureHooksSkipOnErrorPath = "fixtures/hooks/skip-on-error" + testFixtureErrorHooksPath = "fixtures/hooks/error-hooks" + testFixtureHooksOneArgActionPath = "fixtures/hooks/one-arg-action" + testFixtureHooksEmptyStringCommandPath = "fixtures/hooks/bad-arg-action/empty-string-command" + testFixtureHooksEmptyCommandListPath = "fixtures/hooks/bad-arg-action/empty-command-list" + testFixtureHooksInterpolationsPath = "fixtures/hooks/interpolations" + testFixtureHooksInitOnceNoSourceNoBackend = "fixtures/hooks/init-once/no-source-no-backend" + testFixtureHooksInitOnceNoSourceWithBackend = "fixtures/hooks/init-once/no-source-with-backend" + testFixtureHooksInitOnceWithSourceNoBackend = "fixtures/hooks/init-once/with-source-no-backend" + testFixtureHooksInitOnceWithSourceNoBackendSuppressHookStdout = "fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stdout" + testFixtureHooksInitOnceWithSourceNoBackendNoSuppressHookStderr = "fixtures/hooks/init-once/with-source-no-backend-no-suppress-hook-stderr" + testFixtureHooksInitOnceWithSourceNoBackendSuppressHookStderr = "fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stderr" + testFixtureHooksInitOnceWithSourceWithBackend = "fixtures/hooks/init-once/with-source-with-backend" ) func TestTerragruntBeforeHook(t *testing.T) { @@ -385,3 +387,39 @@ func TestTerragruntInfo(t *testing.T) { assert.Equal(t, wrappedBinary(), dat.TerraformBinary) assert.Empty(t, dat.IamRole) } + +func TestTerragruntStderr(t *testing.T) { + t.Parallel() + + helpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceWithSourceNoBackendNoSuppressHookStderr) + tmpEnvPath := helpers.CopyEnvironment(t, "fixtures/hooks/init-once") + rootPath := util.JoinPath(tmpEnvPath, testFixtureHooksInitOnceWithSourceNoBackendNoSuppressHookStderr) + + showStdout := bytes.Buffer{} + showStderr := bytes.Buffer{} + + err := helpers.RunTerragruntCommand(t, "terragrunt terragrunt-info --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &showStdout, &showStderr) + require.NoError(t, err) + + helpers.LogBufferContentsLineByLine(t, showStderr, "show stderr") + + assert.Contains(t, showStderr.String(), "ERROR MESSAGE") +} + +func TestTerragruntStderrSuppress(t *testing.T) { + t.Parallel() + + helpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceWithSourceNoBackendSuppressHookStderr) + tmpEnvPath := helpers.CopyEnvironment(t, "fixtures/hooks/init-once") + rootPath := util.JoinPath(tmpEnvPath, testFixtureHooksInitOnceWithSourceNoBackendSuppressHookStderr) + + showStdout := bytes.Buffer{} + showStderr := bytes.Buffer{} + + err := helpers.RunTerragruntCommand(t, "terragrunt terragrunt-info --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &showStdout, &showStderr) + require.NoError(t, err) + + helpers.LogBufferContentsLineByLine(t, showStderr, "show stderr") + + assert.NotContains(t, showStderr.String(), "ERROR MESSAGE") +} diff --git a/tflint/tflint.go b/tflint/tflint.go index 4c347930ac..91e27dc7e8 100644 --- a/tflint/tflint.go +++ b/tflint/tflint.go @@ -69,7 +69,7 @@ func RunTflintWithOpts(ctx context.Context, opts *options.TerragruntOptions, con if externalTfLint { opts.Logger.Debugf("Running external tflint init with args %v", initArgs) - _, err := shell.RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, false, false, + _, err := shell.RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, false, false, false, initArgs[0], initArgs[1:]...) if err != nil { return errors.New(ErrorRunningTflint{args: initArgs}) @@ -95,7 +95,7 @@ func RunTflintWithOpts(ctx context.Context, opts *options.TerragruntOptions, con if externalTfLint { opts.Logger.Debugf("Running external tflint with args %v", args) - _, err := shell.RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, false, false, + _, err := shell.RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, false, false, false, args[0], args[1:]...) if err != nil { return errors.New(ErrorRunningTflint{args: args})