Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command optional exec style #1016

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/gossfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,18 @@ command:

`stdout` and `stderr` can be a string or [pattern](#patterns)

The `exec` attribute is the command to run; this defaults to the name of
the hash for backwards compatibility
The `exec` attribute specifies the command to run. It can be:

* A string, which will be executed through the shell (e.g., /bin/sh -c on Unix systems).
* An array of strings, where each element represents an argument. In this case, the command is invoked directly without a shell. This is
particularly useful in environments like scratch containers, which do not include a shell.
```yaml
...
exec: ["/someBinary", "argument1"]
...
```

If exec is not provided, it defaults to the name of the hash for backward compatibility.

### dns

Expand Down
10 changes: 7 additions & 3 deletions docs/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ definitions:
default: 500
examples:
- 500
# optional attributes
# optional attributesf
local-address:
type: string
default: 127.0.0.1
Expand All @@ -47,8 +47,12 @@ definitions:
type: integer
description: "Validates the exit-status and output of a command"
exec:
description: "command to execute, defaults to the hash key"
type: string
description: "command to execute, defaults to the hash key, see https://goss.rocks/gossfile#command"
oneOf:
- type: string
- type: array
items:
type: string
stdout:
type: array
description: "can be a string or pattern, see https://goss.rocks/gossfile#patterns"
Expand Down
76 changes: 52 additions & 24 deletions resource/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"io"
"log"
"strings"
"time"

Expand All @@ -13,15 +14,15 @@ import (
)

type Command struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
id string `json:"-" yaml:"-"`
Exec string `json:"exec,omitempty" yaml:"exec,omitempty"`
ExitStatus matcher `json:"exit-status" yaml:"exit-status"`
Stdout matcher `json:"stdout" yaml:"stdout"`
Stderr matcher `json:"stderr" yaml:"stderr"`
Timeout int `json:"timeout" yaml:"timeout"`
Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"`
id string `json:"-" yaml:"-"`
Exec util.ExecCommand `json:"exec,omitempty" yaml:"exec,omitempty"`
ExitStatus matcher `json:"exit-status" yaml:"exit-status"`
Stdout matcher `json:"stdout" yaml:"stdout"`
Stderr matcher `json:"stderr" yaml:"stderr"`
Timeout int `json:"timeout" yaml:"timeout"`
Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"`
}

const (
Expand All @@ -41,11 +42,14 @@ func (c *Command) TypeName() string { return CommandResourceName }

func (c *Command) GetTitle() string { return c.Title }
func (c *Command) GetMeta() meta { return c.Meta }
func (c *Command) GetExec() string {
if c.Exec != "" {
return c.Exec
func (c *Command) GetExec() any {
if c.Exec.CmdStr != "" {
return c.Exec.CmdStr
} else if len(c.Exec.CmdSlice) > 0 {
return c.Exec.CmdSlice
} else {
return c.id
}
return c.id
}

func (c *Command) Validate(sys *system.System) []TestResult {
Expand All @@ -57,24 +61,48 @@ func (c *Command) Validate(sys *system.System) []TestResult {
}

var results []TestResult
sysCommand := sys.NewCommand(ctx, c.GetExec(), sys, util.Config{Timeout: time.Duration(c.Timeout) * time.Millisecond})

cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.ID()))
results = append(results, ValidateValue(c, "exit-status", cExitStatus, sysCommand.ExitStatus, skip))
if isSet(c.Stdout) {
results = append(results, ValidateValue(c, "stdout", c.Stdout, sysCommand.Stdout, skip))
}
if isSet(c.Stderr) {
results = append(results, ValidateValue(c, "stderr", c.Stderr, sysCommand.Stderr, skip))
sysCommand, err := sys.NewCommand(ctx, c.GetExec(), sys, util.Config{Timeout: time.Duration(c.Timeout) * time.Millisecond})
if err != nil {
log.Printf("[ERROR] Could not create new command: %v", err)
startTime := time.Now()
results = append(
results,
TestResult{
Result: FAIL,
ResourceType: "Command",
ResourceId: c.id,
Title: c.Title,
Meta: c.Meta,
Property: "type",
Err: toValidateError(err),
StartTime: startTime,
EndTime: startTime,
Duration: startTime.Sub(startTime),
},
)
} else {
cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.ID()))
results = append(results, ValidateValue(c, "exit-status", cExitStatus, sysCommand.ExitStatus, skip))
if isSet(c.Stdout) {
results = append(results, ValidateValue(c, "stdout", c.Stdout, sysCommand.Stdout, skip))
}
if isSet(c.Stderr) {
results = append(results, ValidateValue(c, "stderr", c.Stderr, sysCommand.Stderr, skip))
}
}
return results
}

func NewCommand(sysCommand system.Command, config util.Config) (*Command, error) {
command := sysCommand.Command()
var id string
if sysCommand.Command().CmdStr != "" {
id = sysCommand.Command().CmdStr
} else {
id = sysCommand.Command().CmdSlice[0]
}
exitStatus, err := sysCommand.ExitStatus()
c := &Command{
id: command,
id: id,
ExitStatus: exitStatus,
Stdout: "",
Stderr: "",
Expand Down
4 changes: 2 additions & 2 deletions resource/resource_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ type CommandMap map[string]*Command

func (r CommandMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Command, error) {
ctx := context.WithValue(context.Background(), idKey{}, sr)
sysres := sys.NewCommand(ctx, sr, sys, config)
sysres, _ := sys.NewCommand(ctx, sr, sys, config)
res, err := NewCommand(sysres, config)
if err != nil {
return nil, err
Expand All @@ -135,7 +135,7 @@ func (r CommandMap) AppendSysResource(sr string, sys *system.System, config util

func (r CommandMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Command, system.Command, bool, error) {
ctx := context.WithValue(context.Background(), idKey{}, sr)
sysres := sys.NewCommand(ctx, sr, sys, util.Config{})
sysres, _ := sys.NewCommand(ctx, sr, sys, util.Config{})
res, err := NewCommand(sysres, util.Config{})
if err != nil {
return nil, nil, false, err
Expand Down
36 changes: 30 additions & 6 deletions system/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

type Command interface {
Command() string
Command() util.ExecCommand
Exists() (bool, error)
ExitStatus() (int, error)
Stdout() (io.Reader, error)
Expand All @@ -21,7 +21,7 @@ type Command interface {

type DefCommand struct {
Ctx context.Context
command string
command util.ExecCommand
exitStatus int
stdout io.Reader
stderr io.Reader
Expand All @@ -30,10 +30,29 @@ type DefCommand struct {
err error
}

func NewDefCommand(ctx context.Context, command string, system *System, config util.Config) Command {
func NewDefCommand(ctx context.Context, command any, system *System, config util.Config) (Command, error) {
switch cmd := command.(type) {
case string:
return newDefCommand(ctx, cmd, config), nil
case []string:
return newDefExecCommand(ctx, cmd, config), nil
default:
return nil, fmt.Errorf("command type must be either string or []string")
}
}

func newDefCommand(ctx context.Context, command string, config util.Config) Command {
return &DefCommand{
Ctx: ctx,
command: util.ExecCommand{CmdStr: command},
Timeout: config.TimeOutMilliSeconds(),
}
}

func newDefExecCommand(ctx context.Context, command []string, config util.Config) Command {
return &DefCommand{
Ctx: ctx,
command: command,
command: util.ExecCommand{CmdSlice: command},
Timeout: config.TimeOutMilliSeconds(),
}
}
Expand All @@ -44,7 +63,12 @@ func (c *DefCommand) setup() error {
}
c.loaded = true

cmd := commandWrapper(c.command)
var cmd *util.Command
if c.command.CmdStr != "" {
cmd = commandWrapper(c.command.CmdStr)
} else {
cmd = util.NewCommand(c.command.CmdSlice[0], c.command.CmdSlice[1:]...)
}
err := runCommand(cmd, c.Timeout)

// We don't care about ExitError since it's covered by status
Expand All @@ -63,7 +87,7 @@ func (c *DefCommand) setup() error {
return c.err
}

func (c *DefCommand) Command() string {
func (c *DefCommand) Command() util.ExecCommand {
return c.command
}

Expand Down
2 changes: 1 addition & 1 deletion system/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type System struct {
NewService func(context.Context, string, *System, util2.Config) Service
NewUser func(context.Context, string, *System, util2.Config) User
NewGroup func(context.Context, string, *System, util2.Config) Group
NewCommand func(context.Context, string, *System, util2.Config) Command
NewCommand func(context.Context, any, *System, util2.Config) (Command, error)
NewDNS func(context.Context, string, *System, util2.Config) DNS
NewProcess func(context.Context, string, *System, util2.Config) Process
NewGossfile func(context.Context, string, *System, util2.Config) Gossfile
Expand Down
40 changes: 40 additions & 0 deletions util/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,52 @@ package util

import (
"bytes"
"encoding/json"

//"fmt"
"os/exec"
"syscall"
)

// Allows passing a shell style command string
// or an exec style slice of strings.
type ExecCommand struct {
CmdStr string
CmdSlice []string
}

func (e *ExecCommand) UnmarshalJSON(data []byte) error {
// Try to unmarshal as a string
if err := json.Unmarshal(data, &e.CmdStr); err != nil {
// If string unmarshalling fails, try as a slice
return json.Unmarshal(data, &e.CmdSlice)
}
return nil
}

func (e *ExecCommand) UnmarshalYAML(unmarshal func(any) error) error {
// Try to unmarshal as a string
if err := unmarshal(&e.CmdStr); err != nil {
// If string unmarshalling fails, try as a slice
return unmarshal(&e.CmdSlice)
}
return nil
}

func (e ExecCommand) MarshalJSON() ([]byte, error) {
if e.CmdStr != "" {
return json.Marshal(e.CmdStr)
}
return json.Marshal(e.CmdSlice)
}

func (e ExecCommand) MarshalYAML() (any, error) {
if e.CmdStr != "" {
return e.CmdStr, nil
}
return e.CmdSlice, nil
}

type Command struct {
name string
Cmd *exec.Cmd
Expand Down
Loading