diff --git a/executor.go b/executor.go index b250ef7..2be708b 100644 --- a/executor.go +++ b/executor.go @@ -282,6 +282,11 @@ func (e *Executor) runRole(ctx context.Context, hosts []string, roleRef *RoleRef // runTaskOnHosts runs a task on all hosts. func (e *Executor) runTaskOnHosts(ctx context.Context, hosts []string, task *Task, play *Play) error { + // run_once executes the task only on the first host in the current batch. + if task.RunOnce && len(hosts) > 1 { + hosts = hosts[:1] + } + // Check tags tags := append(append([]string{}, play.Tags...), task.Tags...) if !e.matchesTags(tags) { diff --git a/executor_test.go b/executor_test.go index b94fcc1..5d425c9 100644 --- a/executor_test.go +++ b/executor_test.go @@ -138,6 +138,45 @@ func TestExecutor_RunPlay_Good_SerialBatchesHosts(t *testing.T) { }, gathered) } +func TestExecutor_RunPlay_Good_RunOnceTaskOnlyRunsOnFirstHost(t *testing.T) { + e := NewExecutor("/tmp") + e.SetInventoryDirect(&Inventory{ + All: &InventoryGroup{ + Hosts: map[string]*Host{ + "host1": {}, + "host2": {}, + }, + }, + }) + + var executed []string + e.OnTaskStart = func(host string, task *Task) { + executed = append(executed, host) + } + + gatherFacts := false + play := &Play{ + Name: "run once", + Hosts: "all", + GatherFacts: &gatherFacts, + Tasks: []Task{ + { + Name: "single host", + Module: "debug", + Args: map[string]any{"msg": "ok"}, + Register: "result", + RunOnce: true, + }, + }, + } + + require.NoError(t, e.runPlay(context.Background(), play)) + assert.Equal(t, []string{"host1"}, executed) + assert.NotNil(t, e.results["host1"]["result"]) + _, ok := e.results["host2"] + assert.False(t, ok) +} + func TestExecutor_RunPlay_Good_PlayTagsApplyToUntaggedTasks(t *testing.T) { e := NewExecutor("/tmp") e.SetInventoryDirect(&Inventory{