diff --git a/executor.go b/executor.go index e86dd34..875b6f2 100644 --- a/executor.go +++ b/executor.go @@ -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 } diff --git a/executor_test.go b/executor_test.go index 6e3b33b..449a916 100644 --- a/executor_test.go +++ b/executor_test.go @@ -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)