Skip to content

Commit

Permalink
feat(executor/exec): 'command' property (#782)
Browse files Browse the repository at this point in the history
* exec: add a 'stdin' attribute

If set, the value is written to the script stdin. This avoid the need to use 'echo' in the script.

Signed-off-by: Christophe de Vienne <[email protected]>

* Add a 'command' property to 'exec'

It is an alternative to 'script' that takes an explicit list of strings
and does not use a shell.

It makes it easier to deal with escaping special characters, and gives
more control on how the command is run.

Signed-off-by: Christophe de Vienne <[email protected]>
  • Loading branch information
yesnault authored Mar 28, 2024
1 parent fd81da2 commit 2291dfb
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 57 deletions.
12 changes: 12 additions & 0 deletions executors/exec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ testcases:
script: cat
```
Explicit command (no shell):
```yaml
name: Title of TestSuite
testcases:
- name: explicit command
steps:
- type: exec
stdin: "{\"foo\":\"bar\"}"
command: ["jq", ".foo"]
```
## Output
```yaml
Expand Down
126 changes: 72 additions & 54 deletions executors/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ func New() venom.Executor {

// Executor represents a Test Exec
type Executor struct {
Stdin *string `json:"stdin,omitempty" yaml:"stdin,omitempty"`
Script *string `json:"script,omitempty" yaml:"script,omitempty"`
Command []string `json:"command,omitempty" yaml:"command,omitempty"`
Stdin *string `json:"stdin,omitempty" yaml:"stdin,omitempty"`
Script *string `json:"script,omitempty" yaml:"script,omitempty"`
}

// Result represents a step result
Expand Down Expand Up @@ -60,75 +61,92 @@ func (Executor) Run(ctx context.Context, step venom.TestStep) (interface{}, erro
return nil, err
}

if e.Script != nil && *e.Script == "" {
if (e.Script == nil || *e.Script == "") && (len(e.Command) == 0) {
return nil, fmt.Errorf("Invalid command")
}
if e.Script != nil && *e.Script != "" && len(e.Command) != 0 {
return nil, fmt.Errorf("Cannot use both 'script' and 'command'")
}

scriptContent := *e.Script

// Default shell is sh
shell := "/bin/sh"
var opts []string
var (
command string
opts []string
)

// If user wants a specific shell, use it
if strings.HasPrefix(scriptContent, "#!") {
t := strings.SplitN(scriptContent, "\n", 2)
shell = strings.TrimPrefix(t[0], "#!")
shell = strings.TrimRight(shell, " \t\r\n")
if len(e.Command) != 0 {
command = e.Command[0]
if len(e.Command) > 1 {
opts = e.Command[1:]
}
}
if e.Script != nil && *e.Script != "" {
scriptContent := *e.Script

// except on windows where it's powershell
if runtime.GOOS == "windows" {
shell = "PowerShell"
opts = append(opts, "-ExecutionPolicy", "Bypass", "-Command")
}
// Default shell is sh
shell := "/bin/sh"

// Create a tmp file
tmpscript, err := os.CreateTemp(os.TempDir(), "venom-")
if err != nil {
return nil, fmt.Errorf("cannot create tmp file: %s", err)
}
// If user wants a specific shell, use it
if strings.HasPrefix(scriptContent, "#!") {
t := strings.SplitN(scriptContent, "\n", 2)
shell = strings.TrimPrefix(t[0], "#!")
shell = strings.TrimRight(shell, " \t\r\n")
}

// except on windows where it's powershell
if runtime.GOOS == "windows" {
shell = "PowerShell"
opts = append(opts, "-ExecutionPolicy", "Bypass", "-Command")
}

// Put script in file
venom.Debug(ctx, "work with tmp file %s", tmpscript.Name())
n, err := tmpscript.Write([]byte(scriptContent))
if err != nil || n != len(scriptContent) {
// Create a tmp file
tmpscript, err := os.CreateTemp(os.TempDir(), "venom-")
if err != nil {
return nil, fmt.Errorf("cannot write script: %s", err)
return nil, fmt.Errorf("cannot create tmp file: %s", err)
}
return nil, fmt.Errorf("cannot write all script: %d/%d", n, len(scriptContent))
}

oldPath := tmpscript.Name()
tmpscript.Close()
var scriptPath string
if runtime.GOOS == "windows" {
// Remove all .txt Extensions, there is not always a .txt extension
newPath := strings.ReplaceAll(oldPath, ".txt", "")
// and add .PS1 extension
newPath += ".PS1"
if err := os.Rename(oldPath, newPath); err != nil {
return nil, fmt.Errorf("cannot rename script to add powershell extension, aborting")
// Put script in file
venom.Debug(ctx, "work with tmp file %s", tmpscript.Name())
n, err := tmpscript.Write([]byte(scriptContent))
if err != nil || n != len(scriptContent) {
if err != nil {
return nil, fmt.Errorf("cannot write script: %s", err)
}
return nil, fmt.Errorf("cannot write all script: %d/%d", n, len(scriptContent))
}

oldPath := tmpscript.Name()
tmpscript.Close()
var scriptPath string
if runtime.GOOS == "windows" {
// Remove all .txt Extensions, there is not always a .txt extension
newPath := strings.ReplaceAll(oldPath, ".txt", "")
// and add .PS1 extension
newPath += ".PS1"
if err := os.Rename(oldPath, newPath); err != nil {
return nil, fmt.Errorf("cannot rename script to add powershell extension, aborting")
}
// This aims to stop a the very first error and return the right exit code
psCommand := fmt.Sprintf("& { $ErrorActionPreference='Stop'; & %s ;exit $LastExitCode}", newPath)
scriptPath = newPath
opts = append(opts, psCommand)
} else {
scriptPath = oldPath
opts = append(opts, scriptPath)
}
defer os.Remove(scriptPath)

// Chmod file
if err := os.Chmod(scriptPath, 0o700); err != nil {
return nil, fmt.Errorf("cannot chmod script %s: %s", scriptPath, err)
}
// This aims to stop a the very first error and return the right exit code
psCommand := fmt.Sprintf("& { $ErrorActionPreference='Stop'; & %s ;exit $LastExitCode}", newPath)
scriptPath = newPath
opts = append(opts, psCommand)
} else {
scriptPath = oldPath
opts = append(opts, scriptPath)
}
defer os.Remove(scriptPath)

// Chmod file
if err := os.Chmod(scriptPath, 0o700); err != nil {
return nil, fmt.Errorf("cannot chmod script %s: %s", scriptPath, err)
command = shell
}

start := time.Now()

cmd := exec.CommandContext(ctx, shell, opts...)
venom.Debug(ctx, "teststep exec '%s %s'", shell, strings.Join(opts, " "))
cmd := exec.CommandContext(ctx, command, opts...)
venom.Debug(ctx, "teststep exec '%s %s'", command, strings.Join(opts, " "))
cmd.Dir = venom.StringVarFromCtx(ctx, "venom.testsuite.workdir")
if e.Stdin != nil {
cmd.Stdin = strings.NewReader(*e.Stdin)
Expand Down
6 changes: 6 additions & 0 deletions tests/exec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ testcases:
script: cat
assertions:
- result.systemoutjson.foo ShouldContainSubstring bar

- name: command
steps:
- command: ["echo", "{{.cat-json.json}}"]
assertions:
- result.systemoutjson.foo ShouldContainSubstring bar
7 changes: 4 additions & 3 deletions venom.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
)

var (
//Version is set with -ldflags "-X github.com/ovh/venom/venom.Version=$(VERSION)"
// Version is set with -ldflags "-X github.com/ovh/venom/venom.Version=$(VERSION)"
Version = "snapshot"
IsTest = ""
)
Expand Down Expand Up @@ -126,7 +126,8 @@ func (v *Venom) RegisterExecutorUser(name string, e Executor) {
func (v *Venom) GetExecutorRunner(ctx context.Context, ts TestStep, h H) (context.Context, ExecutorRunner, error) {
name, _ := ts.StringValue("type")
script, _ := ts.StringValue("script")
if name == "" && script != "" {
command, _ := ts.StringSliceValue("command")
if name == "" && (script != "" || len(command) != 0) {
name = "exec"
}
retry, err := ts.IntValue("retry")
Expand Down Expand Up @@ -346,7 +347,7 @@ func AllVarsFromCtx(ctx context.Context) H {
}

func JSONUnmarshal(btes []byte, i interface{}) error {
var d = json.NewDecoder(bytes.NewReader(btes))
d := json.NewDecoder(bytes.NewReader(btes))
d.UseNumber()
return d.Decode(i)
}

0 comments on commit 2291dfb

Please sign in to comment.