diff --git a/executor.go b/executor.go index 3fa6ad7..0407493 100644 --- a/executor.go +++ b/executor.go @@ -338,6 +338,18 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, task *Task, p } } + // Honour check mode for tasks that would mutate state. + if e.CheckMode && !isCheckModeSafeTask(task) { + result := &TaskResult{Skipped: true, Msg: "Skipped in check mode"} + if task.Register != "" { + e.results[host][task.Register] = result + } + if e.OnTaskEnd != nil { + e.OnTaskEnd(host, task, result) + } + return nil + } + // Get SSH client client, err := e.getClient(host, play) if err != nil { @@ -454,6 +466,43 @@ func (e *Executor) runLoop(ctx context.Context, host string, client *SSHClient, return nil } +// isCheckModeSafeTask reports whether a task can run without changing state +// during check mode. +func isCheckModeSafeTask(task *Task) bool { + if task == nil { + return true + } + + if len(task.Block) > 0 || len(task.Rescue) > 0 || len(task.Always) > 0 { + return true + } + if task.IncludeTasks != "" || task.ImportTasks != "" { + return true + } + if task.IncludeRole != nil || task.ImportRole != nil { + return true + } + + switch NormalizeModule(task.Module) { + case "ansible.builtin.debug", + "ansible.builtin.fail", + "ansible.builtin.assert", + "ansible.builtin.pause", + "ansible.builtin.wait_for", + "ansible.builtin.stat", + "ansible.builtin.slurp", + "ansible.builtin.include_vars", + "ansible.builtin.meta", + "ansible.builtin.set_fact", + "ansible.builtin.add_host", + "ansible.builtin.group_by", + "ansible.builtin.setup": + return true + default: + return false + } +} + // runBlock handles block/rescue/always. func (e *Executor) runBlock(ctx context.Context, hosts []string, task *Task, play *Play) error { var blockErr error diff --git a/executor_test.go b/executor_test.go index 94bb4db..fe4ffbe 100644 --- a/executor_test.go +++ b/executor_test.go @@ -206,6 +206,35 @@ func TestExecutor_RunTaskOnHosts_Good_RunOnceSharesRegisteredResult(t *testing.T assert.Equal(t, "hello", e.results["host2"]["debug_result"].Msg) } +// --- check mode --- + +func TestExecutor_RunTaskOnHost_Good_CheckModeSkipsMutatingTask(t *testing.T) { + e := NewExecutor("/tmp") + e.CheckMode = true + + var ended *TaskResult + task := &Task{ + Name: "Run a shell command", + Module: "shell", + Args: map[string]any{"_raw_params": "echo hello"}, + Register: "shell_result", + } + + e.OnTaskEnd = func(_ string, _ *Task, result *TaskResult) { + ended = result + } + + err := e.runTaskOnHost(context.Background(), "host1", task, &Play{}) + require.NoError(t, err) + + require.NotNil(t, ended) + assert.True(t, ended.Skipped) + assert.False(t, ended.Changed) + assert.Equal(t, "Skipped in check mode", ended.Msg) + require.NotNil(t, e.results["host1"]["shell_result"]) + assert.True(t, e.results["host1"]["shell_result"].Skipped) +} + // --- normalizeConditions --- func TestExecutor_NormalizeConditions_Good_String(t *testing.T) {