Skip to content

Commit 3a39003

Browse files
authored
chore: Adding env and writers to venv (#6404)
1 parent 6331977 commit 3a39003

3 files changed

Lines changed: 165 additions & 52 deletions

File tree

internal/runner/run/venv.go

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,70 @@
11
package run
22

33
import (
4+
"io"
5+
46
"github.com/gruntwork-io/terragrunt/internal/tflint"
57
"github.com/gruntwork-io/terragrunt/internal/venv"
68
"github.com/gruntwork-io/terragrunt/internal/vexec"
79
"github.com/gruntwork-io/terragrunt/internal/vfs"
10+
"github.com/gruntwork-io/terragrunt/internal/writer"
811
)
912

1013
// Venv is the virtualized environment threaded through the hook execution
11-
// chain. It bundles the process-execution handle and the filesystem so
12-
// callers supply both per call rather than the run package holding them as
13-
// package-level state. The name avoids "Env" so it is not confused with
14-
// shell environment variables.
14+
// chain. It bundles the process-execution handle, the filesystem, the
15+
// shell environment, and the stdout/stderr writers so callers supply them
16+
// per call rather than the run package holding them as package-level
17+
// state. The name avoids "Env" so it is not confused with shell
18+
// environment variables. Env is shared by reference across the run and
19+
// mutated in place as hook, inputs, and extra-args contributions resolve.
1520
type Venv struct {
16-
// Exec runs hook commands and the embedded tflint binary.
17-
Exec vexec.Exec
18-
// FS backs filesystem reads performed inside hooks, including
19-
// tflint's .tflint.hcl discovery.
20-
FS vfs.FS
21+
Exec vexec.Exec
22+
FS vfs.FS
23+
Env map[string]string
24+
Writers writer.Writers
2125
}
2226

23-
// OSVenv builds the production [Venv]: real OS process execution and the
24-
// real OS filesystem.
27+
// OSVenv builds the production [Venv]: real OS process execution, real
28+
// OS filesystem, an OS environment snapshot, and the real OS streams.
2529
func OSVenv() Venv {
26-
return Venv{Exec: vexec.NewOSExec(), FS: vfs.NewOSFS()}
30+
return FromRoot(venv.OSVenv())
2731
}
2832

2933
// FromRoot projects the root [venv.Venv] threaded from the CLI entrypoint
30-
// into the run package's local Venv. The two carry the same handles but
31-
// are distinct types so the run package owns its own contract.
34+
// into the run package's local Venv.
3235
func FromRoot(v venv.Venv) Venv {
33-
return Venv{Exec: v.Exec, FS: v.FS}
36+
return Venv{Exec: v.Exec, FS: v.FS, Env: v.Env, Writers: v.Writers}
3437
}
3538

36-
// ToRoot is the inverse of [FromRoot]: it projects a run.Venv back into
37-
// the root [venv.Venv] for callers (notably config.ParsingContext) that
38-
// hold the root type.
39+
// ToRoot is the inverse of [FromRoot], for callers (notably
40+
// config.ParsingContext) that hold the root type.
3941
func (v Venv) ToRoot() venv.Venv {
40-
return venv.Venv{FS: v.FS, Exec: v.Exec}
42+
return venv.Venv{FS: v.FS, Exec: v.Exec, Env: v.Env, Writers: v.Writers}
4143
}
4244

43-
// tflintVenv translates a run.Venv into the tflint package's Venv. The
44-
// two carry the same handles but are distinct types so each package owns
45-
// its own contract.
45+
// tflintVenv translates a run.Venv into the tflint package's Venv.
4646
func (v Venv) tflintVenv() tflint.Venv {
47-
return tflint.Venv{Exec: v.Exec, FS: v.FS}
47+
return tflint.Venv{Exec: v.Exec, FS: v.FS, Env: v.Env, Writers: v.Writers}
48+
}
49+
50+
// RequireEnv panics with [venv.ErrVenvEnvUnset] when Env is nil, guarding
51+
// functions that write into the shared environment.
52+
func (v Venv) RequireEnv() {
53+
if v.Env == nil {
54+
panic(venv.ErrVenvEnvUnset)
55+
}
56+
}
57+
58+
// WithWriter returns a copy of v whose primary writer is w.
59+
func (v Venv) WithWriter(w io.Writer) Venv {
60+
v.Writers.Writer = w
61+
62+
return v
63+
}
64+
65+
// WithErrWriter returns a copy of v whose error writer is w.
66+
func (v Venv) WithErrWriter(w io.Writer) Venv {
67+
v.Writers.ErrWriter = w
68+
69+
return v
4870
}

internal/tflint/venv.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,34 @@ import (
44
"github.com/gruntwork-io/terragrunt/internal/venv"
55
"github.com/gruntwork-io/terragrunt/internal/vexec"
66
"github.com/gruntwork-io/terragrunt/internal/vfs"
7+
"github.com/gruntwork-io/terragrunt/internal/writer"
78
)
89

910
// Venv is the virtualized environment passed to tflint operations. It
10-
// bundles the process-execution handle and the filesystem so callers
11-
// supply both per call rather than the tflint package holding them as
12-
// package-level state. The name avoids "Env" so it is not confused with
13-
// shell environment variables.
11+
// bundles the process-execution handle, the filesystem, the shell
12+
// environment, and the stdout/stderr writers so callers supply them per
13+
// call rather than the tflint package holding them as package-level
14+
// state. The name avoids "Env" so it is not confused with shell
15+
// environment variables.
1416
type Venv struct {
15-
// Exec runs the tflint binary. Tests can substitute a stub via
16-
// [vexec.NewMemExec].
17-
Exec vexec.Exec
18-
// FS is the filesystem tflint reads through when searching for
19-
// .tflint.hcl and filtering optional var-files.
20-
FS vfs.FS
17+
Exec vexec.Exec
18+
FS vfs.FS
19+
Env map[string]string
20+
Writers writer.Writers
2121
}
2222

23-
// OSVenv builds the production [Venv]: real OS process execution and the
24-
// real OS filesystem.
23+
// OSVenv builds the production [Venv] from a real-OS [venv.OSVenv].
2524
func OSVenv() Venv {
26-
return Venv{Exec: vexec.NewOSExec(), FS: vfs.NewOSFS()}
25+
return FromRoot(venv.OSVenv())
2726
}
2827

2928
// FromRoot projects the root [venv.Venv] threaded from the CLI entrypoint
30-
// into the tflint package's local Venv. The two carry the same handles but
31-
// are distinct types so the tflint package owns its own contract.
29+
// into the tflint package's local Venv.
3230
func FromRoot(v venv.Venv) Venv {
33-
return Venv{Exec: v.Exec, FS: v.FS}
31+
return Venv{Exec: v.Exec, FS: v.FS, Env: v.Env, Writers: v.Writers}
32+
}
33+
34+
// ToRoot is the inverse of [FromRoot], for callers that hold the root type.
35+
func (v Venv) ToRoot() venv.Venv {
36+
return venv.Venv{FS: v.FS, Exec: v.Exec, Env: v.Env, Writers: v.Writers}
3437
}

internal/venv/venv.go

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Package venv defines the root virtualized environment threaded from the
22
// Terragrunt binary entrypoint down through the CLI and its commands.
33
//
4-
// A [Venv] bundles the two side-effect handles every layer below the CLI
5-
// needs to do its work: [vfs.FS] for filesystem reads and writes, and
6-
// [vexec.Exec] for spawning subprocesses. Production code constructs the
4+
// A [Venv] bundles the side-effect handles every layer below the CLI needs
5+
// to do its work: [vfs.FS] for filesystem reads and writes, [vexec.Exec]
6+
// for spawning subprocesses, the shell environment variables read at
7+
// startup, and the stdout/stderr writers. Production code constructs the
78
// real bundle once at the top via [OSVenv]; tests construct an in-memory
89
// bundle and drive the full CLI through it.
910
//
@@ -13,22 +14,109 @@
1314
package venv
1415

1516
import (
17+
"errors"
18+
"io"
19+
"os"
20+
"strings"
21+
1622
"github.com/gruntwork-io/terragrunt/internal/vexec"
1723
"github.com/gruntwork-io/terragrunt/internal/vfs"
24+
"github.com/gruntwork-io/terragrunt/internal/writer"
1825
)
1926

20-
// Venv is the root virtualized environment. It carries the filesystem
21-
// and process-execution handles that every Terragrunt operation needs.
27+
// ErrVenvEnvUnset is the panic value [Venv.RequireEnv] raises when Env is
28+
// nil. Production callers build the Venv through [OSVenv], so it points at a
29+
// test that forgot to set Env rather than a runtime condition.
30+
var ErrVenvEnvUnset = errors.New("venv.Venv.Env is required but unset")
31+
32+
// Venv is the root virtualized environment. It carries the filesystem,
33+
// process-execution, environment-variable, and writer handles that every
34+
// Terragrunt operation needs. Env is shared by reference across the run and
35+
// mutated in place as provider-cache, hook, and inputs contributions resolve.
2236
type Venv struct {
23-
// FS backs every filesystem read and write.
24-
FS vfs.FS
25-
// Exec spawns every subprocess: tofu, terraform, git, hooks,
26-
// external auth providers, tflint.
27-
Exec vexec.Exec
37+
FS vfs.FS
38+
Exec vexec.Exec
39+
Env map[string]string
40+
Writers writer.Writers
41+
}
42+
43+
// WithWriter returns a copy of v whose primary writer is w.
44+
func (v Venv) WithWriter(w io.Writer) Venv {
45+
v.Writers.Writer = w
46+
47+
return v
48+
}
49+
50+
// WithErrWriter returns a copy of v whose error writer is w.
51+
func (v Venv) WithErrWriter(w io.Writer) Venv {
52+
v.Writers.ErrWriter = w
53+
54+
return v
55+
}
56+
57+
// WithExec returns a copy of v whose process executor is exec.
58+
func (v Venv) WithExec(exec vexec.Exec) Venv {
59+
v.Exec = exec
60+
61+
return v
62+
}
63+
64+
// WithHandler returns a copy of v whose executor is an in-memory exec driven
65+
// by h, for the in-memory test bundles this package serves.
66+
func (v Venv) WithHandler(h vexec.Handler) Venv {
67+
v.Exec = vexec.NewMemExec(h)
68+
69+
return v
2870
}
2971

30-
// OSVenv builds the production [Venv]: the real OS filesystem and the
31-
// real OS process executor.
72+
// WithFS returns a copy of v backed by fs.
73+
func (v Venv) WithFS(fs vfs.FS) Venv {
74+
v.FS = fs
75+
76+
return v
77+
}
78+
79+
// WithEnv returns a copy of v whose shell environment is env. A nil env
80+
// becomes an empty map so the result still satisfies [Venv.RequireEnv].
81+
func (v Venv) WithEnv(env map[string]string) Venv {
82+
if env == nil {
83+
env = map[string]string{}
84+
}
85+
86+
v.Env = env
87+
88+
return v
89+
}
90+
91+
// RequireEnv panics with [ErrVenvEnvUnset] when Env is nil, guarding
92+
// functions that write into the shared environment.
93+
func (v Venv) RequireEnv() {
94+
if v.Env == nil {
95+
panic(ErrVenvEnvUnset)
96+
}
97+
}
98+
99+
// OSVenv builds the production [Venv]: the real OS filesystem, the real
100+
// OS process executor, a snapshot of the OS environment, and stdout/stderr
101+
// wired to the real OS streams.
32102
func OSVenv() Venv {
33-
return Venv{FS: vfs.NewOSFS(), Exec: vexec.NewOSExec()}
103+
return Venv{
104+
FS: vfs.NewOSFS(),
105+
Exec: vexec.NewOSExec(),
106+
Env: parseEnviron(os.Environ()),
107+
Writers: writer.Writers{Writer: os.Stdout, ErrWriter: os.Stderr},
108+
}
109+
}
110+
111+
// parseEnviron turns os.Environ-style KEY=VALUE entries into a map. An entry
112+
// with no "=" maps the whole string to an empty value.
113+
func parseEnviron(environ []string) map[string]string {
114+
out := make(map[string]string, len(environ))
115+
116+
for _, entry := range environ {
117+
key, value, _ := strings.Cut(entry, "=")
118+
out[key] = value
119+
}
120+
121+
return out
34122
}

0 commit comments

Comments
 (0)