feat(ansible): support stdin for command and shell modules

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 00:47:30 +00:00
parent a475924e6f
commit ce60a583f3
3 changed files with 66 additions and 0 deletions

View file

@ -521,6 +521,10 @@ func moduleShellWithClient(_ *Executor, client sshRunner, args map[string]any) (
cmd = sprintf("cd %q && %s", chdir, cmd)
}
if stdin := getStringArg(args, "stdin", ""); stdin != "" {
cmd = prefixCommandStdin(cmd, stdin, getBoolArg(args, "stdin_add_newline", true))
}
stdout, stderr, rc, err := client.RunScript(context.Background(), cmd)
if err != nil {
return &TaskResult{Failed: true, Msg: err.Error(), Stdout: stdout, Stderr: stderr, RC: rc}, nil
@ -545,6 +549,10 @@ func moduleCommandWithClient(_ *Executor, client sshRunner, args map[string]any)
cmd = sprintf("cd %q && %s", chdir, cmd)
}
if stdin := getStringArg(args, "stdin", ""); stdin != "" {
cmd = prefixCommandStdin(cmd, stdin, getBoolArg(args, "stdin_add_newline", true))
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil {
return &TaskResult{Failed: true, Msg: err.Error()}, nil

View file

@ -14,6 +14,7 @@ import (
"regexp"
"sort"
"strconv"
"strings"
"time"
coreio "dappco.re/go/core/io"
@ -262,6 +263,10 @@ func (e *Executor) moduleShell(ctx context.Context, client sshExecutorClient, ar
cmd = sprintf("cd %q && %s", chdir, cmd)
}
if stdin := getStringArg(args, "stdin", ""); stdin != "" {
cmd = prefixCommandStdin(cmd, stdin, getBoolArg(args, "stdin_add_newline", true))
}
stdout, stderr, rc, err := client.RunScript(ctx, cmd)
if err != nil {
return &TaskResult{Failed: true, Msg: err.Error(), Stdout: stdout, Stderr: stderr, RC: rc}, nil
@ -295,6 +300,10 @@ func (e *Executor) moduleCommand(ctx context.Context, client sshExecutorClient,
cmd = sprintf("cd %q && %s", chdir, cmd)
}
if stdin := getStringArg(args, "stdin", ""); stdin != "" {
cmd = prefixCommandStdin(cmd, stdin, getBoolArg(args, "stdin_add_newline", true))
}
stdout, stderr, rc, err := client.Run(ctx, cmd)
if err != nil {
return &TaskResult{Failed: true, Msg: err.Error()}, nil
@ -1896,6 +1905,20 @@ func quoteArgs(values []string) []string {
return quoted
}
func prefixCommandStdin(cmd, stdin string, addNewline bool) string {
if stdin == "" {
return cmd
}
if addNewline {
stdin += "\n"
}
return sprintf("printf %%s %s | %s", shellSingleQuote(stdin), cmd)
}
func shellSingleQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}
// --- Helpers ---
func getStringArg(args map[string]any, key, def string) string {

View file

@ -256,6 +256,25 @@ func TestModulesCmd_ModuleCommand_Good_WithChdir(t *testing.T) {
assert.Contains(t, last.Cmd, "ls")
}
func TestModulesCmd_ModuleCommand_Good_WithStdin(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("cat", "input\n", "", 0)
result, err := moduleCommandWithClient(e, mock, map[string]any{
"_raw_params": "cat",
"stdin": "payload",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.Equal(t, "input\n", result.Stdout)
last := mock.lastCommand()
assert.Equal(t, "Run", last.Method)
assert.Contains(t, last.Cmd, "printf %s")
assert.Contains(t, last.Cmd, "| cat")
assert.Contains(t, last.Cmd, "payload\n")
}
func TestModulesCmd_ModuleCommand_Bad_NoCommand(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -705,6 +724,22 @@ func TestModulesCmd_ModuleDifferentiation_Good_ShellUsesRunScript(t *testing.T)
assert.Equal(t, "RunScript", cmds[0].Method, "shell module must use RunScript()")
}
func TestModulesCmd_ModuleDifferentiation_Good_ShellWithStdinStillUsesRunScript(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo test", "test\n", "", 0)
_, _ = moduleShellWithClient(e, mock, map[string]any{
"_raw_params": "echo test",
"stdin": "payload",
})
cmds := mock.executedCommands()
require.Len(t, cmds, 1)
assert.Equal(t, "RunScript", cmds[0].Method, "shell module must still use RunScript()")
assert.Contains(t, cmds[0].Cmd, "printf %s")
assert.Contains(t, cmds[0].Cmd, "| echo test")
}
func TestModulesCmd_ModuleDifferentiation_Good_RawUsesRun(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo test", "test\n", "", 0)