feat(ansible): implement run_once task handling

Co-authored-by: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 19:25:16 +00:00
parent a973604e95
commit acf0a16349
2 changed files with 93 additions and 0 deletions

View file

@ -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()

View file

@ -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) {