github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/notify/notify.go (about) 1 package notify 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "net/mail" 7 "os" 8 "path" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/evergreen-ci/evergreen" 14 "github.com/evergreen-ci/evergreen/model" 15 "github.com/evergreen-ci/evergreen/model/build" 16 "github.com/evergreen-ci/evergreen/model/patch" 17 "github.com/evergreen-ci/evergreen/model/task" 18 "github.com/evergreen-ci/evergreen/model/user" 19 "github.com/evergreen-ci/evergreen/model/version" 20 "github.com/evergreen-ci/evergreen/util" 21 "github.com/evergreen-ci/evergreen/web" 22 "github.com/mongodb/grip" 23 "github.com/pkg/errors" 24 "gopkg.in/yaml.v2" 25 ) 26 27 const ( 28 // number of times to try sending a notification 29 NumSmtpRetries = 3 30 31 // period to sleep between tries 32 SmtpSleepTime = 1 * time.Second 33 34 // smtp port to connect to 35 SmtpPort = 25 36 37 // smtp relay host to connect to 38 SmtpServer = "localhost" 39 40 // Thus on each run of the notifier, we check if the last notification time 41 // (LNT) is within this window. If it is, then we use the retrieved LNT. 42 // If not, we use the current time 43 LNRWindow = 60 * time.Minute 44 45 // MCI ops notification prefaces 46 ProvisionFailurePreface = "[PROVISION-FAILURE]" 47 ProvisionTimeoutPreface = "[PROVISION-TIMEOUT]" 48 ProvisionLatePreface = "[PROVISION-LATE]" 49 TeardownFailurePreface = "[TEARDOWN-FAILURE]" 50 51 // repotracker notification prefaces 52 RepotrackerFailurePreface = "[REPOTRACKER-FAILURE %v] on %v" 53 54 // branch name to use if the project reference is not found 55 UnknownProjectBranch = "" 56 57 DefaultNotificationsConfig = "/etc/mci-notifications.yml" 58 ) 59 60 var ( 61 // notifications can be either build-level or task-level 62 // we might subsequently want to submit js test notifications 63 // within each task 64 buildType = "build" 65 taskType = "task" 66 67 // notification key types 68 buildFailureKey = "build_failure" 69 buildSuccessKey = "build_success" 70 buildSuccessToFailureKey = "build_success_to_failure" 71 buildCompletionKey = "build_completion" 72 taskFailureKey = "task_failure" 73 taskSuccessKey = "task_success" 74 taskSuccessToFailureKey = "task_success_to_failure" 75 taskCompletionKey = "task_completion" 76 taskFailureKeys = []string{taskFailureKey, taskSuccessToFailureKey} 77 buildFailureKeys = []string{buildFailureKey, buildSuccessToFailureKey} 78 79 // notification subjects 80 failureSubject = "failed" 81 completionSubject = "completed" 82 successSubject = "succeeded" 83 transitionSubject = "transitioned to failure" 84 85 // task/build status notification prefaces 86 mciSuccessPreface = "[MCI-SUCCESS %v]" 87 mciFailurePreface = "[MCI-FAILURE %v]" 88 mciCompletionPreface = "[MCI-COMPLETION %v]" 89 90 // patch notification prefaces 91 patchSuccessPreface = "[PATCH-SUCCESS %v]" 92 patchFailurePreface = "[PATCH-FAILURE %v]" 93 patchCompletionPreface = "[PATCH-COMPLETION %v]" 94 95 buildNotificationHandler = BuildNotificationHandler{buildType} 96 taskNotificationHandler = TaskNotificationHandler{taskType} 97 98 Handlers = map[string]NotificationHandler{ 99 buildFailureKey: &BuildFailureHandler{buildNotificationHandler, buildFailureKey}, 100 buildSuccessKey: &BuildSuccessHandler{buildNotificationHandler, buildSuccessKey}, 101 buildCompletionKey: &BuildCompletionHandler{buildNotificationHandler, buildCompletionKey}, 102 buildSuccessToFailureKey: &BuildSuccessToFailureHandler{buildNotificationHandler, buildSuccessToFailureKey}, 103 taskFailureKey: &TaskFailureHandler{taskNotificationHandler, taskFailureKey}, 104 taskSuccessKey: &TaskSuccessHandler{taskNotificationHandler, taskSuccessKey}, 105 taskCompletionKey: &TaskCompletionHandler{taskNotificationHandler, taskCompletionKey}, 106 taskSuccessToFailureKey: &TaskSuccessToFailureHandler{taskNotificationHandler, taskSuccessToFailureKey}, 107 } 108 ) 109 110 var ( 111 // These help us to limit the tasks/builds we pull 112 // from the database on each run of the notifier 113 lastProjectNotificationTime = make(map[string]time.Time) 114 newProjectNotificationTime = make(map[string]time.Time) 115 116 // Simple map for optimization 117 cachedProjectRecords = make(map[string]interface{}) 118 119 // Simple map for caching project refs 120 cachedProjectRef = make(map[string]*model.ProjectRef) 121 ) 122 123 func ConstructMailer(notifyConfig evergreen.NotifyConfig) Mailer { 124 if notifyConfig.SMTP != nil { 125 return SmtpMailer{ 126 notifyConfig.SMTP.From, 127 notifyConfig.SMTP.Server, 128 notifyConfig.SMTP.Port, 129 notifyConfig.SMTP.UseSSL, 130 notifyConfig.SMTP.Username, 131 notifyConfig.SMTP.Password, 132 } 133 } else { 134 return SmtpMailer{ 135 Server: SmtpServer, 136 Port: SmtpPort, 137 UseSSL: false, 138 } 139 } 140 } 141 142 // This function is responsible for running the notifications pipeline 143 // 144 // ParseNotifications 145 // ↓↓ 146 // ValidateNotifications 147 // ↓↓ 148 // ProcessNotifications 149 // ↓↓ 150 // SendNotifications 151 // ↓↓ 152 // UpdateNotificationTimes 153 // 154 func Run(settings *evergreen.Settings) error { 155 // get the notifications 156 mciNotification, err := ParseNotifications(settings.ConfigDir) 157 if err != nil { 158 grip.Errorf("parsing notifications: %+v", err) 159 return err 160 } 161 162 // validate the notifications 163 err = ValidateNotifications(mciNotification) 164 if err != nil { 165 grip.Errorf("validating notifications: %+v", err) 166 return err 167 } 168 169 templateGlobals := map[string]interface{}{ 170 "UIRoot": settings.Ui.Url, 171 } 172 173 ae, err := createEnvironment(settings, templateGlobals) 174 if err != nil { 175 return err 176 } 177 178 // process the notifications 179 emails, err := ProcessNotifications(ae, mciNotification, true) 180 if err != nil { 181 grip.Errorf("processing notifications: %+v", err) 182 return err 183 } 184 185 // Remnants are outstanding build/task notifications that 186 // we couldn't process on a prior run of the notifier 187 // Currently, this can only happen if an administrator 188 // bumps up the priority of a build/task 189 190 // send the notifications 191 192 err = SendNotifications(settings, mciNotification, emails, 193 ConstructMailer(settings.Notify)) 194 if err != nil { 195 grip.Errorln("Error sending notifications:", err) 196 return err 197 } 198 199 // update notification times 200 err = UpdateNotificationTimes() 201 if err != nil { 202 grip.Errorln("Error updating notification times:", err) 203 return err 204 } 205 return nil 206 } 207 208 // This function is responsible for reading the notifications file 209 func ParseNotifications(configName string) (*MCINotification, error) { 210 grip.Info("Parsing notifications...") 211 212 evgHome := evergreen.FindEvergreenHome() 213 configs := []string{ 214 filepath.Join(evgHome, configName, evergreen.NotificationsFile), 215 filepath.Join(evgHome, evergreen.NotificationsFile), 216 DefaultNotificationsConfig, 217 } 218 219 var notificationsFile string 220 for _, fn := range configs { 221 if _, err := os.Stat(fn); os.IsNotExist(err) { 222 continue 223 } 224 225 notificationsFile = fn 226 } 227 228 data, err := ioutil.ReadFile(notificationsFile) 229 if err != nil { 230 return nil, err 231 } 232 233 // unmarshal file contents into MCINotification struct 234 mciNotification := &MCINotification{} 235 236 err = yaml.Unmarshal(data, mciNotification) 237 if err != nil { 238 return nil, errors.Wrapf(err, "Parse error unmarshalling notifications %v", notificationsFile) 239 } 240 return mciNotification, nil 241 } 242 243 // This function is responsible for validating the notifications file 244 func ValidateNotifications(mciNotification *MCINotification) error { 245 grip.Info("Validating notifications...") 246 allNotifications := []string{} 247 248 projectNameToBuildVariants, err := findProjectBuildVariants() 249 if err != nil { 250 return errors.Wrap(err, "Error loading project build variants") 251 } 252 253 // Validate default notification recipients 254 for _, notification := range mciNotification.Notifications { 255 if notification.Project == "" { 256 return errors.Errorf("Must specify a project for each notification - see %v", notification.Name) 257 } 258 259 buildVariants, ok := projectNameToBuildVariants[notification.Project] 260 if !ok { 261 return errors.Errorf("Notifications validation failed: "+ 262 "project `%v` not found", notification.Project) 263 } 264 265 // ensure all supplied build variants are valid 266 for _, buildVariant := range notification.SkipVariants { 267 if !util.SliceContains(buildVariants, buildVariant) { 268 return errors.Errorf("Nonexistent buildvariant - ”%v” - specified for ”%v” notification", buildVariant, notification.Name) 269 } 270 } 271 272 allNotifications = append(allNotifications, notification.Name) 273 } 274 275 // Validate team name and addresses 276 for _, team := range mciNotification.Teams { 277 if team.Name == "" { 278 return errors.Errorf("Each notification team must have a name") 279 } 280 281 for _, subscription := range team.Subscriptions { 282 for _, notification := range subscription.NotifyOn { 283 if !util.SliceContains(allNotifications, notification) { 284 return errors.Errorf("Team ”%v” contains a non-existent subscription - %v", team.Name, notification) 285 } 286 } 287 for _, buildVariant := range subscription.SkipVariants { 288 buildVariants, ok := projectNameToBuildVariants[subscription.Project] 289 if !ok { 290 return errors.Errorf("Teams validation failed: project `%v` not found", subscription.Project) 291 } 292 293 if !util.SliceContains(buildVariants, buildVariant) { 294 return errors.Errorf("Nonexistent buildvariant - ”%v” - specified for team ”%v” ", buildVariant, team.Name) 295 } 296 } 297 } 298 } 299 300 // Validate patch notifications 301 for _, subscription := range mciNotification.PatchNotifications { 302 for _, notification := range subscription.NotifyOn { 303 if !util.SliceContains(allNotifications, notification) { 304 return errors.Errorf("Nonexistent patch notification - ”%v” - specified", notification) 305 } 306 } 307 } 308 309 // validate the patch notification buildvariatns 310 for _, subscription := range mciNotification.PatchNotifications { 311 buildVariants, ok := projectNameToBuildVariants[subscription.Project] 312 if !ok { 313 return errors.Errorf("Patch notification build variants validation failed: "+ 314 "project `%v` not found", subscription.Project) 315 } 316 317 for _, buildVariant := range subscription.SkipVariants { 318 if !util.SliceContains(buildVariants, buildVariant) { 319 return errors.Errorf("Nonexistent buildvariant - ”%v” - specified for patch notifications", buildVariant) 320 } 321 } 322 } 323 324 // all good! 325 return nil 326 } 327 328 // This function is responsible for all notifications processing 329 func ProcessNotifications(ae *web.App, mciNotification *MCINotification, updateTimes bool) (map[NotificationKey][]Email, error) { 330 // create MCI notifications 331 allNotificationsSlice := notificationsToStruct(mciNotification) 332 333 // get the last notification time for all projects 334 if updateTimes { 335 err := getLastProjectNotificationTime(allNotificationsSlice) 336 if err != nil { 337 return nil, err 338 } 339 } 340 341 grip.Info("Processing notifications...") 342 343 emails := make(map[NotificationKey][]Email) 344 for _, key := range allNotificationsSlice { 345 emailsForKey, err := Handlers[key.NotificationName].GetNotifications(ae, &key) 346 if err != nil { 347 grip.Infof("Error processing %s on %s: %+v", key.NotificationName, key.Project, err) 348 continue 349 } 350 emails[key] = emailsForKey 351 } 352 353 return emails, nil 354 } 355 356 // This function is responsible for managing the sending triggered email notifications 357 func SendNotifications(settings *evergreen.Settings, mciNotification *MCINotification, 358 emails map[NotificationKey][]Email, mailer Mailer) (err error) { 359 grip.Info("Sending notifications...") 360 361 // parse all notifications, sending it to relevant recipients 362 for _, notification := range mciNotification.Notifications { 363 key := NotificationKey{ 364 Project: notification.Project, 365 NotificationName: notification.Name, 366 NotificationType: getType(notification.Name), 367 NotificationRequester: evergreen.RepotrackerVersionRequester, 368 } 369 370 for _, recipient := range notification.Recipients { 371 // send all triggered notifications 372 for _, email := range emails[key] { 373 374 // determine if this notification should be skipped - based on the buildvariant 375 if email.ShouldSkip(notification.SkipVariants) { 376 continue 377 } 378 379 // send to individual subscriber, or the admin team if it's not their fault 380 recipients := []string{} 381 if settings.Notify.SMTP != nil { 382 recipients = settings.Notify.SMTP.AdminEmail 383 } 384 if !email.IsLikelySystemFailure() { 385 recipients = email.GetRecipients(recipient) 386 } 387 err = TrySendNotification(recipients, email.GetSubject(), email.GetBody(), mailer) 388 if err != nil { 389 grip.Errorf("Unable to send individual notification %#v: %+v", key, err) 390 continue 391 } 392 } 393 } 394 } 395 396 // Send team subscribed notifications 397 for _, team := range mciNotification.Teams { 398 for _, subscription := range team.Subscriptions { 399 for _, name := range subscription.NotifyOn { 400 key := NotificationKey{ 401 Project: subscription.Project, 402 NotificationName: name, 403 NotificationType: getType(name), 404 NotificationRequester: evergreen.RepotrackerVersionRequester, 405 } 406 407 // send all triggered notifications for this key 408 for _, email := range emails[key] { 409 // determine if this notification should be skipped - based on the buildvariant 410 if email.ShouldSkip(subscription.SkipVariants) { 411 continue 412 } 413 414 teamEmail := fmt.Sprintf("%v <%v>", team.Name, team.Address) 415 err = TrySendNotification([]string{teamEmail}, email.GetSubject(), email.GetBody(), mailer) 416 if err != nil { 417 grip.Errorf("Unable to send notification %#v: %v", key, err) 418 continue 419 } 420 } 421 } 422 } 423 } 424 425 // send patch notifications 426 /* XXX temporarily disable patch notifications 427 for _, subscription := range mciNotification.PatchNotifications { 428 for _, notification := range subscription.NotifyOn { 429 key := NotificationKey{ 430 Project: subscription.Project, 431 NotificationName: notification, 432 NotificationType: getType(notification), 433 NotificationRequester: evergreen.PatchVersionRequester, 434 } 435 436 for _, email := range emails[key] { 437 // determine if this notification should be skipped - 438 // based on the buildvariant 439 if email.ShouldSkip(subscription.SkipVariants) { 440 continue 441 } 442 443 // send to the appropriate patch requester 444 for _, changeInfo := range email.GetChangeInfo() { 445 // send notification to each member of the blamelist 446 patchRequester := fmt.Sprintf("%v <%v>", changeInfo.Author, 447 changeInfo.Email) 448 err = TrySendNotification([]string{patchRequester}, 449 email.GetSubject(), email.GetBody(), mailer) 450 if err != nil { 451 grip.Errorf("Unable to send notification %#v: %+v", key, err) 452 continue 453 } 454 } 455 } 456 } 457 }*/ 458 459 return nil 460 } 461 462 // This stores the last time threshold after which 463 // we search for possible new notification events 464 func UpdateNotificationTimes() (err error) { 465 grip.Info("Updating notification times...") 466 for project, time := range newProjectNotificationTime { 467 grip.Infof("Updating %s notification time...", project) 468 err = model.SetLastNotificationsEventTime(project, time) 469 if err != nil { 470 return err 471 } 472 } 473 return nil 474 } 475 476 //***********************************\/ 477 // Notification Helper Functions \/ 478 //***********************************\/ 479 480 // Construct a map of project names to build variants for that project 481 func findProjectBuildVariants() (map[string][]string, error) { 482 projectNameToBuildVariants := make(map[string][]string) 483 484 allProjects, err := model.FindAllTrackedProjectRefs() 485 if err != nil { 486 return nil, err 487 } 488 489 for _, projectRef := range allProjects { 490 if !projectRef.Enabled { 491 continue 492 } 493 var buildVariants []string 494 var proj *model.Project 495 var err error 496 if projectRef.LocalConfig != "" { 497 proj, err = model.FindProject("", &projectRef) 498 if err != nil { 499 return nil, errors.Wrap(err, "unable to find project file") 500 } 501 } else { 502 lastGood, err := version.FindOne(version.ByLastKnownGoodConfig(projectRef.Identifier)) 503 if err != nil { 504 return nil, errors.Wrap(err, "unable to find last valid config") 505 } 506 if lastGood == nil { // brand new project + no valid config yet, just return an empty map 507 return projectNameToBuildVariants, nil 508 } 509 510 proj = &model.Project{} 511 err = model.LoadProjectInto([]byte(lastGood.Config), projectRef.Identifier, proj) 512 if err != nil { 513 return nil, errors.Wrapf(err, "error loading project '%v' from version", 514 projectRef.Identifier) 515 } 516 } 517 518 for _, buildVariant := range proj.BuildVariants { 519 buildVariants = append(buildVariants, buildVariant.Name) 520 } 521 522 projectNameToBuildVariants[projectRef.Identifier] = buildVariants 523 } 524 525 return projectNameToBuildVariants, nil 526 } 527 528 // construct the change information 529 // struct from a given version struct 530 func constructChangeInfo(v *version.Version, notification *NotificationKey) (changeInfo *ChangeInfo) { 531 changeInfo = &ChangeInfo{} 532 switch notification.NotificationRequester { 533 case evergreen.RepotrackerVersionRequester: 534 changeInfo.Project = v.Identifier 535 changeInfo.Author = v.Author 536 changeInfo.Message = v.Message 537 changeInfo.Revision = v.Revision 538 changeInfo.Email = v.AuthorEmail 539 540 case evergreen.PatchVersionRequester: 541 // get the author and description from the patch request 542 patch, err := patch.FindOne(patch.ByVersion(v.Id)) 543 if err != nil { 544 grip.Errorf("Error finding patch for version %s: %+v", v.Id, err) 545 return 546 } 547 548 if patch == nil { 549 grip.Errorln(notification, "notification was unable to locate patch with version:", v.Id) 550 return 551 } 552 // get the display name and email for this user 553 dbUser, err := user.FindOne(user.ById(patch.Author)) 554 if err != nil { 555 grip.Errorf("Error finding user %s: %+v", patch.Author, err) 556 changeInfo.Author = patch.Author 557 changeInfo.Email = patch.Author 558 } else if dbUser == nil { 559 grip.Errorf("User %s not found", patch.Author) 560 changeInfo.Author = patch.Author 561 changeInfo.Email = patch.Author 562 } else { 563 changeInfo.Email = dbUser.Email() 564 changeInfo.Author = dbUser.DisplayName() 565 } 566 567 changeInfo.Project = patch.Project 568 changeInfo.Message = patch.Description 569 changeInfo.Revision = patch.Id.Hex() 570 } 571 return 572 } 573 574 // use mail's rfc2047 to encode any string 575 func encodeRFC2047(String string) string { 576 addr := mail.Address{ 577 Name: String, 578 Address: "", 579 } 580 return strings.Trim(addr.String(), " <>") 581 } 582 583 // get the display name for a build variant's 584 // task given the build variant name 585 func getDisplayName(buildVariant string) (displayName string) { 586 build, err := build.FindOne(build.ByVariant(buildVariant)) 587 if err != nil || build == nil { 588 grip.Error(errors.Wrap(err, "Error fetching buildvariant name")) 589 displayName = buildVariant 590 } else { 591 displayName = build.DisplayName 592 } 593 return 594 } 595 596 // get the failed task(s) for a given build 597 func getFailedTasks(current *build.Build, notificationName string) (failedTasks []build.TaskCache) { 598 if util.SliceContains(buildFailureKeys, notificationName) { 599 for _, t := range current.Tasks { 600 if t.Status == evergreen.TaskFailed { 601 failedTasks = append(failedTasks, t) 602 } 603 } 604 } 605 return 606 } 607 608 // get the specific failed test(s) for this task 609 func getFailedTests(current *task.Task, notificationName string) (failedTests []task.TestResult) { 610 if util.SliceContains(taskFailureKeys, notificationName) { 611 for _, test := range current.TestResults { 612 if test.Status == "fail" { 613 // get the base name for windows/non-windows paths 614 test.TestFile = path.Base(strings.Replace(test.TestFile, "\\", "/", -1)) 615 failedTests = append(failedTests, test) 616 } 617 } 618 } 619 620 return 621 } 622 623 // gets the project ref project name corresponding to this identifier 624 func getProjectRef(identifier string) (projectRef *model.ProjectRef, 625 err error) { 626 if cachedProjectRef[identifier] == nil { 627 projectRef, err = model.FindOneProjectRef(identifier) 628 if err != nil { 629 return 630 } 631 cachedProjectRecords[identifier] = projectRef 632 return projectRef, nil 633 } 634 return cachedProjectRef[identifier], nil 635 } 636 637 // This gets the time threshold - events before which 638 // we searched for possible notification events 639 func getLastProjectNotificationTime(keys []NotificationKey) error { 640 for _, key := range keys { 641 lastNotificationTime, err := model.LastNotificationsEventTime(key.Project) 642 if err != nil { 643 return errors.WithStack(err) 644 } 645 if lastNotificationTime.Before(time.Now().Add(-LNRWindow)) { 646 lastNotificationTime = time.Now() 647 } 648 lastProjectNotificationTime[key.Project] = lastNotificationTime 649 newProjectNotificationTime[key.Project] = time.Now() 650 } 651 return nil 652 } 653 654 // This is used to pull recently finished builds 655 func getRecentlyFinishedBuilds(notificationKey *NotificationKey) (builds []build.Build, err error) { 656 if cachedProjectRecords[notificationKey.String()] == nil { 657 builds, err = build.Find(build.ByFinishedAfter(lastProjectNotificationTime[notificationKey.Project], notificationKey.Project, notificationKey.NotificationRequester)) 658 if err != nil { 659 return nil, errors.WithStack(err) 660 } 661 cachedProjectRecords[notificationKey.String()] = builds 662 return builds, errors.WithStack(err) 663 } 664 return cachedProjectRecords[notificationKey.String()].([]build.Build), nil 665 } 666 667 // This is used to pull recently finished tasks 668 func getRecentlyFinishedTasks(notificationKey *NotificationKey) (tasks []task.Task, err error) { 669 if cachedProjectRecords[notificationKey.String()] == nil { 670 671 tasks, err = task.Find(task.ByRecentlyFinished(lastProjectNotificationTime[notificationKey.Project], 672 notificationKey.Project, notificationKey.NotificationRequester)) 673 if err != nil { 674 return nil, errors.WithStack(err) 675 } 676 cachedProjectRecords[notificationKey.String()] = tasks 677 return tasks, errors.WithStack(err) 678 } 679 return cachedProjectRecords[notificationKey.String()].([]task.Task), nil 680 } 681 682 // gets the type of notification - we support build/task level notification 683 func getType(notification string) (nkType string) { 684 nkType = taskType 685 if strings.Contains(notification, buildType) { 686 nkType = buildType 687 } 688 return 689 } 690 691 // creates/returns slice of 'relevant' NotificationKeys a 692 // notification is relevant if it has at least one recipient 693 func notificationsToStruct(mciNotification *MCINotification) (notifyOn []NotificationKey) { 694 // Get default notifications 695 for _, notification := range mciNotification.Notifications { 696 if len(notification.Recipients) != 0 { 697 // flag the notification as needed 698 key := NotificationKey{ 699 Project: notification.Project, 700 NotificationName: notification.Name, 701 NotificationType: getType(notification.Name), 702 NotificationRequester: evergreen.RepotrackerVersionRequester, 703 } 704 705 // prevent duplicate notifications from being sent 706 if !util.SliceContains(notifyOn, key) { 707 notifyOn = append(notifyOn, key) 708 } 709 } 710 } 711 712 // Get team notifications 713 for _, team := range mciNotification.Teams { 714 for _, subscription := range team.Subscriptions { 715 for _, name := range subscription.NotifyOn { 716 key := NotificationKey{ 717 Project: subscription.Project, 718 NotificationName: name, 719 NotificationType: getType(name), 720 NotificationRequester: evergreen.RepotrackerVersionRequester, 721 } 722 723 // prevent duplicate notifications from being sent 724 if !util.SliceContains(notifyOn, key) { 725 notifyOn = append(notifyOn, key) 726 } 727 } 728 } 729 } 730 731 // Get patch notifications 732 for _, subscription := range mciNotification.PatchNotifications { 733 for _, notification := range subscription.NotifyOn { 734 key := NotificationKey{ 735 Project: subscription.Project, 736 NotificationName: notification, 737 NotificationType: getType(notification), 738 NotificationRequester: evergreen.PatchVersionRequester, 739 } 740 741 // prevent duplicate notifications from being sent 742 if !util.SliceContains(notifyOn, key) { 743 notifyOn = append(notifyOn, key) 744 } 745 } 746 } 747 return 748 } 749 750 // NotifyAdmins is a helper method to send a notification to the MCI admin team 751 func NotifyAdmins(subject, message string, settings *evergreen.Settings) error { 752 if settings.Notify.SMTP != nil { 753 return TrySendNotification(settings.Notify.SMTP.AdminEmail, 754 subject, message, ConstructMailer(settings.Notify)) 755 } 756 err := errors.New("Cannot notify admins: admin_email not set") 757 grip.Error(err) 758 return err 759 } 760 761 // String method for notification key 762 func (nk NotificationKey) String() string { 763 return fmt.Sprintf("%v-%v-%v", nk.Project, nk.NotificationType, nk.NotificationRequester) 764 } 765 766 // Helper function to send notifications 767 func TrySendNotification(recipients []string, subject, body string, mailer Mailer) (err error) { 768 // grip.Debugf("address: %s subject: %s body: %s", recipients, subject, body) 769 // return nil 770 _, err = util.Retry(func() error { 771 err = mailer.SendMail(recipients, subject, body) 772 if err != nil { 773 grip.Errorln("Error sending notification:", err) 774 return util.RetriableError{err} 775 } 776 return nil 777 }, NumSmtpRetries, SmtpSleepTime) 778 return errors.WithStack(err) 779 } 780 781 // Helper function to send notification to a given user 782 func TrySendNotificationToUser(userId string, subject, body string, mailer Mailer) error { 783 dbUser, err := user.FindOne(user.ById(userId)) 784 if err != nil { 785 return errors.Wrapf(err, "Error finding user %v", userId) 786 } else if dbUser == nil { 787 return errors.Errorf("User %v not found", userId) 788 } else { 789 return errors.WithStack(TrySendNotification([]string{dbUser.Email()}, subject, body, mailer)) 790 } 791 } 792 793 //***************************\/ 794 // Notification Structs \/ 795 //***************************\/ 796 797 // stores supported notifications 798 type Notification struct { 799 Name string `yaml:"name"` 800 Project string `yaml:"project"` 801 Recipients []string `yaml:"recipients"` 802 SkipVariants []string `yaml:"skip_variants"` 803 } 804 805 // stores notifications subscription for a team 806 type Subscription struct { 807 Project string `yaml:"project"` 808 SkipVariants []string `yaml:"skip_variants"` 809 NotifyOn []string `yaml:"notify_on"` 810 } 811 812 // stores 10gen team information 813 type Team struct { 814 Name string `yaml:"name"` 815 Address string `yaml:"address"` 816 Subscriptions []Subscription `yaml:"subscriptions"` 817 } 818 819 // store notifications file 820 type MCINotification struct { 821 Notifications []Notification `yaml:"notifications"` 822 Teams []Team `yaml:"teams"` 823 PatchNotifications []Subscription `yaml:"patch_notifications"` 824 } 825 826 // stores high level notifications key 827 type NotificationKey struct { 828 Project string 829 NotificationName string 830 NotificationType string 831 NotificationRequester string 832 } 833 834 // stores information pertaining to a repository's changes 835 type ChangeInfo struct { 836 Revision string 837 Author string 838 Email string 839 Pushtime string 840 Project string 841 Message string 842 }