| 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 |
| |
| } |