feat(ansible): support legacy action shorthands

This commit is contained in:
Virgil 2026-04-01 23:31:40 +00:00
parent bfa9a8d0ba
commit 0e3a362269
3 changed files with 135 additions and 0 deletions

View file

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
// --- NewExecutor ---
@ -273,6 +274,25 @@ func TestExecutor_RunTaskOnHost_Good_DelegateToUsesDelegatedClient(t *testing.T)
assert.Equal(t, 1, mock.commandCount())
}
func TestExecutor_RunTaskOnHost_Good_ActionAliasExecutesCommand(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo action-alias`, "action-alias", "", 0)
var task Task
require.NoError(t, yaml.Unmarshal([]byte(`
name: Legacy action
action: command echo action-alias
`), &task))
task.Register = "action_result"
err := e.runTaskOnHost(context.Background(), "host1", []string{"host1"}, &task, &Play{})
require.NoError(t, err)
require.NotNil(t, e.results["host1"]["action_result"])
assert.Equal(t, "action-alias", e.results["host1"]["action_result"].Stdout)
assert.True(t, mock.hasExecuted(`echo action-alias`))
}
func TestExecutor_Run_Good_VarsFilesMergeIntoPlayVars(t *testing.T) {
dir := t.TempDir()

View file

@ -4,6 +4,8 @@ import (
"iter"
"maps"
"slices"
"strings"
"unicode"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
@ -352,6 +354,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
"delegate_to": true, "run_once": true, "tags": true,
"block": true, "rescue": true, "always": true, "notify": true, "listen": true,
"retries": true, "delay": true, "until": true,
"action": true, "local_action": true,
"include_tasks": true, "import_tasks": true,
"include_role": true, "import_role": true,
"with_items": true, "with_dict": true, "with_indexed_items": true, "with_nested": true, "with_file": true, "with_fileglob": true, "with_sequence": true,
@ -459,9 +462,92 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.WithSequence = sequence
}
// Support legacy action/local_action shorthands.
if t.Module == "" {
if localAction, ok := m["local_action"]; ok {
if module, args := parseActionSpec(localAction); module != "" {
t.Module = module
t.Args = args
t.Delegate = "localhost"
}
}
}
if t.Module == "" {
if action, ok := m["action"]; ok {
if module, args := parseActionSpec(action); module != "" {
t.Module = module
t.Args = args
}
}
}
return nil
}
// parseActionSpec converts action/local_action values into a module name and
// argument map.
func parseActionSpec(value any) (string, map[string]any) {
switch v := value.(type) {
case string:
return parseActionSpecString(v)
case map[string]any:
module := getStringArg(v, "module", "")
if module == "" {
module = getStringArg(v, "_raw_params", "")
}
args := make(map[string]any)
for key, val := range v {
if key == "module" || key == "_raw_params" {
continue
}
args[key] = val
}
if raw, ok := v["_raw_params"]; ok {
args["_raw_params"] = raw
}
if module == "" {
return "", nil
}
if len(args) == 0 {
args = nil
}
return module, args
default:
return parseActionSpecString(sprintf("%v", value))
}
}
func parseActionSpecString(raw string) (string, map[string]any) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", nil
}
parts := fields(raw)
if len(parts) == 0 {
return "", nil
}
module := parts[0]
if module == "" {
return "", nil
}
if len(parts) == 1 {
return module, nil
}
sepIndex := strings.IndexFunc(raw, unicode.IsSpace)
if sepIndex < 0 || sepIndex >= len(raw)-1 {
return module, nil
}
return module, map[string]any{"_raw_params": strings.TrimSpace(raw[sepIndex:])}
}
// expandNestedLoop converts with_nested input into a loop of cartesian
// product items. Each output item is a slice containing one value from each
// nested list.

View file

@ -304,6 +304,35 @@ with_sequence: "start=1 end=3 format=%02d"
assert.Equal(t, "start=1 end=3 format=%02d", sequence)
}
func TestTypes_Task_UnmarshalYAML_Good_ActionAlias(t *testing.T) {
input := `
name: Legacy action
action: command echo hello world
`
var task Task
err := yaml.Unmarshal([]byte(input), &task)
require.NoError(t, err)
assert.Equal(t, "command", task.Module)
require.NotNil(t, task.Args)
assert.Equal(t, "echo hello world", task.Args["_raw_params"])
}
func TestTypes_Task_UnmarshalYAML_Good_LocalAction(t *testing.T) {
input := `
name: Legacy local action
local_action: shell echo local
`
var task Task
err := yaml.Unmarshal([]byte(input), &task)
require.NoError(t, err)
assert.Equal(t, "shell", task.Module)
assert.Equal(t, "localhost", task.Delegate)
require.NotNil(t, task.Args)
assert.Equal(t, "echo local", task.Args["_raw_params"])
}
func TestTypes_Task_UnmarshalYAML_Good_WithNested(t *testing.T) {
input := `
name: Nested loop values