github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/slackevents/slackevents.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 slackevents
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"strings"
    23  
    24  	"k8s.io/test-infra/prow/github"
    25  	"k8s.io/test-infra/prow/pluginhelp"
    26  	"k8s.io/test-infra/prow/plugins"
    27  )
    28  
    29  const (
    30  	pluginName = "slackevents"
    31  )
    32  
    33  var sigMatcher = regexp.MustCompile(`(?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)`)
    34  
    35  type slackClient interface {
    36  	WriteMessage(text string, channel string) error
    37  }
    38  
    39  type githubClient interface {
    40  	BotName() (string, error)
    41  }
    42  
    43  type client struct {
    44  	GithubClient githubClient
    45  	SlackClient  slackClient
    46  	SlackConfig  plugins.Slack
    47  }
    48  
    49  func init() {
    50  	plugins.RegisterPushEventHandler(pluginName, handlePush, helpProvider)
    51  	plugins.RegisterGenericCommentHandler(pluginName, handleComment, helpProvider)
    52  }
    53  
    54  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    55  	configInfo := map[string]string{
    56  		"": fmt.Sprintf("SIG mentions on Github are reiterated for the following SIG Slack channels: %s.", strings.Join(config.Slack.MentionChannels, ", ")),
    57  	}
    58  	for _, repo := range enabledRepos {
    59  		parts := strings.Split(repo, "/")
    60  		if len(parts) != 2 {
    61  			return nil, fmt.Errorf("invalid repo in enabledRepos: %q", repo)
    62  		}
    63  		if mw := getMergeWarning(config.Slack.MergeWarnings, parts[0], parts[1]); mw != nil {
    64  			configInfo[repo] = fmt.Sprintf("In this repo merges are considered "+
    65  				"manual and trigger manual merge warnings if the user who merged is not "+
    66  				"a member of this universal whitelist: %s or merged to a branch they "+
    67  				"are not specifically whitelisted for: %#v.<br>Warnings are sent to the "+
    68  				"following Slack channels: %s.", strings.Join(mw.WhiteList, ", "),
    69  				mw.BranchWhiteList, strings.Join(mw.Channels, ", "))
    70  		} else {
    71  			configInfo[repo] = "There are no manual merge warnings configured for this repo."
    72  		}
    73  	}
    74  	return &pluginhelp.PluginHelp{
    75  			Description: `The slackevents plugin reacts to various Github events by commenting in Slack channels.
    76  <ol><li>The plugin can create comments to alert on manual merges. Manual merges are merges made by a normal user instead of a bot or trusted user.</li>
    77  <li>The plugin can create comments to reiterate SIG mentions like '@kubernetes/sig-testing-bugs' from Github.</li></ol>`,
    78  			Config: configInfo,
    79  		},
    80  		nil
    81  }
    82  
    83  func handleComment(pc plugins.Agent, e github.GenericCommentEvent) error {
    84  	c := client{
    85  		GithubClient: pc.GitHubClient,
    86  		SlackConfig:  pc.PluginConfig.Slack,
    87  		SlackClient:  pc.SlackClient,
    88  	}
    89  	return echoToSlack(c, e)
    90  }
    91  
    92  func handlePush(pc plugins.Agent, pe github.PushEvent) error {
    93  	c := client{
    94  		GithubClient: pc.GitHubClient,
    95  		SlackConfig:  pc.PluginConfig.Slack,
    96  		SlackClient:  pc.SlackClient,
    97  	}
    98  	return notifyOnSlackIfManualMerge(c, pe)
    99  }
   100  
   101  func notifyOnSlackIfManualMerge(pc client, pe github.PushEvent) error {
   102  	//Fetch MergeWarning for the repo we received the merge event.
   103  	if mw := getMergeWarning(pc.SlackConfig.MergeWarnings, pe.Repo.Owner.Login, pe.Repo.Name); mw != nil {
   104  		//If the MergeWarning whitelist has the merge user then no need to send a message.
   105  		if wl := !isWhiteListed(mw, pe); wl {
   106  			var message string
   107  			switch {
   108  			case pe.Created:
   109  				message = fmt.Sprintf("*Warning:* %s (<@%s>) pushed a new branch (%s): %s", pe.Sender.Login, pe.Sender.Login, pe.Branch(), pe.Compare)
   110  			case pe.Deleted:
   111  				message = fmt.Sprintf("*Warning:* %s (<@%s>) deleted a branch (%s): %s", pe.Sender.Login, pe.Sender.Login, pe.Branch(), pe.Compare)
   112  			case pe.Forced:
   113  				message = fmt.Sprintf("*Warning:* %s (<@%s>) *force* merged %d commit(s) into %s: %s", pe.Sender.Login, pe.Sender.Login, len(pe.Commits), pe.Branch(), pe.Compare)
   114  			default:
   115  				message = fmt.Sprintf("*Warning:* %s (<@%s>) manually merged %d commit(s) into %s: %s", pe.Sender.Login, pe.Sender.Login, len(pe.Commits), pe.Branch(), pe.Compare)
   116  			}
   117  			for _, channel := range mw.Channels {
   118  				if err := pc.SlackClient.WriteMessage(message, channel); err != nil {
   119  					return err
   120  				}
   121  			}
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  func isWhiteListed(mw *plugins.MergeWarning, pe github.PushEvent) bool {
   128  	bwl := mw.BranchWhiteList[pe.Branch()]
   129  	inWhiteList := stringInArray(pe.Pusher.Name, mw.WhiteList) || stringInArray(pe.Sender.Login, mw.WhiteList)
   130  	inBranchWhiteList := stringInArray(pe.Pusher.Name, bwl) || stringInArray(pe.Sender.Login, bwl)
   131  	return inWhiteList || inBranchWhiteList
   132  }
   133  
   134  func getMergeWarning(mergeWarnings []plugins.MergeWarning, org, repo string) *plugins.MergeWarning {
   135  	for _, mw := range mergeWarnings {
   136  		if stringInArray(org, mw.Repos) || stringInArray(fmt.Sprintf("%s/%s", org, repo), mw.Repos) {
   137  			return &mw
   138  		}
   139  	}
   140  	return nil
   141  }
   142  
   143  func stringInArray(str string, list []string) bool {
   144  	for _, v := range list {
   145  		if v == str {
   146  			return true
   147  		}
   148  	}
   149  	return false
   150  }
   151  
   152  func echoToSlack(pc client, e github.GenericCommentEvent) error {
   153  	// Ignore bot comments and comments that aren't new.
   154  	botName, err := pc.GithubClient.BotName()
   155  	if err != nil {
   156  		return err
   157  	}
   158  	if e.User.Login == botName {
   159  		return nil
   160  	}
   161  	if e.Action != github.GenericCommentActionCreated {
   162  		return nil
   163  	}
   164  
   165  	sigMatches := sigMatcher.FindAllStringSubmatch(e.Body, -1)
   166  
   167  	for _, match := range sigMatches {
   168  		sig := "sig-" + match[1]
   169  		// Check if this sig is a slack channel that should be messaged.
   170  		found := false
   171  		for _, channel := range pc.SlackConfig.MentionChannels {
   172  			if channel == sig {
   173  				found = true
   174  				break
   175  			}
   176  		}
   177  		if !found {
   178  			continue
   179  		}
   180  
   181  		msg := fmt.Sprintf("%s was mentioned by %s (<@%s>) on Github. (%s)\n>>>%s", sig, e.User.Login, e.User.Login, e.HTMLURL, e.Body)
   182  		if err := pc.SlackClient.WriteMessage(msg, sig); err != nil {
   183  			return fmt.Errorf("Failed to send message on slack channel: %q with message %q. Err: %v", sig, msg, err)
   184  		}
   185  	}
   186  	return nil
   187  }