feat(ansible): support meta end_play

Co-authored-by: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 19:57:12 +00:00
parent 709b1f5dc4
commit 692c2cf58a
2 changed files with 51 additions and 0 deletions

View file

@ -2,6 +2,7 @@ package ansible
import (
"context"
"errors"
"regexp"
"slices"
"strconv"
@ -14,6 +15,8 @@ import (
coreerr "dappco.re/go/core/log"
)
var errEndPlay = errors.New("end play")
// Executor runs Ansible playbooks.
//
// Example:
@ -159,6 +162,9 @@ func (e *Executor) runPlay(ctx context.Context, play *Play) error {
// Execute pre_tasks
for _, task := range play.PreTasks {
if err := e.runTaskOnHosts(ctx, batch, &task, play); err != nil {
if errors.Is(err, errEndPlay) {
return nil
}
return err
}
}
@ -166,6 +172,9 @@ func (e *Executor) runPlay(ctx context.Context, play *Play) error {
// Execute roles
for _, roleRef := range play.Roles {
if err := e.runRole(ctx, batch, &roleRef, play); err != nil {
if errors.Is(err, errEndPlay) {
return nil
}
return err
}
}
@ -173,6 +182,9 @@ func (e *Executor) runPlay(ctx context.Context, play *Play) error {
// Execute tasks
for _, task := range play.Tasks {
if err := e.runTaskOnHosts(ctx, batch, &task, play); err != nil {
if errors.Is(err, errEndPlay) {
return nil
}
return err
}
}
@ -180,12 +192,18 @@ func (e *Executor) runPlay(ctx context.Context, play *Play) error {
// Execute post_tasks
for _, task := range play.PostTasks {
if err := e.runTaskOnHosts(ctx, batch, &task, play); err != nil {
if errors.Is(err, errEndPlay) {
return nil
}
return err
}
}
// Run notified handlers for this batch.
if err := e.runNotifiedHandlers(ctx, batch, play); err != nil {
if errors.Is(err, errEndPlay) {
return nil
}
return err
}
}
@ -1518,6 +1536,8 @@ func (e *Executor) handleMetaAction(ctx context.Context, hosts []string, play *P
switch action {
case "flush_handlers":
return e.runNotifiedHandlers(ctx, hosts, play)
case "end_play":
return errEndPlay
default:
return nil
}

View file

@ -377,6 +377,37 @@ func TestExecutor_RunTaskOnHosts_Good_MetaFlushesHandlers(t *testing.T) {
assert.Equal(t, []string{"change config", "flush handlers", "restart app"}, executed)
}
func TestExecutor_RunPlay_Good_MetaEndPlayStopsRemainingTasks(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
Hosts: map[string]*Host{
"host1": {},
},
},
})
e.clients["host1"] = &SSHClient{}
gatherFacts := false
play := &Play{
Hosts: "all",
GatherFacts: &gatherFacts,
Tasks: []Task{
{Name: "before", Module: "debug", Args: map[string]any{"msg": "before"}},
{Name: "stop", Module: "meta", Args: map[string]any{"_raw_params": "end_play"}},
{Name: "after", Module: "debug", Args: map[string]any{"msg": "after"}},
},
}
var executed []string
e.OnTaskEnd = func(_ string, task *Task, _ *TaskResult) {
executed = append(executed, task.Name)
}
require.NoError(t, e.runPlay(context.Background(), play))
assert.Equal(t, []string{"before", "stop"}, executed)
}
func TestExecutor_NormalizeConditions_Good_StringSlice(t *testing.T) {
result := normalizeConditions([]string{"cond1", "cond2"})
assert.Equal(t, []string{"cond1", "cond2"}, result)