diff --git a/executor.go b/executor.go index f5d84a5..3fa6ad7 100644 --- a/executor.go +++ b/executor.go @@ -234,6 +234,20 @@ func (e *Executor) runTaskOnHosts(ctx context.Context, hosts []string, task *Tas return nil } + // run_once executes the task against a single host, then shares any + // registered result with the rest of the host set. + if task.RunOnce && len(hosts) > 0 { + single := *task + single.RunOnce = false + + if err := e.runTaskOnHosts(ctx, hosts[:1], &single, play); err != nil { + return err + } + + e.copyRegisteredResultToHosts(hosts, hosts[0], task.Register) + return nil + } + // Handle block tasks if len(task.Block) > 0 { return e.runBlock(ctx, hosts, task, play) @@ -258,6 +272,45 @@ func (e *Executor) runTaskOnHosts(ctx context.Context, hosts []string, task *Tas return nil } +// copyRegisteredResultToHosts shares a registered task result from one host to +// the rest of the current host set. +func (e *Executor) copyRegisteredResultToHosts(hosts []string, sourceHost, register string) { + if register == "" { + return + } + + sourceResults := e.results[sourceHost] + if sourceResults == nil { + return + } + + result, ok := sourceResults[register] + if !ok || result == nil { + return + } + + for _, host := range hosts { + if host == sourceHost { + continue + } + if e.results[host] == nil { + e.results[host] = make(map[string]*TaskResult) + } + + clone := *result + if result.Results != nil { + clone.Results = append([]TaskResult(nil), result.Results...) + } + if result.Data != nil { + clone.Data = make(map[string]any, len(result.Data)) + for k, v := range result.Data { + clone.Data[k] = v + } + } + e.results[host][register] = &clone + } +} + // runTaskOnHost runs a task on a single host. func (e *Executor) runTaskOnHost(ctx context.Context, host string, task *Task, play *Play) error { start := time.Now() diff --git a/executor_test.go b/executor_test.go index f197d39..94bb4db 100644 --- a/executor_test.go +++ b/executor_test.go @@ -1,9 +1,11 @@ package ansible import ( + "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // --- NewExecutor --- @@ -166,6 +168,44 @@ func TestExecutor_HandleNotify_Good_AnyList(t *testing.T) { assert.True(t, e.notified["reload config"]) } +// --- run_once --- + +func TestExecutor_RunTaskOnHosts_Good_RunOnceSharesRegisteredResult(t *testing.T) { + e := NewExecutor("/tmp") + e.SetInventoryDirect(&Inventory{ + All: &InventoryGroup{ + Hosts: map[string]*Host{ + "host1": {}, + "host2": {}, + }, + }, + }) + + var started []string + task := &Task{ + Name: "Run once debug", + Module: "debug", + Args: map[string]any{"msg": "hello"}, + Register: "debug_result", + RunOnce: true, + } + + e.OnTaskStart = func(host string, _ *Task) { + started = append(started, host) + } + + err := e.runTaskOnHosts(context.Background(), []string{"host1", "host2"}, task, &Play{}) + require.NoError(t, err) + + assert.Len(t, started, 1) + assert.Len(t, e.results["host1"], 1) + assert.Len(t, e.results["host2"], 1) + require.NotNil(t, e.results["host1"]["debug_result"]) + require.NotNil(t, e.results["host2"]["debug_result"]) + assert.Equal(t, "hello", e.results["host1"]["debug_result"].Msg) + assert.Equal(t, "hello", e.results["host2"]["debug_result"].Msg) +} + // --- normalizeConditions --- func TestExecutor_NormalizeConditions_Good_String(t *testing.T) {