Skip to content

Commit d2d457c

Browse files
authored
fix: child execs should inherit parent argument values (#284)
1 parent ffce9de commit d2d457c

File tree

20 files changed

+463
-89
lines changed

20 files changed

+463
-89
lines changed

.golangci.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ linters:
9595
text: (SA5011|SA5001)
9696
- path: (.+)\.go$
9797
text: declaration of "(err|ctx)" shadows declaration at
98+
# The serial and parallel runners have inherently complex logic.
99+
- path: internal/runner/(parallel|serial)
100+
linters:
101+
- cyclop
102+
- nestif
103+
- funlen
104+
- gocognit
105+
- gocyclo
98106
paths:
99107
- third_party$
100108
- builtin$

cmd/internal/exec.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,6 @@ func execFunc(ctx *context.Context, cmd *cobra.Command, verb executable.Verb, ar
9595
logger.Log().FatalErr(err)
9696
}
9797

98-
// populate args for environment handling ignoring arg 0 (the ref)
99-
if len(args) >= 2 {
100-
ctx.Args = args[1:]
101-
}
102-
10398
var ref executable.Ref
10499
if len(args) == 0 {
105100
ref = context.ExpandRef(ctx, executable.NewRef("", verb))
@@ -166,7 +161,13 @@ func execFunc(ctx *context.Context, cmd *cobra.Command, verb executable.Verb, ar
166161
}
167162
startTime := time.Now()
168163
eng := engine.NewExecEngine()
169-
if err := runner.Exec(ctx, e, eng, envMap); err != nil {
164+
165+
var execArgs []string
166+
if len(args) >= 2 {
167+
execArgs = args[1:]
168+
}
169+
170+
if err := runner.Exec(ctx, e, eng, envMap, execArgs); err != nil {
170171
logger.Log().FatalErr(err)
171172
}
172173
dur := time.Since(startTime)

internal/context/context.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ type Context struct {
3535
WorkspacesCache cache.WorkspaceCache
3636
ExecutableCache cache.ExecutableCache
3737

38-
// Args includes the command line arguments passed to the exec command. It is only populated when that command is used.
39-
Args []string
38+
// RootExecutable is the executable that is being run in the current context.
39+
// This will be nil if the context is not associated with an executable run.
40+
RootExecutable *executable.Executable
4041

4142
// ProcessTmpDir is the temporary directory for the current process. If set, it will be
4243
// used to store temporary files all executable runs when the tmpDir value is specified.

internal/runner/exec/exec.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ func (r *execRunner) Exec(
3434
e *executable.Executable,
3535
_ engine.Engine,
3636
inputEnv map[string]string,
37+
inputArgs []string,
3738
) error {
3839
execSpec := e.Exec
3940
defaultEnv := env.DefaultEnv(ctx, e)
40-
envMap, err := env.BuildEnvMap(ctx.Config.CurrentVaultName(), e.Env(), ctx.Args, inputEnv, defaultEnv)
41+
envMap, err := env.BuildEnvMap(ctx.Config.CurrentVaultName(), e.Env(), inputArgs, inputEnv, defaultEnv)
4142
if err != nil {
4243
return errors.Wrap(err, "unable to set parameters to env")
4344
}
@@ -48,7 +49,7 @@ func (r *execRunner) Exec(
4849
e.FlowFilePath(),
4950
e.WorkspacePath(),
5051
e.Env(),
51-
ctx.Args,
52+
inputArgs,
5253
envMap,
5354
); err != nil {
5455
ctx.AddCallback(cb)

internal/runner/launch/launch.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,20 @@ func (r *launchRunner) Exec(
3737
e *executable.Executable,
3838
_ engine.Engine,
3939
inputEnv map[string]string,
40+
inputArgs []string,
4041
) error {
4142
launchSpec := e.Launch
4243
envMap, err := env.BuildEnvMap(
4344
ctx.Config.CurrentVaultName(),
4445
e.Env(),
45-
ctx.Args,
46+
inputArgs,
4647
inputEnv,
4748
env.DefaultEnv(ctx, e),
4849
)
4950
if err != nil {
5051
return errors.Wrap(err, "unable to set parameters to env")
5152
}
52-
if err := env.SetEnv(ctx.Config.CurrentVaultName(), e.Env(), ctx.Args, envMap); err != nil {
53+
if err := env.SetEnv(ctx.Config.CurrentVaultName(), e.Env(), inputArgs, envMap); err != nil {
5354
return errors.Wrap(err, "unable to set parameters to env")
5455
}
5556

@@ -58,7 +59,7 @@ func (r *launchRunner) Exec(
5859
e.FlowFilePath(),
5960
e.WorkspacePath(),
6061
e.Env(),
61-
ctx.Args,
62+
inputArgs,
6263
envMap,
6364
); err != nil {
6465
ctx.AddCallback(cb)

internal/runner/mocks/mock_runner.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/runner/parallel/parallel.go

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
stdCtx "context"
55
"fmt"
66
"maps"
7+
"os"
8+
"path/filepath"
9+
"strings"
710

811
"github.com/pkg/errors"
912
"golang.org/x/sync/errgroup"
@@ -41,9 +44,10 @@ func (r *parallelRunner) Exec(
4144
e *executable.Executable,
4245
eng engine.Engine,
4346
inputEnv map[string]string,
47+
inputArgs []string,
4448
) error {
4549
parallelSpec := e.Parallel
46-
if err := envUtils.SetEnv(ctx.Config.CurrentVaultName(), e.Env(), ctx.Args, inputEnv); err != nil {
50+
if err := envUtils.SetEnv(ctx.Config.CurrentVaultName(), e.Env(), inputArgs, inputEnv); err != nil {
4751
return errors.Wrap(err, "unable to set parameters to env")
4852
}
4953

@@ -52,7 +56,7 @@ func (r *parallelRunner) Exec(
5256
e.FlowFilePath(),
5357
e.WorkspacePath(),
5458
e.Env(),
55-
ctx.Args,
59+
inputArgs,
5660
inputEnv,
5761
); err != nil {
5862
ctx.AddCallback(cb)
@@ -83,12 +87,11 @@ func (r *parallelRunner) Exec(
8387
return fmt.Errorf("no parallel executables to run")
8488
}
8589

86-
//nolint:funlen,gocognit
8790
func handleExec(
8891
ctx *context.Context, parent *executable.Executable,
8992
eng engine.Engine,
9093
parallelSpec *executable.ParallelExecutableType,
91-
promptedEnv map[string]string,
94+
inputEnv map[string]string,
9295
cacheData map[string]string,
9396
) error {
9497
groupCtx, cancel := stdCtx.WithCancel(ctx.Ctx)
@@ -100,18 +103,44 @@ func handleExec(
100103
}
101104
group.SetLimit(limit)
102105

103-
dataMap := expr.ExpressionEnv(ctx, parent, cacheData, promptedEnv)
106+
// Expand the directory of the parallel execution. The root / parent's directory is used if one is not specified.
107+
var root *executable.Executable
108+
if ctx.RootExecutable != nil {
109+
root = ctx.RootExecutable
110+
} else {
111+
root = parent
112+
}
113+
if parallelSpec.Dir == "" {
114+
parallelSpec.Dir = executable.Directory(filepath.Dir(root.FlowFilePath()))
115+
}
116+
targetDir, isTmp, err := parallelSpec.Dir.ExpandDirectory(
117+
root.WorkspacePath(),
118+
root.FlowFilePath(),
119+
ctx.ProcessTmpDir,
120+
inputEnv,
121+
)
122+
if err != nil {
123+
return errors.Wrap(err, "unable to expand directory")
124+
} else if isTmp && ctx.ProcessTmpDir == "" {
125+
ctx.ProcessTmpDir = targetDir
126+
}
104127

128+
// Build the list of steps to execute
105129
var execs []engine.Exec
130+
conditionalData := expr.ExpressionEnv(ctx, parent, cacheData, inputEnv)
131+
106132
for i, refConfig := range parallelSpec.Execs {
133+
// Skip over steps that do not match the condition
107134
if refConfig.If != "" {
108-
if truthy, err := expr.IsTruthy(refConfig.If, &dataMap); err != nil {
135+
if truthy, err := expr.IsTruthy(refConfig.If, &conditionalData); err != nil {
109136
return err
110137
} else if !truthy {
111138
logger.Log().Debugf("skipping execution %d/%d", i+1, len(parallelSpec.Execs))
112139
continue
113140
}
114141
}
142+
143+
// Get the executable for the step
115144
var exec *executable.Executable
116145
switch {
117146
case len(refConfig.Ref) > 0:
@@ -126,8 +155,10 @@ func handleExec(
126155
return errors.New("parallel executable must have a ref or cmd")
127156
}
128157

129-
execPromptedEnv := make(map[string]string)
130-
maps.Copy(promptedEnv, execPromptedEnv)
158+
// Prepare the environment and arguments for the child executable
159+
childEnv := make(map[string]string)
160+
childArgs := make([]string, 0)
161+
maps.Copy(childEnv, inputEnv)
131162
if len(refConfig.Args) > 0 {
132163
execEnv := exec.Env()
133164
if execEnv == nil || execEnv.Args == nil {
@@ -136,14 +167,31 @@ func handleExec(
136167
exec.Ref().String(),
137168
)
138169
} else {
139-
a, err := envUtils.BuildArgsEnvMap(execEnv.Args, refConfig.Args, execPromptedEnv)
170+
for _, arg := range os.Environ() {
171+
kv := strings.SplitN(arg, "=", 2)
172+
if len(kv) == 2 {
173+
childEnv[kv[0]] = kv[1]
174+
}
175+
}
176+
177+
if parallelSpec.Args == nil {
178+
childArgs = refConfig.Args
179+
} else {
180+
childArgs = envUtils.BuildArgsFromEnv(execEnv.Args, childEnv)
181+
if len(childArgs) == 0 {
182+
childArgs = refConfig.Args // If no resolved args, fallback to original args
183+
}
184+
}
185+
186+
a, err := envUtils.BuildArgsEnvMap(execEnv.Args, childArgs, childEnv)
140187
if err != nil {
141188
logger.Log().Error(err, "unable to process arguments")
142189
}
143-
maps.Copy(execPromptedEnv, a)
190+
maps.Copy(childEnv, a)
144191
}
145192
}
146193

194+
// Set log fields and directory for the executable
147195
switch {
148196
case exec.Exec != nil:
149197
fields := map[string]interface{}{"step": exec.Ref().String()}
@@ -159,10 +207,18 @@ func handleExec(
159207
if parallelSpec.Dir != "" && exec.Serial.Dir == "" {
160208
exec.Serial.Dir = parallelSpec.Dir
161209
}
210+
case exec.Request != nil:
211+
if exec.Request.ResponseFile != nil && parallelSpec.Dir != "" && exec.Request.ResponseFile.Dir == "" {
212+
exec.Request.ResponseFile.Dir = parallelSpec.Dir
213+
}
214+
case exec.Render != nil:
215+
if parallelSpec.Dir != "" && exec.Render.Dir == "" {
216+
exec.Render.Dir = parallelSpec.Dir
217+
}
162218
}
163219

164220
runExec := func() error {
165-
err := runner.Exec(ctx, exec, eng, execPromptedEnv)
221+
err := runner.Exec(ctx, exec, eng, childEnv, childArgs)
166222
if err != nil {
167223
return err
168224
}
@@ -171,6 +227,7 @@ func handleExec(
171227

172228
execs = append(execs, engine.Exec{ID: exec.Ref().String(), Function: runExec, MaxRetries: refConfig.Retries})
173229
}
230+
174231
results := eng.Execute(
175232
ctx.Ctx, execs,
176233
engine.WithMode(engine.Parallel),

internal/runner/parallel/parallel_test.go

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
. "github.com/onsi/gomega"
1010
"go.uber.org/mock/gomock"
1111

12+
"github.com/flowexec/flow/internal/context"
1213
"github.com/flowexec/flow/internal/runner"
1314
"github.com/flowexec/flow/internal/runner/engine"
1415
"github.com/flowexec/flow/internal/runner/engine/mocks"
@@ -108,7 +109,7 @@ var _ = Describe("ParallelRunner", func() {
108109
mockEngine.EXPECT().
109110
Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
110111
Return(results).Times(1)
111-
Expect(parallelRnr.Exec(ctx.Ctx, rootExec, mockEngine, promptedEnv)).To(Succeed())
112+
Expect(parallelRnr.Exec(ctx.Ctx, rootExec, mockEngine, promptedEnv, nil)).To(Succeed())
112113
})
113114

114115
It("fail when there is an engine error", func() {
@@ -128,7 +129,7 @@ var _ = Describe("ParallelRunner", func() {
128129
mockEngine.EXPECT().
129130
Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
130131
Return(results).Times(1)
131-
Expect(parallelRnr.Exec(ctx.Ctx, rootExec, mockEngine, promptedEnv)).ToNot(Succeed())
132+
Expect(parallelRnr.Exec(ctx.Ctx, rootExec, mockEngine, promptedEnv, nil)).ToNot(Succeed())
132133
})
133134

134135
It("should skip execution when condition is false", func() {
@@ -145,7 +146,58 @@ var _ = Describe("ParallelRunner", func() {
145146
mockEngine.EXPECT().
146147
Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
147148
Return(results).Times(1)
148-
Expect(parallelRnr.Exec(ctx.Ctx, rootExec, mockEngine, make(map[string]string))).To(Succeed())
149+
Expect(parallelRnr.Exec(ctx.Ctx, rootExec, mockEngine, make(map[string]string), nil)).To(Succeed())
150+
})
151+
152+
It("should pass environment args from parent to child executables", func() {
153+
pos1 := 1
154+
parentExec := &executable.Executable{
155+
Parallel: &executable.ParallelExecutableType{
156+
Args: executable.ArgumentList{{EnvKey: "TEST_VAR", Pos: &pos1}},
157+
Execs: []executable.ParallelRefConfig{{Ref: "test:child", Args: []string{"var=$TEST_VAR"}}},
158+
},
159+
}
160+
parentExec.SetContext("test", "/test", "test", "/test/parent.flow")
161+
162+
childExec := &executable.Executable{
163+
Exec: &executable.ExecExecutableType{
164+
Cmd: "echo $TEST_VAR",
165+
Args: executable.ArgumentList{{EnvKey: "TEST_VAR", Flag: "var"}},
166+
},
167+
}
168+
childExec.SetContext("test", "/test", "test", "/test/child.flow")
169+
170+
mockCache := ctx.ExecutableCache
171+
mockCache.EXPECT().GetExecutableByRef(gomock.Any()).Return(childExec, nil).Times(1)
172+
173+
ctx.RunnerMock.EXPECT().IsCompatible(gomock.Any()).Return(true).Times(1)
174+
ctx.RunnerMock.EXPECT().
175+
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"var=test_value"}).
176+
DoAndReturn(func(
177+
_ *context.Context,
178+
exec *executable.Executable,
179+
_ engine.Engine,
180+
inputEnv map[string]string,
181+
inputArgs []string,
182+
) error {
183+
Expect(inputEnv).To(HaveKeyWithValue("TEST_VAR", "test_value"))
184+
Expect(inputArgs).To(ContainElement("var=test_value"))
185+
return nil
186+
}).Times(1)
187+
188+
results := engine.ResultSummary{Results: []engine.Result{{}}}
189+
mockEngine.EXPECT().
190+
Execute(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
191+
DoAndReturn(func(
192+
_ stdCtx.Context, execs []engine.Exec, _ ...engine.OptionFunc) engine.ResultSummary {
193+
for _, exec := range execs {
194+
Expect(exec.Function()).To(Succeed())
195+
}
196+
return results
197+
})
198+
199+
Expect(parallelRnr.Exec(ctx.Ctx, parentExec, mockEngine, make(map[string]string), []string{"test_value"})).
200+
To(Succeed())
149201
})
150202
})
151203
})

0 commit comments

Comments
 (0)