Switch to a single key for command and command shell
All checks were successful
continuous-integration/drone/push Build is passing

This makes the configuration more similar to Minitor-py and
docker-compose. If a string is passed, it will be executed in a shell.
If an array is passed, it will be executed in as a command directly.

This breaks compatiblity with previous versions of Minitor-go, but
closer to compatiblity with Minitor-py.
This commit is contained in:
IamTheFij 2020-02-16 13:25:11 -08:00
parent 162e8618cb
commit d4e2cb7b9f
10 changed files with 79 additions and 85 deletions

View File

@ -8,29 +8,8 @@ Initial target is meant to be roughly compatible requiring only minor changes to
## Differences from Python version
There are a few key differences between the Python version and the v0.x Go version.
First, configuration keys cannot have multiple types in Go, so a different key must be used when specifying a Shell command as a string rather than a list of args. Instead of `command`, you must use `command_shell`. Eg:
minitor-py:
```yaml
monitors:
- name: Exec command
command: ['echo', 'test']
- name: Shell command
command: echo 'test'
```
minitor-go:
```yaml
monitors:
- name: Exec command
command: ['echo', 'test']
- name: Shell command
command_shell: echo 'test'
```
Second, templating for Alert messages has been updated. In the Python version, `str.format(...)` was used with certain keys passed in that could be used to format messages. In the Go version, we use a struct, `AlertNotice` defined in `alert.go` and the built in Go templating format. Eg.
Templating for Alert messages has been updated. In the Python version, `str.format(...)` was used with certain keys passed in that could be used to format messages. In the Go version, we use a struct, `AlertNotice` defined in `alert.go` and the built in Go templating format. Eg.
minitor-py:
```yaml
@ -38,7 +17,7 @@ alerts:
log_command:
command: ['echo', '{monitor_name}']
log_shell:
command_shell: 'echo {monitor_name}'
command: 'echo {monitor_name}'
```
minitor-go:
@ -47,7 +26,7 @@ alerts:
log_command:
command: ['echo', '{{.MonitorName}}']
log_shell:
command_shell: 'echo {{.MonitorName}}'
command: 'echo {{.MonitorName}}'
```
Finally, newlines in a shell command don't terminate a particular command. Semicolons must be used and continuations should not.
@ -56,7 +35,7 @@ minitor-py:
```yaml
alerts:
log_shell:
command_shell: >
command: >
echo "line 1"
echo "line 2"
echo "continued" \
@ -67,7 +46,7 @@ minitor-go:
```yaml
alerts:
log_shell:
command_shell: >
command: >
echo "line 1";
echo "line 2";
echo "continued"
@ -87,6 +66,7 @@ Pairity:
- [x] Implement Prometheus client to export metrics
- [x] Test coverage
- [x] Integration testing (manual or otherwise)
- [x] Allow commands and shell commands in the same config key
Improvement (potentially breaking):

View File

@ -12,8 +12,7 @@ import (
// Alert is a config driven mechanism for sending a notice
type Alert struct {
Name string
Command []string
CommandShell string `yaml:"command_shell"`
Command CommandOrShell
commandTemplate []*template.Template
commandShellTemplate *template.Template
}
@ -31,9 +30,7 @@ type AlertNotice struct {
// IsValid returns a boolean indicating if the Alert has been correctly
// configured
func (alert Alert) IsValid() bool {
atLeastOneCommand := (alert.CommandShell != "" || alert.Command != nil)
atMostOneCommand := (alert.CommandShell == "" || alert.Command == nil)
return atLeastOneCommand && atMostOneCommand
return !alert.Command.Empty()
}
// BuildTemplates compiles command templates for the Alert
@ -41,16 +38,16 @@ func (alert *Alert) BuildTemplates() error {
if LogDebug {
log.Printf("DEBUG: Building template for alert %s", alert.Name)
}
if alert.commandTemplate == nil && alert.Command != nil {
if alert.commandTemplate == nil && alert.Command.Command != nil {
alert.commandTemplate = []*template.Template{}
for i, cmdPart := range alert.Command {
for i, cmdPart := range alert.Command.Command {
alert.commandTemplate = append(alert.commandTemplate, template.Must(
template.New(alert.Name+string(i)).Parse(cmdPart),
))
}
} else if alert.commandShellTemplate == nil && alert.CommandShell != "" {
} else if alert.commandShellTemplate == nil && alert.Command.ShellCommand != "" {
alert.commandShellTemplate = template.Must(
template.New(alert.Name).Parse(alert.CommandShell),
template.New(alert.Name).Parse(alert.Command.ShellCommand),
)
} else {
return fmt.Errorf("No template provided for alert %s", alert.Name)

View File

@ -11,14 +11,9 @@ func TestAlertIsValid(t *testing.T) {
expected bool
name string
}{
{Alert{Command: []string{"echo", "test"}}, true, "Command only"},
{Alert{CommandShell: "echo test"}, true, "CommandShell only"},
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, true, "Command only"},
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, true, "CommandShell only"},
{Alert{}, false, "No commands"},
{
Alert{Command: []string{"echo", "test"}, CommandShell: "echo test"},
false,
"Both commands",
},
}
for _, c := range cases {
@ -41,28 +36,28 @@ func TestAlertSend(t *testing.T) {
name string
}{
{
Alert{Command: []string{"echo", "{{.MonitorName}}"}},
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.MonitorName}}"}}},
AlertNotice{MonitorName: "test"},
"test\n",
false,
"Command with template",
},
{
Alert{CommandShell: "echo {{.MonitorName}}"},
Alert{Command: CommandOrShell{ShellCommand: "echo {{.MonitorName}}"}},
AlertNotice{MonitorName: "test"},
"test\n",
false,
"Command shell with template",
},
{
Alert{Command: []string{"echo", "{{.Bad}}"}},
Alert{Command: CommandOrShell{Command: []string{"echo", "{{.Bad}}"}}},
AlertNotice{MonitorName: "test"},
"",
true,
"Command with bad template",
},
{
Alert{CommandShell: "echo {{.Bad}}"},
Alert{Command: CommandOrShell{ShellCommand: "echo {{.Bad}}"}},
AlertNotice{MonitorName: "test"},
"",
true,
@ -103,8 +98,8 @@ func TestAlertBuildTemplate(t *testing.T) {
expectErr bool
name string
}{
{Alert{Command: []string{"echo", "test"}}, false, "Command only"},
{Alert{CommandShell: "echo test"}, false, "CommandShell only"},
{Alert{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "Command only"},
{Alert{Command: CommandOrShell{ShellCommand: "echo test"}}, false, "CommandShell only"},
{Alert{}, true, "No commands"},
}

View File

@ -15,6 +15,37 @@ type Config struct {
Alerts map[string]*Alert
}
// CommandOrShell type wraps a string or list of strings
// for executing a command directly or in a shell
type CommandOrShell struct {
ShellCommand string
Command []string
}
// Empty checks if the Command has a value
func (cos CommandOrShell) Empty() bool {
return (cos.ShellCommand == "" && cos.Command == nil)
}
// UnmarshalYAML allows unmarshalling either a string or slice of strings
// and parsing them as either a command or a shell command.
func (cos *CommandOrShell) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd []string
err := unmarshal(&cmd)
// Error indicates this is shell command
if err != nil {
var shellCmd string
err := unmarshal(&shellCmd)
if err != nil {
return err
}
cos.ShellCommand = shellCmd
} else {
cos.Command = cmd
}
return nil
}
// IsValid checks config validity and returns true if valid
func (config Config) IsValid() (isValid bool) {
isValid = true

View File

@ -42,7 +42,7 @@ func TestMultiLineConfig(t *testing.T) {
log.Println("-----")
log.Println("TestMultiLineConfig(parse > string)")
expected := "echo 'Some string with stuff'; echo \"<angle brackets>\"; exit 1\n"
actual := config.Monitors[0].CommandShell
actual := config.Monitors[0].Command.ShellCommand
if expected != actual {
t.Errorf("TestMultiLineConfig(>) failed")
t.Logf("string expected=`%v`", expected)
@ -70,7 +70,7 @@ func TestMultiLineConfig(t *testing.T) {
log.Println("-----")
log.Println("TestMultiLineConfig(parse | string)")
expected = "echo 'Some string with stuff'\necho '<angle brackets>'\n"
actual = config.Alerts["log_shell"].CommandShell
actual = config.Alerts["log_shell"].Command.ShellCommand
if expected != actual {
t.Errorf("TestMultiLineConfig(|) failed")
t.Logf("string expected=`%v`", expected)

View File

@ -18,7 +18,7 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Success",
Command: []string{"true"},
Command: CommandOrShell{Command: []string{"true"}},
},
},
},
@ -30,12 +30,12 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Failure",
Command: []string{"false"},
Command: CommandOrShell{Command: []string{"false"}},
AlertAfter: 1,
},
&Monitor{
Name: "Failure",
Command: []string{"false"},
Command: CommandOrShell{Command: []string{"false"}},
AlertDown: []string{"unknown"},
AlertAfter: 1,
},
@ -49,12 +49,12 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Success",
Command: []string{"ls"},
Command: CommandOrShell{Command: []string{"ls"}},
alertCount: 1,
},
&Monitor{
Name: "Success",
Command: []string{"true"},
Command: CommandOrShell{Command: []string{"true"}},
AlertUp: []string{"unknown"},
alertCount: 1,
},
@ -68,14 +68,14 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Failure",
Command: []string{"false"},
Command: CommandOrShell{Command: []string{"false"}},
AlertDown: []string{"good"},
AlertAfter: 1,
},
},
Alerts: map[string]*Alert{
"good": &Alert{
Command: []string{"true"},
Command: CommandOrShell{Command: []string{"true"}},
},
},
},
@ -87,7 +87,7 @@ func TestCheckMonitors(t *testing.T) {
Monitors: []*Monitor{
&Monitor{
Name: "Failure",
Command: []string{"false"},
Command: CommandOrShell{Command: []string{"false"}},
AlertDown: []string{"bad"},
AlertAfter: 1,
},
@ -95,7 +95,7 @@ func TestCheckMonitors(t *testing.T) {
Alerts: map[string]*Alert{
"bad": &Alert{
Name: "bad",
Command: []string{"false"},
Command: CommandOrShell{Command: []string{"false"}},
},
},
},

View File

@ -11,8 +11,7 @@ import (
type Monitor struct {
// Config values
Name string
Command []string
CommandShell string `yaml:"command_shell"`
Command CommandOrShell
AlertDown []string `yaml:"alert_down"`
AlertUp []string `yaml:"alert_up"`
CheckInterval float64 `yaml:"check_interval"`
@ -29,10 +28,7 @@ type Monitor struct {
// IsValid returns a boolean indicating if the Monitor has been correctly
// configured
func (monitor Monitor) IsValid() bool {
atLeastOneCommand := (monitor.CommandShell != "" || monitor.Command != nil)
atMostOneCommand := (monitor.CommandShell == "" || monitor.Command == nil)
return (atLeastOneCommand &&
atMostOneCommand &&
return (!monitor.Command.Empty() &&
monitor.getAlertAfter() > 0 &&
monitor.AlertDown != nil)
}
@ -52,10 +48,10 @@ func (monitor Monitor) ShouldCheck() bool {
// and a possible AlertNotice
func (monitor *Monitor) Check() (bool, *AlertNotice) {
var cmd *exec.Cmd
if monitor.Command != nil {
cmd = exec.Command(monitor.Command[0], monitor.Command[1:]...)
if monitor.Command.Command != nil {
cmd = exec.Command(monitor.Command.Command[0], monitor.Command.Command[1:]...)
} else {
cmd = ShellCommand(monitor.CommandShell)
cmd = ShellCommand(monitor.Command.ShellCommand)
}
output, err := cmd.CombinedOutput()

View File

@ -13,16 +13,11 @@ func TestMonitorIsValid(t *testing.T) {
expected bool
name string
}{
{Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"},
{Monitor{CommandShell: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"},
{Monitor{Command: []string{"echo", "test"}}, false, "No AlertDown"},
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}}, true, "Command only"},
{Monitor{Command: CommandOrShell{ShellCommand: "echo test"}, AlertDown: []string{"log"}}, true, "CommandShell only"},
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}}, false, "No AlertDown"},
{Monitor{AlertDown: []string{"log"}}, false, "No commands"},
{
Monitor{Command: []string{"echo", "test"}, CommandShell: "echo test", AlertDown: []string{"log"}},
false,
"Both commands",
},
{Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}, AlertAfter: -1}, false, "Invalid alert threshold, -1"},
{Monitor{Command: CommandOrShell{Command: []string{"echo", "test"}}, AlertDown: []string{"log"}, AlertAfter: -1}, false, "Invalid alert threshold, -1"},
}
for _, c := range cases {
@ -254,22 +249,22 @@ func TestMonitorCheck(t *testing.T) {
name string
}{
{
Monitor{Command: []string{"echo", "success"}},
Monitor{Command: CommandOrShell{Command: []string{"echo", "success"}}},
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
"Test successful command",
},
{
Monitor{CommandShell: "echo success"},
Monitor{Command: CommandOrShell{ShellCommand: "echo success"}},
expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"},
"Test successful command shell",
},
{
Monitor{Command: []string{"total", "failure"}},
Monitor{Command: CommandOrShell{Command: []string{"total", "failure"}}},
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command",
},
{
Monitor{CommandShell: "false"},
Monitor{Command: CommandOrShell{ShellCommand: "false"}},
expected{isSuccess: false, hasNotice: true, lastOutput: ""},
"Test failed command shell",
},

View File

@ -7,7 +7,7 @@ monitors:
alert_down: ['log_command', 'log_shell']
alert_every: 0
- name: Shell
command_shell: >
command: >
echo 'Some string with stuff';
echo 'another line';
echo $PATH;
@ -20,4 +20,4 @@ alerts:
log_command:
command: ['echo', 'regular', '"command!!!"', "{{.MonitorName}}"]
log_shell:
command_shell: echo "Failure on {{.MonitorName}} User is $USER"
command: echo "Failure on {{.MonitorName}} User is $USER"

View File

@ -3,7 +3,7 @@ check_interval: 1
monitors:
- name: Shell
command_shell: >
command: >
echo 'Some string with stuff';
echo "<angle brackets>";
exit 1
@ -13,6 +13,6 @@ monitors:
alerts:
log_shell:
command_shell: |
command: |
echo 'Some string with stuff'
echo '<angle brackets>'