diff --git a/executor.go b/executor.go index 55b0be5..b4db7df 100644 --- a/executor.go +++ b/executor.go @@ -970,8 +970,11 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin e.results[host] = make(map[string]*TaskResult) } - // Check when condition - if task.When != nil { + hasLoop := task.Loop != nil || task.WithFile != nil || task.WithFileGlob != nil || task.WithSequence != nil || task.WithTogether != nil || task.WithSubelements != nil + + // Check when condition before allocating a client for simple tasks. Loop + // tasks evaluate when per item. + if task.When != nil && !hasLoop { if !e.evaluateWhen(task.When, host, task) { result := &TaskResult{Skipped: true, Msg: "Skipped due to when condition"} if task.Register != "" { @@ -1012,7 +1015,7 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin // Handle loops, including legacy with_file, with_fileglob, with_sequence, // with_together, and with_subelements syntax. - if task.Loop != nil || task.WithFile != nil || task.WithFileGlob != nil || task.WithSequence != nil || task.WithTogether != nil || task.WithSubelements != nil { + if hasLoop { return e.runLoop(ctx, host, client, task, play, start) } @@ -1949,9 +1952,44 @@ func (e *Executor) runBlock(ctx context.Context, hosts []string, task *Task, pla var blockErr error var rescueErr error + inherit := func(child *Task) { + if child == nil || task == nil { + return + } + + child.Vars = mergeTaskVars(task.Vars, child.Vars) + child.Environment = mergeStringMap(task.Environment, child.Environment) + if task.When != nil { + child.When = mergeConditions(task.When, child.When) + } + if len(task.Tags) > 0 { + child.Tags = mergeStringSlices(task.Tags, child.Tags) + } + if task.Become != nil && child.Become == nil { + child.Become = task.Become + } + if task.BecomeUser != "" && child.BecomeUser == "" { + child.BecomeUser = task.BecomeUser + } + if task.Delegate != "" && child.Delegate == "" { + child.Delegate = task.Delegate + } + if task.RunOnce { + child.RunOnce = true + } + if task.NoLog { + child.NoLog = true + } + if task.IgnoreErrors { + child.IgnoreErrors = true + } + } + // Try block for _, t := range task.Block { - if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { + effective := t + inherit(&effective) + if err := e.runTaskOnHosts(ctx, hosts, &effective, play); err != nil { blockErr = err break } @@ -1960,7 +1998,9 @@ func (e *Executor) runBlock(ctx context.Context, hosts []string, task *Task, pla // Run rescue if block failed if blockErr != nil && len(task.Rescue) > 0 { for _, t := range task.Rescue { - if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { + effective := t + inherit(&effective) + if err := e.runTaskOnHosts(ctx, hosts, &effective, play); err != nil { rescueErr = err break } @@ -1969,7 +2009,9 @@ func (e *Executor) runBlock(ctx context.Context, hosts []string, task *Task, pla // Always run always block for _, t := range task.Always { - if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { + effective := t + inherit(&effective) + if err := e.runTaskOnHosts(ctx, hosts, &effective, play); err != nil { if blockErr == nil { blockErr = err } diff --git a/executor_extra_test.go b/executor_extra_test.go index 3ad0f8e..bff6af6 100644 --- a/executor_extra_test.go +++ b/executor_extra_test.go @@ -539,6 +539,53 @@ func TestExecutorExtra_RunBlock_Good_RescueSuccessClearsBlockFailure(t *testing. require.NoError(t, err) } +func TestExecutorExtra_RunBlock_Good_InheritsBlockVars(t *testing.T) { + e, mock := newTestExecutorWithMock("host1") + mock.expectCommand("echo inherited", "inherited\n", "", 0) + + task := &Task{ + Vars: map[string]any{ + "message": "inherited", + }, + Block: []Task{ + { + Name: "use inherited vars", + Module: "command", + Args: map[string]any{ + "_raw_params": "echo {{ message }}", + }, + }, + }, + } + + err := e.runBlock(context.Background(), []string{"host1"}, task, &Play{}) + + require.NoError(t, err) + assert.True(t, mock.hasExecuted("echo inherited")) +} + +func TestExecutorExtra_RunBlock_Good_InheritsBlockWhen(t *testing.T) { + e, mock := newTestExecutorWithMock("host1") + + task := &Task{ + When: "false", + Block: []Task{ + { + Name: "should be skipped", + Module: "command", + Args: map[string]any{ + "_raw_params": "echo blocked", + }, + }, + }, + } + + err := e.runBlock(context.Background(), []string{"host1"}, task, &Play{}) + + require.NoError(t, err) + assert.Equal(t, 0, mock.commandCount()) +} + func TestExecutorExtra_RunTaskOnHosts_Good_EndHostSkipsFutureTasks(t *testing.T) { e := NewExecutor("/tmp") e.SetInventoryDirect(&Inventory{