From ce60a583f393122ccd8e21211e826a13805f48e9 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 00:47:30 +0000 Subject: [PATCH] feat(ansible): support stdin for command and shell modules Co-Authored-By: Virgil --- mock_ssh_test.go | 8 ++++++++ modules.go | 23 +++++++++++++++++++++++ modules_cmd_test.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/mock_ssh_test.go b/mock_ssh_test.go index 447b665..5a27014 100644 --- a/mock_ssh_test.go +++ b/mock_ssh_test.go @@ -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 diff --git a/modules.go b/modules.go index d899bde..20fb5ac 100644 --- a/modules.go +++ b/modules.go @@ -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 { diff --git a/modules_cmd_test.go b/modules_cmd_test.go index e05dba4..476dd17 100644 --- a/modules_cmd_test.go +++ b/modules_cmd_test.go @@ -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)