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  }