github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/plugins/label/label.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 label 18 19 import ( 20 "fmt" 21 "regexp" 22 "strings" 23 24 "github.com/sirupsen/logrus" 25 26 "k8s.io/test-infra/prow/github" 27 "k8s.io/test-infra/prow/plugins" 28 ) 29 30 const pluginName = "label" 31 32 type assignEvent struct { 33 body string 34 login string 35 org string 36 repo string 37 url string 38 number int 39 issue github.Issue 40 comment github.IssueComment 41 } 42 43 var ( 44 labelRegex = regexp.MustCompile(`(?m)^/(area|priority|kind|sig)\s*(.*)$`) 45 removeLabelRegex = regexp.MustCompile(`(?m)^/remove-(area|priority|kind|sig)\s*(.*)$`) 46 statusRegex = regexp.MustCompile(`(?m)^/status\s+(.+)$`) 47 sigMatcher = regexp.MustCompile(`(?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)`) 48 chatBack = "Reiterating the mentions to trigger a notification: \n%v" 49 nonExistentLabelOnIssue = "Those labels are not set on the issue: `%v`" 50 mustBeSigLead = "You must be a member of the @kubernetes/kubernetes-milestone-maintainers github team to add status labels" 51 statusMap = map[string]string{ 52 "approved-for-milestone": "status/approved-for-milestone", 53 "in-progress": "status/in-progress", 54 "in-review": "status/in-review", 55 } 56 kindMap = map[string]string{ 57 "bugs": "kind/bug", 58 "feature-requests": "kind/feature", 59 "api-reviews": "kind/api-change", 60 "proposals": "kind/design", 61 } 62 ) 63 64 func init() { 65 plugins.RegisterIssueCommentHandler(pluginName, handleIssueComment) 66 plugins.RegisterIssueHandler(pluginName, handleIssue) 67 plugins.RegisterPullRequestHandler(pluginName, handlePullRequest) 68 } 69 70 type githubClient interface { 71 CreateComment(owner, repo string, number int, comment string) error 72 IsMember(org, user string) (bool, error) 73 AddLabel(owner, repo string, number int, label string) error 74 RemoveLabel(owner, repo string, number int, label string) error 75 GetRepoLabels(owner, repo string) ([]github.Label, error) 76 BotName() (string, error) 77 ListTeamMembers(id int) ([]github.TeamMember, error) 78 } 79 80 type slackClient interface { 81 WriteMessage(msg string, channel string) error 82 } 83 84 func handleIssueComment(pc plugins.PluginClient, ic github.IssueCommentEvent) error { 85 if ic.Action != github.IssueCommentActionCreated { 86 return nil 87 } 88 89 ae := assignEvent{ 90 body: ic.Comment.Body, 91 login: ic.Comment.User.Login, 92 org: ic.Repo.Owner.Login, 93 repo: ic.Repo.Name, 94 url: ic.Comment.HTMLURL, 95 number: ic.Issue.Number, 96 issue: ic.Issue, 97 comment: ic.Comment, 98 } 99 return handle(pc.GitHubClient, pc.Logger, ae, pc.SlackClient, pc.PluginConfig.Label.MilestoneMaintainersID) 100 } 101 102 func handleIssue(pc plugins.PluginClient, i github.IssueEvent) error { 103 if i.Action != github.IssueActionOpened { 104 return nil 105 } 106 107 ae := assignEvent{ 108 body: i.Issue.Body, 109 login: i.Issue.User.Login, 110 org: i.Repo.Owner.Login, 111 repo: i.Repo.Name, 112 url: i.Issue.HTMLURL, 113 number: i.Issue.Number, 114 issue: i.Issue, 115 } 116 return handle(pc.GitHubClient, pc.Logger, ae, pc.SlackClient, pc.PluginConfig.Label.MilestoneMaintainersID) 117 } 118 119 func handlePullRequest(pc plugins.PluginClient, pr github.PullRequestEvent) error { 120 if pr.Action != github.PullRequestActionOpened { 121 return nil 122 } 123 124 ae := assignEvent{ 125 body: pr.PullRequest.Body, 126 login: pr.PullRequest.User.Login, 127 org: pr.PullRequest.Base.Repo.Owner.Login, 128 repo: pr.PullRequest.Base.Repo.Name, 129 url: pr.PullRequest.HTMLURL, 130 number: pr.Number, 131 } 132 return handle(pc.GitHubClient, pc.Logger, ae, pc.SlackClient, pc.PluginConfig.Label.MilestoneMaintainersID) 133 } 134 135 // Get Labels from Regexp matches 136 func getLabelsFromREMatches(matches [][]string) (labels []string) { 137 for _, match := range matches { 138 for _, label := range strings.Split(match[0], " ")[1:] { 139 label = strings.ToLower(match[1] + "/" + strings.TrimSpace(label)) 140 labels = append(labels, label) 141 } 142 } 143 return 144 } 145 146 func (ae assignEvent) getRepeats(sigMatches [][]string, existingLabels map[string]string) (toRepeat []string) { 147 toRepeat = []string{} 148 for _, sigMatch := range sigMatches { 149 sigLabel := strings.ToLower("sig" + "/" + strings.TrimSpace(sigMatch[1])) 150 151 if _, ok := existingLabels[sigLabel]; ok { 152 toRepeat = append(toRepeat, sigMatch[0]) 153 } 154 } 155 return 156 } 157 158 // TODO: refactor this function. It's grown too complex 159 func handle(gc githubClient, log *logrus.Entry, ae assignEvent, sc slackClient, maintainersID int) error { 160 // only parse newly created comments/issues/PRs and if non bot author 161 botName, err := gc.BotName() 162 if err != nil { 163 return err 164 } 165 if ae.login == botName { 166 return nil 167 } 168 169 labelMatches := labelRegex.FindAllStringSubmatch(ae.body, -1) 170 removeLabelMatches := removeLabelRegex.FindAllStringSubmatch(ae.body, -1) 171 sigMatches := sigMatcher.FindAllStringSubmatch(ae.body, -1) 172 statusMatches := statusRegex.FindAllStringSubmatch(ae.body, -1) 173 if len(labelMatches) == 0 && len(sigMatches) == 0 && len(removeLabelMatches) == 0 && len(statusMatches) == 0 { 174 return nil 175 } 176 177 labels, err := gc.GetRepoLabels(ae.org, ae.repo) 178 if err != nil { 179 return err 180 } 181 182 existingLabels := map[string]string{} 183 for _, l := range labels { 184 existingLabels[strings.ToLower(l.Name)] = l.Name 185 } 186 var ( 187 nonexistent []string 188 noSuchLabelsOnIssue []string 189 labelsToAdd []string 190 labelsToRemove []string 191 ) 192 193 // Get labels to add and labels to remove from regexp matches 194 labelsToAdd = getLabelsFromREMatches(labelMatches) 195 labelsToRemove = getLabelsFromREMatches(removeLabelMatches) 196 197 // Add labels 198 for _, labelToAdd := range labelsToAdd { 199 if ae.issue.HasLabel(labelToAdd) { 200 continue 201 } 202 203 if _, ok := existingLabels[labelToAdd]; !ok { 204 nonexistent = append(nonexistent, labelToAdd) 205 continue 206 } 207 208 if err := gc.AddLabel(ae.org, ae.repo, ae.number, existingLabels[labelToAdd]); err != nil { 209 log.WithError(err).Errorf("Github failed to add the following label: %s", labelToAdd) 210 } 211 } 212 213 // Remove labels 214 for _, labelToRemove := range labelsToRemove { 215 if !ae.issue.HasLabel(labelToRemove) { 216 noSuchLabelsOnIssue = append(noSuchLabelsOnIssue, labelToRemove) 217 continue 218 } 219 220 if _, ok := existingLabels[labelToRemove]; !ok { 221 nonexistent = append(nonexistent, labelToRemove) 222 continue 223 } 224 225 if err := gc.RemoveLabel(ae.org, ae.repo, ae.number, labelToRemove); err != nil { 226 log.WithError(err).Errorf("Github failed to remove the following label: %s", labelToRemove) 227 } 228 } 229 230 maintainersMap := map[string]bool{} 231 milestoneMaintainers, err := gc.ListTeamMembers(maintainersID) 232 if err != nil { 233 log.WithError(err).Errorf("Failed to list the teammembers for the milestone maintainers team") 234 } else { 235 for _, person := range milestoneMaintainers { 236 maintainersMap[person.Login] = true 237 } 238 } 239 240 for _, statusMatch := range statusMatches { 241 status := strings.TrimSpace(statusMatch[1]) 242 sLabel, validStatus := statusMap[status] 243 if validStatus { 244 _, ok := maintainersMap[ae.login] 245 if ok { 246 if err := gc.AddLabel(ae.org, ae.repo, ae.number, sLabel); err != nil { 247 log.WithError(err).Errorf("Github failed to add the following label: %s", sLabel) 248 } 249 } else { 250 // not in the milestone maintainers team 251 if err := gc.CreateComment(ae.org, ae.repo, ae.number, mustBeSigLead); err != nil { 252 log.WithError(err).Errorf("Could not create comment \"%s\".", mustBeSigLead) 253 } 254 } 255 256 } 257 } 258 259 for _, sigMatch := range sigMatches { 260 sigLabel := strings.ToLower("sig" + "/" + strings.TrimSpace(sigMatch[1])) 261 kind := sigMatch[2] 262 if ae.issue.HasLabel(sigLabel) { 263 continue 264 } 265 if _, ok := existingLabels[sigLabel]; !ok { 266 nonexistent = append(nonexistent, sigLabel) 267 continue 268 } 269 if err := gc.AddLabel(ae.org, ae.repo, ae.number, sigLabel); err != nil { 270 log.WithError(err).Errorf("Github failed to add the following label: %s", sigLabel) 271 } 272 273 if kindLabel, ok := kindMap[kind]; ok { 274 if err := gc.AddLabel(ae.org, ae.repo, ae.number, kindLabel); err != nil { 275 log.WithError(err).Errorf("Github failed to add the following label: %s", kindLabel) 276 } 277 } 278 } 279 280 toRepeat := []string{} 281 isMember := false 282 if len(sigMatches) > 0 { 283 isMember, err = gc.IsMember(ae.org, ae.login) 284 if err != nil { 285 log.WithError(err).Errorf("Github error occurred when checking if the user: %s is a member of org: %s.", ae.login, ae.org) 286 } 287 toRepeat = ae.getRepeats(sigMatches, existingLabels) 288 } 289 if len(toRepeat) > 0 && !isMember { 290 msg := fmt.Sprintf(chatBack, strings.Join(toRepeat, ", ")) 291 if err := gc.CreateComment(ae.org, ae.repo, ae.number, plugins.FormatResponseRaw(ae.body, ae.url, ae.login, msg)); err != nil { 292 log.WithError(err).Errorf("Could not create comment \"%s\".", msg) 293 } 294 } 295 296 //TODO(grodrigues3): Once labels are standardized, make this reply with a comment. 297 if len(nonexistent) > 0 { 298 log.Infof("Nonexistent labels: %v", nonexistent) 299 } 300 301 // Tried to remove Labels that were not present on the Issue 302 if len(noSuchLabelsOnIssue) > 0 { 303 msg := fmt.Sprintf(nonExistentLabelOnIssue, strings.Join(noSuchLabelsOnIssue, ", ")) 304 if err := gc.CreateComment(ae.org, ae.repo, ae.number, plugins.FormatResponseRaw(ae.body, ae.url, ae.login, msg)); err != nil { 305 log.WithError(err).Errorf("Could not create comment \"%s\".", msg) 306 } 307 } 308 309 return nil 310 }