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 }