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  }