Skip to content

Commit

Permalink
support command lists
Browse files Browse the repository at this point in the history
This updates the exec{} block's `command` setting to support command lists.
That is a command formatted like `["echo", "hello", "world"]`.
It is backwards compatible with the old format, turning single commands
w/o spaces into lists.
Single commands with spaces still use the `sh -c` setup.

This will allow for multi-word commands on Windows and in setups (like
bare docker containers) without `sh` installed.
  • Loading branch information
eikenb committed Feb 10, 2022
1 parent 14a926e commit be5fcd8
Show file tree
Hide file tree
Showing 16 changed files with 290 additions and 163 deletions.
47 changes: 28 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,25 +239,34 @@ users the ability to further customize their command script.
The command configured for running on template rendering must take one of two
forms.

The first is as a single command without spaces in its name and no arguments.
This form of command will be called directly by consul-template and is good for
any situation. The command can be a shell script or an executable, anything
called via a single word, and must be either on the runtime search PATH or the
absolute path to the executable. The single word limination is necessary to
eliminate any need for parsing the command line. For example..

`command = "/opt/foo"` or, if on PATH, `command = "foo"`

The second form is as a multi-word command, a command with arguments or a more
complex shell command. This form **requires** a shell named `sh` be on the
executable search path (eg. PATH on *nix). This is the standard on all *nix
systems and should work out of the box on those systems. This won't work on,
for example, Docker images with only the executable and not a minimal system
like Alpine. Using this form you can join multiple commands with logical
operators, `&&` and `||`, use pipelines with `|`, conditionals, etc. Note that
the shell `sh` is normally `/bin/sh` on *nix systems and is either a POSIX
shell or a shell run in POSIX compatible mode, so it is best to stick to POSIX
shell syntax in this command. For example..
The first is as a list of the command and arguments split at spaces. The
command can use an absolute path or be found on the execution environment's
PATH and must be the first item in the list. This form allows for single or
multi-word commands that can be executed directly with a system call. For
example...

`command = ["echo", "hello"]`
`command = ["/opt/foo-package/bin/run-foo"]`
`command = ["foo"]`

Note that if you give a single command without the list denoting square
brackets (`[]`) it is converted into a list with a single argument.

This:
`command = "foo"`
is equivalent to:
`command = ["foo"]`

The second form is as a single quoted command using system shell features. This
form **requires** a shell named `sh` be on the executable search path (eg. PATH
on *nix). This is the standard on all *nix systems and should work out of the
box on those systems. This won't work on, for example, Docker images with only
the executable and without a minimal system like Alpine. Using this form you
can join multiple commands with logical operators, `&&` and `||`, use pipelines
with `|`, conditionals, etc. Note that the shell `sh` is normally `/bin/sh` on
\*nix systems and is either a POSIX shell or a shell run in POSIX compatible
mode, so it is best to stick to POSIX shell syntax in this command. For
example..

`command = "/opt/foo && /opt/bar"`

Expand Down
2 changes: 1 addition & 1 deletion cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ func (cli *CLI) ParseFlags(args []string) (

flags.Var((funcVar)(func(s string) error {
c.Exec.Enabled = config.Bool(true)
c.Exec.Command = config.String(s)
c.Exec.Command = []string{s}
return nil
}), "exec", "")

Expand Down
2 changes: 1 addition & 1 deletion cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ func TestCLI_ParseFlags(t *testing.T) {
&config.Config{
Exec: &config.ExecConfig{
Enabled: config.Bool(true),
Command: config.String("command"),
Command: []string{"command"},
},
},
false,
Expand Down
12 changes: 6 additions & 6 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ func TestParse(t *testing.T) {
}`,
&Config{
Exec: &ExecConfig{
Command: String("command"),
Command: []string{"command"},
},
},
false,
Expand Down Expand Up @@ -726,7 +726,7 @@ func TestParse(t *testing.T) {
&Config{
Templates: &TemplateConfigs{
&TemplateConfig{
Command: String("command"),
Command: []string{"command"},
},
},
},
Expand Down Expand Up @@ -813,7 +813,7 @@ func TestParse(t *testing.T) {
Templates: &TemplateConfigs{
&TemplateConfig{
Exec: &ExecConfig{
Command: String("command"),
Command: []string{"command"},
},
},
},
Expand Down Expand Up @@ -1855,17 +1855,17 @@ func TestConfig_Merge(t *testing.T) {
"exec",
&Config{
Exec: &ExecConfig{
Command: String("command"),
Command: []string{"command"},
},
},
&Config{
Exec: &ExecConfig{
Command: String("command-diff"),
Command: []string{"command-diff"},
},
},
&Config{
Exec: &ExecConfig{
Command: String("command-diff"),
Command: []string{"command-diff"},
},
},
},
Expand Down
15 changes: 11 additions & 4 deletions config/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var (
// exec/supervise mode.
type ExecConfig struct {
// Command is the command to execute and watch as a child process.
Command *string `mapstructure:"command"`
Command commandList `mapstructure:"command"`

// Enabled controls if this exec is enabled.
Enabled *bool `mapstructure:"enabled"`
Expand Down Expand Up @@ -61,6 +61,13 @@ type ExecConfig struct {
Timeout *time.Duration `mapstructure:"timeout"`
}

// commandList is a []string with a common method for testing for content
type commandList []string

func (c commandList) Empty() bool {
return len(c) == 0 || c[0] == ""
}

// DefaultExecConfig returns a configuration that is populated with the
// default values.
func DefaultExecConfig() *ExecConfig {
Expand Down Expand Up @@ -154,11 +161,11 @@ func (c *ExecConfig) Merge(o *ExecConfig) *ExecConfig {
// Finalize ensures there no nil pointers.
func (c *ExecConfig) Finalize() {
if c.Enabled == nil {
c.Enabled = Bool(StringPresent(c.Command))
c.Enabled = Bool(!c.Command.Empty())
}

if c.Command == nil {
c.Command = String("")
c.Command = []string{}
}

if c.Env == nil {
Expand Down Expand Up @@ -203,7 +210,7 @@ func (c *ExecConfig) GoString() string {
"Splay:%s, "+
"Timeout:%s"+
"}",
StringGoString(c.Command),
c.Command,
BoolGoString(c.Enabled),
c.Env,
SignalGoString(c.KillSignal),
Expand Down
74 changes: 60 additions & 14 deletions config/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestExecConfig_Copy(t *testing.T) {
{
"copy",
&ExecConfig{
Command: String("command"),
Command: []string{"command"},
Enabled: Bool(true),
Env: &EnvConfig{Pristine: Bool(true)},
KillSignal: Signal(syscall.SIGINT),
Expand Down Expand Up @@ -81,27 +81,27 @@ func TestExecConfig_Merge(t *testing.T) {
},
{
"command_overrides",
&ExecConfig{Command: String("command")},
&ExecConfig{Command: String("")},
&ExecConfig{Command: String("")},
&ExecConfig{Command: []string{"command"}},
&ExecConfig{Command: []string{}},
&ExecConfig{Command: []string{}},
},
{
"command_empty_one",
&ExecConfig{Command: String("command")},
&ExecConfig{Command: []string{"command"}},
&ExecConfig{},
&ExecConfig{Command: String("command")},
&ExecConfig{Command: []string{"command"}},
},
{
"command_empty_two",
&ExecConfig{},
&ExecConfig{Command: String("command")},
&ExecConfig{Command: String("command")},
&ExecConfig{Command: []string{"command"}},
&ExecConfig{Command: []string{"command"}},
},
{
"command_same",
&ExecConfig{Command: String("command")},
&ExecConfig{Command: String("command")},
&ExecConfig{Command: String("command")},
&ExecConfig{Command: []string{"command"}},
&ExecConfig{Command: []string{"command"}},
&ExecConfig{Command: []string{"command"}},
},
{
"enabled_overrides",
Expand Down Expand Up @@ -294,7 +294,7 @@ func TestExecConfig_Finalize(t *testing.T) {
"empty",
&ExecConfig{},
&ExecConfig{
Command: String(""),
Command: []string{},
Enabled: Bool(false),
Env: &EnvConfig{
Allowlist: []string{},
Expand All @@ -314,10 +314,56 @@ func TestExecConfig_Finalize(t *testing.T) {
{
"with_command",
&ExecConfig{
Command: String("command"),
Command: []string{"command"},
},
&ExecConfig{
Command: String("command"),
Command: []string{"command"},
Enabled: Bool(true),
Env: &EnvConfig{
Denylist: []string{},
DenylistDeprecated: []string{},
Custom: []string{},
Pristine: Bool(false),
Allowlist: []string{},
AllowlistDeprecated: []string{},
},
KillSignal: Signal(DefaultExecKillSignal),
KillTimeout: TimeDuration(DefaultExecKillTimeout),
ReloadSignal: Signal(DefaultExecReloadSignal),
Splay: TimeDuration(0 * time.Second),
Timeout: TimeDuration(DefaultExecTimeout),
},
},
{
"with_command_list",
&ExecConfig{
Command: []string{"command", "argument1", "argument2"},
},
&ExecConfig{
Command: []string{"command", "argument1", "argument2"},
Enabled: Bool(true),
Env: &EnvConfig{
Denylist: []string{},
DenylistDeprecated: []string{},
Custom: []string{},
Pristine: Bool(false),
Allowlist: []string{},
AllowlistDeprecated: []string{},
},
KillSignal: Signal(DefaultExecKillSignal),
KillTimeout: TimeDuration(DefaultExecKillTimeout),
ReloadSignal: Signal(DefaultExecReloadSignal),
Splay: TimeDuration(0 * time.Second),
Timeout: TimeDuration(DefaultExecTimeout),
},
},
{
"with_shell_command",
&ExecConfig{
Command: []string{"command | pipe && command"},
},
&ExecConfig{
Command: []string{"command | pipe && command"},
Enabled: Bool(true),
Env: &EnvConfig{
Denylist: []string{},
Expand Down
19 changes: 12 additions & 7 deletions config/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type TemplateConfig struct {

// Command is the arbitrary command to execute after a template has
// successfully rendered. This is DEPRECATED. Use Exec instead.
Command *string `mapstructure:"command"`
Command commandList `mapstructure:"command"`

// CommandTimeout is the amount of time to wait for the command to finish
// before force-killing it. This is DEPRECATED. Use Exec instead.
Expand Down Expand Up @@ -268,7 +268,7 @@ func (c *TemplateConfig) Finalize() {
}

if c.Command == nil {
c.Command = String("")
c.Command = []string{}
}

if c.CommandTimeout == nil {
Expand Down Expand Up @@ -303,8 +303,12 @@ func (c *TemplateConfig) Finalize() {
if c.Exec.Command == nil && c.Command != nil {
c.Exec.Command = c.Command
}
if c.Exec.Timeout == nil && c.CommandTimeout != nil {
// backwards compat with command_timeout and default support for exec.timeout
switch {
case c.Exec.Timeout == nil && c.CommandTimeout != nil:
c.Exec.Timeout = c.CommandTimeout
case c.Exec.Timeout == nil:
c.Exec.Timeout = TimeDuration(DefaultTemplateCommandTimeout)
}
c.Exec.Finalize()

Expand Down Expand Up @@ -366,7 +370,7 @@ func (c *TemplateConfig) GoString() string {
"SandboxPath:%s"+
"}",
BoolGoString(c.Backup),
StringGoString(c.Command),
c.Command,
TimeDurationGoString(c.CommandTimeout),
StringGoString(c.Contents),
BoolGoString(c.CreateDestDirs),
Expand Down Expand Up @@ -492,20 +496,21 @@ func ParseTemplateConfig(s string) (*TemplateConfig, error) {
command = strings.Join(parts[2:], ":")
}

var sourcePtr, destinationPtr, commandPtr *string
var sourcePtr, destinationPtr *string
var commandL commandList
if source != "" {
sourcePtr = String(source)
}
if destination != "" {
destinationPtr = String(destination)
}
if command != "" {
commandPtr = String(command)
commandL = []string{command}
}

return &TemplateConfig{
Source: sourcePtr,
Destination: destinationPtr,
Command: commandPtr,
Command: commandL,
}, nil
}
Loading

0 comments on commit be5fcd8

Please sign in to comment.