From d4e2cb7b9f2b31ddc7df5e91b97bc88f81f5ee58 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Sun, 16 Feb 2020 13:25:11 -0800 Subject: [PATCH] Switch to a single key for command and command shell 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. --- README.md | 32 ++++++-------------------------- alert.go | 15 ++++++--------- alert_test.go | 21 ++++++++------------- config.go | 31 +++++++++++++++++++++++++++++++ config_test.go | 4 ++-- main_test.go | 18 +++++++++--------- monitor.go | 14 +++++--------- monitor_test.go | 21 ++++++++------------- test/valid-config.yml | 4 ++-- test/valid-verify-multi-line.yml | 4 ++-- 10 files changed, 79 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 70f2b81..72e00b4 100644 --- a/README.md +++ b/README.md @@ -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): diff --git a/alert.go b/alert.go index 24e2b81..c4b4f79 100644 --- a/alert.go +++ b/alert.go @@ -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) diff --git a/alert_test.go b/alert_test.go index 8bf3a61..8a0e434 100644 --- a/alert_test.go +++ b/alert_test.go @@ -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"}, } diff --git a/config.go b/config.go index 0e2e953..fd237a4 100644 --- a/config.go +++ b/config.go @@ -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 diff --git a/config_test.go b/config_test.go index 06d0e08..9321e80 100644 --- a/config_test.go +++ b/config_test.go @@ -42,7 +42,7 @@ func TestMultiLineConfig(t *testing.T) { log.Println("-----") log.Println("TestMultiLineConfig(parse > string)") expected := "echo 'Some string with stuff'; echo \"\"; 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 ''\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) diff --git a/main_test.go b/main_test.go index c0ca1ee..0238767 100644 --- a/main_test.go +++ b/main_test.go @@ -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"}}, }, }, }, diff --git a/monitor.go b/monitor.go index 8e83bc6..764b788 100644 --- a/monitor.go +++ b/monitor.go @@ -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() diff --git a/monitor_test.go b/monitor_test.go index 29ab3ba..d5a1b1b 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -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", }, diff --git a/test/valid-config.yml b/test/valid-config.yml index 23b763e..e192977 100644 --- a/test/valid-config.yml +++ b/test/valid-config.yml @@ -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" diff --git a/test/valid-verify-multi-line.yml b/test/valid-verify-multi-line.yml index 3cbad19..fa631aa 100644 --- a/test/valid-verify-multi-line.yml +++ b/test/valid-verify-multi-line.yml @@ -3,7 +3,7 @@ check_interval: 1 monitors: - name: Shell - command_shell: > + command: > echo 'Some string with stuff'; echo ""; exit 1 @@ -13,6 +13,6 @@ monitors: alerts: log_shell: - command_shell: | + command: | echo 'Some string with stuff' echo ''