github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/plugins/plugins.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package plugins
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"path"
    24  	"regexp"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/ghodss/yaml"
    30  	"github.com/sirupsen/logrus"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  
    33  	"k8s.io/test-infra/prow/commentpruner"
    34  	"k8s.io/test-infra/prow/config"
    35  	"k8s.io/test-infra/prow/git"
    36  	"k8s.io/test-infra/prow/github"
    37  	"k8s.io/test-infra/prow/kube"
    38  	"k8s.io/test-infra/prow/pluginhelp"
    39  	"k8s.io/test-infra/prow/repoowners"
    40  	"k8s.io/test-infra/prow/slack"
    41  )
    42  
    43  const (
    44  	defaultBlunderbussReviewerCount = 2
    45  )
    46  
    47  var (
    48  	pluginHelp                 = map[string]HelpProvider{}
    49  	genericCommentHandlers     = map[string]GenericCommentHandler{}
    50  	issueHandlers              = map[string]IssueHandler{}
    51  	issueCommentHandlers       = map[string]IssueCommentHandler{}
    52  	pullRequestHandlers        = map[string]PullRequestHandler{}
    53  	pushEventHandlers          = map[string]PushEventHandler{}
    54  	reviewEventHandlers        = map[string]ReviewEventHandler{}
    55  	reviewCommentEventHandlers = map[string]ReviewCommentEventHandler{}
    56  	statusEventHandlers        = map[string]StatusEventHandler{}
    57  )
    58  
    59  type HelpProvider func(config *Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error)
    60  
    61  func HelpProviders() map[string]HelpProvider {
    62  	return pluginHelp
    63  }
    64  
    65  type IssueHandler func(PluginClient, github.IssueEvent) error
    66  
    67  func RegisterIssueHandler(name string, fn IssueHandler, help HelpProvider) {
    68  	pluginHelp[name] = help
    69  	issueHandlers[name] = fn
    70  }
    71  
    72  type IssueCommentHandler func(PluginClient, github.IssueCommentEvent) error
    73  
    74  func RegisterIssueCommentHandler(name string, fn IssueCommentHandler, help HelpProvider) {
    75  	pluginHelp[name] = help
    76  	issueCommentHandlers[name] = fn
    77  }
    78  
    79  type PullRequestHandler func(PluginClient, github.PullRequestEvent) error
    80  
    81  func RegisterPullRequestHandler(name string, fn PullRequestHandler, help HelpProvider) {
    82  	pluginHelp[name] = help
    83  	pullRequestHandlers[name] = fn
    84  }
    85  
    86  type StatusEventHandler func(PluginClient, github.StatusEvent) error
    87  
    88  func RegisterStatusEventHandler(name string, fn StatusEventHandler, help HelpProvider) {
    89  	pluginHelp[name] = help
    90  	statusEventHandlers[name] = fn
    91  }
    92  
    93  type PushEventHandler func(PluginClient, github.PushEvent) error
    94  
    95  func RegisterPushEventHandler(name string, fn PushEventHandler, help HelpProvider) {
    96  	pluginHelp[name] = help
    97  	pushEventHandlers[name] = fn
    98  }
    99  
   100  type ReviewEventHandler func(PluginClient, github.ReviewEvent) error
   101  
   102  func RegisterReviewEventHandler(name string, fn ReviewEventHandler, help HelpProvider) {
   103  	pluginHelp[name] = help
   104  	reviewEventHandlers[name] = fn
   105  }
   106  
   107  type ReviewCommentEventHandler func(PluginClient, github.ReviewCommentEvent) error
   108  
   109  func RegisterReviewCommentEventHandler(name string, fn ReviewCommentEventHandler, help HelpProvider) {
   110  	pluginHelp[name] = help
   111  	reviewCommentEventHandlers[name] = fn
   112  }
   113  
   114  type GenericCommentHandler func(PluginClient, github.GenericCommentEvent) error
   115  
   116  func RegisterGenericCommentHandler(name string, fn GenericCommentHandler, help HelpProvider) {
   117  	pluginHelp[name] = help
   118  	genericCommentHandlers[name] = fn
   119  }
   120  
   121  // PluginClient may be used concurrently, so each entry must be thread-safe.
   122  type PluginClient struct {
   123  	GitHubClient *github.Client
   124  	KubeClient   *kube.Client
   125  	GitClient    *git.Client
   126  	SlackClient  *slack.Client
   127  	OwnersClient repoowners.Interface
   128  
   129  	CommentPruner *commentpruner.EventClient
   130  
   131  	// Config provides information about the jobs
   132  	// that we know how to run for repos.
   133  	Config *config.Config
   134  	// PluginConfig provides plugin-specific options
   135  	PluginConfig *Configuration
   136  
   137  	Logger *logrus.Entry
   138  }
   139  
   140  type PluginAgent struct {
   141  	PluginClient
   142  
   143  	mut           sync.Mutex
   144  	configuration *Configuration
   145  }
   146  
   147  // Configuration is the top-level serialization
   148  // target for plugin Configuration
   149  type Configuration struct {
   150  	// Plugins is a map of repositories (eg "k/k") to lists of
   151  	// plugin names.
   152  	// TODO: Link to the list of supported plugins.
   153  	// https://github.com/kubernetes/test-infra/issues/3476
   154  	Plugins map[string][]string `json:"plugins,omitempty"`
   155  
   156  	// ExternalPlugins is a map of repositories (eg "k/k") to lists of
   157  	// external plugins.
   158  	ExternalPlugins map[string][]ExternalPlugin `json:"external_plugins,omitempty"`
   159  
   160  	// Owners contains configuration related to handling OWNERS files.
   161  	Owners Owners `json:"owners,omitempty"`
   162  
   163  	// Built-in plugins specific configuration.
   164  	Triggers      []Trigger            `json:"triggers,omitempty"`
   165  	Heart         Heart                `json:"heart,omitempty"`
   166  	RepoMilestone map[string]Milestone `json:"repo_milestone,omitempty"`
   167  	Slack         Slack                `json:"slack,omitempty"`
   168  	ConfigUpdater ConfigUpdater        `json:"config_updater,omitempty"`
   169  	Blockades     []Blockade           `json:"blockades,omitempty"`
   170  	Approve       []Approve            `json:"approve,omitempty"`
   171  	Blunderbuss   Blunderbuss          `json:"blunderbuss,omitempty"`
   172  	RequireSIG    RequireSIG           `json:"requiresig,omitempty"`
   173  	SigMention    SigMention           `json:"sigmention,omitempty"`
   174  	Cat           Cat                  `json:"cat,omitempty"`
   175  	Label         *Label               `json:"label,omitempty"`
   176  	Lgtm          []Lgtm               `json:"lgtm,omitempty"`
   177  	Welcome       Welcome              `json:"welcome,omitempty"`
   178  }
   179  
   180  // ExternalPlugin holds configuration for registering an external
   181  // plugin in prow.
   182  type ExternalPlugin struct {
   183  	// Name of the plugin.
   184  	Name string `json:"name"`
   185  	// Endpoint is the location of the external plugin. Defaults to
   186  	// the name of the plugin, ie. "http://{{name}}".
   187  	Endpoint string `json:"endpoint,omitempty"`
   188  	// Events are the events that need to be demuxed by the hook
   189  	// server to the external plugin. If no events are specified,
   190  	// everything is sent.
   191  	Events []string `json:"events,omitempty"`
   192  }
   193  
   194  type Blunderbuss struct {
   195  	// ReviewerCount is the minimum number of reviewers to request
   196  	// reviews from. Defaults to requesting reviews from 2 reviewers
   197  	// if FileWeightCount is not set.
   198  	ReviewerCount *int `json:"request_count,omitempty"`
   199  	// MaxReviewerCount is the maximum number of reviewers to request
   200  	// reviews from. Defaults to 0 meaning no limit.
   201  	MaxReviewerCount int `json:"max_request_count,omitempty"`
   202  	// FileWeightCount is the maximum number of reviewers to request
   203  	// reviews from. Selects reviewers based on file weighting.
   204  	// This and request_count are mutually exclusive options.
   205  	FileWeightCount *int `json:"file_weight_count,omitempty"`
   206  	// ExcludeApprovers controls whether approvers are considered to be
   207  	// reviewers. By default, approvers are considered as reviewers if
   208  	// insufficient reviewers are available. If ExcludeApprovers is true,
   209  	// approvers will never be considered as reviewers.
   210  	ExcludeApprovers bool `json:"exclude_approvers,omitempty"`
   211  }
   212  
   213  // Owners contains configuration related to handling OWNERS files.
   214  type Owners struct {
   215  	// MDYAMLRepos is a list of org and org/repo strings specifying the repos that support YAML
   216  	// OWNERS config headers at the top of markdown (*.md) files. These headers function just like
   217  	// the config in an OWNERS file, but only apply to the file itself instead of the entire
   218  	// directory and all sub-directories.
   219  	// The yaml header must be at the start of the file and be bracketed with "---" like so:
   220  	/*
   221  		---
   222  		approvers:
   223  		- mikedanese
   224  		- thockin
   225  
   226  		---
   227  	*/
   228  	MDYAMLRepos []string `json:"mdyamlrepos,omitempty"`
   229  
   230  	// SkipCollaborators disables collaborator cross-checks and forces both
   231  	// the approve and lgtm plugins to use solely OWNERS files for access
   232  	// control in the provided repos.
   233  	SkipCollaborators []string `json:"skip_collaborators,omitempty"`
   234  
   235  	// LabelsBlackList holds a list of labels that should not be present in any
   236  	// OWNERS file, preventing their automatic addition by the owners-label plugin.
   237  	// This check is performed by the verify-owners plugin.
   238  	LabelsBlackList []string `json:"labels_blacklist,omitempty"`
   239  }
   240  
   241  func (pa *PluginAgent) MDYAMLEnabled(org, repo string) bool {
   242  	full := fmt.Sprintf("%s/%s", org, repo)
   243  	for _, elem := range pa.Config().Owners.MDYAMLRepos {
   244  		if elem == org || elem == full {
   245  			return true
   246  		}
   247  	}
   248  	return false
   249  }
   250  
   251  func (pa *PluginAgent) SkipCollaborators(org, repo string) bool {
   252  	full := fmt.Sprintf("%s/%s", org, repo)
   253  	for _, elem := range pa.Config().Owners.SkipCollaborators {
   254  		if elem == org || elem == full {
   255  			return true
   256  		}
   257  	}
   258  	return false
   259  }
   260  
   261  // RequireSIG specifies configuration for the require-sig plugin.
   262  type RequireSIG struct {
   263  	// GroupListURL is the URL where a list of the available SIGs can be found.
   264  	GroupListURL string `json:"group_list_url,omitempty"`
   265  }
   266  
   267  // SigMention specifies configuration for the sigmention plugin.
   268  type SigMention struct {
   269  	// Regexp parses comments and should return matches to team mentions.
   270  	// These mentions enable labeling issues or PRs with sig/team labels.
   271  	// Furthermore, teams with the following suffixes will be mapped to
   272  	// kind/* labels:
   273  	//
   274  	// * @org/team-bugs             --maps to--> kind/bug
   275  	// * @org/team-feature-requests --maps to--> kind/feature
   276  	// * @org/team-api-reviews      --maps to--> kind/api-change
   277  	// * @org/team-proposals        --maps to--> kind/design
   278  	//
   279  	// Note that you need to make sure your regexp covers the above
   280  	// mentions if you want to use the extra labeling. Defaults to:
   281  	// (?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)
   282  	//
   283  	// Compiles into Re during config load.
   284  	Regexp string         `json:"regexp,omitempty"`
   285  	Re     *regexp.Regexp `json:"-"`
   286  }
   287  
   288  /*
   289    Blockade specifies a configuration for a single blockade.blockade. The configuration for the
   290    blockade plugin is defined as a list of these structures. Here is an example of a complete
   291    yaml config for the blockade plugin that is composed of 2 Blockade structs:
   292  
   293  	blockades:
   294  	- repos:
   295  	  - kubernetes-incubator
   296  	  - kubernetes/kubernetes
   297  	  - kubernetes/test-infra
   298  	  blockregexps:
   299  	  - 'docs/.*'
   300  	  - 'other-docs/.*'
   301  	  exceptionregexps:
   302  	  - '.*OWNERS'
   303  	  explanation: "Files in the 'docs' directory should not be modified except for OWNERS files"
   304  	- repos:
   305  	  - kubernetes/test-infra
   306  	  blockregexps:
   307  	  - 'mungegithub/.*'
   308  	  exceptionregexps:
   309  	  - 'mungegithub/DeprecationWarning.md'
   310  	  explanation: "Don't work on mungegithub! Work on Prow!"
   311  */
   312  type Blockade struct {
   313  	// Repos are either of the form org/repos or just org.
   314  	Repos []string `json:"repos,omitempty"`
   315  	// BlockRegexps are regular expressions matching the file paths to block.
   316  	BlockRegexps []string `json:"blockregexps,omitempty"`
   317  	// ExceptionRegexps are regular expressions matching the file paths that are exceptions to the BlockRegexps.
   318  	ExceptionRegexps []string `json:"exceptionregexps,omitempty"`
   319  	// Explanation is a string that will be included in the comment left when blocking a PR. This should
   320  	// be an explanation of why the paths specified are blockaded.
   321  	Explanation string `json:"explanation,omitempty"`
   322  }
   323  
   324  type Approve struct {
   325  	// Repos is either of the form org/repos or just org.
   326  	Repos []string `json:"repos,omitempty"`
   327  	// IssueRequired indicates if an associated issue is required for approval in
   328  	// the specified repos.
   329  	IssueRequired bool `json:"issue_required,omitempty"`
   330  	// ImplicitSelfApprove indicates if authors implicitly approve their own PRs
   331  	// in the specified repos.
   332  	ImplicitSelfApprove bool `json:"implicit_self_approve,omitempty"`
   333  	// LgtmActsAsApprove indicates that the lgtm command should be used to
   334  	// indicate approval
   335  	LgtmActsAsApprove bool `json:"lgtm_acts_as_approve,omitempty"`
   336  	// ReviewActsAsApprove indicates that GitHub review state should be used to
   337  	// indicate approval.
   338  	ReviewActsAsApprove bool `json:"review_acts_as_approve,omitempty"`
   339  }
   340  
   341  type Lgtm struct {
   342  	// Repos is either of the form org/repos or just org.
   343  	Repos []string `json:"repos,omitempty"`
   344  	// ReviewActsAsLgtm indicates that a Github review of "approve" or "request changes"
   345  	// acts as adding or removing the lgtm label
   346  	ReviewActsAsLgtm bool `json:"review_acts_as_lgtm,omitempty"`
   347  }
   348  
   349  type Cat struct {
   350  	// Path to file containing an api key for thecatapi.com
   351  	KeyPath string `json:"key_path,omitempty"`
   352  }
   353  
   354  type Label struct {
   355  	// AdditionalLabels is a set of additional labels enabled for use
   356  	// on top of the existing "kind/*", "priority/*", and "area/*" labels.
   357  	AdditionalLabels []string `json:"additional_labels"`
   358  }
   359  
   360  type Trigger struct {
   361  	// Repos is either of the form org/repos or just org.
   362  	Repos []string `json:"repos,omitempty"`
   363  	// TrustedOrg is the org whose members' PRs will be automatically built
   364  	// for PRs to the above repos. The default is the PR's org.
   365  	TrustedOrg string `json:"trusted_org,omitempty"`
   366  	// JoinOrgURL is a link that redirects users to a location where they
   367  	// should be able to read more about joining the organization in order
   368  	// to become trusted members. Defaults to the Github link of TrustedOrg.
   369  	JoinOrgURL string `json:"join_org_url,omitempty"`
   370  	// OnlyOrgMembers requires PRs and/or /ok-to-test comments to come from org members.
   371  	// By default, trigger also include repo collaborators.
   372  	OnlyOrgMembers bool `json:"only_org_members,omitempty"`
   373  }
   374  
   375  type Heart struct {
   376  	// Adorees is a list of GitHub logins for members
   377  	// for whom we will add emojis to comments
   378  	Adorees []string `json:"adorees,omitempty"`
   379  }
   380  
   381  // Milestone contains the configuration options for the milestone and
   382  // milestonestatus plugins.
   383  type Milestone struct {
   384  	// ID of the github team for the milestone maintainers (used for setting status labels)
   385  	// You can curl the following endpoint in order to determine the github ID of your team
   386  	// responsible for maintaining the milestones:
   387  	// curl -H "Authorization: token <token>" https://api.github.com/orgs/<org-name>/teams
   388  	MaintainersID   int    `json:"maintainers_id,omitempty"`
   389  	MaintainersTeam string `json:"maintainers_team,omitempty"`
   390  }
   391  
   392  type Slack struct {
   393  	MentionChannels []string       `json:"mentionchannels,omitempty"`
   394  	MergeWarnings   []MergeWarning `json:"mergewarnings,omitempty"`
   395  }
   396  
   397  // ConfigMapSpec contains configuration options for the configMap being updated by the ConfigUpdater plugin
   398  type ConfigMapSpec struct {
   399  	// Name of ConfigMap
   400  	Name string `json:"name"`
   401  	// Key is the key in the ConfigMap to update with the file contents.
   402  	// If no explicit key is given, the basename of the file will be used.
   403  	Key string `json:"key,omitempty"`
   404  	// Namespace in which the configMap needs to be deployed. If no namespace is specified
   405  	// it will be deployed to the ProwJobNamespace.
   406  	Namespace string `json:"namespace,omitempty"`
   407  }
   408  
   409  type ConfigUpdater struct {
   410  	// A map of filename => ConfigMapSpec.
   411  	// Whenever a commit changes filename, prow will update the corresponding configmap.
   412  	// map[string]ConfigMapSpec{ "/my/path.yaml": {Name: "foo", Namespace: "otherNamespace" }}
   413  	// will result in replacing the foo configmap whenever path.yaml changes
   414  	Maps map[string]ConfigMapSpec `json:"maps,omitempty"`
   415  	// The location of the prow configuration file inside the repository
   416  	// where the config-updater plugin is enabled. This needs to be relative
   417  	// to the root of the repository, eg. "prow/config.yaml" will match
   418  	// github.com/kubernetes/test-infra/prow/config.yaml assuming the config-updater
   419  	// plugin is enabled for kubernetes/test-infra. Defaults to "prow/config.yaml".
   420  	ConfigFile string `json:"config_file,omitempty"`
   421  	// The location of the prow plugin configuration file inside the repository
   422  	// where the config-updater plugin is enabled. This needs to be relative
   423  	// to the root of the repository, eg. "prow/plugins.yaml" will match
   424  	// github.com/kubernetes/test-infra/prow/plugins.yaml assuming the config-updater
   425  	// plugin is enabled for kubernetes/test-infra. Defaults to "prow/plugins.yaml".
   426  	PluginFile string `json:"plugin_file,omitempty"`
   427  }
   428  
   429  // MergeWarning is a config for the slackevents plugin's manual merge warings.
   430  // If a PR is pushed to any of the repos listed in the config
   431  // then send messages to the all the  slack channels listed if pusher is NOT in the whitelist.
   432  type MergeWarning struct {
   433  	// Repos is either of the form org/repos or just org.
   434  	Repos []string `json:"repos,omitempty"`
   435  	// List of channels on which a event is published.
   436  	Channels []string `json:"channels,omitempty"`
   437  	// A slack event is published if the user is not part of the WhiteList.
   438  	WhiteList []string `json:"whitelist,omitempty"`
   439  	// A slack event is published if the user is not on the branch whitelist
   440  	BranchWhiteList map[string][]string `json:"branch_whitelist,omitempty"`
   441  }
   442  
   443  // Welcome is config for the welcome plugin
   444  type Welcome struct {
   445  	// MessageTemplate is the welcome message template to post on new-contributor PRs
   446  	// For the info struct see prow/plugins/welcome/welcome.go's PRInfo
   447  	// TODO(bentheelder): make this be configurable per-repo?
   448  	MessageTemplate string `json:"message_template,omitempty"`
   449  }
   450  
   451  // TriggerFor finds the Trigger for a repo, if one exists
   452  // a trigger can be listed for the repo itself or for the
   453  // owning organization
   454  func (c *Configuration) TriggerFor(org, repo string) *Trigger {
   455  	for _, tr := range c.Triggers {
   456  		for _, r := range tr.Repos {
   457  			if r == org || r == fmt.Sprintf("%s/%s", org, repo) {
   458  				return &tr
   459  			}
   460  		}
   461  	}
   462  	return nil
   463  }
   464  
   465  func (c *Configuration) setDefaults() {
   466  	if len(c.ConfigUpdater.Maps) == 0 {
   467  		cf := c.ConfigUpdater.ConfigFile
   468  		if cf == "" {
   469  			cf = "prow/config.yaml"
   470  		} else {
   471  			logrus.Warnf(`config_file is deprecated, please switch to "maps": {"%s": "config"} before July 2018`, cf)
   472  		}
   473  		pf := c.ConfigUpdater.PluginFile
   474  		if pf == "" {
   475  			pf = "prow/plugins.yaml"
   476  		} else {
   477  			logrus.Warnf(`plugin_file is deprecated, please switch to "maps": {"%s": "plugins"} before July 2018`, pf)
   478  		}
   479  		c.ConfigUpdater.Maps = map[string]ConfigMapSpec{
   480  			cf: {
   481  				Name: "config",
   482  			},
   483  			pf: {
   484  				Name: "plugins",
   485  			},
   486  		}
   487  	}
   488  	for repo, plugins := range c.ExternalPlugins {
   489  		for i, p := range plugins {
   490  			if p.Endpoint != "" {
   491  				continue
   492  			}
   493  			c.ExternalPlugins[repo][i].Endpoint = fmt.Sprintf("http://%s", p.Name)
   494  		}
   495  	}
   496  	if c.Blunderbuss.ReviewerCount == nil && c.Blunderbuss.FileWeightCount == nil {
   497  		c.Blunderbuss.ReviewerCount = new(int)
   498  		*c.Blunderbuss.ReviewerCount = defaultBlunderbussReviewerCount
   499  	}
   500  	for i, trigger := range c.Triggers {
   501  		if trigger.TrustedOrg == "" || trigger.JoinOrgURL != "" {
   502  			continue
   503  		}
   504  		c.Triggers[i].JoinOrgURL = fmt.Sprintf("https://github.com/orgs/%s/people", trigger.TrustedOrg)
   505  	}
   506  	if c.SigMention.Regexp == "" {
   507  		c.SigMention.Regexp = `(?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)`
   508  	}
   509  	if c.Owners.LabelsBlackList == nil {
   510  		c.Owners.LabelsBlackList = []string{"approved", "lgtm"}
   511  	}
   512  }
   513  
   514  // Load attempts to load config from the path. It returns an error if either
   515  // the file can't be read or it contains an unknown plugin.
   516  func (pa *PluginAgent) Load(path string) error {
   517  	b, err := ioutil.ReadFile(path)
   518  	if err != nil {
   519  		return err
   520  	}
   521  	np := &Configuration{}
   522  	if err := yaml.Unmarshal(b, np); err != nil {
   523  		return err
   524  	}
   525  
   526  	if len(np.Plugins) == 0 {
   527  		logrus.Warn("no plugins specified-- check syntax?")
   528  	}
   529  
   530  	// Defaulting should run before validation.
   531  	np.setDefaults()
   532  	if err := validatePlugins(np.Plugins); err != nil {
   533  		return err
   534  	}
   535  	if err := validateExternalPlugins(np.ExternalPlugins); err != nil {
   536  		return err
   537  	}
   538  	if err := validateBlunderbuss(&np.Blunderbuss); err != nil {
   539  		return err
   540  	}
   541  	if err := validateConfigUpdater(&np.ConfigUpdater); err != nil {
   542  		return err
   543  	}
   544  	// regexp compilation should run after defaulting
   545  	if err := compileRegexps(np); err != nil {
   546  		return err
   547  	}
   548  
   549  	pa.Set(np)
   550  	return nil
   551  }
   552  
   553  func (pa *PluginAgent) Config() *Configuration {
   554  	pa.mut.Lock()
   555  	defer pa.mut.Unlock()
   556  	return pa.configuration
   557  }
   558  
   559  // validatePlugins will return error if
   560  // there are unknown or duplicated plugins.
   561  func validatePlugins(plugins map[string][]string) error {
   562  	var errors []string
   563  	for _, configuration := range plugins {
   564  		for _, plugin := range configuration {
   565  			if _, ok := pluginHelp[plugin]; !ok {
   566  				errors = append(errors, fmt.Sprintf("unknown plugin: %s", plugin))
   567  			}
   568  		}
   569  	}
   570  	for repo, repoConfig := range plugins {
   571  		if strings.Contains(repo, "/") {
   572  			org := strings.Split(repo, "/")[0]
   573  			if dupes := findDuplicatedPluginConfig(repoConfig, plugins[org]); len(dupes) > 0 {
   574  				errors = append(errors, fmt.Sprintf("plugins %v are duplicated for %s and %s", dupes, repo, org))
   575  			}
   576  		}
   577  	}
   578  
   579  	if len(errors) > 0 {
   580  		return fmt.Errorf("invalid plugin configuration:\n\t%v", strings.Join(errors, "\n\t"))
   581  	}
   582  	return nil
   583  }
   584  
   585  func findDuplicatedPluginConfig(repoConfig, orgConfig []string) []string {
   586  	var dupes []string
   587  	for _, repoPlugin := range repoConfig {
   588  		for _, orgPlugin := range orgConfig {
   589  			if repoPlugin == orgPlugin {
   590  				dupes = append(dupes, repoPlugin)
   591  			}
   592  		}
   593  	}
   594  
   595  	return dupes
   596  }
   597  
   598  func validateExternalPlugins(pluginMap map[string][]ExternalPlugin) error {
   599  	var errors []string
   600  
   601  	for repo, plugins := range pluginMap {
   602  		if !strings.Contains(repo, "/") {
   603  			continue
   604  		}
   605  		org := strings.Split(repo, "/")[0]
   606  
   607  		var orgConfig []string
   608  		for _, p := range pluginMap[org] {
   609  			orgConfig = append(orgConfig, p.Name)
   610  		}
   611  
   612  		var repoConfig []string
   613  		for _, p := range plugins {
   614  			repoConfig = append(repoConfig, p.Name)
   615  		}
   616  
   617  		if dupes := findDuplicatedPluginConfig(repoConfig, orgConfig); len(dupes) > 0 {
   618  			errors = append(errors, fmt.Sprintf("external plugins %v are duplicated for %s and %s", dupes, repo, org))
   619  		}
   620  	}
   621  
   622  	if len(errors) > 0 {
   623  		return fmt.Errorf("invalid plugin configuration:\n\t%v", strings.Join(errors, "\n\t"))
   624  	}
   625  	return nil
   626  }
   627  
   628  func validateBlunderbuss(b *Blunderbuss) error {
   629  	if b.ReviewerCount != nil && b.FileWeightCount != nil {
   630  		return errors.New("cannot use both request_count and file_weight_count in blunderbuss")
   631  	}
   632  	if b.ReviewerCount != nil && *b.ReviewerCount < 1 {
   633  		return fmt.Errorf("invalid request_count: %v (needs to be positive)", *b.ReviewerCount)
   634  	}
   635  	if b.FileWeightCount != nil && *b.FileWeightCount < 1 {
   636  		return fmt.Errorf("invalid file_weight_count: %v (needs to be positive)", *b.FileWeightCount)
   637  	}
   638  	return nil
   639  }
   640  
   641  func validateConfigUpdater(updater *ConfigUpdater) error {
   642  	files := sets.NewString()
   643  	configMapKeys := map[string]sets.String{}
   644  	for file, config := range updater.Maps {
   645  		if files.Has(file) {
   646  			return fmt.Errorf("file %s listed more than once in config updater config", file)
   647  		}
   648  		files.Insert(file)
   649  
   650  		key := config.Key
   651  		if key == "" {
   652  			key = path.Base(file)
   653  		}
   654  
   655  		if _, ok := configMapKeys[config.Name]; ok {
   656  			if configMapKeys[config.Name].Has(key) {
   657  				return fmt.Errorf("key %s in configmap %s updated with more than one file", key, config.Name)
   658  			}
   659  			configMapKeys[config.Name].Insert(key)
   660  		} else {
   661  			configMapKeys[config.Name] = sets.NewString(key)
   662  		}
   663  	}
   664  	return nil
   665  }
   666  
   667  func compileRegexps(pc *Configuration) error {
   668  	cRe, err := regexp.Compile(pc.SigMention.Regexp)
   669  	if err != nil {
   670  		return err
   671  	}
   672  	pc.SigMention.Re = cRe
   673  	return nil
   674  }
   675  
   676  // Set attempts to set the plugins that are enabled on repos. Plugins are listed
   677  // as a map from repositories to the list of plugins that are enabled on them.
   678  // Specifying simply an org name will also work, and will enable the plugin on
   679  // all repos in the org.
   680  func (pa *PluginAgent) Set(pc *Configuration) {
   681  	pa.mut.Lock()
   682  	defer pa.mut.Unlock()
   683  	pa.configuration = pc
   684  }
   685  
   686  // Start starts polling path for plugin config. If the first attempt fails,
   687  // then start returns the error. Future errors will halt updates but not stop.
   688  func (pa *PluginAgent) Start(path string) error {
   689  	if err := pa.Load(path); err != nil {
   690  		return err
   691  	}
   692  	ticker := time.Tick(1 * time.Minute)
   693  	go func() {
   694  		for range ticker {
   695  			if err := pa.Load(path); err != nil {
   696  				logrus.WithField("path", path).WithError(err).Error("Error loading plugin config.")
   697  			}
   698  		}
   699  	}()
   700  	return nil
   701  }
   702  
   703  // GenericCommentHandlers returns a map of plugin names to handlers for the repo.
   704  func (pa *PluginAgent) GenericCommentHandlers(owner, repo string) map[string]GenericCommentHandler {
   705  	pa.mut.Lock()
   706  	defer pa.mut.Unlock()
   707  
   708  	hs := map[string]GenericCommentHandler{}
   709  	for _, p := range pa.getPlugins(owner, repo) {
   710  		if h, ok := genericCommentHandlers[p]; ok {
   711  			hs[p] = h
   712  		}
   713  	}
   714  	return hs
   715  }
   716  
   717  // IssueHandlers returns a map of plugin names to handlers for the repo.
   718  func (pa *PluginAgent) IssueHandlers(owner, repo string) map[string]IssueHandler {
   719  	pa.mut.Lock()
   720  	defer pa.mut.Unlock()
   721  
   722  	hs := map[string]IssueHandler{}
   723  	for _, p := range pa.getPlugins(owner, repo) {
   724  		if h, ok := issueHandlers[p]; ok {
   725  			hs[p] = h
   726  		}
   727  	}
   728  	return hs
   729  }
   730  
   731  // IssueCommentHandlers returns a map of plugin names to handlers for the repo.
   732  func (pa *PluginAgent) IssueCommentHandlers(owner, repo string) map[string]IssueCommentHandler {
   733  	pa.mut.Lock()
   734  	defer pa.mut.Unlock()
   735  
   736  	hs := map[string]IssueCommentHandler{}
   737  	for _, p := range pa.getPlugins(owner, repo) {
   738  		if h, ok := issueCommentHandlers[p]; ok {
   739  			hs[p] = h
   740  		}
   741  	}
   742  
   743  	return hs
   744  }
   745  
   746  // PullRequestHandlers returns a map of plugin names to handlers for the repo.
   747  func (pa *PluginAgent) PullRequestHandlers(owner, repo string) map[string]PullRequestHandler {
   748  	pa.mut.Lock()
   749  	defer pa.mut.Unlock()
   750  
   751  	hs := map[string]PullRequestHandler{}
   752  	for _, p := range pa.getPlugins(owner, repo) {
   753  		if h, ok := pullRequestHandlers[p]; ok {
   754  			hs[p] = h
   755  		}
   756  	}
   757  
   758  	return hs
   759  }
   760  
   761  // ReviewEventHandlers returns a map of plugin names to handlers for the repo.
   762  func (pa *PluginAgent) ReviewEventHandlers(owner, repo string) map[string]ReviewEventHandler {
   763  	pa.mut.Lock()
   764  	defer pa.mut.Unlock()
   765  
   766  	hs := map[string]ReviewEventHandler{}
   767  	for _, p := range pa.getPlugins(owner, repo) {
   768  		if h, ok := reviewEventHandlers[p]; ok {
   769  			hs[p] = h
   770  		}
   771  	}
   772  
   773  	return hs
   774  }
   775  
   776  // ReviewCommentEventHandlers returns a map of plugin names to handlers for the repo.
   777  func (pa *PluginAgent) ReviewCommentEventHandlers(owner, repo string) map[string]ReviewCommentEventHandler {
   778  	pa.mut.Lock()
   779  	defer pa.mut.Unlock()
   780  
   781  	hs := map[string]ReviewCommentEventHandler{}
   782  	for _, p := range pa.getPlugins(owner, repo) {
   783  		if h, ok := reviewCommentEventHandlers[p]; ok {
   784  			hs[p] = h
   785  		}
   786  	}
   787  
   788  	return hs
   789  }
   790  
   791  // StatusEventHandlers returns a map of plugin names to handlers for the repo.
   792  func (pa *PluginAgent) StatusEventHandlers(owner, repo string) map[string]StatusEventHandler {
   793  	pa.mut.Lock()
   794  	defer pa.mut.Unlock()
   795  
   796  	hs := map[string]StatusEventHandler{}
   797  	for _, p := range pa.getPlugins(owner, repo) {
   798  		if h, ok := statusEventHandlers[p]; ok {
   799  			hs[p] = h
   800  		}
   801  	}
   802  
   803  	return hs
   804  }
   805  
   806  // PushEventHandlers returns a map of plugin names to handlers for the repo.
   807  func (pa *PluginAgent) PushEventHandlers(owner, repo string) map[string]PushEventHandler {
   808  	pa.mut.Lock()
   809  	defer pa.mut.Unlock()
   810  
   811  	hs := map[string]PushEventHandler{}
   812  	for _, p := range pa.getPlugins(owner, repo) {
   813  		if h, ok := pushEventHandlers[p]; ok {
   814  			hs[p] = h
   815  		}
   816  	}
   817  
   818  	return hs
   819  }
   820  
   821  // getPlugins returns a list of plugins that are enabled on a given (org, repository).
   822  func (pa *PluginAgent) getPlugins(owner, repo string) []string {
   823  	var plugins []string
   824  
   825  	fullName := fmt.Sprintf("%s/%s", owner, repo)
   826  	plugins = append(plugins, pa.configuration.Plugins[owner]...)
   827  	plugins = append(plugins, pa.configuration.Plugins[fullName]...)
   828  
   829  	return plugins
   830  }
   831  
   832  func EventsForPlugin(name string) []string {
   833  	var events []string
   834  	if _, ok := issueHandlers[name]; ok {
   835  		events = append(events, "issue")
   836  	}
   837  	if _, ok := issueCommentHandlers[name]; ok {
   838  		events = append(events, "issue_comment")
   839  	}
   840  	if _, ok := pullRequestHandlers[name]; ok {
   841  		events = append(events, "pull_request")
   842  	}
   843  	if _, ok := pushEventHandlers[name]; ok {
   844  		events = append(events, "push")
   845  	}
   846  	if _, ok := reviewEventHandlers[name]; ok {
   847  		events = append(events, "pull_request_review")
   848  	}
   849  	if _, ok := reviewCommentEventHandlers[name]; ok {
   850  		events = append(events, "pull_request_review_comment")
   851  	}
   852  	if _, ok := statusEventHandlers[name]; ok {
   853  		events = append(events, "status")
   854  	}
   855  	if _, ok := genericCommentHandlers[name]; ok {
   856  		events = append(events, "GenericCommentEvent (any event for user text)")
   857  	}
   858  	return events
   859  }
   860  
   861  func (c *Configuration) EnabledReposForPlugin(plugin string) (orgs, repos []string) {
   862  	for repo, plugins := range c.Plugins {
   863  		found := false
   864  		for _, candidate := range plugins {
   865  			if candidate == plugin {
   866  				found = true
   867  				break
   868  			}
   869  		}
   870  		if found {
   871  			if strings.Contains(repo, "/") {
   872  				repos = append(repos, repo)
   873  			} else {
   874  				orgs = append(orgs, repo)
   875  			}
   876  		}
   877  	}
   878  	return
   879  }
   880  
   881  func (c *Configuration) EnabledReposForExternalPlugin(plugin string) (orgs, repos []string) {
   882  	for repo, plugins := range c.ExternalPlugins {
   883  		found := false
   884  		for _, candidate := range plugins {
   885  			if candidate.Name == plugin {
   886  				found = true
   887  				break
   888  			}
   889  		}
   890  		if found {
   891  			if strings.Contains(repo, "/") {
   892  				repos = append(repos, repo)
   893  			} else {
   894  				orgs = append(orgs, repo)
   895  			}
   896  		}
   897  	}
   898  	return
   899  }