github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/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 "fmt" 21 "io/ioutil" 22 "strings" 23 "sync" 24 "time" 25 26 "github.com/ghodss/yaml" 27 "github.com/sirupsen/logrus" 28 29 "k8s.io/test-infra/prow/config" 30 "k8s.io/test-infra/prow/git" 31 "k8s.io/test-infra/prow/github" 32 "k8s.io/test-infra/prow/kube" 33 "k8s.io/test-infra/prow/slack" 34 ) 35 36 var ( 37 allPlugins = map[string]struct{}{} 38 genericCommentHandlers = map[string]GenericCommentHandler{} 39 issueHandlers = map[string]IssueHandler{} 40 issueCommentHandlers = map[string]IssueCommentHandler{} 41 pullRequestHandlers = map[string]PullRequestHandler{} 42 pushEventHandlers = map[string]PushEventHandler{} 43 reviewEventHandlers = map[string]ReviewEventHandler{} 44 reviewCommentEventHandlers = map[string]ReviewCommentEventHandler{} 45 statusEventHandlers = map[string]StatusEventHandler{} 46 ) 47 48 type IssueHandler func(PluginClient, github.IssueEvent) error 49 50 func RegisterIssueHandler(name string, fn IssueHandler) { 51 allPlugins[name] = struct{}{} 52 issueHandlers[name] = fn 53 } 54 55 type IssueCommentHandler func(PluginClient, github.IssueCommentEvent) error 56 57 func RegisterIssueCommentHandler(name string, fn IssueCommentHandler) { 58 allPlugins[name] = struct{}{} 59 issueCommentHandlers[name] = fn 60 } 61 62 type PullRequestHandler func(PluginClient, github.PullRequestEvent) error 63 64 func RegisterPullRequestHandler(name string, fn PullRequestHandler) { 65 allPlugins[name] = struct{}{} 66 pullRequestHandlers[name] = fn 67 } 68 69 type StatusEventHandler func(PluginClient, github.StatusEvent) error 70 71 func RegisterStatusEventHandler(name string, fn StatusEventHandler) { 72 allPlugins[name] = struct{}{} 73 statusEventHandlers[name] = fn 74 } 75 76 type PushEventHandler func(PluginClient, github.PushEvent) error 77 78 func RegisterPushEventHandler(name string, fn PushEventHandler) { 79 allPlugins[name] = struct{}{} 80 pushEventHandlers[name] = fn 81 } 82 83 type ReviewEventHandler func(PluginClient, github.ReviewEvent) error 84 85 func RegisterReviewEventHandler(name string, fn ReviewEventHandler) { 86 allPlugins[name] = struct{}{} 87 reviewEventHandlers[name] = fn 88 } 89 90 type ReviewCommentEventHandler func(PluginClient, github.ReviewCommentEvent) error 91 92 func RegisterReviewCommentEventHandler(name string, fn ReviewCommentEventHandler) { 93 allPlugins[name] = struct{}{} 94 reviewCommentEventHandlers[name] = fn 95 } 96 97 type GenericCommentHandler func(PluginClient, github.GenericCommentEvent) error 98 99 func RegisterGenericCommentHandler(name string, fn GenericCommentHandler) { 100 allPlugins[name] = struct{}{} 101 genericCommentHandlers[name] = fn 102 } 103 104 // PluginClient may be used concurrently, so each entry must be thread-safe. 105 type PluginClient struct { 106 GitHubClient *github.Client 107 KubeClient *kube.Client 108 GitClient *git.Client 109 SlackClient *slack.Client 110 111 // Config provides information about the jobs 112 // that we know how to run for repos. 113 Config *config.Config 114 // PluginConfig provides plugin-specific options 115 PluginConfig *Configuration 116 117 Logger *logrus.Entry 118 } 119 120 type PluginAgent struct { 121 PluginClient 122 123 mut sync.Mutex 124 configuration *Configuration 125 } 126 127 // Configuration is the top-level serialization 128 // target for plugin Configuration 129 type Configuration struct { 130 // Repo (eg "k/k") -> list of handler names. 131 Plugins map[string][]string `json:"plugins,omitempty"` 132 Triggers []Trigger `json:"triggers,omitempty"` 133 Heart Heart `json:"heart,omitempty"` 134 Label Label `json:"label,omitempty"` 135 Slack Slack `json:"slack,omitempty"` 136 // ConfigUpdater holds config for the config-updater plugin. 137 ConfigUpdater ConfigUpdater `json:"config_updater,omitempty"` 138 } 139 140 type Trigger struct { 141 // Repos is either of the form org/repos or just org. 142 Repos []string `json:"repos,omitempty"` 143 // TrustedOrg is the org whose members' PRs will be automatically built 144 // for PRs to the above repos. The default is the PR's org. 145 TrustedOrg string `json:"trusted_org,omitempty"` 146 } 147 148 type Heart struct { 149 // Adorees is a list of GitHub logins for members 150 // for whom we will add emojis to comments 151 Adorees []string `json:"adorees,omitempty"` 152 } 153 154 type Label struct { 155 // SigOrg is the organization that owns the 156 // special interest groups tagged in this repo 157 SigOrg string `json:"sig_org,omitempty"` 158 // ID of the github team for the milestone maintainers (used for setting status labels) 159 // You can curl the following endpoint in order to determine the github ID of your team 160 // responsible for maintaining the milestones: 161 // curl -H "Authorization: token <token>" https://api.github.com/orgs/<org-name>/teams 162 MilestoneMaintainersID int `json:"milestone_maintainers_id,omitempty"` 163 } 164 165 type Slack struct { 166 MentionChannels []string `json:"mentionchannels,omitempty"` 167 MergeWarnings []MergeWarning `json:"mergewarnings,omitempty"` 168 } 169 170 type ConfigUpdater struct { 171 // The location of the prow configuration file inside the repository 172 // where the config-updater plugin is enabled. This needs to be relative 173 // to the root of the repository, eg. "prow/config.yaml" will match 174 // github.com/kubernetes/test-infra/prow/config.yaml assuming the config-updater 175 // plugin is enabled for kubernetes/test-infra. Defaults to "prow/config.yaml". 176 ConfigFile string `json:"config_file,omitempty"` 177 // The location of the prow plugin configuration file inside the repository 178 // where the config-updater plugin is enabled. This needs to be relative 179 // to the root of the repository, eg. "prow/plugins.yaml" will match 180 // github.com/kubernetes/test-infra/prow/plugins.yaml assuming the config-updater 181 // plugin is enabled for kubernetes/test-infra. Defaults to "prow/plugins.yaml". 182 PluginFile string `json:"plugin_file,omitempty"` 183 } 184 185 // MergeWarning is a config for the slackevents plugin's manual merge warings. 186 // If a PR is pushed to any of the repos listed in the config 187 // then send messages to the all the slack channels listed if pusher is NOT in the whitelist. 188 type MergeWarning struct { 189 // Repos is either of the form org/repos or just org. 190 Repos []string `json:"repos,omitempty"` 191 // List of channels on which a event is published. 192 Channels []string `json:"channels,omitempty"` 193 // A slack event is published if the user is not part of the WhiteList. 194 WhiteList []string `json:"whitelist,omitempty"` 195 } 196 197 // TriggerFor finds the Trigger for a repo, if one exists 198 // a trigger can be listed for the repo itself or for the 199 // owning organization 200 func (c *Configuration) TriggerFor(org, repo string) *Trigger { 201 for _, tr := range c.Triggers { 202 for _, r := range tr.Repos { 203 if r == org || r == fmt.Sprintf("%s/%s", org, repo) { 204 return &tr 205 } 206 } 207 } 208 return nil 209 } 210 211 func (c *Configuration) setDefaults() { 212 if c.ConfigUpdater.ConfigFile == "" { 213 c.ConfigUpdater.ConfigFile = "prow/config.yaml" 214 } 215 if c.ConfigUpdater.PluginFile == "" { 216 c.ConfigUpdater.PluginFile = "prow/plugins.yaml" 217 } 218 } 219 220 // Load attempts to load config from the path. It returns an error if either 221 // the file can't be read or it contains an unknown plugin. 222 func (pa *PluginAgent) Load(path string) error { 223 b, err := ioutil.ReadFile(path) 224 if err != nil { 225 return err 226 } 227 np := &Configuration{} 228 if err := yaml.Unmarshal(b, np); err != nil { 229 return err 230 } 231 232 if len(np.Plugins) == 0 { 233 logrus.Warn("no plugins specified-- check syntax?") 234 } 235 236 if err := validatePlugins(np.Plugins); err != nil { 237 return err 238 } 239 np.setDefaults() 240 pa.Set(np) 241 return nil 242 } 243 244 func (pa *PluginAgent) Config() *Configuration { 245 pa.mut.Lock() 246 defer pa.mut.Unlock() 247 return pa.configuration 248 } 249 250 // validatePlugins will return error if 251 // there are unknown or duplicated plugins. 252 func validatePlugins(plugins map[string][]string) error { 253 errors := []string{} 254 for _, configuration := range plugins { 255 for _, plugin := range configuration { 256 if _, ok := allPlugins[plugin]; !ok { 257 errors = append(errors, fmt.Sprintf("unknown plugin: %s", plugin)) 258 } 259 } 260 } 261 for repo, repoConfig := range plugins { 262 if strings.Contains(repo, "/") { 263 org := strings.Split(repo, "/")[0] 264 if dupes := findDuplicatedPluginConfig(repoConfig, plugins[org]); len(dupes) > 0 { 265 errors = append(errors, fmt.Sprintf("plugins %v are duplicated for %s and %s", dupes, repo, org)) 266 } 267 } 268 } 269 270 if len(errors) > 0 { 271 return fmt.Errorf("invalid plugin configuration:\n\t%v", strings.Join(errors, "\n\t")) 272 } 273 return nil 274 } 275 276 func findDuplicatedPluginConfig(repoConfig, orgConfig []string) []string { 277 dupes := []string{} 278 for _, repoPlugin := range repoConfig { 279 for _, orgPlugin := range orgConfig { 280 if repoPlugin == orgPlugin { 281 dupes = append(dupes, repoPlugin) 282 } 283 } 284 } 285 286 return dupes 287 } 288 289 // Set attempts to set the plugins that are enabled on repos. Plugins are listed 290 // as a map from repositories to the list of plugins that are enabled on them. 291 // Specifying simply an org name will also work, and will enable the plugin on 292 // all repos in the org. 293 func (pa *PluginAgent) Set(pc *Configuration) { 294 pa.mut.Lock() 295 defer pa.mut.Unlock() 296 pa.configuration = pc 297 } 298 299 // Start starts polling path for plugin config. If the first attempt fails, 300 // then start returns the error. Future errors will halt updates but not stop. 301 func (pa *PluginAgent) Start(path string) error { 302 if err := pa.Load(path); err != nil { 303 return err 304 } 305 ticker := time.Tick(1 * time.Minute) 306 go func() { 307 for range ticker { 308 if err := pa.Load(path); err != nil { 309 logrus.WithField("path", path).WithError(err).Error("Error loading plugin config.") 310 } 311 } 312 }() 313 return nil 314 } 315 316 // GenericCommentHandlers returns a map of plugin names to handlers for the repo. 317 func (pa *PluginAgent) GenericCommentHandlers(owner, repo string) map[string]GenericCommentHandler { 318 pa.mut.Lock() 319 defer pa.mut.Unlock() 320 321 hs := map[string]GenericCommentHandler{} 322 for _, p := range pa.getPlugins(owner, repo) { 323 if h, ok := genericCommentHandlers[p]; ok { 324 hs[p] = h 325 } 326 } 327 return hs 328 } 329 330 // IssueHandlers returns a map of plugin names to handlers for the repo. 331 func (pa *PluginAgent) IssueHandlers(owner, repo string) map[string]IssueHandler { 332 pa.mut.Lock() 333 defer pa.mut.Unlock() 334 335 hs := map[string]IssueHandler{} 336 for _, p := range pa.getPlugins(owner, repo) { 337 if h, ok := issueHandlers[p]; ok { 338 hs[p] = h 339 } 340 } 341 return hs 342 } 343 344 // IssueCommentHandlers returns a map of plugin names to handlers for the repo. 345 func (pa *PluginAgent) IssueCommentHandlers(owner, repo string) map[string]IssueCommentHandler { 346 pa.mut.Lock() 347 defer pa.mut.Unlock() 348 349 hs := map[string]IssueCommentHandler{} 350 for _, p := range pa.getPlugins(owner, repo) { 351 if h, ok := issueCommentHandlers[p]; ok { 352 hs[p] = h 353 } 354 } 355 356 return hs 357 } 358 359 // PullRequestHandlers returns a map of plugin names to handlers for the repo. 360 func (pa *PluginAgent) PullRequestHandlers(owner, repo string) map[string]PullRequestHandler { 361 pa.mut.Lock() 362 defer pa.mut.Unlock() 363 364 hs := map[string]PullRequestHandler{} 365 for _, p := range pa.getPlugins(owner, repo) { 366 if h, ok := pullRequestHandlers[p]; ok { 367 hs[p] = h 368 } 369 } 370 371 return hs 372 } 373 374 // ReviewEventHandlers returns a map of plugin names to handlers for the repo. 375 func (pa *PluginAgent) ReviewEventHandlers(owner, repo string) map[string]ReviewEventHandler { 376 pa.mut.Lock() 377 defer pa.mut.Unlock() 378 379 hs := map[string]ReviewEventHandler{} 380 for _, p := range pa.getPlugins(owner, repo) { 381 if h, ok := reviewEventHandlers[p]; ok { 382 hs[p] = h 383 } 384 } 385 386 return hs 387 } 388 389 // ReviewCommentEventHandlers returns a map of plugin names to handlers for the repo. 390 func (pa *PluginAgent) ReviewCommentEventHandlers(owner, repo string) map[string]ReviewCommentEventHandler { 391 pa.mut.Lock() 392 defer pa.mut.Unlock() 393 394 hs := map[string]ReviewCommentEventHandler{} 395 for _, p := range pa.getPlugins(owner, repo) { 396 if h, ok := reviewCommentEventHandlers[p]; ok { 397 hs[p] = h 398 } 399 } 400 401 return hs 402 } 403 404 // StatusEventHandlers returns a map of plugin names to handlers for the repo. 405 func (pa *PluginAgent) StatusEventHandlers(owner, repo string) map[string]StatusEventHandler { 406 pa.mut.Lock() 407 defer pa.mut.Unlock() 408 409 hs := map[string]StatusEventHandler{} 410 for _, p := range pa.getPlugins(owner, repo) { 411 if h, ok := statusEventHandlers[p]; ok { 412 hs[p] = h 413 } 414 } 415 416 return hs 417 } 418 419 // PushEventHandlers returns a map of plugin names to handlers for the repo. 420 func (pa *PluginAgent) PushEventHandlers(owner, repo string) map[string]PushEventHandler { 421 pa.mut.Lock() 422 defer pa.mut.Unlock() 423 424 hs := map[string]PushEventHandler{} 425 for _, p := range pa.getPlugins(owner, repo) { 426 if h, ok := pushEventHandlers[p]; ok { 427 hs[p] = h 428 } 429 } 430 431 return hs 432 } 433 434 // getPlugins returns a list of plugins that are enabled on a given (org, repository). 435 func (pa *PluginAgent) getPlugins(owner, repo string) []string { 436 var plugins []string 437 438 fullName := fmt.Sprintf("%s/%s", owner, repo) 439 plugins = append(plugins, pa.configuration.Plugins[owner]...) 440 plugins = append(plugins, pa.configuration.Plugins[fullName]...) 441 442 return plugins 443 }