Skip to content

Commit

Permalink
Echo command (#96)
Browse files Browse the repository at this point in the history
* add echo command

* lint: missing var names

* add to readme
  • Loading branch information
umputun committed May 15, 2023
1 parent 8b26794 commit adc8338
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ Spot supports the following command types:
- `sync`: syncs directory from the local machine to the remote host(s). Optionally supports deleting files on the remote host(s) that don't exist locally. Example: `sync: {"src": "testdata", "dst": "/tmp/things", "delete": true}`
- `delete`: deletes a file or directory on the remote host(s), optionally can remove recursively. Example: `delete: {"path": "/tmp/things", "recur": true}`
- `wait`: waits for the specified command to finish on the remote host(s) with 0 error code. This command is useful when you need to wait for a service to start before executing the next command. Allows to specify the timeout as well as check interval. Example: `wait: {"cmd": "curl -s --fail localhost:8080", "timeout": "30s", "interval": "1s"}`
- `echo`: prints the specified message to the console. Example: `echo: "Hello World $some_var"`. This command is useful for debugging purposes and also to print the value of variables to the console.

### Command options

Expand Down
2 changes: 2 additions & 0 deletions pkg/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Cmd struct {
Delete DeleteInternal `yaml:"delete" toml:"delete"`
Wait WaitInternal `yaml:"wait" toml:"wait"`
Script string `yaml:"script" toml:"script,multiline"`
Echo string `yaml:"echo" toml:"echo"`
Environment map[string]string `yaml:"env" toml:"env"`
Options CmdOptions `yaml:"options" toml:"options,omitempty"`
Condition string `yaml:"cond" toml:"cond,omitempty"`
Expand Down Expand Up @@ -330,6 +331,7 @@ func (cmd *Cmd) validate() error {
{"delete", func() bool { return cmd.Delete.Location != "" }},
{"sync", func() bool { return cmd.Sync.Source != "" && cmd.Sync.Dest != "" }},
{"wait", func() bool { return cmd.Wait.Command != "" }},
{"echo", func() bool { return cmd.Echo != "" }},
}

setCmds, names := []string{}, []string{}
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ func TestCmd_validate(t *testing.T) {
{"only wait", Cmd{Wait: WaitInternal{Command: "command"}}, ""},
{"multiple fields set", Cmd{Script: "example_script", Copy: CopyInternal{Source: "source", Dest: "dest"}},
"only one of [script, copy] is allowed"},
{"nothing set", Cmd{}, "one of [script, copy, mcopy, delete, sync, wait] must be set"},
{"nothing set", Cmd{}, "one of [script, copy, mcopy, delete, sync, wait, echo] must be set"},
}

for _, tt := range tbl {
Expand Down
19 changes: 19 additions & 0 deletions pkg/runner/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,25 @@ func (ec *execCmd) Wait(ctx context.Context) (details string, vars map[string]st
}
}

// Echo prints a message. It enforces the echo command to start with "echo " and adds sudo if needed.
// It returns the result of the echo command as details string.
func (ec *execCmd) Echo(ctx context.Context) (details string, vars map[string]string, err error) {
tmpl := templater{hostAddr: ec.hostAddr, hostName: ec.hostName, task: ec.tsk, command: ec.cmd.Name, env: ec.cmd.Environment}
echoCmd := tmpl.apply(ec.cmd.Echo)
if !strings.HasPrefix(echoCmd, "echo ") {
echoCmd = fmt.Sprintf("echo %s", echoCmd)
}
if ec.cmd.Options.Sudo {
echoCmd = fmt.Sprintf("sudo %s", echoCmd)
}
out, err := ec.exec.Run(ctx, echoCmd, false)
if err != nil {
return "", nil, fmt.Errorf("can't run echo command on %s: %w", ec.hostAddr, err)
}
details = fmt.Sprintf(" {echo: %s}", strings.Join(out, "; "))
return details, nil, nil
}

func (ec *execCmd) checkCondition(ctx context.Context) (bool, error) {
if ec.cmd.Condition == "" {
return true, nil // no condition, always allow
Expand Down
17 changes: 17 additions & 0 deletions pkg/runner/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,21 @@ func Test_execCmd(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, " {skip: test}", 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"}}
details, _, err := ec.Echo(ctx)
require.NoError(t, err)
assert.Equal(t, " {echo: welcome back}", details)

ec = execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "echo welcome back", Name: "test"}}
details, _, err = ec.Echo(ctx)
require.NoError(t, err)
assert.Equal(t, " {echo: welcome back}", 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"}}}
details, _, err = ec.Echo(ctx)
require.NoError(t, err)
assert.Equal(t, " {echo: foo welcome back}", details)
})
}
3 changes: 3 additions & 0 deletions pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ func (p *Process) execCommand(ctx context.Context, ec execCmd) (details string,
case ec.cmd.Wait.Command != "":
log.Printf("[DEBUG] wait for command on %s", ec.hostAddr)
return ec.Wait(ctx)
case ec.cmd.Echo != "":
log.Printf("[DEBUG] echo on %s", ec.hostAddr)
return ec.Echo(ctx)
default:
return "", nil, fmt.Errorf("unknown command %q", ec.cmd.Name)
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,26 @@ func TestProcess_Run(t *testing.T) {
assert.Contains(t, outWriter.String(), `uploaded testdata/conf.yml to localhost:/tmp/.spot/conf.yml`)
})

t.Run("echo with variables", 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)
p := Process{
Concurrency: 1,
Connector: connector,
Config: conf,
ColorWriter: executor.NewColorizedWriter(wr, "", "", "", nil),
Only: []string{"copy configuration", "some command", "echo things"},
Verbose: true,
}
log.SetOutput(io.Discard)
res, err := p.Run(ctx, "task1", testingHostAndPort)
require.NoError(t, err)
assert.Equal(t, 3, res.Commands)
assert.Contains(t, outWriter.String(), `completed command "echo things" {echo: vars - 6, 9, zzzzz}`)
})
}

func TestProcess_RunWithSudo(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions pkg/runner/testdata/conf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ tasks:
copy: {src: "${filename}", dst: "/srv/conf.yml"}
options: {no_auto: true, sudo: true}

- name: echo things
echo: vars - $foo, $bar, $baz
options: {no_auto: true}

- name: failed_task
commands:
- name: good command
Expand Down

0 comments on commit adc8338

Please sign in to comment.