diff --git a/Gopkg.lock b/Gopkg.lock index ca40a829..4a544d93 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -118,7 +118,7 @@ revision = "2315d5715e36303a941d907f038da7f7c44c773b" [[projects]] - digest = "1:dafb48fd4fb82b3e0a15b789da6da152180e9c28b1b8846058903eff5eadc01d" + digest = "1:be364f871284e2e8f787235fe31d904cf9958f64b7b06fd52f97bf9af2672f91" name = "github.com/sensu/sensu-go" packages = [ "api/core/v2", @@ -127,7 +127,19 @@ "util/strings", ] pruneopts = "UT" - revision = "2daf9d442deec0afd2c6f53c183460e879a10646" + revision = "32aea478ae74c3753710887d2ce2556b598d82bb" + version = "5.7.0" + +[[projects]] + digest = "1:8cd72fbcf8f18cf753ca9c40723eaef3356e166437f33d219d977581b7024d14" + name = "github.com/sensu/sensu-plugins-go-library" + packages = [ + "args", + "sensu", + ] + pruneopts = "UT" + revision = "a4d9fcd58e069a1c9e51984b9f4b466accd366c8" + version = "0.2.0" [[projects]] digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939" @@ -146,15 +158,15 @@ version = "v1.0.3" [[projects]] - digest = "1:c40d65817cdd41fac9aa7af8bed56927bb2d6d47e4fea566a74880f5c2b1c41e" + digest = "1:5da8ce674952566deae4dbc23d07c85caafc6cfa815b0b3e03e41979cedb8750" name = "github.com/stretchr/testify" packages = [ "assert", "require", ] pruneopts = "UT" - revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" - version = "v1.2.2" + revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" + version = "v1.3.0" [[projects]] branch = "master" @@ -263,7 +275,7 @@ input-imports = [ "github.com/bluele/slack", "github.com/sensu/sensu-go/types", - "github.com/spf13/cobra", + "github.com/sensu/sensu-plugins-go-library/sensu", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", ] diff --git a/Gopkg.toml b/Gopkg.toml index f119e207..7948859f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -31,11 +31,11 @@ [[constraint]] name = "github.com/sensu/sensu-go" - revision = "2daf9d442deec0afd2c6f53c183460e879a10646" + version = "5.9.0" [[constraint]] - name = "github.com/spf13/cobra" - version = "0.0.3" + name = "github.com/sensu/sensu-plugins-go-library" + version = "0.2.0" [[constraint]] name = "github.com/stretchr/testify" diff --git a/main.go b/main.go index 41123fd0..5c2210d5 100644 --- a/main.go +++ b/main.go @@ -1,168 +1,93 @@ package main import ( - "encoding/json" - "errors" "fmt" - "io/ioutil" - "log" - "os" - "path" + corev2 "github.com/sensu/sensu-go/api/core/v2" + "github.com/sensu/sensu-plugins-go-library/sensu" "strings" "github.com/bluele/slack" - "github.com/sensu/sensu-go/types" - "github.com/spf13/cobra" ) -type HandlerConfigOption struct { - Value string - Path string - Env string -} - type HandlerConfig struct { - SlackWebhookUrl HandlerConfigOption - SlackChannel HandlerConfigOption - SlackUsername HandlerConfigOption - SlackIconUrl HandlerConfigOption - Timeout int - Keyspace string -} + sensu.PluginConfig + SlackWebhookUrl string + SlackChannel string + SlackUsername string + SlackIconUrl string +} + +const ( + webHookUrl = "webhook-url" + channel = "channel" + userName = "username" + iconUrl = "icon-url" +) var ( - stdin *os.File config = HandlerConfig{ - // default values - SlackWebhookUrl: HandlerConfigOption{Path: "webhook-url", Env: "SENSU_SLACK_WEHBOOK_URL"}, - SlackChannel: HandlerConfigOption{Path: "channel", Env: "SENSU_SLACK_CHANNEL"}, - SlackUsername: HandlerConfigOption{Path: "username", Env: "SENSU_SLACK_USERNAME"}, - SlackIconUrl: HandlerConfigOption{Path: "icon-url", Env: "SENSU_SLACK_ICON_URL"}, - Timeout: 10, - Keyspace: "sensu.io/plugins/slack/config", + PluginConfig: sensu.PluginConfig{ + Name: "sensu-slack-handler", + Short: "The Sensu Go Slack handler for notifying a channel", + Timeout: 10, + Keyspace: "sensu.io/plugins/slack/config", + }, } - options = []*HandlerConfigOption{ - // iterable slice of user-overridable configuration options - &config.SlackWebhookUrl, - &config.SlackChannel, - &config.SlackUsername, - &config.SlackIconUrl, + + slackConfigOptions = []*sensu.PluginConfigOption{ + { + Path: webHookUrl, + Env: "SENSU_SLACK_WEBHOOK_URL", + Argument: webHookUrl, + Shorthand: "w", + Default: "", + Usage: "The webhook url to send messages to, defaults to value of SLACK_WEBHOOK_URL env variable", + Value: &config.SlackWebhookUrl, + }, + { + Path: channel, + Env: "SENSU_SLACK_CHANNEL", + Argument: channel, + Shorthand: "c", + Default: "#general", + Usage: "The channel to post messages to", + Value: &config.SlackChannel, + }, + { + Path: userName, + Env: "SENSU_SLACK_USERNAME", + Argument: userName, + Shorthand: "u", + Default: "sensu", + Usage: "The username that messages will be sent as", + Value: &config.SlackUsername, + }, + { + Path: iconUrl, + Env: "SENSU_SLACK_ICON_URL", + Argument: iconUrl, + Shorthand: "i", + Default: "http://s3-us-west-2.amazonaws.com/sensuapp.org/sensu.png", + Usage: "A URL to an image to use as the user avatar", + Value: &config.SlackIconUrl, + }, } ) func main() { - rootCmd := configureRootCommand() - if err := rootCmd.Execute(); err != nil { - log.Fatal(err) - } -} - -func configureRootCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "sensu-slack-handler", - Short: "The Sensu Go Slack handler for notifying a channel", - RunE: run, - } - - /* - Sensitive flags - default to using envvar value - do not mark as required - manually test for empty value - */ - cmd.Flags().StringVarP(&config.SlackWebhookUrl.Value, - "webhook-url", - "w", - os.Getenv("SLACK_WEBHOOK_URL"), - "The webhook url to send messages to, defaults to value of SLACK_WEBHOOK_URL env variable") - - cmd.Flags().StringVarP(&config.SlackChannel.Value, - "channel", - "c", - "#general", - "The channel to post messages to") - - cmd.Flags().StringVarP(&config.SlackUsername.Value, - "username", - "u", - "sensu", - "The username that messages will be sent as") - - cmd.Flags().StringVarP(&config.SlackIconUrl.Value, - "icon-url", - "i", - "http://s3-us-west-2.amazonaws.com/sensuapp.org/sensu.png", - "A URL to an image to use as the user avatar") - - cmd.Flags().IntVarP(&config.Timeout, - "timeout", - "t", - 10, - "The amount of seconds to wait before terminating the handler") - - return cmd + goHandler := sensu.NewGoHandler(&config.PluginConfig, slackConfigOptions, checkArgs, sendMessage) + goHandler.Execute() } -func run(cmd *cobra.Command, args []string) error { - if len(args) != 0 { - _ = cmd.Help() - return errors.New("invalid argument(s) received") - } - - // load & parse stdin - if stdin == nil { - stdin = os.Stdin - } - eventJSON, err := ioutil.ReadAll(stdin) - if err != nil { - return fmt.Errorf("failed to read stdin: %s", err.Error()) - } - event := &types.Event{} - err = json.Unmarshal(eventJSON, event) - if err != nil { - return fmt.Errorf("failed to unmarshal stdin data: %s", eventJSON) - } - - // configuration validation & overrides - if config.SlackWebhookUrl.Value == "" { - _ = cmd.Help() - return fmt.Errorf("webhook url is empty") - } - - configurationOverrides(&config, options, event) - - if err = validateEvent(event); err != nil { - return errors.New(err.Error()) - } - - if err = sendMessage(event); err != nil { - return errors.New(err.Error()) +func checkArgs(_ *corev2.Event) error { + if len(config.SlackWebhookUrl) == 0 { + return fmt.Errorf("--webhook-url or SENSU_SLACK_WEBHOOK_URL environment variable is required") } return nil } -func configurationOverrides(config *HandlerConfig, options []*HandlerConfigOption, event *types.Event) { - if config.Keyspace == "" { - return - } - for _, opt := range options { - if opt.Path != "" { - // compile the Annotation keyspace to look for configuration overrides - k := path.Join(config.Keyspace, opt.Path) - switch { - case event.Check.Annotations[k] != "": - opt.Value = event.Check.Annotations[k] - log.Printf("Overriding default handler configuration with value of \"Check.Annotations.%s\" (\"%s\")\n", k, event.Check.Annotations[k]) - case event.Entity.Annotations[k] != "": - opt.Value = event.Entity.Annotations[k] - log.Printf("Overriding default handler configuration with value of \"Entity.Annotations.%s\" (\"%s\")\n", k, event.Entity.Annotations[k]) - } - } - } -} - -func formattedEventAction(event *types.Event) string { +func formattedEventAction(event *corev2.Event) string { switch event.Check.Status { case 0: return "RESOLVED" @@ -175,11 +100,11 @@ func chomp(s string) string { return strings.Trim(strings.Trim(strings.Trim(s, "\n"), "\r"), "\r\n") } -func eventKey(event *types.Event) string { +func eventKey(event *corev2.Event) string { return fmt.Sprintf("%s/%s", event.Entity.Name, event.Check.Name) } -func eventSummary(event *types.Event, maxLength int) string { +func eventSummary(event *corev2.Event, maxLength int) string { output := chomp(event.Check.Output) if len(event.Check.Output) > maxLength { output = output[0:maxLength] + "..." @@ -187,11 +112,11 @@ func eventSummary(event *types.Event, maxLength int) string { return fmt.Sprintf("%s:%s", eventKey(event), output) } -func formattedMessage(event *types.Event) string { +func formattedMessage(event *corev2.Event) string { return fmt.Sprintf("%s - %s", formattedEventAction(event), eventSummary(event, 100)) } -func messageColor(event *types.Event) string { +func messageColor(event *corev2.Event) string { switch event.Check.Status { case 0: return "good" @@ -202,7 +127,7 @@ func messageColor(event *types.Event) string { } } -func messageStatus(event *types.Event) string { +func messageStatus(event *corev2.Event) string { switch event.Check.Status { case 0: return "Resolved" @@ -213,24 +138,24 @@ func messageStatus(event *types.Event) string { } } -func messageAttachment(event *types.Event) *slack.Attachment { +func messageAttachment(event *corev2.Event) *slack.Attachment { attachment := &slack.Attachment{ Title: "Description", Text: event.Check.Output, Fallback: formattedMessage(event), Color: messageColor(event), Fields: []*slack.AttachmentField{ - &slack.AttachmentField{ + { Title: "Status", Value: messageStatus(event), Short: false, }, - &slack.AttachmentField{ + { Title: "Entity", Value: event.Entity.Name, Short: true, }, - &slack.AttachmentField{ + { Title: "Check", Value: event.Check.Name, Short: true, @@ -240,36 +165,12 @@ func messageAttachment(event *types.Event) *slack.Attachment { return attachment } -func sendMessage(event *types.Event) error { - hook := slack.NewWebHook(config.SlackWebhookUrl.Value) +func sendMessage(event *corev2.Event) error { + hook := slack.NewWebHook(config.SlackWebhookUrl) return hook.PostMessage(&slack.WebHookPostPayload{ Attachments: []*slack.Attachment{messageAttachment(event)}, - Channel: config.SlackChannel.Value, - IconUrl: config.SlackIconUrl.Value, - Username: config.SlackUsername.Value, + Channel: config.SlackChannel, + IconUrl: config.SlackIconUrl, + Username: config.SlackUsername, }) } - -func validateEvent(event *types.Event) error { - if event.Timestamp <= 0 { - return errors.New("timestamp is missing or must be greater than zero") - } - - if event.Entity == nil { - return errors.New("entity is missing from event") - } - - if !event.HasCheck() { - return errors.New("check is missing from event") - } - - if err := event.Entity.Validate(); err != nil { - return err - } - - if err := event.Check.Validate(); err != nil { - return err - } - - return nil -} diff --git a/main_test.go b/main_test.go index 694be29b..5c2102b3 100644 --- a/main_test.go +++ b/main_test.go @@ -8,14 +8,14 @@ import ( "os" "testing" - "github.com/sensu/sensu-go/types" + corev2 "github.com/sensu/sensu-go/api/core/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFormattedEventAction(t *testing.T) { assert := assert.New(t) - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") action := formattedEventAction(event) assert.Equal("RESOLVED", action) @@ -43,14 +43,14 @@ func TestChomp(t *testing.T) { func TestEventKey(t *testing.T) { assert := assert.New(t) - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") eventKey := eventKey(event) assert.Equal("entity1/check1", eventKey) } func TestEventSummary(t *testing.T) { assert := assert.New(t) - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") event.Check.Output = "disk is full" eventKey := eventSummary(event, 100) @@ -62,7 +62,7 @@ func TestEventSummary(t *testing.T) { func TestFormattedMessage(t *testing.T) { assert := assert.New(t) - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") event.Check.Output = "disk is full" event.Check.Status = 1 formattedMsg := formattedMessage(event) @@ -71,7 +71,7 @@ func TestFormattedMessage(t *testing.T) { func TestMessageColor(t *testing.T) { assert := assert.New(t) - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") event.Check.Status = 0 color := messageColor(event) @@ -88,7 +88,7 @@ func TestMessageColor(t *testing.T) { func TestMessageStatus(t *testing.T) { assert := assert.New(t) - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") event.Check.Status = 0 status := messageStatus(event) @@ -105,7 +105,7 @@ func TestMessageStatus(t *testing.T) { func TestSendMessage(t *testing.T) { assert := assert.New(t) - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") var apiStub = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := ioutil.ReadAll(r.Body) @@ -116,8 +116,8 @@ func TestSendMessage(t *testing.T) { require.NoError(t, err) })) - config.SlackWebhookUrl.Value = apiStub.URL - config.SlackChannel.Value = "#test" + config.SlackWebhookUrl = apiStub.URL + config.SlackChannel = "#test" err := sendMessage(event) assert.NoError(err) } @@ -129,14 +129,14 @@ func TestMain(t *testing.T) { _ = os.Remove(file.Name()) }() - event := types.FixtureEvent("entity1", "check1") + event := corev2.FixtureEvent("entity1", "check1") eventJSON, _ := json.Marshal(event) _, err := file.WriteString(string(eventJSON)) require.NoError(t, err) require.NoError(t, file.Sync()) _, err = file.Seek(0, 0) require.NoError(t, err) - stdin = file + os.Stdin = file requestReceived := false var apiStub = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {