blob: 12c5d2b8689bb9a56a3806bef21b422496eef30a [file] [log] [blame]
package notifier
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"cloud.google.com/go/pubsub"
"go.skia.org/infra/email/go/emailclient"
"go.skia.org/infra/go/chatbot"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/issues"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
const (
emailFromAddress = "noreply@skia.org"
)
// Notifier is an interface used for sending notifications from an AutoRoller.
type Notifier interface {
// Send the given message to the given thread. This should be safe to
// run in a goroutine.
Send(ctx context.Context, thread string, msg *Message) error
}
// Config provides configuration for a Notifier.
type Config struct {
// Required fields.
// Configuration for filtering out messages. Exactly one of these should
// be specified.
Filter string `json:"filter,omitempty"`
IncludeMsgTypes []string `json:"includeMsgTypes,omitempty"`
// Exactly one of these should be specified.
Email *EmailNotifierConfig `json:"email,omitempty"`
Chat *ChatNotifierConfig `json:"chat,omitempty"`
Monorail *MonorailNotifierConfig `json:"monorail,omitempty"`
PubSub *PubSubNotifierConfig `json:"pubsub,omitempty"`
// Optional fields.
// If present, all messages inherit this subject line.
Subject string `json:"subject,omitempty"`
}
// Validate the Config.
func (c *Config) Validate() error {
if c.Filter == "" && c.IncludeMsgTypes == nil {
return errors.New("Either Filter or IncludeMsgTypes is required.")
}
if c.Filter != "" && c.IncludeMsgTypes != nil {
return errors.New("Only one of Filter or IncludeMsgTypes may be provided.")
}
if c.Filter != "" {
if _, err := ParseFilter(c.Filter); err != nil {
return err
}
}
n := []util.Validator{}
if c.Email != nil {
n = append(n, c.Email)
}
if c.Chat != nil {
n = append(n, c.Chat)
}
if c.PubSub != nil {
n = append(n, c.PubSub)
}
if c.Monorail != nil {
n = append(n, c.Monorail)
}
if len(n) != 1 {
return fmt.Errorf("Exactly one notification config must be supplied, but got %d", len(n))
}
return n[0].Validate()
}
// Create a Notifier from the Config.
func (c *Config) Create(ctx context.Context, client *http.Client, emailer emailclient.Client, chatBotConfigReader chatbot.ConfigReader) (Notifier, Filter, []string, string, error) {
if err := c.Validate(); err != nil {
return nil, FILTER_SILENT, nil, "", err
}
filter, err := ParseFilter(c.Filter)
if err != nil {
return nil, FILTER_SILENT, nil, "", err
}
var n Notifier
if c.Email != nil {
n, err = EmailNotifier(c.Email.Emails, emailer, "")
} else if c.Chat != nil {
n, err = ChatNotifier(c.Chat.RoomID, chatBotConfigReader)
} else if c.PubSub != nil {
n, err = PubSubNotifier(ctx, c.PubSub.Topic)
} else if c.Monorail != nil {
n, err = MonorailNotifier(client, c.Monorail.Project, c.Monorail.Owner, c.Monorail.CC, c.Monorail.Components, c.Monorail.Labels)
} else {
return nil, FILTER_SILENT, nil, "", fmt.Errorf("No config specified!")
}
if err != nil {
return nil, FILTER_SILENT, nil, "", err
}
return n, filter, c.IncludeMsgTypes, c.Subject, nil
}
// Create a copy of this Config.
func (c *Config) Copy() *Config {
configCopy := &Config{
Filter: c.Filter,
IncludeMsgTypes: util.CopyStringSlice(c.IncludeMsgTypes),
Subject: c.Subject,
}
if c.Email != nil {
configCopy.Email = &EmailNotifierConfig{
Emails: util.CopyStringSlice(c.Email.Emails),
}
}
if c.Chat != nil {
configCopy.Chat = &ChatNotifierConfig{
RoomID: c.Chat.RoomID,
}
}
if c.PubSub != nil {
configCopy.PubSub = &PubSubNotifierConfig{
Topic: c.PubSub.Topic,
}
}
if c.Monorail != nil {
configCopy.Monorail = &MonorailNotifierConfig{
Project: c.Monorail.Project,
Owner: c.Monorail.Owner,
CC: util.CopyStringSlice(c.Monorail.CC),
Components: util.CopyStringSlice(c.Monorail.Components),
Labels: util.CopyStringSlice(c.Monorail.Labels),
}
}
return configCopy
}
// Configuration for EmailNotifier.
type EmailNotifierConfig struct {
// List of email addresses to notify. Required.
Emails []string `json:"emails"`
}
// Validate the EmailNotifierConfig.
func (c *EmailNotifierConfig) Validate() error {
if c.Emails == nil || len(c.Emails) == 0 {
return fmt.Errorf("Emails is required.")
}
return nil
}
// emailNotifier is a Notifier implementation which sends email to interested
// parties.
type emailNotifier struct {
from string
emailer emailclient.Client
markup string
to []string
}
// See documentation for Notifier interface.
func (n *emailNotifier) Send(_ context.Context, subject string, msg *Message) error {
if !n.emailer.Valid() {
sklog.Warning("No gmail API client; cannot send email!")
return nil
}
// Replace all newlines with <br/> since gmail uses HTML format.
body := strings.ReplaceAll(msg.Body, "\n", "<br/>")
recipients := append(util.CopyStringSlice(n.to), msg.ExtraRecipients...)
sklog.Infof("Sending email to %s: %s", strings.Join(recipients, ","), subject)
_, err := n.emailer.SendWithMarkup("", n.from, recipients, subject, body, n.markup, "")
return err
}
// EmailNotifier returns a Notifier which sends email to interested parties.
// Sends the same ViewAction markup with each message.
func EmailNotifier(emails []string, emailer emailclient.Client, markup string) (Notifier, error) {
return &emailNotifier{
from: emailFromAddress,
emailer: emailer,
markup: markup,
to: emails,
}, nil
}
// Configuration for ChatNotifier.
type ChatNotifierConfig struct {
RoomID string `json:"room"`
}
// Validate the ChatNotifierConfig.
func (c *ChatNotifierConfig) Validate() error {
if c.RoomID == "" {
return fmt.Errorf("RoomID is required.")
}
return nil
}
// chatNotifier is a Notifier implementation which sends chat messages.
type chatNotifier struct {
configReader chatbot.ConfigReader
roomId string
}
// See documentation for Notifier interface.
func (n *chatNotifier) Send(_ context.Context, thread string, msg *Message) error {
return chatbot.SendUsingConfig(msg.Body, n.roomId, thread, n.configReader)
}
// ChatNotifier returns a Notifier which sends email to interested parties.
func ChatNotifier(roomId string, configReader chatbot.ConfigReader) (Notifier, error) {
return &chatNotifier{
configReader: configReader,
roomId: roomId,
}, nil
}
// Configuration for a PubSubNotifier.
type PubSubNotifierConfig struct {
Topic string `json:"topic"`
}
// Validate the PubSubNotifierConfig.
func (c *PubSubNotifierConfig) Validate() error {
if c.Topic == "" {
return errors.New("Topic is required.")
}
return nil
}
// pubSubNotifier is a Notifier implementation which sends pub/sub messages.
type pubSubNotifier struct {
topic *pubsub.Topic
}
// See documentation for Notifier interface.
func (n *pubSubNotifier) Send(ctx context.Context, subject string, msg *Message) error {
res := n.topic.Publish(ctx, &pubsub.Message{
Attributes: map[string]string{
"severity": msg.Severity.String(),
"subject": subject,
},
Data: []byte(msg.Body),
})
_, err := res.Get(ctx)
return err
}
// PubSubNotifier returns a Notifier which sends messages via PubSub.
func PubSubNotifier(ctx context.Context, topic string) (Notifier, error) {
client, err := pubsub.NewClient(ctx, common.PROJECT_ID)
if err != nil {
return nil, err
}
// Create the topic if it doesn't exist.
t := client.Topic(topic)
if exists, err := t.Exists(ctx); err != nil {
return nil, err
} else if !exists {
t, err = client.CreateTopic(ctx, topic)
if err != nil {
return nil, err
}
}
return &pubSubNotifier{
topic: t,
}, nil
}
// Configuration for a MonorailNotifier.
type MonorailNotifierConfig struct {
// Project name under which to file bugs. Required.
Project string `json:"project"`
// Owner of bugs filed in Monorail. Required.
Owner string `json:"owner"`
// List of people to CC on bugs filed in Monorail. Optional.
CC []string `json:"cc,omitempty"`
// List of components to apply to the bugs. Optional.
Components []string `json:"components,omitempty"`
// List of labels to apply to bugs filed in Monorail. Optional.
Labels []string `json:"labels,omitempty"`
}
// Validate the MonorailNotifierConfig.
func (c *MonorailNotifierConfig) Validate() error {
if c.Project == "" {
return errors.New("Project is required.")
}
return nil
}
// monorailNotifier is a Notifier implementation which files Monorail issues.
type monorailNotifier struct {
tk issues.IssueTracker
cc []issues.MonorailPerson
components []string
labels []string
owner issues.MonorailPerson
}
// See documentation for Notifier interface.
func (n *monorailNotifier) Send(ctx context.Context, subject string, msg *Message) error {
req := issues.IssueRequest{
CC: n.cc,
Components: n.components,
Description: msg.Body,
Labels: n.labels,
Owner: n.owner,
Status: "New",
Summary: subject,
}
return n.tk.AddIssue(req)
}
// MonorailNotifier returns a Notifier which files bugs in Monorail.
func MonorailNotifier(c *http.Client, project, owner string, cc, components, labels []string) (Notifier, error) {
var personCC []issues.MonorailPerson
if cc != nil {
for _, name := range cc {
personCC = append(personCC, issues.MonorailPerson{
Name: name,
})
}
}
return &monorailNotifier{
tk: issues.NewMonorailIssueTracker(c, project),
cc: personCC,
components: components,
labels: labels,
owner: issues.MonorailPerson{
Name: owner,
},
}, nil
}