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

no shell call on single command exec on *nix #1509

Merged
merged 2 commits into from
Sep 20, 2021
Merged
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
41 changes: 26 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,21 +234,32 @@ users the ability to further customize their command script.

#### Multiple Commands

The command configured for running on template rendering must be a single
command. That is you cannot join multiple commands with `&&`, `;`, `|`, etc.
This is a restriction of how they are executed. **However** you are able to do
this by combining the multiple commands in an explicit shell command using `sh
-c`. This is probably best explained by example.

Say you have a couple scripts you need to run when a template is rendered,
`/opt/foo` and `/opt/bar`, and you only want `/opt/bar` to run if `/opt/foo` is
successful. You can do that with the command...

`command = "sh -c '/opt/foo && /opt/bar'"`

As this is a full shell command you can even use conditionals. So accomplishes the same thing.

`command = "sh -c 'if /opt/foo; then /opt/bar ; fi'"`
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..

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

`command = "if /opt/foo ; then /opt/bar ; fi"`

Using this method you can run as many shell commands as you need with whatever
logic you need. Though it is suggested that if it gets too long you might want
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ template {
# command will only run if the resulting template changes. The command must
# return within 30s (configurable), and it must have a successful exit code.
# Consul Template is not a replacement for a process monitor or init system.
# Please see the [Command](#command) section below for more.
# Please see the Commands section in the README for more.
command = "restart service foo"

# This is the maximum amount of time to wait for the optional command to
Expand Down
43 changes: 9 additions & 34 deletions docs/modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,40 +105,15 @@ Consul Template will proxy any signals it receives to the child process. This
enables a scheduler to control the lifecycle of the process and also eases the
friction of running inside a container.

A common point of confusion is that the command string behaves the same as the
shell; it does not. In the shell, when you run `foo | bar` or `foo > bar`, that
is actually running as a subprocess of your shell (bash, zsh, csh, etc.). When
Consul Template spawns the exec process, it runs outside of your shell. This
behavior is _different_ from when Consul Template executes the template-specific
reload command. If you want the ability to pipe or redirect in the exec command,
you will need to spawn the process in subshell, for example:

```hcl
exec {
command = "/bin/bash -c 'my-server > /var/log/my-server.log'"
}
```

Note that when spawning like this, most shells do not proxy signals to their
child by default, so your child process will not receive the signals that Consul
Template sends to the shell. You can avoid this by writing a tiny shell wrapper
and executing that instead:

```bash
#!/usr/bin/env bash
trap "kill -TERM $child" SIGTERM

/bin/my-server -config /tmp/server.conf
child=$!
wait "$child"
```

Alternatively, you can use your shell's exec function directly, if it exists:

```bash
#!/usr/bin/env bash
exec /bin/my-server -config /tmp/server.conf > /var/log/my-server.log
```
The same rules that apply to the [commands](../README.md#commands) apply here,
that is if you want to use a complex, shell-like command you need to be running
on a system with `sh` on your PATH. These commands are run using `sh -c` with
the shell handling all shell parsing. Otherwise you want the command to be a
single word (no spaces) found on the search PATH or using an absolute path.

Note that on supporing systems (*nix, with `sh`) the
[`setpgid`](https://man7.org/linux/man-pages/man2/setpgid.2.html) flag is set
on the exectution which ensures all signals are sent to all processes.

There are some additional caveats with Exec Mode, which should be considered
carefully before use:
Expand Down
26 changes: 24 additions & 2 deletions manager/command-prep.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,33 @@

package manager

import (
"os/exec"
"strings"
)

func prepCommand(command string) ([]string, error) {
if len(command) == 0 {
switch len(strings.Fields(command)) {
case 0:
return []string{}, nil
case 1:
return []string{command}, nil
}

// default to 'sh' on path, else try a couple common absolute paths
shell := "sh"
if _, err := exec.LookPath(shell); err != nil {
for _, sh := range []string{"/bin/sh", "/usr/bin/sh"} {
if sh, err := exec.LookPath(sh); err == nil {
shell = sh
break
}
}
}
if shell == "" {
return []string{}, exec.ErrNotFound
}

cmd := []string{"sh", "-c", command}
cmd := []string{shell, "-c", command}
return cmd, nil
}
10 changes: 5 additions & 5 deletions manager/command-prep_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
)

func prepCommand(command string) ([]string, error) {
switch {
case len(command) == 0:
switch len(strings.Fields(command)) {
case 0:
return []string{}, nil
case len(strings.Fields(command)) > 1:
return []string{}, fmt.Errorf("only single commands supported on windows")
case 1:
return []string{command}, nil
}
return []string{command}, nil
return []string{}, fmt.Errorf("only single commands supported on windows")
}
20 changes: 19 additions & 1 deletion manager/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -1118,7 +1120,7 @@ func TestRunner_command(t *testing.T) {
{
name: "single",
input: "echo",
parsed: []string{"sh", "-c", "echo"},
parsed: []string{"echo"},
out: "\n",
},
{
Expand Down Expand Up @@ -1209,3 +1211,19 @@ func TestRunner_command(t *testing.T) {
})
}
}

func TestRunner_commandPath(t *testing.T) {
PATH := os.Getenv("PATH")
defer os.Setenv("PATH", PATH)
os.Setenv("PATH", "")
cmd, err := prepCommand("echo hi")
if err != nil && err != exec.ErrNotFound {
t.Fatal(err)
}
if len(cmd) != 3 {
t.Fatalf("unexpected command: %#v\n", cmd)
}
if filepath.Base(cmd[0]) != "sh" {
t.Fatalf("unexpected shell: %#v\n", cmd)
}
}