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  }