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 }