From c0c8057d84c51fe3dc3d7f3914b6edd6375617bf Mon Sep 17 00:00:00 2001 From: Umputun Date: Fri, 20 Dec 2024 13:49:44 -0600 Subject: [PATCH] fix echo with sudo before we expanded wars too late and usage of sudo with echo command expansion, like $(id) didn't work under sudo --- pkg/runner/commands.go | 2 +- pkg/runner/commands_test.go | 211 +++++++++++++++++++++-------------- pkg/runner/runner_test.go | 34 +++++- pkg/runner/testdata/conf.yml | 4 + 4 files changed, 165 insertions(+), 86 deletions(-) diff --git a/pkg/runner/commands.go b/pkg/runner/commands.go index 0454e245..6a8b87eb 100644 --- a/pkg/runner/commands.go +++ b/pkg/runner/commands.go @@ -363,7 +363,7 @@ func (ec *execCmd) Echo(ctx context.Context) (resp execCmdResp, err error) { echoCmd = fmt.Sprintf("echo %q", echoCmd) } if ec.cmd.Options.Sudo { - echoCmd = fmt.Sprintf("sudo %q", echoCmd) + echoCmd = fmt.Sprintf("sudo %s -c '%s'", ec.shell(), echoCmd) } out, err := ec.exec.Run(ctx, echoCmd, nil) if err != nil { diff --git a/pkg/runner/commands_test.go b/pkg/runner/commands_test.go index a665428b..f355397b 100644 --- a/pkg/runner/commands_test.go +++ b/pkg/runner/commands_test.go @@ -330,89 +330,6 @@ func Test_execCmd(t *testing.T) { assert.Equal(t, " {skip: test}", resp.details) }) - t.Run("echo command", func(t *testing.T) { - ec := execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "welcome back", Name: "test"}} - resp, err := ec.Echo(ctx) - require.NoError(t, err) - assert.Equal(t, " {echo: welcome back}", resp.details) - - ec = execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "echo welcome back", Name: "test"}} - resp, err = ec.Echo(ctx) - require.NoError(t, err) - assert.Equal(t, " {echo: welcome back}", resp.details) - - ec = execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "$var1 welcome back", Name: "test", Environment: map[string]string{"var1": "foo"}}} - resp, err = ec.Echo(ctx) - require.NoError(t, err) - assert.Equal(t, " {echo: foo welcome back}", resp.details) - }) - - t.Run("echo command with condition true", func(t *testing.T) { - defer os.Remove("/tmp/test.condition") - _, err := sess.Run(ctx, "touch /tmp/test.condition", nil) - require.NoError(t, err) - ec := execCmd{ - exec: sess, - tsk: &config.Task{Name: "test"}, - cmd: config.Cmd{ - Echo: "welcome back", - Name: "test", - Condition: "test -f /tmp/test.condition", - }, - } - resp, err := ec.Echo(ctx) - require.NoError(t, err) - assert.Equal(t, " {echo: welcome back}", resp.details) - }) - - t.Run("echo command with condition false", func(t *testing.T) { - ec := execCmd{ - exec: sess, - tsk: &config.Task{Name: "test"}, - cmd: config.Cmd{ - Echo: "welcome back", - Name: "test", - Condition: "test -f /tmp/nonexistent.condition", - }, - } - resp, err := ec.Echo(ctx) - require.NoError(t, err) - assert.Equal(t, " {skip: test}", resp.details) - }) - - t.Run("echo command with condition true inverted", func(t *testing.T) { - ec := execCmd{ - exec: sess, - tsk: &config.Task{Name: "test"}, - cmd: config.Cmd{ - Echo: "welcome back", - Name: "test", - Condition: "! test -f /tmp/nonexistent.condition", - }, - } - resp, err := ec.Echo(ctx) - require.NoError(t, err) - assert.Equal(t, " {echo: welcome back}", resp.details) - }) - - t.Run("echo command with condition false inverted", func(t *testing.T) { - defer os.Remove("/tmp/test2.condition") - _, err := sess.Run(ctx, "touch /tmp/test2.condition", nil) - require.NoError(t, err) - ec := execCmd{ - exec: sess, - tsk: &config.Task{Name: "test"}, - cmd: config.Cmd{ - Echo: "welcome back", - Name: "test", - Condition: "! test -f /tmp/test2.condition", - }, - } - resp, err := ec.Echo(ctx) - require.NoError(t, err) - assert.Equal(t, " {skip: test}", resp.details) - }) - t.Run("sync command", func(t *testing.T) { ec := execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Sync: config.SyncInternal{ Source: "testdata", Dest: "/tmp/sync.testdata", Exclude: []string{"conf2.yml"}}, Name: "test"}} @@ -516,6 +433,134 @@ func Test_execCmd(t *testing.T) { }) } +func Test_execEcho(t *testing.T) { + testingHostAndPort, teardown := startTestContainer(t) + defer teardown() + logs := executor.MakeLogs(false, false, nil) + ctx := context.Background() + connector, connErr := executor.NewConnector("testdata/test_ssh_key", time.Second*10, logs) + require.NoError(t, connErr) + sess, errSess := connector.Connect(ctx, testingHostAndPort, "my-hostAddr", "test") + require.NoError(t, errSess) + + t.Run("echo command", func(t *testing.T) { + ec := execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "welcome back", Name: "test"}} + resp, err := ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {echo: welcome back}", resp.details) + + ec = execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "echo welcome back", Name: "test"}} + resp, err = ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {echo: welcome back}", resp.details) + + ec = execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "$var1 welcome back", Name: "test", Environment: map[string]string{"var1": "foo"}}} + resp, err = ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {echo: foo welcome back}", resp.details) + }) + + t.Run("echo command with condition true", func(t *testing.T) { + defer os.Remove("/tmp/test.condition") + _, err := sess.Run(ctx, "touch /tmp/test.condition", nil) + require.NoError(t, err) + ec := execCmd{ + exec: sess, + tsk: &config.Task{Name: "test"}, + cmd: config.Cmd{ + Echo: "welcome back", + Name: "test", + Condition: "test -f /tmp/test.condition", + }, + } + resp, err := ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {echo: welcome back}", resp.details) + }) + + t.Run("echo command with condition false", func(t *testing.T) { + ec := execCmd{ + exec: sess, + tsk: &config.Task{Name: "test"}, + cmd: config.Cmd{ + Echo: "welcome back", + Name: "test", + Condition: "test -f /tmp/nonexistent.condition", + }, + } + resp, err := ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {skip: test}", resp.details) + }) + + t.Run("echo command with condition true inverted", func(t *testing.T) { + ec := execCmd{ + exec: sess, + tsk: &config.Task{Name: "test"}, + cmd: config.Cmd{ + Echo: "welcome back", + Name: "test", + Condition: "! test -f /tmp/nonexistent.condition", + }, + } + resp, err := ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {echo: welcome back}", resp.details) + }) + + t.Run("echo command with condition false inverted", func(t *testing.T) { + defer os.Remove("/tmp/test2.condition") + _, err := sess.Run(ctx, "touch /tmp/test2.condition", nil) + require.NoError(t, err) + ec := execCmd{ + exec: sess, + tsk: &config.Task{Name: "test"}, + cmd: config.Cmd{ + Echo: "welcome back", + Name: "test", + Condition: "! test -f /tmp/test2.condition", + }, + } + resp, err := ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {skip: test}", resp.details) + }) + + t.Run("echo command with sudo", func(t *testing.T) { + ec := execCmd{ + exec: sess, + tsk: &config.Task{Name: "test"}, + cmd: config.Cmd{ + Echo: "hello from sudo", + Name: "test", + Options: config.CmdOptions{ + Sudo: true, + }, + }, + } + resp, err := ec.Echo(ctx) + require.NoError(t, err) + assert.Equal(t, " {echo: hello from sudo}", resp.details) + + ec = execCmd{ + exec: sess, + tsk: &config.Task{Name: "test"}, + cmd: config.Cmd{ + Echo: "$(id)", + Name: "test", + }, + } + resp, err = ec.Echo(ctx) + require.NoError(t, err) + assert.Contains(t, resp.details, "uid=911(test)") + + ec.cmd.Options.Sudo = true + resp, err = ec.Echo(ctx) + require.NoError(t, err) + assert.Contains(t, resp.details, "uid=0(root)") + }) +} + func Test_execCmdWithTmp(t *testing.T) { testingHostAndPort, teardown := startTestContainer(t) defer teardown() diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 843c735e..bd6bbef3 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -256,7 +256,7 @@ func TestProcess_Run(t *testing.T) { assert.Contains(t, outWriter.String(), `/conf.yml in`) }) - t.Run("echo with variables", func(t *testing.T) { + t.Run("script with echo, with variables", func(t *testing.T) { conf, err := config.New("testdata/conf.yml", nil, nil) require.NoError(t, err) @@ -283,7 +283,7 @@ func TestProcess_Run(t *testing.T) { assert.Contains(t, outWriter.String(), `completed command "echo things" {echo: vars - 6, 9, zzzzz}`) }) - t.Run("echo with variables, verbose", func(t *testing.T) { + t.Run("script with echo and with variables, verbose", func(t *testing.T) { conf, err := config.New("testdata/conf.yml", nil, nil) require.NoError(t, err) @@ -310,6 +310,36 @@ func TestProcess_Run(t *testing.T) { t.Log("out:\n", outWriter.String()) assert.Contains(t, outWriter.String(), `completed command "echo things" {echo: vars - 6, 9, zzzzz}`) }) + + t.Run("echo with variables and sudo, verbose", func(t *testing.T) { + conf, err := config.New("testdata/conf.yml", nil, nil) + require.NoError(t, err) + + outWriter := &bytes.Buffer{} + wr := io.MultiWriter(outWriter, os.Stderr) + lgs := executor.MakeLogs(true, false, conf.AllSecretValues()) + lgs.Info = lgs.Info.WithWriter(wr) + + conn, err := executor.NewConnector("testdata/test_ssh_key", time.Second*10, lgs) + require.NoError(t, err) + + p := Process{ + Concurrency: 1, + Connector: conn, + Playbook: conf, + Logs: lgs, + Only: []string{"copy configuration", "some command", "echo things sudo"}, + Verbose: true, + } + log.SetOutput(io.Discard) + res, err := p.Run(ctx, "task1", testingHostAndPort) + require.NoError(t, err) + assert.Equal(t, 3, res.Commands) + t.Log("out:\n", outWriter.String()) + assert.Contains(t, outWriter.String(), `completed command "echo things sudo" {echo: vars - 6, 9, zzzzz`) + assert.Contains(t, outWriter.String(), `uid=0(root) gid=0(root)`) + }) + t.Run("delete multiple files", func(t *testing.T) { conf, err := config.New("testdata/conf.yml", nil, nil) require.NoError(t, err) diff --git a/pkg/runner/testdata/conf.yml b/pkg/runner/testdata/conf.yml index 851af39a..8da1fc51 100644 --- a/pkg/runner/testdata/conf.yml +++ b/pkg/runner/testdata/conf.yml @@ -108,6 +108,10 @@ tasks: echo: vars - $foo, $bar, $baz options: {no_auto: true} + - name: echo things sudo + echo: vars - $foo, $bar, $baz $(id) + options: {no_auto: true, sudo: true} + - name: prep multiple files for delete copy: - {src: testdata/conf.yml, dst: /tmp/deleteme.1}