feat(ansible): honour check mode for mutating tasks

This commit is contained in:
Virgil 2026-04-01 19:28:25 +00:00
parent acf0a16349
commit 6fb5ebe920
2 changed files with 78 additions and 0 deletions

View file

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

View file

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