github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/alerts/alerts.go (about) 1 package alerts 2 3 import ( 4 "fmt" 5 "path/filepath" 6 7 "github.com/evergreen-ci/evergreen" 8 "github.com/evergreen-ci/evergreen/model" 9 "github.com/evergreen-ci/evergreen/model/alert" 10 "github.com/evergreen-ci/evergreen/model/build" 11 "github.com/evergreen-ci/evergreen/model/host" 12 "github.com/evergreen-ci/evergreen/model/patch" 13 "github.com/evergreen-ci/evergreen/model/task" 14 "github.com/evergreen-ci/evergreen/model/user" 15 "github.com/evergreen-ci/evergreen/model/version" 16 "github.com/evergreen-ci/evergreen/thirdparty" 17 "github.com/evergreen-ci/render" 18 "github.com/mongodb/grip" 19 "github.com/pkg/errors" 20 "gopkg.in/mgo.v2/bson" 21 ) 22 23 const ( 24 EmailProvider = "email" 25 JiraProvider = "jira" 26 ) 27 28 // QueueProcessor handles looping over any unprocessed alerts in the queue and delivers them 29 type QueueProcessor struct { 30 config *evergreen.Settings 31 superUsersConfigs []model.AlertConfig 32 projectsCache map[string]*model.ProjectRef 33 render *render.Render 34 } 35 36 // Deliverer is an interface which handles the actual delivery of an alert. 37 // (e.g. sending an e-mail, posting to flowdock, etc) 38 type Deliverer interface { 39 Deliver(AlertContext, model.AlertConfig) error 40 } 41 42 // AlertContext is the set of full documents in the DB that are associated with the 43 // values found in a given AlertRequest 44 type AlertContext struct { 45 AlertRequest *alert.AlertRequest 46 ProjectRef *model.ProjectRef 47 Task *task.Task 48 Build *build.Build 49 Version *version.Version 50 Patch *patch.Patch 51 Host *host.Host 52 FailedTests []task.TestResult 53 Settings *evergreen.Settings 54 } 55 56 func (qp *QueueProcessor) Name() string { 57 return "alerter" 58 } 59 60 func (qp *QueueProcessor) Description() string { 61 return "build and enqueue failure notifications" 62 } 63 64 // loadAlertContext fetches details from the database for all documents that are associated with the 65 // AlertRequest. For example, it populates the task/build/version/project using the 66 // task/build/version/project ids in the alert requeset document. 67 func (qp *QueueProcessor) loadAlertContext(a *alert.AlertRequest) (*AlertContext, error) { 68 aCtx := &AlertContext{AlertRequest: a} 69 aCtx.Settings = qp.config 70 taskId, projectId, buildId, versionId := a.TaskId, a.ProjectId, a.BuildId, a.VersionId 71 patchId := a.PatchId 72 var err error 73 if len(a.HostId) > 0 { 74 aCtx.Host, err = host.FindOne(host.ById(a.HostId)) 75 if err != nil { 76 return nil, errors.WithStack(err) 77 } 78 } 79 // Fetch task if there's a task ID present; if we find one, populate build/version IDs from it 80 if len(taskId) > 0 { 81 aCtx.Task, err = task.FindOne(task.ById(taskId)) 82 if err != nil { 83 return nil, errors.WithStack(err) 84 } 85 if aCtx.Task != nil && aCtx.Task.Execution != a.Execution { 86 oldTaskId := fmt.Sprintf("%s_%v", taskId, a.Execution) 87 aCtx.Task, err = task.FindOneOld(task.ById(oldTaskId)) 88 if err != nil { 89 return nil, errors.WithStack(err) 90 } 91 } 92 93 if aCtx.Task != nil { 94 // override build and version ID with the ones this task belongs to 95 buildId = aCtx.Task.BuildId 96 versionId = aCtx.Task.Version 97 projectId = aCtx.Task.Project 98 aCtx.FailedTests = []task.TestResult{} 99 for _, test := range aCtx.Task.TestResults { 100 if test.Status == "fail" { 101 aCtx.FailedTests = append(aCtx.FailedTests, test) 102 } 103 } 104 } 105 } 106 107 // Fetch build if there's a build ID present; if we find one, populate version ID from it 108 if len(buildId) > 0 { 109 aCtx.Build, err = build.FindOne(build.ById(buildId)) 110 if err != nil { 111 return nil, errors.WithStack(err) 112 } 113 if aCtx.Build != nil { 114 versionId = aCtx.Build.Version 115 projectId = aCtx.Build.Project 116 } 117 } 118 if len(versionId) > 0 { 119 aCtx.Version, err = version.FindOne(version.ById(versionId)) 120 if err != nil { 121 return nil, errors.WithStack(err) 122 } 123 if aCtx.Version != nil { 124 projectId = aCtx.Version.Identifier 125 } 126 } 127 128 if len(patchId) > 0 { 129 if !patch.IsValidId(patchId) { 130 return nil, errors.Errorf("patch id '%s' is not an object id", patchId) 131 } 132 aCtx.Patch, err = patch.FindOne(patch.ById(patch.NewId(patchId)).Project(patch.ExcludePatchDiff)) 133 if err != nil { 134 return nil, errors.WithStack(err) 135 } 136 } else if aCtx.Version != nil { 137 // patch isn't in URL but the version in context has one, get it 138 aCtx.Patch, err = patch.FindOne(patch.ByVersion(aCtx.Version.Id).Project(patch.ExcludePatchDiff)) 139 if err != nil { 140 return nil, errors.WithStack(err) 141 } 142 } 143 144 // If there's a finalized patch loaded into context but not a version, load the version 145 // associated with the patch as the context's version. 146 if aCtx.Version == nil && aCtx.Patch != nil && aCtx.Patch.Version != "" { 147 aCtx.Version, err = version.FindOne(version.ById(aCtx.Patch.Version).WithoutFields(version.ConfigKey)) 148 if err != nil { 149 return nil, errors.WithStack(err) 150 } 151 } 152 153 if len(projectId) > 0 { 154 aCtx.ProjectRef, err = qp.findProject(projectId) 155 if err != nil { 156 return nil, errors.WithStack(err) 157 } 158 } 159 return aCtx, nil 160 } 161 162 // findProject is a wrapper around FindProjectRef that caches results by their ID to prevent 163 // redundantly querying for the same projectref over and over 164 // again. In the Run() method, we wipe the cache at the beginning of each 165 // run to avoid stale configurations. 166 func (qp *QueueProcessor) findProject(projectId string) (*model.ProjectRef, error) { 167 if qp.projectsCache == nil { // lazily initialize the cache 168 qp.projectsCache = map[string]*model.ProjectRef{} 169 } 170 if project, ok := qp.projectsCache[projectId]; ok { 171 return project, nil 172 } 173 project, err := model.FindOneProjectRef(projectId) 174 if err != nil { 175 return nil, errors.WithStack(err) 176 } 177 if project == nil { 178 return nil, nil 179 } 180 qp.projectsCache[projectId] = project 181 return project, nil 182 } 183 184 func (qp *QueueProcessor) newJIRAProvider(alertConf model.AlertConfig) (Deliverer, error) { 185 // load and validate "project" JSON value 186 projectField, ok := alertConf.Settings["project"] 187 if !ok { 188 return nil, errors.New("missing JIRA project field") 189 } 190 project, ok := projectField.(string) 191 if !ok { 192 return nil, errors.New("JIRA project name must be string") 193 } 194 issueField, ok := alertConf.Settings["issue"] 195 if !ok { 196 return nil, errors.New("missing JIRA issue field") 197 } 198 issue, ok := issueField.(string) 199 if !ok { 200 return nil, errors.New("JIRA issue type must be string") 201 } 202 // validate Evergreen settings 203 if (qp.config.Jira.Host == "") || qp.config.Jira.Username == "" || qp.config.Jira.Password == "" { 204 return nil, errors.New( 205 "invalid JIRA settings (ensure a 'jira' field exists in Evergreen settings)") 206 } 207 if qp.config.Ui.Url == "" { 208 return nil, errors.New("'ui.url' must be set in Evergreen settings") 209 } 210 handler := thirdparty.NewJiraHandler( 211 qp.config.Jira.Host, 212 qp.config.Jira.Username, 213 qp.config.Jira.Password, 214 ) 215 return &jiraDeliverer{ 216 project: project, 217 issueType: issue, 218 handler: &handler, 219 uiRoot: qp.config.Ui.Url, 220 }, nil 221 } 222 223 // getDeliverer returns the correct implementation of Deliverer according to the provider 224 // specified in a project's alerts configuration. 225 func (qp *QueueProcessor) getDeliverer(alertConf model.AlertConfig) (Deliverer, error) { 226 switch alertConf.Provider { 227 case JiraProvider: 228 return qp.newJIRAProvider(alertConf) 229 case EmailProvider: 230 return &EmailDeliverer{ 231 SMTPSettings{ 232 Server: qp.config.Alerts.SMTP.Server, 233 Port: qp.config.Alerts.SMTP.Port, 234 UseSSL: qp.config.Alerts.SMTP.UseSSL, 235 Username: qp.config.Alerts.SMTP.Username, 236 Password: qp.config.Alerts.SMTP.Password, 237 From: qp.config.Alerts.SMTP.From, 238 }, 239 qp.render, 240 }, nil 241 default: 242 return nil, errors.Errorf("unknown provider: %v", alertConf.Provider) 243 } 244 } 245 246 func (qp *QueueProcessor) Deliver(req *alert.AlertRequest, ctx *AlertContext) error { 247 var alertConfigs []model.AlertConfig 248 if ctx.ProjectRef != nil { 249 // Project-specific alert - use alert configs defined on the project 250 // TODO(EVG-223) patch alerts should go to patch owner 251 alertConfigs = ctx.ProjectRef.Alerts[req.Trigger] 252 } else if ctx.Host != nil { 253 // Host-specific alert - use superuser alert configs for now 254 // TODO(EVG-224) spawnhost alerts should go to spawnhost owner 255 alertConfigs = qp.superUsersConfigs 256 } 257 258 for _, alertConfig := range alertConfigs { 259 deliverer, err := qp.getDeliverer(alertConfig) 260 if err != nil { 261 return errors.Wrap(err, "Failed to get email deliverer") 262 } 263 err = deliverer.Deliver(*ctx, alertConfig) 264 if err != nil { 265 return errors.Wrap(err, "Failed to send alert") 266 } 267 } 268 return nil 269 } 270 271 // Run loops while there are any unprocessed alerts and attempts to deliver them. 272 func (qp *QueueProcessor) Run(config *evergreen.Settings) error { 273 grip.Info("Starting alert queue processor run") 274 home := evergreen.FindEvergreenHome() 275 qp.config = config 276 qp.projectsCache = map[string]*model.ProjectRef{} // wipe the project cache between each run to prevent stale configs. 277 qp.render = render.New(render.Options{ 278 Directory: filepath.Join(home, "alerts", "templates"), 279 DisableCache: !config.Ui.CacheTemplates, 280 TextFuncs: nil, 281 HtmlFuncs: nil, 282 }) 283 284 if len(qp.config.SuperUsers) == 0 { 285 grip.Warning("no superusers configured, some alerts may have no recipient") 286 } 287 superUsers, err := user.Find(user.ByIds(qp.config.SuperUsers...)) 288 if err != nil { 289 grip.Errorf("Error getting superuser list: %+v", err) 290 return err 291 } 292 qp.superUsersConfigs = []model.AlertConfig{} 293 for _, u := range superUsers { 294 qp.superUsersConfigs = append(qp.superUsersConfigs, model.AlertConfig{"email", bson.M{"rcpt": u.Email()}}) 295 } 296 297 grip.Info("Running alert queue processing") 298 for { 299 nextAlert, err := alert.DequeueAlertRequest() 300 301 if err != nil { 302 grip.Errorf("Failed to dequeue alert request: %+v", err) 303 return err 304 } 305 if nextAlert == nil { 306 grip.Info("Reached end of queue items - stopping") 307 break 308 } 309 310 grip.Debugf("Processing queue item %s", nextAlert.Id.Hex()) 311 312 alertContext, err := qp.loadAlertContext(nextAlert) 313 if err != nil { 314 grip.Errorf("Failed to load alert context: %s", err) 315 return err 316 } 317 318 grip.Debugln("Delivering queue item", nextAlert.Id.Hex()) 319 320 err = qp.Deliver(nextAlert, alertContext) 321 if err != nil { 322 grip.Errorf("Got error delivering message: %+v", err) 323 } 324 325 } 326 327 grip.Info("Finished alert queue processor run.") 328 return nil 329 }