Skip to content

Commit 25096ff

Browse files
committed
chore: add usage for venv for shell
1 parent 9a0d322 commit 25096ff

6 files changed

Lines changed: 65 additions & 70 deletions

File tree

internal/os/exec/opts.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ package exec
33
import (
44
"time"
55

6-
"github.com/gruntwork-io/go-commons/collections"
6+
"github.com/gruntwork-io/terragrunt/internal/util"
77
"github.com/gruntwork-io/terragrunt/pkg/log"
88
)
99

10-
const envVarsListFormat = "%s=%s"
11-
1210
// Option is type for passing options to the Cmd.
1311
type Option func(*Cmd)
1412

@@ -29,7 +27,7 @@ func WithUsePTY(state bool) Option {
2927
// WithEnv sets envs to the Cmd.
3028
func WithEnv(env map[string]string) Option {
3129
return func(cmd *Cmd) {
32-
cmd.Env = collections.KeyValueStringSliceWithFormat(env, envVarsListFormat)
30+
cmd.Env = util.EnvSliceFromMap(env)
3331
}
3432
}
3533

internal/shell/run_cmd.go

Lines changed: 37 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,6 @@ import (
2222
"github.com/gruntwork-io/terragrunt/internal/util"
2323
)
2424

25-
type execContextKey struct{}
26-
27-
// WithExec returns ctx with the given vexec.Exec installed for use by RunCommandWithOutput.
28-
// When set, the executor replaces the default os/exec backend; intended for tests that need to intercept subprocess execution.
29-
func WithExec(ctx context.Context, e vexec.Exec) context.Context {
30-
return context.WithValue(ctx, execContextKey{}, e)
31-
}
32-
33-
func execFromContext(ctx context.Context) vexec.Exec {
34-
e, _ := ctx.Value(execContextKey{}).(vexec.Exec)
35-
return e
36-
}
37-
38-
func envSliceFromMap(env map[string]string) []string {
39-
out := make([]string, 0, len(env))
40-
41-
for k, v := range env {
42-
out = append(out, k+"="+v)
43-
}
44-
45-
return out
46-
}
47-
4825
// SignalForwardingDelay is the time to wait before forwarding the signal to the subcommand.
4926
//
5027
// The signal can be sent to the main process (only `terragrunt`) as well as the process group (`terragrunt` and `terraform`), for example:
@@ -61,6 +38,10 @@ type ShellOptions struct {
6138
EngineConfig *engine.EngineConfig
6239
Telemetry *telemetry.Options
6340
Env map[string]string
41+
// Exec, when non-nil, replaces the default os/exec backend used by
42+
// RunCommandWithOutput. Intended for tests that need to intercept
43+
// subprocess execution; production code leaves this nil.
44+
Exec vexec.Exec
6445

6546
RootWorkingDir string
6647
WorkingDir string
@@ -176,6 +157,14 @@ func (o *ShellOptions) WithForwardTFStdout(f bool) *ShellOptions {
176157
return o
177158
}
178159

160+
// WithExec installs a vexec.Exec backend that replaces the default os/exec
161+
// path used by RunCommandWithOutput. Pass nil to clear it. Intended for tests.
162+
func (o *ShellOptions) WithExec(e vexec.Exec) *ShellOptions {
163+
o.Exec = e
164+
165+
return o
166+
}
167+
179168
// NoEngine returns true if the user explicitly disabled the engine via --no-engine.
180169
// Returns false when EngineOptions is nil (default: don't disable), letting the
181170
// other guards (EngineConfig != nil, experiment enabled) decide whether to run.
@@ -272,24 +261,15 @@ func RunCommandWithOutput(
272261
}
273262
}
274263

275-
if injected := execFromContext(ctx); injected != nil {
276-
injectedCmd := injected.Command(ctx, command, args...)
264+
if runOpts.Exec != nil {
265+
injectedCmd := runOpts.Exec.Command(ctx, command, args...)
277266
injectedCmd.SetDir(commandDir)
278-
injectedCmd.SetEnv(envSliceFromMap(runOpts.Env))
267+
injectedCmd.SetEnv(util.EnvSliceFromMap(runOpts.Env))
279268
injectedCmd.SetStdout(cmdStdout)
280269
injectedCmd.SetStderr(cmdStderr)
281270

282271
if err := injectedCmd.Run(); err != nil {
283-
return errors.New(util.ProcessExecutionError{
284-
Err: err,
285-
Args: args,
286-
Command: command,
287-
Output: output,
288-
WorkingDir: commandDir,
289-
RootWorkingDir: runOpts.RootWorkingDir,
290-
LogShowAbsPaths: runOpts.Writers.LogShowAbsPaths,
291-
DisableSummary: runOpts.Writers.LogDisableErrorSummary,
292-
})
272+
return runOpts.procExecError(err, command, commandDir, args, &output)
293273
}
294274

295275
return nil
@@ -311,39 +291,37 @@ func RunCommandWithOutput(
311291
defer savedConsole.Restore()
312292

313293
if err := cmd.Start(); err != nil { //nolint:contextcheck // context already passed to exec.Command
314-
err = util.ProcessExecutionError{
315-
Err: err,
316-
Args: args,
317-
Command: command,
318-
WorkingDir: cmd.Dir,
319-
RootWorkingDir: runOpts.RootWorkingDir,
320-
LogShowAbsPaths: runOpts.Writers.LogShowAbsPaths,
321-
DisableSummary: runOpts.Writers.LogDisableErrorSummary,
322-
}
323-
324-
return errors.New(err)
294+
return runOpts.procExecError(err, command, cmd.Dir, args, nil)
325295
}
326296

327297
cancelShutdown := cmd.RegisterGracefullyShutdown(ctx)
328298
defer cancelShutdown()
329299

330300
if err := cmd.Wait(); err != nil {
331-
err = util.ProcessExecutionError{
332-
Err: err,
333-
Args: args,
334-
Command: command,
335-
Output: output,
336-
WorkingDir: cmd.Dir,
337-
RootWorkingDir: runOpts.RootWorkingDir,
338-
LogShowAbsPaths: runOpts.Writers.LogShowAbsPaths,
339-
DisableSummary: runOpts.Writers.LogDisableErrorSummary,
340-
}
341-
342-
return errors.New(err)
301+
return runOpts.procExecError(err, command, cmd.Dir, args, &output)
343302
}
344303

345304
return nil
346305
})
347306

348307
return &output, err
349308
}
309+
310+
// procExecError builds a ProcessExecutionError using the standard fields from ShellOptions.
311+
// Pass nil for output when the failure occurred before any output was captured.
312+
func (o *ShellOptions) procExecError(err error, command, dir string, args []string, output *util.CmdOutput) error {
313+
pe := util.ProcessExecutionError{
314+
Err: err,
315+
Args: args,
316+
Command: command,
317+
WorkingDir: dir,
318+
RootWorkingDir: o.RootWorkingDir,
319+
LogShowAbsPaths: o.Writers.LogShowAbsPaths,
320+
DisableSummary: o.Writers.LogDisableErrorSummary,
321+
}
322+
if output != nil {
323+
pe.Output = *output
324+
}
325+
326+
return errors.New(pe)
327+
}

internal/util/collections.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,17 @@ func SplitUrls(s, sep string) []string {
133133

134134
return urls
135135
}
136+
137+
// EnvSliceFromMap converts an env-var map to the KEY=VALUE slice form
138+
// expected by os/exec.Cmd.Env. The result is sorted for deterministic output.
139+
func EnvSliceFromMap(env map[string]string) []string {
140+
out := make([]string, 0, len(env))
141+
142+
for k, v := range env {
143+
out = append(out, k+"="+v)
144+
}
145+
146+
slices.Sort(out)
147+
148+
return out
149+
}

pkg/config/dependency.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1459,7 +1459,8 @@ func shellRunOptsFromPctx(pctx *ParsingContext) *shell.ShellOptions {
14591459
WithRootWorkingDir(pctx.RootWorkingDir).
14601460
WithExperiments(pctx.Experiments).
14611461
WithHeadless(pctx.Headless).
1462-
WithForwardTFStdout(pctx.ForwardTFStdout)
1462+
WithForwardTFStdout(pctx.ForwardTFStdout).
1463+
WithExec(pctx.Exec)
14631464
}
14641465

14651466
// tfRunOptsFromPctx builds a *tf.RunOptions from ParsingContext flat fields.

pkg/config/fuzz_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"testing"
99
"time"
1010

11-
"github.com/gruntwork-io/terragrunt/internal/shell"
1211
"github.com/gruntwork-io/terragrunt/internal/vexec"
1312
"github.com/gruntwork-io/terragrunt/pkg/config"
1413
"github.com/gruntwork-io/terragrunt/test/helpers/logger"
@@ -71,9 +70,9 @@ func FuzzHCLStringHelpers(f *testing.F) {
7170
}
7271

7372
// FuzzHCLRunCommand fuzzes config.RunCommand with arbitrary argv. Subprocess execution
74-
// is intercepted by an in-memory vexec backend installed via shell.WithExec, so no real
75-
// host commands ever run — even mutator-supplied paths like "/bin/sh\x00-c\x00rm -rf /"
76-
// are captured by the mock instead of reaching the operating system.
73+
// is intercepted by an in-memory vexec backend installed via pctx.Exec, so no real host
74+
// commands ever run — even mutator-supplied paths like "/bin/sh\x00-c\x00rm -rf /" are
75+
// captured by the mock instead of reaching the operating system.
7776
//
7877
// Asserts:
7978
// - On the conflict path (--terragrunt-no-cache + --terragrunt-global-cache):
@@ -128,12 +127,11 @@ func FuzzHCLRunCommand(f *testing.F) {
128127
baseCtx, pctx := newTestParsingContext(t, "")
129128
pctx.Writers.Writer = io.Discard
130129
pctx.Writers.ErrWriter = io.Discard
130+
pctx.Exec = memExec
131131

132132
ctx, cancel := context.WithTimeout(baseCtx, 2*time.Second)
133133
defer cancel()
134134

135-
ctx = shell.WithExec(ctx, memExec)
136-
137135
l := logger.CreateLogger()
138136
out, err := config.RunCommand(ctx, pctx, l, argsForCall)
139137

pkg/config/parsing_context.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/gruntwork-io/terragrunt/internal/strict"
2222
"github.com/gruntwork-io/terragrunt/internal/telemetry"
2323
"github.com/gruntwork-io/terragrunt/internal/tfimpl"
24+
"github.com/gruntwork-io/terragrunt/internal/vexec"
2425
"github.com/gruntwork-io/terragrunt/internal/writer"
2526
"github.com/gruntwork-io/terragrunt/pkg/config/hclparse"
2627
"github.com/gruntwork-io/terragrunt/pkg/log"
@@ -52,6 +53,11 @@ type ParsingContext struct {
5253
Features *cty.Value
5354
Locals *cty.Value
5455

56+
// Exec, when non-nil, replaces the default os/exec backend used by run_cmd
57+
// and other shell-outs that read shell.ShellOptions from this context.
58+
// Intended for tests; production code leaves this nil.
59+
Exec vexec.Exec
60+
5561
Env map[string]string
5662
SourceMap map[string]string
5763
PredefinedFunctions map[string]function.Function

0 commit comments

Comments
 (0)