Clean up subprocess handling and make shell use optional (#3509)

* Clean up handling of subprocesses and make using a shell optional

* Update docs for subprocess changes

* Fix tests for new subprocess behavior

* More cleanup of subprocesses

* Minor adjustments and cleanup for subprocess logic

* Makes the watch handler reload test use the new path.

* Adds check tests for new args path, and updates existing tests to use new path.

* Adds support for script args in Docker checks.

* Fixes the sanitize unit test.

* Adds panic for unknown watch type, and reverts back to Run().

* Adds shell option back to consul lock command.

* Adds shell option back to consul exec command.

* Adds shell back into consul watch command.

* Refactors signal forwarding and makes Windows-friendly.

* Adds a clarifying comment.

* Changes error wording to a warning.

* Scopes signals to interrupt and kill.

This avoids us trying to send SIGCHILD to the dead process.

* Adds an error for shell=false for consul exec.

* Adds notes about the deprecated script and handler fields.

* De-nests an if statement.
This commit is contained in:
Kyle Havlovitz 2017-10-04 16:48:00 -07:00 committed by James Phillips
parent a61929a597
commit 198ed6076d
25 changed files with 472 additions and 167 deletions

View File

@ -535,16 +535,40 @@ func (a *Agent) reloadWatches(cfg *config.RuntimeConfig) error {
var watchPlans []*watch.Plan var watchPlans []*watch.Plan
for _, params := range cfg.Watches { for _, params := range cfg.Watches {
// Parse the watches, excluding the handler // Parse the watches, excluding the handler
wp, err := watch.ParseExempt(params, []string{"handler"}) wp, err := watch.ParseExempt(params, []string{"handler", "args"})
if err != nil { if err != nil {
return fmt.Errorf("Failed to parse watch (%#v): %v", params, err) return fmt.Errorf("Failed to parse watch (%#v): %v", params, err)
} }
// Get the handler // Get the handler and subprocess arguments
h := wp.Exempt["handler"] handler, hasHandler := wp.Exempt["handler"]
if _, ok := h.(string); h == nil || !ok { args, hasArgs := wp.Exempt["args"]
if hasHandler {
a.logger.Printf("[WARN] agent: The 'handler' field in watches has been deprecated " +
"and replaced with the 'args' field. See https://www.consul.io/docs/agent/watches.html")
}
if _, ok := handler.(string); hasHandler && !ok {
return fmt.Errorf("Watch handler must be a string") return fmt.Errorf("Watch handler must be a string")
} }
if raw, ok := args.([]interface{}); hasArgs && ok {
var parsed []string
for _, arg := range raw {
if v, ok := arg.(string); !ok {
return fmt.Errorf("Watch args must be a list of strings")
} else {
parsed = append(parsed, v)
}
}
wp.Exempt["args"] = parsed
} else if hasArgs && !ok {
return fmt.Errorf("Watch args must be a list of strings")
}
if hasHandler && hasArgs {
return fmt.Errorf("Cannot define both watch handler and args")
}
if !hasHandler && !hasArgs {
return fmt.Errorf("Must define either watch handler or args")
}
// Store the watch plan // Store the watch plan
watchPlans = append(watchPlans, wp) watchPlans = append(watchPlans, wp)
@ -566,7 +590,13 @@ func (a *Agent) reloadWatches(cfg *config.RuntimeConfig) error {
for _, wp := range watchPlans { for _, wp := range watchPlans {
a.watchPlans = append(a.watchPlans, wp) a.watchPlans = append(a.watchPlans, wp)
go func(wp *watch.Plan) { go func(wp *watch.Plan) {
wp.Handler = makeWatchHandler(a.LogOutput, wp.Exempt["handler"]) var handler interface{}
if h, ok := wp.Exempt["handler"]; ok {
handler = h
} else {
handler = wp.Exempt["args"]
}
wp.Handler = makeWatchHandler(a.LogOutput, handler)
wp.LogOutput = a.LogOutput wp.LogOutput = a.LogOutput
if err := wp.Run(addr); err != nil { if err := wp.Run(addr); err != nil {
a.logger.Printf("[ERR] Failed to run watch: %v", err) a.logger.Printf("[ERR] Failed to run watch: %v", err)
@ -1684,6 +1714,7 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *structs.CheckType,
DockerContainerID: chkType.DockerContainerID, DockerContainerID: chkType.DockerContainerID,
Shell: chkType.Shell, Shell: chkType.Shell,
Script: chkType.Script, Script: chkType.Script,
ScriptArgs: chkType.ScriptArgs,
Interval: chkType.Interval, Interval: chkType.Interval,
Logger: a.logger, Logger: a.logger,
client: a.dockerClient, client: a.dockerClient,
@ -1697,15 +1728,21 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *structs.CheckType,
delete(a.checkMonitors, check.CheckID) delete(a.checkMonitors, check.CheckID)
} }
if chkType.Interval < MinInterval { if chkType.Interval < MinInterval {
a.logger.Println(fmt.Sprintf("[WARN] agent: check '%s' has interval below minimum of %v", a.logger.Printf("[WARN] agent: check '%s' has interval below minimum of %v",
check.CheckID, MinInterval)) check.CheckID, MinInterval)
chkType.Interval = MinInterval chkType.Interval = MinInterval
} }
if chkType.Script != "" {
a.logger.Printf("[WARN] agent: check %q has the 'script' field, which has been deprecated "+
"and replaced with the 'args' field. See https://www.consul.io/docs/agent/checks.html",
check.CheckID)
}
monitor := &CheckMonitor{ monitor := &CheckMonitor{
Notify: a.state, Notify: a.state,
CheckID: check.CheckID, CheckID: check.CheckID,
Script: chkType.Script, Script: chkType.Script,
ScriptArgs: chkType.ScriptArgs,
Interval: chkType.Interval, Interval: chkType.Interval,
Timeout: chkType.Timeout, Timeout: chkType.Timeout,
Logger: a.logger, Logger: a.logger,

View File

@ -1930,7 +1930,7 @@ func TestAgent_reloadWatches(t *testing.T) {
{ {
"type": "key", "type": "key",
"key": "asdf", "key": "asdf",
"handler": "ls", "args": []interface{}{"ls"},
}, },
} }
if err := a.reloadWatches(&newConf); err != nil { if err := a.reloadWatches(&newConf); err != nil {
@ -1944,7 +1944,7 @@ func TestAgent_reloadWatches(t *testing.T) {
{ {
"type": "key", "type": "key",
"key": "asdf", "key": "asdf",
"handler": "ls", "args": []interface{}{"ls"},
}, },
} }
if err := a.reloadWatches(&newConf); err != nil { if err := a.reloadWatches(&newConf); err != nil {
@ -1957,7 +1957,7 @@ func TestAgent_reloadWatches(t *testing.T) {
{ {
"type": "key", "type": "key",
"key": "asdf", "key": "asdf",
"handler": "ls", "args": []interface{}{"ls"},
}, },
} }
if err := a.reloadWatches(&newConf); err == nil || !strings.Contains(err.Error(), "watch plans require an HTTP or HTTPS endpoint") { if err := a.reloadWatches(&newConf); err == nil || !strings.Contains(err.Error(), "watch plans require an HTTP or HTTPS endpoint") {

View File

@ -52,6 +52,7 @@ type CheckMonitor struct {
Notify CheckNotifier Notify CheckNotifier
CheckID types.CheckID CheckID types.CheckID
Script string Script string
ScriptArgs []string
Interval time.Duration Interval time.Duration
Timeout time.Duration Timeout time.Duration
Logger *log.Logger Logger *log.Logger
@ -101,7 +102,13 @@ func (c *CheckMonitor) run() {
// check is invoked periodically to perform the script check // check is invoked periodically to perform the script check
func (c *CheckMonitor) check() { func (c *CheckMonitor) check() {
// Create the command // Create the command
cmd, err := ExecScript(c.Script) var cmd *exec.Cmd
var err error
if len(c.ScriptArgs) > 0 {
cmd, err = ExecSubprocess(c.ScriptArgs)
} else {
cmd, err = ExecScript(c.Script)
}
if err != nil { if err != nil {
c.Logger.Printf("[ERR] agent: failed to setup invoke '%s': %s", c.Script, err) c.Logger.Printf("[ERR] agent: failed to setup invoke '%s': %s", c.Script, err)
c.Notify.UpdateCheck(c.CheckID, api.HealthCritical, err.Error()) c.Notify.UpdateCheck(c.CheckID, api.HealthCritical, err.Error())
@ -524,6 +531,7 @@ type CheckDocker struct {
Notify CheckNotifier Notify CheckNotifier
CheckID types.CheckID CheckID types.CheckID
Script string Script string
ScriptArgs []string
DockerContainerID string DockerContainerID string
Shell string Shell string
Interval time.Duration Interval time.Duration
@ -599,7 +607,13 @@ func (c *CheckDocker) check() {
} }
func (c *CheckDocker) doCheck() (string, *circbuf.Buffer, error) { func (c *CheckDocker) doCheck() (string, *circbuf.Buffer, error) {
cmd := []string{c.Shell, "-c", c.Script} var cmd []string
if len(c.ScriptArgs) > 0 {
cmd = c.ScriptArgs
} else {
cmd = []string{c.Shell, "-c", c.Script}
}
execID, err := c.client.CreateExec(c.DockerContainerID, cmd) execID, err := c.client.CreateExec(c.DockerContainerID, cmd)
if err != nil { if err != nil {
return api.HealthCritical, nil, err return api.HealthCritical, nil, err

View File

@ -20,7 +20,7 @@ import (
"github.com/hashicorp/consul/types" "github.com/hashicorp/consul/types"
) )
func TestCheckMonitor(t *testing.T) { func TestCheckMonitor_Script(t *testing.T) {
tests := []struct { tests := []struct {
script, status string script, status string
}{ }{
@ -54,13 +54,48 @@ func TestCheckMonitor(t *testing.T) {
} }
} }
func TestCheckMonitor_Args(t *testing.T) {
tests := []struct {
args []string
status string
}{
{[]string{"sh", "-c", "exit 0"}, "passing"},
{[]string{"sh", "-c", "exit 1"}, "warning"},
{[]string{"sh", "-c", "exit 2"}, "critical"},
{[]string{"foobarbaz"}, "critical"},
}
for _, tt := range tests {
t.Run(tt.status, func(t *testing.T) {
notif := mock.NewNotify()
check := &CheckMonitor{
Notify: notif,
CheckID: types.CheckID("foo"),
ScriptArgs: tt.args,
Interval: 25 * time.Millisecond,
Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags),
}
check.Start()
defer check.Stop()
retry.Run(t, func(r *retry.R) {
if got, want := notif.Updates("foo"), 2; got < want {
r.Fatalf("got %d updates want at least %d", got, want)
}
if got, want := notif.State("foo"), tt.status; got != want {
r.Fatalf("got state %q want %q", got, want)
}
})
})
}
}
func TestCheckMonitor_Timeout(t *testing.T) { func TestCheckMonitor_Timeout(t *testing.T) {
// t.Parallel() // timing test. no parallel // t.Parallel() // timing test. no parallel
notif := mock.NewNotify() notif := mock.NewNotify()
check := &CheckMonitor{ check := &CheckMonitor{
Notify: notif, Notify: notif,
CheckID: types.CheckID("foo"), CheckID: types.CheckID("foo"),
Script: "sleep 1 && exit 0", ScriptArgs: []string{"sh", "-c", "sleep 1 && exit 0"},
Interval: 50 * time.Millisecond, Interval: 50 * time.Millisecond,
Timeout: 25 * time.Millisecond, Timeout: 25 * time.Millisecond,
Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags),
@ -85,7 +120,7 @@ func TestCheckMonitor_RandomStagger(t *testing.T) {
check := &CheckMonitor{ check := &CheckMonitor{
Notify: notif, Notify: notif,
CheckID: types.CheckID("foo"), CheckID: types.CheckID("foo"),
Script: "exit 0", ScriptArgs: []string{"sh", "-c", "exit 0"},
Interval: 25 * time.Millisecond, Interval: 25 * time.Millisecond,
Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags),
} }
@ -110,7 +145,7 @@ func TestCheckMonitor_LimitOutput(t *testing.T) {
check := &CheckMonitor{ check := &CheckMonitor{
Notify: notif, Notify: notif,
CheckID: types.CheckID("foo"), CheckID: types.CheckID("foo"),
Script: "od -N 81920 /dev/urandom", ScriptArgs: []string{"od", "-N", "81920", "/dev/urandom"},
Interval: 25 * time.Millisecond, Interval: 25 * time.Millisecond,
Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags), Logger: log.New(ioutil.Discard, UniqueID(), log.LstdFlags),
} }
@ -775,7 +810,7 @@ func TestCheck_Docker(t *testing.T) {
check := &CheckDocker{ check := &CheckDocker{
Notify: notif, Notify: notif,
CheckID: id, CheckID: id,
Script: "/health.sh", ScriptArgs: []string{"/health.sh"},
DockerContainerID: "123", DockerContainerID: "123",
Interval: 25 * time.Millisecond, Interval: 25 * time.Millisecond,
client: c, client: c,

View File

@ -871,6 +871,7 @@ func (b *Builder) checkVal(v *CheckDefinition) *structs.CheckDefinition {
Token: b.stringVal(v.Token), Token: b.stringVal(v.Token),
Status: b.stringVal(v.Status), Status: b.stringVal(v.Status),
Script: b.stringVal(v.Script), Script: b.stringVal(v.Script),
ScriptArgs: v.ScriptArgs,
HTTP: b.stringVal(v.HTTP), HTTP: b.stringVal(v.HTTP),
Header: v.Header, Header: v.Header,
Method: b.stringVal(v.Method), Method: b.stringVal(v.Method),

View File

@ -312,6 +312,7 @@ type CheckDefinition struct {
Token *string `json:"token,omitempty" hcl:"token" mapstructure:"token"` Token *string `json:"token,omitempty" hcl:"token" mapstructure:"token"`
Status *string `json:"status,omitempty" hcl:"status" mapstructure:"status"` Status *string `json:"status,omitempty" hcl:"status" mapstructure:"status"`
Script *string `json:"script,omitempty" hcl:"script" mapstructure:"script"` Script *string `json:"script,omitempty" hcl:"script" mapstructure:"script"`
ScriptArgs []string `json:"args,omitempty" hcl:"args" mapstructure:"args"`
HTTP *string `json:"http,omitempty" hcl:"http" mapstructure:"http"` HTTP *string `json:"http,omitempty" hcl:"http" mapstructure:"http"`
Header map[string][]string `json:"header,omitempty" hcl:"header" mapstructure:"header"` Header map[string][]string `json:"header,omitempty" hcl:"header" mapstructure:"header"`
Method *string `json:"method,omitempty" hcl:"method" mapstructure:"method"` Method *string `json:"method,omitempty" hcl:"method" mapstructure:"method"`

View File

@ -1967,6 +1967,7 @@ func TestFullConfig(t *testing.T) {
"token": "oo4BCTgJ", "token": "oo4BCTgJ",
"status": "qLykAl5u", "status": "qLykAl5u",
"script": "dhGfIF8n", "script": "dhGfIF8n",
"args": ["f3BemRjy", "e5zgpef7"],
"http": "29B93haH", "http": "29B93haH",
"header": { "header": {
"hBq0zn1q": [ "2a9o9ZKP", "vKwA5lR6" ], "hBq0zn1q": [ "2a9o9ZKP", "vKwA5lR6" ],
@ -1991,6 +1992,7 @@ func TestFullConfig(t *testing.T) {
"token": "toO59sh8", "token": "toO59sh8",
"status": "9RlWsXMV", "status": "9RlWsXMV",
"script": "8qbd8tWw", "script": "8qbd8tWw",
"args": ["4BAJttck", "4D2NPtTQ"],
"http": "dohLcyQ2", "http": "dohLcyQ2",
"header": { "header": {
"ZBfTin3L": [ "1sDbEqYG", "lJGASsWK" ], "ZBfTin3L": [ "1sDbEqYG", "lJGASsWK" ],
@ -2014,6 +2016,7 @@ func TestFullConfig(t *testing.T) {
"token": "a3nQzHuy", "token": "a3nQzHuy",
"status": "irj26nf3", "status": "irj26nf3",
"script": "FJsI1oXt", "script": "FJsI1oXt",
"args": ["9s526ogY", "gSlOHj1w"],
"http": "yzhgsQ7Y", "http": "yzhgsQ7Y",
"header": { "header": {
"zcqwA8dO": [ "qb1zx0DL", "sXCxPFsD" ], "zcqwA8dO": [ "qb1zx0DL", "sXCxPFsD" ],
@ -2139,6 +2142,7 @@ func TestFullConfig(t *testing.T) {
"status": "rCvn53TH", "status": "rCvn53TH",
"notes": "fti5lfF3", "notes": "fti5lfF3",
"script": "rtj34nfd", "script": "rtj34nfd",
"args": ["16WRUmwS", "QWk7j7ae"],
"http": "dl3Fgme3", "http": "dl3Fgme3",
"header": { "header": {
"rjm4DEd3": ["2m3m2Fls"], "rjm4DEd3": ["2m3m2Fls"],
@ -2161,6 +2165,7 @@ func TestFullConfig(t *testing.T) {
"notes": "yP5nKbW0", "notes": "yP5nKbW0",
"status": "7oLMEyfu", "status": "7oLMEyfu",
"script": "NlUQ3nTE", "script": "NlUQ3nTE",
"args": ["5wEZtZpv", "0Ihyk8cS"],
"http": "KyDjGY9H", "http": "KyDjGY9H",
"header": { "header": {
"gv5qefTz": [ "5Olo2pMG", "PvvKWQU5" ], "gv5qefTz": [ "5Olo2pMG", "PvvKWQU5" ],
@ -2182,6 +2187,7 @@ func TestFullConfig(t *testing.T) {
"notes": "SVqApqeM", "notes": "SVqApqeM",
"status": "XXkVoZXt", "status": "XXkVoZXt",
"script": "IXLZTM6E", "script": "IXLZTM6E",
"args": ["wD05Bvao", "rLYB7kQC"],
"http": "kyICZsn8", "http": "kyICZsn8",
"header": { "header": {
"4ebP5vL4": [ "G20SrL5Q", "DwPKlMbo" ], "4ebP5vL4": [ "G20SrL5Q", "DwPKlMbo" ],
@ -2214,6 +2220,7 @@ func TestFullConfig(t *testing.T) {
"status": "pDQKEhWL", "status": "pDQKEhWL",
"notes": "Yt8EDLev", "notes": "Yt8EDLev",
"script": "MDu7wjlD", "script": "MDu7wjlD",
"args": ["81EDZLPa", "bPY5X8xd"],
"http": "qzHYvmJO", "http": "qzHYvmJO",
"header": { "header": {
"UkpmZ3a3": ["2dfzXuxZ"], "UkpmZ3a3": ["2dfzXuxZ"],
@ -2245,6 +2252,7 @@ func TestFullConfig(t *testing.T) {
"notes": "CQy86DH0", "notes": "CQy86DH0",
"status": "P0SWDvrk", "status": "P0SWDvrk",
"script": "6BhLJ7R9", "script": "6BhLJ7R9",
"args": ["EXvkYIuG", "BATOyt6h"],
"http": "u97ByEiW", "http": "u97ByEiW",
"header": { "header": {
"MUlReo8L": [ "AUZG7wHG", "gsN0Dc2N" ], "MUlReo8L": [ "AUZG7wHG", "gsN0Dc2N" ],
@ -2266,6 +2274,7 @@ func TestFullConfig(t *testing.T) {
"notes": "jKChDOdl", "notes": "jKChDOdl",
"status": "5qFz6OZn", "status": "5qFz6OZn",
"script": "PbdxFZ3K", "script": "PbdxFZ3K",
"args": ["NMtYWlT9", "vj74JXsm"],
"http": "1LBDJhw4", "http": "1LBDJhw4",
"header": { "header": {
"cXPmnv1M": [ "imDqfaBx", "NFxZ1bQe" ], "cXPmnv1M": [ "imDqfaBx", "NFxZ1bQe" ],
@ -2338,6 +2347,11 @@ func TestFullConfig(t *testing.T) {
"datacenter": "GyE6jpeW", "datacenter": "GyE6jpeW",
"key": "j9lF1Tve", "key": "j9lF1Tve",
"handler": "90N7S4LN" "handler": "90N7S4LN"
}, {
"type": "keyprefix",
"datacenter": "fYrl3F5d",
"key": "sl3Dffu7",
"args": ["dltjDJ2a", "flEa7C2d"]
} }
] ]
}`, }`,
@ -2383,6 +2397,7 @@ func TestFullConfig(t *testing.T) {
token = "oo4BCTgJ" token = "oo4BCTgJ"
status = "qLykAl5u" status = "qLykAl5u"
script = "dhGfIF8n" script = "dhGfIF8n"
args = ["f3BemRjy", "e5zgpef7"]
http = "29B93haH" http = "29B93haH"
header = { header = {
hBq0zn1q = [ "2a9o9ZKP", "vKwA5lR6" ] hBq0zn1q = [ "2a9o9ZKP", "vKwA5lR6" ]
@ -2407,6 +2422,7 @@ func TestFullConfig(t *testing.T) {
token = "toO59sh8" token = "toO59sh8"
status = "9RlWsXMV" status = "9RlWsXMV"
script = "8qbd8tWw" script = "8qbd8tWw"
args = ["4BAJttck", "4D2NPtTQ"]
http = "dohLcyQ2" http = "dohLcyQ2"
header = { header = {
"ZBfTin3L" = [ "1sDbEqYG", "lJGASsWK" ] "ZBfTin3L" = [ "1sDbEqYG", "lJGASsWK" ]
@ -2430,6 +2446,7 @@ func TestFullConfig(t *testing.T) {
token = "a3nQzHuy" token = "a3nQzHuy"
status = "irj26nf3" status = "irj26nf3"
script = "FJsI1oXt" script = "FJsI1oXt"
args = ["9s526ogY", "gSlOHj1w"]
http = "yzhgsQ7Y" http = "yzhgsQ7Y"
header = { header = {
"zcqwA8dO" = [ "qb1zx0DL", "sXCxPFsD" ] "zcqwA8dO" = [ "qb1zx0DL", "sXCxPFsD" ]
@ -2555,6 +2572,7 @@ func TestFullConfig(t *testing.T) {
status = "rCvn53TH" status = "rCvn53TH"
notes = "fti5lfF3" notes = "fti5lfF3"
script = "rtj34nfd" script = "rtj34nfd"
args = ["16WRUmwS", "QWk7j7ae"]
http = "dl3Fgme3" http = "dl3Fgme3"
header = { header = {
rjm4DEd3 = [ "2m3m2Fls" ] rjm4DEd3 = [ "2m3m2Fls" ]
@ -2577,6 +2595,7 @@ func TestFullConfig(t *testing.T) {
notes = "yP5nKbW0" notes = "yP5nKbW0"
status = "7oLMEyfu" status = "7oLMEyfu"
script = "NlUQ3nTE" script = "NlUQ3nTE"
args = ["5wEZtZpv", "0Ihyk8cS"]
http = "KyDjGY9H" http = "KyDjGY9H"
header = { header = {
"gv5qefTz" = [ "5Olo2pMG", "PvvKWQU5" ] "gv5qefTz" = [ "5Olo2pMG", "PvvKWQU5" ]
@ -2598,6 +2617,7 @@ func TestFullConfig(t *testing.T) {
notes = "SVqApqeM" notes = "SVqApqeM"
status = "XXkVoZXt" status = "XXkVoZXt"
script = "IXLZTM6E" script = "IXLZTM6E"
args = ["wD05Bvao", "rLYB7kQC"]
http = "kyICZsn8" http = "kyICZsn8"
header = { header = {
"4ebP5vL4" = [ "G20SrL5Q", "DwPKlMbo" ] "4ebP5vL4" = [ "G20SrL5Q", "DwPKlMbo" ]
@ -2630,6 +2650,7 @@ func TestFullConfig(t *testing.T) {
status = "pDQKEhWL" status = "pDQKEhWL"
notes = "Yt8EDLev" notes = "Yt8EDLev"
script = "MDu7wjlD" script = "MDu7wjlD"
args = ["81EDZLPa", "bPY5X8xd"]
http = "qzHYvmJO" http = "qzHYvmJO"
header = { header = {
UkpmZ3a3 = [ "2dfzXuxZ" ] UkpmZ3a3 = [ "2dfzXuxZ" ]
@ -2661,6 +2682,7 @@ func TestFullConfig(t *testing.T) {
notes = "CQy86DH0" notes = "CQy86DH0"
status = "P0SWDvrk" status = "P0SWDvrk"
script = "6BhLJ7R9" script = "6BhLJ7R9"
args = ["EXvkYIuG", "BATOyt6h"]
http = "u97ByEiW" http = "u97ByEiW"
header = { header = {
"MUlReo8L" = [ "AUZG7wHG", "gsN0Dc2N" ] "MUlReo8L" = [ "AUZG7wHG", "gsN0Dc2N" ]
@ -2682,6 +2704,7 @@ func TestFullConfig(t *testing.T) {
notes = "jKChDOdl" notes = "jKChDOdl"
status = "5qFz6OZn" status = "5qFz6OZn"
script = "PbdxFZ3K" script = "PbdxFZ3K"
args = ["NMtYWlT9", "vj74JXsm"]
http = "1LBDJhw4" http = "1LBDJhw4"
header = { header = {
"cXPmnv1M" = [ "imDqfaBx", "NFxZ1bQe" ], "cXPmnv1M" = [ "imDqfaBx", "NFxZ1bQe" ],
@ -2753,6 +2776,11 @@ func TestFullConfig(t *testing.T) {
datacenter = "GyE6jpeW" datacenter = "GyE6jpeW"
key = "j9lF1Tve" key = "j9lF1Tve"
handler = "90N7S4LN" handler = "90N7S4LN"
}, {
type = "keyprefix"
datacenter = "fYrl3F5d"
key = "sl3Dffu7"
args = ["dltjDJ2a", "flEa7C2d"]
}] }]
`} `}
@ -2936,6 +2964,7 @@ func TestFullConfig(t *testing.T) {
Token: "toO59sh8", Token: "toO59sh8",
Status: "9RlWsXMV", Status: "9RlWsXMV",
Script: "8qbd8tWw", Script: "8qbd8tWw",
ScriptArgs: []string{"4BAJttck", "4D2NPtTQ"},
HTTP: "dohLcyQ2", HTTP: "dohLcyQ2",
Header: map[string][]string{ Header: map[string][]string{
"ZBfTin3L": []string{"1sDbEqYG", "lJGASsWK"}, "ZBfTin3L": []string{"1sDbEqYG", "lJGASsWK"},
@ -2959,6 +2988,7 @@ func TestFullConfig(t *testing.T) {
Token: "a3nQzHuy", Token: "a3nQzHuy",
Status: "irj26nf3", Status: "irj26nf3",
Script: "FJsI1oXt", Script: "FJsI1oXt",
ScriptArgs: []string{"9s526ogY", "gSlOHj1w"},
HTTP: "yzhgsQ7Y", HTTP: "yzhgsQ7Y",
Header: map[string][]string{ Header: map[string][]string{
"zcqwA8dO": []string{"qb1zx0DL", "sXCxPFsD"}, "zcqwA8dO": []string{"qb1zx0DL", "sXCxPFsD"},
@ -2982,6 +3012,7 @@ func TestFullConfig(t *testing.T) {
Token: "oo4BCTgJ", Token: "oo4BCTgJ",
Status: "qLykAl5u", Status: "qLykAl5u",
Script: "dhGfIF8n", Script: "dhGfIF8n",
ScriptArgs: []string{"f3BemRjy", "e5zgpef7"},
HTTP: "29B93haH", HTTP: "29B93haH",
Header: map[string][]string{ Header: map[string][]string{
"hBq0zn1q": {"2a9o9ZKP", "vKwA5lR6"}, "hBq0zn1q": {"2a9o9ZKP", "vKwA5lR6"},
@ -3095,6 +3126,7 @@ func TestFullConfig(t *testing.T) {
Status: "pDQKEhWL", Status: "pDQKEhWL",
Notes: "Yt8EDLev", Notes: "Yt8EDLev",
Script: "MDu7wjlD", Script: "MDu7wjlD",
ScriptArgs: []string{"81EDZLPa", "bPY5X8xd"},
HTTP: "qzHYvmJO", HTTP: "qzHYvmJO",
Header: map[string][]string{ Header: map[string][]string{
"UkpmZ3a3": {"2dfzXuxZ"}, "UkpmZ3a3": {"2dfzXuxZ"},
@ -3127,6 +3159,7 @@ func TestFullConfig(t *testing.T) {
Notes: "CQy86DH0", Notes: "CQy86DH0",
Status: "P0SWDvrk", Status: "P0SWDvrk",
Script: "6BhLJ7R9", Script: "6BhLJ7R9",
ScriptArgs: []string{"EXvkYIuG", "BATOyt6h"},
HTTP: "u97ByEiW", HTTP: "u97ByEiW",
Header: map[string][]string{ Header: map[string][]string{
"MUlReo8L": {"AUZG7wHG", "gsN0Dc2N"}, "MUlReo8L": {"AUZG7wHG", "gsN0Dc2N"},
@ -3148,6 +3181,7 @@ func TestFullConfig(t *testing.T) {
Notes: "jKChDOdl", Notes: "jKChDOdl",
Status: "5qFz6OZn", Status: "5qFz6OZn",
Script: "PbdxFZ3K", Script: "PbdxFZ3K",
ScriptArgs: []string{"NMtYWlT9", "vj74JXsm"},
HTTP: "1LBDJhw4", HTTP: "1LBDJhw4",
Header: map[string][]string{ Header: map[string][]string{
"cXPmnv1M": {"imDqfaBx", "NFxZ1bQe"}, "cXPmnv1M": {"imDqfaBx", "NFxZ1bQe"},
@ -3180,6 +3214,7 @@ func TestFullConfig(t *testing.T) {
Notes: "yP5nKbW0", Notes: "yP5nKbW0",
Status: "7oLMEyfu", Status: "7oLMEyfu",
Script: "NlUQ3nTE", Script: "NlUQ3nTE",
ScriptArgs: []string{"5wEZtZpv", "0Ihyk8cS"},
HTTP: "KyDjGY9H", HTTP: "KyDjGY9H",
Header: map[string][]string{ Header: map[string][]string{
"gv5qefTz": {"5Olo2pMG", "PvvKWQU5"}, "gv5qefTz": {"5Olo2pMG", "PvvKWQU5"},
@ -3201,6 +3236,7 @@ func TestFullConfig(t *testing.T) {
Notes: "SVqApqeM", Notes: "SVqApqeM",
Status: "XXkVoZXt", Status: "XXkVoZXt",
Script: "IXLZTM6E", Script: "IXLZTM6E",
ScriptArgs: []string{"wD05Bvao", "rLYB7kQC"},
HTTP: "kyICZsn8", HTTP: "kyICZsn8",
Header: map[string][]string{ Header: map[string][]string{
"4ebP5vL4": {"G20SrL5Q", "DwPKlMbo"}, "4ebP5vL4": {"G20SrL5Q", "DwPKlMbo"},
@ -3222,6 +3258,7 @@ func TestFullConfig(t *testing.T) {
Status: "rCvn53TH", Status: "rCvn53TH",
Notes: "fti5lfF3", Notes: "fti5lfF3",
Script: "rtj34nfd", Script: "rtj34nfd",
ScriptArgs: []string{"16WRUmwS", "QWk7j7ae"},
HTTP: "dl3Fgme3", HTTP: "dl3Fgme3",
Header: map[string][]string{ Header: map[string][]string{
"rjm4DEd3": {"2m3m2Fls"}, "rjm4DEd3": {"2m3m2Fls"},
@ -3297,6 +3334,12 @@ func TestFullConfig(t *testing.T) {
"key": "j9lF1Tve", "key": "j9lF1Tve",
"handler": "90N7S4LN", "handler": "90N7S4LN",
}, },
map[string]interface{}{
"type": "keyprefix",
"datacenter": "fYrl3F5d",
"key": "sl3Dffu7",
"args": []interface{}{"dltjDJ2a", "flEa7C2d"},
},
}, },
} }
@ -3632,6 +3675,7 @@ func TestSanitize(t *testing.T) {
"Name": "zoo", "Name": "zoo",
"Notes": "", "Notes": "",
"Script": "", "Script": "",
"ScriptArgs": [],
"ServiceID": "", "ServiceID": "",
"Shell": "", "Shell": "",
"Status": "", "Status": "",
@ -3755,6 +3799,7 @@ func TestSanitize(t *testing.T) {
"Name": "blurb", "Name": "blurb",
"Notes": "", "Notes": "",
"Script": "", "Script": "",
"ScriptArgs": [],
"Shell": "", "Shell": "",
"Status": "", "Status": "",
"TCP": "", "TCP": "",

View File

@ -49,6 +49,7 @@ type remoteExecEvent struct {
// It is stored in the KV store // It is stored in the KV store
type remoteExecSpec struct { type remoteExecSpec struct {
Command string Command string
Args []string
Script []byte Script []byte
Wait time.Duration Wait time.Duration
} }
@ -160,7 +161,13 @@ func (a *Agent) handleRemoteExec(msg *UserEvent) {
// Create the exec.Cmd // Create the exec.Cmd
a.logger.Printf("[INFO] agent: remote exec '%s'", script) a.logger.Printf("[INFO] agent: remote exec '%s'", script)
cmd, err := ExecScript(script) var cmd *exec.Cmd
var err error
if len(spec.Args) > 0 {
cmd, err = ExecSubprocess(spec.Args)
} else {
cmd, err = ExecScript(script)
}
if err != nil { if err != nil {
a.logger.Printf("[DEBUG] agent: failed to start remote exec: %v", err) a.logger.Printf("[DEBUG] agent: failed to start remote exec: %v", err)
exitCode = 255 exitCode = 255
@ -178,8 +185,7 @@ func (a *Agent) handleRemoteExec(msg *UserEvent) {
cmd.Stderr = writer cmd.Stderr = writer
// Start execution // Start execution
err = cmd.Start() if err := cmd.Start(); err != nil {
if err != nil {
a.logger.Printf("[DEBUG] agent: failed to start remote exec: %v", err) a.logger.Printf("[DEBUG] agent: failed to start remote exec: %v", err)
exitCode = 255 exitCode = 255
return return

View File

@ -22,6 +22,7 @@ type CheckDefinition struct {
// ID (CheckID), Name, Status, Notes // ID (CheckID), Name, Status, Notes
// //
Script string Script string
ScriptArgs []string
HTTP string HTTP string
Header map[string][]string Header map[string][]string
Method string Method string
@ -61,6 +62,7 @@ func (c *CheckDefinition) CheckType() *CheckType {
Notes: c.Notes, Notes: c.Notes,
Script: c.Script, Script: c.Script,
ScriptArgs: c.ScriptArgs,
HTTP: c.HTTP, HTTP: c.HTTP,
Header: c.Header, Header: c.Header,
Method: c.Method, Method: c.Method,

View File

@ -24,6 +24,7 @@ type CheckType struct {
// Update CheckDefinition when adding fields here // Update CheckDefinition when adding fields here
Script string Script string
ScriptArgs []string
HTTP string HTTP string
Header map[string][]string Header map[string][]string
Method string Method string
@ -49,7 +50,7 @@ func (c *CheckType) Valid() bool {
// IsScript checks if this is a check that execs some kind of script. // IsScript checks if this is a check that execs some kind of script.
func (c *CheckType) IsScript() bool { func (c *CheckType) IsScript() bool {
return c.Script != "" return c.Script != "" || len(c.ScriptArgs) > 0
} }
// IsTTL checks if this is a TTL type // IsTTL checks if this is a TTL type
@ -59,7 +60,7 @@ func (c *CheckType) IsTTL() bool {
// IsMonitor checks if this is a Monitor type // IsMonitor checks if this is a Monitor type
func (c *CheckType) IsMonitor() bool { func (c *CheckType) IsMonitor() bool {
return c.Script != "" && c.DockerContainerID == "" && c.Interval != 0 return c.IsScript() && c.DockerContainerID == "" && c.Interval != 0
} }
// IsHTTP checks if this is a HTTP type // IsHTTP checks if this is a HTTP type

View File

@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"math" "math"
"os" "os"
"os/exec"
"os/signal"
osuser "os/user" osuser "os/user"
"strconv" "strconv"
"time" "time"
@ -113,3 +115,34 @@ GROUP:
return nil return nil
} }
// ExecSubprocess returns a command to execute a subprocess directly.
func ExecSubprocess(args []string) (*exec.Cmd, error) {
if len(args) == 0 {
return nil, fmt.Errorf("need an executable to run")
}
return exec.Command(args[0], args[1:]...), nil
}
// ForwardSignals will fire up a goroutine to forward signals to the given
// subprocess until the shutdown channel is closed.
func ForwardSignals(cmd *exec.Cmd, logFn func(error), shutdownCh <-chan struct{}) {
go func() {
signalCh := make(chan os.Signal, 10)
signal.Notify(signalCh, os.Interrupt, os.Kill)
defer signal.Stop(signalCh)
for {
select {
case sig := <-signalCh:
if err := cmd.Process.Signal(sig); err != nil {
logFn(fmt.Errorf("failed to send signal %q: %v", sig, err))
}
case <-shutdownCh:
return
}
}
}()
}

View File

@ -7,7 +7,7 @@ import (
"os/exec" "os/exec"
) )
// ExecScript returns a command to execute a script // ExecScript returns a command to execute a script through a shell.
func ExecScript(script string) (*exec.Cmd, error) { func ExecScript(script string) (*exec.Cmd, error) {
shell := "/bin/sh" shell := "/bin/sh"
if other := os.Getenv("SHELL"); other != "" { if other := os.Getenv("SHELL"); other != "" {

View File

@ -9,7 +9,7 @@ import (
"syscall" "syscall"
) )
// ExecScript returns a command to execute a script // ExecScript returns a command to execute a script through a shell.
func ExecScript(script string) (*exec.Cmd, error) { func ExecScript(script string) (*exec.Cmd, error) {
shell := "cmd" shell := "cmd"
if other := os.Getenv("SHELL"); other != "" { if other := os.Getenv("SHELL"); other != "" {

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"log" "log"
"os" "os"
"os/exec"
"strconv" "strconv"
"github.com/armon/circbuf" "github.com/armon/circbuf"
@ -21,16 +22,36 @@ const (
) )
// makeWatchHandler returns a handler for the given watch // makeWatchHandler returns a handler for the given watch
func makeWatchHandler(logOutput io.Writer, params interface{}) watch.HandlerFunc { func makeWatchHandler(logOutput io.Writer, handler interface{}) watch.HandlerFunc {
script := params.(string) var args []string
var script string
// Figure out whether to run in shell or raw subprocess mode
switch h := handler.(type) {
case string:
script = h
case []string:
args = h
default:
panic(fmt.Errorf("unknown handler type %T", handler))
}
logger := log.New(logOutput, "", log.LstdFlags) logger := log.New(logOutput, "", log.LstdFlags)
fn := func(idx uint64, data interface{}) { fn := func(idx uint64, data interface{}) {
// Create the command // Create the command
cmd, err := ExecScript(script) var cmd *exec.Cmd
var err error
if len(args) > 0 {
cmd, err = ExecSubprocess(args)
} else {
cmd, err = ExecScript(script)
}
if err != nil { if err != nil {
logger.Printf("[ERR] agent: Failed to setup watch: %v", err) logger.Printf("[ERR] agent: Failed to setup watch: %v", err)
return return
} }
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
"CONSUL_INDEX="+strconv.FormatUint(idx, 10), "CONSUL_INDEX="+strconv.FormatUint(idx, 10),
) )
@ -44,14 +65,14 @@ func makeWatchHandler(logOutput io.Writer, params interface{}) watch.HandlerFunc
var inp bytes.Buffer var inp bytes.Buffer
enc := json.NewEncoder(&inp) enc := json.NewEncoder(&inp)
if err := enc.Encode(data); err != nil { if err := enc.Encode(data); err != nil {
logger.Printf("[ERR] agent: Failed to encode data for watch '%s': %v", script, err) logger.Printf("[ERR] agent: Failed to encode data for watch '%v': %v", handler, err)
return return
} }
cmd.Stdin = &inp cmd.Stdin = &inp
// Run the handler // Run the handler
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
logger.Printf("[ERR] agent: Failed to invoke watch handler '%s': %v", script, err) logger.Printf("[ERR] agent: Failed to run watch handler '%v': %v", handler, err)
} }
// Get the output, add a message about truncation // Get the output, add a message about truncation
@ -62,7 +83,7 @@ func makeWatchHandler(logOutput io.Writer, params interface{}) watch.HandlerFunc
} }
// Log the output // Log the output
logger.Printf("[DEBUG] agent: watch handler '%s' output: %s", script, outputStr) logger.Printf("[DEBUG] agent: watch handler '%v' output: %s", handler, outputStr)
} }
return fn return fn
} }

View File

@ -363,7 +363,7 @@ func (cmd *AgentCommand) run(args []string) int {
logGate.Flush() logGate.Flush()
// wait for signal // wait for signal
signalCh := make(chan os.Signal, 4) signalCh := make(chan os.Signal, 10)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE) signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE)

View File

@ -53,6 +53,7 @@ const (
// rExecConf is used to pass around configuration // rExecConf is used to pass around configuration
type rExecConf struct { type rExecConf struct {
prefix string prefix string
shell bool
foreignDC bool foreignDC bool
localDC string localDC string
@ -66,6 +67,7 @@ type rExecConf struct {
replWait time.Duration replWait time.Duration
cmd string cmd string
args []string
script []byte script []byte
verbose bool verbose bool
@ -83,6 +85,9 @@ type rExecSpec struct {
// Command is a single command to run directly in the shell // Command is a single command to run directly in the shell
Command string `json:",omitempty"` Command string `json:",omitempty"`
// Args is the list of arguments to run the subprocess directly
Args []string `json:",omitempty"`
// Script should be spilled to a file and executed // Script should be spilled to a file and executed
Script []byte `json:",omitempty"` Script []byte `json:",omitempty"`
@ -134,6 +139,8 @@ func (c *ExecCommand) Run(args []string) int {
"Regular expression to filter on service tags. Must be used with -service.") "Regular expression to filter on service tags. Must be used with -service.")
f.StringVar(&c.conf.prefix, "prefix", rExecPrefix, f.StringVar(&c.conf.prefix, "prefix", rExecPrefix,
"Prefix in the KV store to use for request data.") "Prefix in the KV store to use for request data.")
f.BoolVar(&c.conf.shell, "shell", true,
"Use a shell to run the command.")
f.DurationVar(&c.conf.wait, "wait", rExecQuietWait, f.DurationVar(&c.conf.wait, "wait", rExecQuietWait,
"Period to wait with no responses before terminating execution.") "Period to wait with no responses before terminating execution.")
f.DurationVar(&c.conf.replWait, "wait-repl", rExecReplicationWait, f.DurationVar(&c.conf.replWait, "wait-repl", rExecReplicationWait,
@ -151,6 +158,11 @@ func (c *ExecCommand) Run(args []string) int {
// If there is no command, read stdin for a script input // If there is no command, read stdin for a script input
if c.conf.cmd == "-" { if c.conf.cmd == "-" {
if !c.conf.shell {
c.UI.Error("Cannot configure -shell=false when reading from stdin")
return 1
}
c.conf.cmd = "" c.conf.cmd = ""
var buf bytes.Buffer var buf bytes.Buffer
_, err := io.Copy(&buf, os.Stdin) _, err := io.Copy(&buf, os.Stdin)
@ -161,10 +173,13 @@ func (c *ExecCommand) Run(args []string) int {
return 1 return 1
} }
c.conf.script = buf.Bytes() c.conf.script = buf.Bytes()
} else if !c.conf.shell {
c.conf.cmd = ""
c.conf.args = f.Args()
} }
// Ensure we have a command or script // Ensure we have a command or script
if c.conf.cmd == "" && len(c.conf.script) == 0 { if c.conf.cmd == "" && len(c.conf.script) == 0 && len(c.conf.args) == 0 {
c.UI.Error("Must specify a command to execute") c.UI.Error("Must specify a command to execute")
c.UI.Error("") c.UI.Error("")
c.UI.Error(c.Help()) c.UI.Error(c.Help())
@ -545,6 +560,7 @@ func (c *ExecCommand) destroySession() error {
func (c *ExecCommand) makeRExecSpec() ([]byte, error) { func (c *ExecCommand) makeRExecSpec() ([]byte, error) {
spec := &rExecSpec{ spec := &rExecSpec{
Command: c.conf.cmd, Command: c.conf.cmd,
Args: c.conf.args,
Script: c.conf.script, Script: c.conf.script,
Wait: c.conf.wait, Wait: c.conf.wait,
} }

View File

@ -34,7 +34,27 @@ func TestExecCommandRun(t *testing.T) {
defer a.Shutdown() defer a.Shutdown()
ui, c := testExecCommand(t) ui, c := testExecCommand(t)
args := []string{"-http-addr=" + a.HTTPAddr(), "-wait=500ms", "uptime"} args := []string{"-http-addr=" + a.HTTPAddr(), "-wait=1s", "uptime"}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. Error:%#v (std)Output:%#v", code, ui.ErrorWriter.String(), ui.OutputWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), "load") {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}
func TestExecCommandRun_NoShell(t *testing.T) {
t.Parallel()
a := agent.NewTestAgent(t.Name(), `
disable_remote_exec = false
`)
defer a.Shutdown()
ui, c := testExecCommand(t)
args := []string{"-http-addr=" + a.HTTPAddr(), "-shell=false", "-wait=1s", "uptime"}
code := c.Run(args) code := c.Run(args)
if code != 0 { if code != 0 {

View File

@ -3,6 +3,7 @@ package command
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path" "path"
"strings" "strings"
"sync" "sync"
@ -79,6 +80,7 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
var name string var name string
var passStdin bool var passStdin bool
var propagateChildCode bool var propagateChildCode bool
var shell bool
var timeout time.Duration var timeout time.Duration
f := c.BaseCommand.NewFlagSet(c) f := c.BaseCommand.NewFlagSet(c)
@ -101,6 +103,9 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
"is generated based on the provided child command.") "is generated based on the provided child command.")
f.BoolVar(&passStdin, "pass-stdin", false, f.BoolVar(&passStdin, "pass-stdin", false,
"Pass stdin to the child process.") "Pass stdin to the child process.")
f.BoolVar(&shell, "shell", true,
"Use a shell to run the command (can set a custom shell via the SHELL "+
"environment variable).")
f.DurationVar(&timeout, "timeout", 0, f.DurationVar(&timeout, "timeout", 0,
"Maximum amount of time to wait to acquire the lock, specified as a "+ "Maximum amount of time to wait to acquire the lock, specified as a "+
"duration like \"1s\" or \"3h\". The default value is 0.") "duration like \"1s\" or \"3h\". The default value is 0.")
@ -129,7 +134,6 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
} }
prefix := extra[0] prefix := extra[0]
prefix = strings.TrimPrefix(prefix, "/") prefix = strings.TrimPrefix(prefix, "/")
script := strings.Join(extra[1:], " ")
if timeout < 0 { if timeout < 0 {
c.UI.Error("Timeout must be positive") c.UI.Error("Timeout must be positive")
@ -138,7 +142,7 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
// Calculate a session name if none provided // Calculate a session name if none provided
if name == "" { if name == "" {
name = fmt.Sprintf("Consul lock for '%s' at '%s'", script, prefix) name = fmt.Sprintf("Consul lock for '%s' at '%s'", strings.Join(extra[1:], " "), prefix)
} }
// Calculate oneshot // Calculate oneshot
@ -200,7 +204,7 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
// Start the child process // Start the child process
childErr = make(chan error, 1) childErr = make(chan error, 1)
go func() { go func() {
childErr <- c.startChild(script, passStdin) childErr <- c.startChild(f.Args()[1:], passStdin, shell)
}() }()
// Monitor for shutdown, child termination, or lock loss // Monitor for shutdown, child termination, or lock loss
@ -335,12 +339,19 @@ func (c *LockCommand) setupSemaphore(client *api.Client, limit int, prefix, name
// startChild is a long running routine used to start and // startChild is a long running routine used to start and
// wait for the child process to exit. // wait for the child process to exit.
func (c *LockCommand) startChild(script string, passStdin bool) error { func (c *LockCommand) startChild(args []string, passStdin, shell bool) error {
if c.verbose { if c.verbose {
c.UI.Info(fmt.Sprintf("Starting handler '%s'", script)) c.UI.Info("Starting handler")
} }
// Create the command // Create the command
cmd, err := agent.ExecScript(script) var cmd *exec.Cmd
var err error
if !shell {
cmd, err = agent.ExecSubprocess(args)
} else {
cmd, err = agent.ExecScript(strings.Join(args, " "))
}
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error executing handler: %s", err)) c.UI.Error(fmt.Sprintf("Error executing handler: %s", err))
return err return err
@ -369,6 +380,14 @@ func (c *LockCommand) startChild(script string, passStdin bool) error {
return err return err
} }
// Set up signal forwarding.
doneCh := make(chan struct{})
defer close(doneCh)
logFn := func(err error) {
c.UI.Error(fmt.Sprintf("Warning, could not forward signal: %s", err))
}
agent.ForwardSignals(cmd, logFn, doneCh)
// Setup the child info // Setup the child info
c.child = cmd.Process c.child = cmd.Process
c.childLock.Unlock() c.childLock.Unlock()

View File

@ -1,7 +1,6 @@
package command package command
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"strings" "strings"
@ -53,8 +52,28 @@ func TestLockCommand_Run(t *testing.T) {
ui, c := testLockCommand(t) ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch") filePath := filepath.Join(a.Config.DataDir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath) args := []string{"-http-addr=" + a.HTTPAddr(), "test/prefix", "touch", filePath}
args := []string{"-http-addr=" + a.HTTPAddr(), "test/prefix", touchCmd}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
// Check for the file
_, err := ioutil.ReadFile(filePath)
if err != nil {
t.Fatalf("err: %v", err)
}
}
func TestLockCommand_Run_NoShell(t *testing.T) {
t.Parallel()
a := agent.NewTestAgent(t.Name(), ``)
defer a.Shutdown()
ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch")
args := []string{"-http-addr=" + a.HTTPAddr(), "-shell=false", "test/prefix", "touch", filePath}
code := c.Run(args) code := c.Run(args)
if code != 0 { if code != 0 {
@ -75,8 +94,7 @@ func TestLockCommand_Try_Lock(t *testing.T) {
ui, c := testLockCommand(t) ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch") filePath := filepath.Join(a.Config.DataDir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath) args := []string{"-http-addr=" + a.HTTPAddr(), "-try=10s", "test/prefix", "touch", filePath}
args := []string{"-http-addr=" + a.HTTPAddr(), "-try=10s", "test/prefix", touchCmd}
// Run the command. // Run the command.
var lu *LockUnlock var lu *LockUnlock
@ -106,8 +124,7 @@ func TestLockCommand_Try_Semaphore(t *testing.T) {
ui, c := testLockCommand(t) ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch") filePath := filepath.Join(a.Config.DataDir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath) args := []string{"-http-addr=" + a.HTTPAddr(), "-n=3", "-try=10s", "test/prefix", "touch", filePath}
args := []string{"-http-addr=" + a.HTTPAddr(), "-n=3", "-try=10s", "test/prefix", touchCmd}
// Run the command. // Run the command.
var lu *LockUnlock var lu *LockUnlock
@ -137,8 +154,7 @@ func TestLockCommand_MonitorRetry_Lock_Default(t *testing.T) {
ui, c := testLockCommand(t) ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch") filePath := filepath.Join(a.Config.DataDir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath) args := []string{"-http-addr=" + a.HTTPAddr(), "test/prefix", "touch", filePath}
args := []string{"-http-addr=" + a.HTTPAddr(), "test/prefix", touchCmd}
// Run the command. // Run the command.
var lu *LockUnlock var lu *LockUnlock
@ -169,8 +185,7 @@ func TestLockCommand_MonitorRetry_Semaphore_Default(t *testing.T) {
ui, c := testLockCommand(t) ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch") filePath := filepath.Join(a.Config.DataDir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath) args := []string{"-http-addr=" + a.HTTPAddr(), "-n=3", "test/prefix", "touch", filePath}
args := []string{"-http-addr=" + a.HTTPAddr(), "-n=3", "test/prefix", touchCmd}
// Run the command. // Run the command.
var lu *LockUnlock var lu *LockUnlock
@ -201,8 +216,7 @@ func TestLockCommand_MonitorRetry_Lock_Arg(t *testing.T) {
ui, c := testLockCommand(t) ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch") filePath := filepath.Join(a.Config.DataDir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath) args := []string{"-http-addr=" + a.HTTPAddr(), "-monitor-retry=9", "test/prefix", "touch", filePath}
args := []string{"-http-addr=" + a.HTTPAddr(), "-monitor-retry=9", "test/prefix", touchCmd}
// Run the command. // Run the command.
var lu *LockUnlock var lu *LockUnlock
@ -233,8 +247,7 @@ func TestLockCommand_MonitorRetry_Semaphore_Arg(t *testing.T) {
ui, c := testLockCommand(t) ui, c := testLockCommand(t)
filePath := filepath.Join(a.Config.DataDir, "test_touch") filePath := filepath.Join(a.Config.DataDir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath) args := []string{"-http-addr=" + a.HTTPAddr(), "-n=3", "-monitor-retry=9", "test/prefix", "touch", filePath}
args := []string{"-http-addr=" + a.HTTPAddr(), "-n=3", "-monitor-retry=9", "test/prefix", touchCmd}
// Run the command. // Run the command.
var lu *LockUnlock var lu *LockUnlock
@ -265,7 +278,7 @@ func TestLockCommand_ChildExitCode(t *testing.T) {
t.Run("clean exit", func(t *testing.T) { t.Run("clean exit", func(t *testing.T) {
_, c := testLockCommand(t) _, c := testLockCommand(t)
args := []string{"-http-addr=" + a.HTTPAddr(), "-child-exit-code", "test/prefix", "exit 0"} args := []string{"-http-addr=" + a.HTTPAddr(), "-child-exit-code", "test/prefix", "sh", "-c", "exit", "0"}
if got, want := c.Run(args), 0; got != want { if got, want := c.Run(args), 0; got != want {
t.Fatalf("got %d want %d", got, want) t.Fatalf("got %d want %d", got, want)
} }
@ -273,7 +286,7 @@ func TestLockCommand_ChildExitCode(t *testing.T) {
t.Run("error exit", func(t *testing.T) { t.Run("error exit", func(t *testing.T) {
_, c := testLockCommand(t) _, c := testLockCommand(t)
args := []string{"-http-addr=" + a.HTTPAddr(), "-child-exit-code", "test/prefix", "exit 1"} args := []string{"-http-addr=" + a.HTTPAddr(), "-child-exit-code", "test/prefix", "exit", "1"}
if got, want := c.Run(args), 2; got != want { if got, want := c.Run(args), 2; got != want {
t.Fatalf("got %d want %d", got, want) t.Fatalf("got %d want %d", got, want)
} }
@ -281,7 +294,7 @@ func TestLockCommand_ChildExitCode(t *testing.T) {
t.Run("not propagated", func(t *testing.T) { t.Run("not propagated", func(t *testing.T) {
_, c := testLockCommand(t) _, c := testLockCommand(t)
args := []string{"-http-addr=" + a.HTTPAddr(), "test/prefix", "exit 1"} args := []string{"-http-addr=" + a.HTTPAddr(), "test/prefix", "sh", "-c", "exit", "1"}
if got, want := c.Run(args), 0; got != want { if got, want := c.Run(args), 0; got != want {
t.Fatalf("got %d want %d", got, want) t.Fatalf("got %d want %d", got, want)
} }

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"strconv" "strconv"
"strings" "strings"
@ -37,6 +38,7 @@ Usage: consul watch [options] [child...]
func (c *WatchCommand) Run(args []string) int { func (c *WatchCommand) Run(args []string) int {
var watchType, key, prefix, service, tag, passingOnly, state, name string var watchType, key, prefix, service, tag, passingOnly, state, name string
var shell bool
f := c.BaseCommand.NewFlagSet(c) f := c.BaseCommand.NewFlagSet(c)
f.StringVar(&watchType, "type", "", f.StringVar(&watchType, "type", "",
@ -54,6 +56,9 @@ func (c *WatchCommand) Run(args []string) int {
f.StringVar(&passingOnly, "passingonly", "", f.StringVar(&passingOnly, "passingonly", "",
"Specifies if only hosts passing all checks are displayed. "+ "Specifies if only hosts passing all checks are displayed. "+
"Optional for 'service' type, must be one of `[true|false]`. Defaults false.") "Optional for 'service' type, must be one of `[true|false]`. Defaults false.")
f.BoolVar(&shell, "shell", true,
"Use a shell to run the command (can set a custom shell via the SHELL "+
"environment variable).")
f.StringVar(&state, "state", "", f.StringVar(&state, "state", "",
"Specifies the states to watch. Optional for 'checks' type.") "Specifies the states to watch. Optional for 'checks' type.")
f.StringVar(&name, "name", "", f.StringVar(&name, "name", "",
@ -71,9 +76,6 @@ func (c *WatchCommand) Run(args []string) int {
return 1 return 1
} }
// Grab the script to execute if any
script := strings.Join(f.Args(), " ")
// Compile the watch parameters // Compile the watch parameters
params := make(map[string]interface{}) params := make(map[string]interface{})
if watchType != "" { if watchType != "" {
@ -140,7 +142,7 @@ func (c *WatchCommand) Run(args []string) int {
// 0: false // 0: false
// 1: true // 1: true
errExit := 0 errExit := 0
if script == "" { if len(f.Args()) == 0 {
wp.Handler = func(idx uint64, data interface{}) { wp.Handler = func(idx uint64, data interface{}) {
defer wp.Stop() defer wp.Stop()
buf, err := json.MarshalIndent(data, "", " ") buf, err := json.MarshalIndent(data, "", " ")
@ -152,10 +154,21 @@ func (c *WatchCommand) Run(args []string) int {
} }
} else { } else {
wp.Handler = func(idx uint64, data interface{}) { wp.Handler = func(idx uint64, data interface{}) {
doneCh := make(chan struct{})
defer close(doneCh)
logFn := func(err error) {
c.UI.Error(fmt.Sprintf("Warning, could not forward signal: %s", err))
}
// Create the command // Create the command
var buf bytes.Buffer var buf bytes.Buffer
var err error var err error
cmd, err := agent.ExecScript(script) var cmd *exec.Cmd
if !shell {
cmd, err = agent.ExecSubprocess(f.Args())
} else {
cmd, err = agent.ExecScript(strings.Join(f.Args(), " "))
}
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error executing handler: %s", err)) c.UI.Error(fmt.Sprintf("Error executing handler: %s", err))
goto ERR goto ERR
@ -173,8 +186,17 @@ func (c *WatchCommand) Run(args []string) int {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
// Run the handler // Run the handler.
if err := cmd.Run(); err != nil { if err := cmd.Start(); err != nil {
c.UI.Error(fmt.Sprintf("Error starting handler: %s", err))
goto ERR
}
// Set up signal forwarding.
agent.ForwardSignals(cmd, logFn, doneCh)
// Wait for the handler to complete.
if err := cmd.Wait(); err != nil {
c.UI.Error(fmt.Sprintf("Error executing handler: %s", err)) c.UI.Error(fmt.Sprintf("Error executing handler: %s", err))
goto ERR goto ERR
} }

View File

@ -97,7 +97,7 @@ A script check:
"check": { "check": {
"id": "mem-util", "id": "mem-util",
"name": "Memory utilization", "name": "Memory utilization",
"script": "/usr/local/bin/check_mem.py", "args": ["/usr/local/bin/check_mem.py", "-limit", "256MB"],
"interval": "10s", "interval": "10s",
"timeout": "1s" "timeout": "1s"
} }
@ -157,7 +157,7 @@ A Docker check:
"name": "Memory utilization", "name": "Memory utilization",
"docker_container_id": "f972c95ebf0e", "docker_container_id": "f972c95ebf0e",
"shell": "/bin/bash", "shell": "/bin/bash",
"script": "/usr/local/bin/check_mem.py", "args": ["/usr/local/bin/check_mem.py"],
"interval": "10s" "interval": "10s"
} }
} }
@ -218,6 +218,11 @@ In Consul 0.9.0 and later, the agent must be configured with
[`enable_script_checks`](/docs/agent/options.html#_enable_script_checks) set to `true` [`enable_script_checks`](/docs/agent/options.html#_enable_script_checks) set to `true`
in order to enable script checks. in order to enable script checks.
Prior to Consul 1.0, checks used a single `script` field to define the command to run, and
would always run in a shell. In Consul 1.0, the `args` array was added so that checks can be
run without a shell. The `script` field is deprecated, and you should include the shell in
the `args` to run under a shell, eg. `"args": ["sh", "-c", "..."]`.
## Initial Health Check Status ## Initial Health Check Status
By default, when checks are registered against a Consul agent, the state is set By default, when checks are registered against a Consul agent, the state is set
@ -231,7 +236,7 @@ health check definition, like so:
{ {
"check": { "check": {
"id": "mem", "id": "mem",
"script": "/bin/check_mem", "args": ["/bin/check_mem", "-limit", "256MB"],
"interval": "10s", "interval": "10s",
"status": "passing" "status": "passing"
} }
@ -274,7 +279,7 @@ key in your configuration file.
{ {
"id": "chk1", "id": "chk1",
"name": "mem", "name": "mem",
"script": "/bin/check_mem", "args": ["/bin/check_mem", "-limit", "256MB"],
"interval": "5s" "interval": "5s"
}, },
{ {

View File

@ -45,6 +45,11 @@ Additionally, the `CONSUL_INDEX` environment variable will be set.
This maps to the `X-Consul-Index` value in responses from the This maps to the `X-Consul-Index` value in responses from the
[HTTP API](/api/index.html). [HTTP API](/api/index.html).
Prior to Consul 1.0, watches used a single `handler` field to define the command to run, and
would always run in a shell. In Consul 1.0, the `args` array was added so that handlers can be
run without a shell. The `handler` field is deprecated, and you should include the shell in
the `args` to run under a shell, eg. `"args": ["sh", "-c", "..."]`.
## Global Parameters ## Global Parameters
In addition to the parameters supported by each option type, there In addition to the parameters supported by each option type, there
@ -52,7 +57,8 @@ are a few global parameters that all watches support:
* `datacenter` - Can be provided to override the agent's default datacenter. * `datacenter` - Can be provided to override the agent's default datacenter.
* `token` - Can be provided to override the agent's default ACL token. * `token` - Can be provided to override the agent's default ACL token.
* `handler` - The handler to invoke when the data view updates. * `args` - The handler subprocess and arguments to invoke when the data view updates.
* `handler` - The handler shell command to invoke when the data view updates.
## Watch Types ## Watch Types
@ -80,7 +86,7 @@ Here is an example configuration:
{ {
"type": "key", "type": "key",
"key": "foo/bar/baz", "key": "foo/bar/baz",
"handler": "/usr/bin/my-key-handler.sh" "args": ["/usr/bin/my-service-handler.sh", "-redis"]
} }
``` ```
@ -117,7 +123,7 @@ Here is an example configuration:
{ {
"type": "keyprefix", "type": "keyprefix",
"prefix": "foo/", "prefix": "foo/",
"handler": "/usr/bin/my-prefix-handler.sh" "args": ["/usr/bin/my-service-handler.sh", "-redis"]
} }
``` ```
@ -231,7 +237,7 @@ Here is an example configuration:
{ {
"type": "service", "type": "service",
"service": "redis", "service": "redis",
"handler": "/usr/bin/my-service-handler.sh" "args": ["/usr/bin/my-service-handler.sh", "-redis"]
} }
``` ```
@ -322,13 +328,13 @@ Here is an example configuration:
{ {
"type": "event", "type": "event",
"name": "web-deploy", "name": "web-deploy",
"handler": "/usr/bin/my-deploy-handler.sh" "args": ["/usr/bin/my-service-handler.sh", "-web-deploy"]
} }
``` ```
Or, using the watch command: Or, using the watch command:
$ consul watch -type=event -name=web-deploy /usr/bin/my-deploy-handler.sh $ consul watch -type=event -name=web-deploy /usr/bin/my-deploy-handler.sh -web-deploy
An example of the output of this command: An example of the output of this command:

View File

@ -61,6 +61,8 @@ completion as a script to evaluate.
* `-service` - Regular expression to filter to only nodes with matching services. * `-service` - Regular expression to filter to only nodes with matching services.
* `-shell` - Optional, use a shell to run the command. The default value is true.
* `-tag` - Regular expression to filter to only nodes with a service that has * `-tag` - Regular expression to filter to only nodes with a service that has
a matching tag. This must be used with `-service`. As an example, you may a matching tag. This must be used with `-service`. As an example, you may
do `-service mysql -tag secondary`. do `-service mysql -tag secondary`.

View File

@ -69,9 +69,12 @@ Windows has no POSIX compatible notion for `SIGTERM`.
* `-name` - Optional name to associate with the underlying session. * `-name` - Optional name to associate with the underlying session.
If not provided, one is generated based on the child command. If not provided, one is generated based on the child command.
* `-shell` - Optional, use a shell to run the command (can set a custom shell via the
SHELL environment variable). The default value is true.
* `-pass-stdin` - Pass stdin to child process. * `-pass-stdin` - Pass stdin to child process.
* `timeout` - Maximum amount of time to wait to acquire the lock, specified * `-timeout` - Maximum amount of time to wait to acquire the lock, specified
as a duration like `1s` or `3h`. The default value is 0. as a duration like `1s` or `3h`. The default value is 0.
* `-try` - Attempt to acquire the lock up to the given timeout. The timeout is a * `-try` - Attempt to acquire the lock up to the given timeout. The timeout is a
@ -87,4 +90,4 @@ default to `/bin/sh`. It should be noted that not all shells terminate child
processes when they receive `SIGTERM`. Under Ubuntu, `/bin/sh` is linked to `dash`, processes when they receive `SIGTERM`. Under Ubuntu, `/bin/sh` is linked to `dash`,
which does **not** terminate its children. In order to ensure that child processes which does **not** terminate its children. In order to ensure that child processes
are killed when the lock is lost, be sure to set the `SHELL` environment variable are killed when the lock is lost, be sure to set the `SHELL` environment variable
appropriately. appropriately, or run without a shell by setting `-shell=false`.

View File

@ -45,6 +45,9 @@ or optionally provided. There is more documentation on watch
* `-service` - Service to watch. Required for `service` type, optional for `checks` type. * `-service` - Service to watch. Required for `service` type, optional for `checks` type.
* `-shell` - Optional, use a shell to run the command (can set a custom shell via the
SHELL environment variable). The default value is true.
* `-state` - Check state to filter on. Optional for `checks` type. * `-state` - Check state to filter on. Optional for `checks` type.
* `-tag` - Service tag to filter on. Optional for `service` type. * `-tag` - Service tag to filter on. Optional for `service` type.