github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/verify-owners/verify-owners.go (about)

     1  /*
     2  Copyright 2018 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 verifyowners
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"path/filepath"
    23  	"regexp"
    24  	"strconv"
    25  
    26  	"github.com/sirupsen/logrus"
    27  
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"k8s.io/test-infra/prow/git"
    30  	"k8s.io/test-infra/prow/github"
    31  	"k8s.io/test-infra/prow/labels"
    32  	"k8s.io/test-infra/prow/pluginhelp"
    33  	"k8s.io/test-infra/prow/plugins"
    34  	"k8s.io/test-infra/prow/plugins/golint"
    35  	"k8s.io/test-infra/prow/repoowners"
    36  )
    37  
    38  const (
    39  	// PluginName defines this plugin's registered name.
    40  	PluginName     = "verify-owners"
    41  	ownersFileName = "OWNERS"
    42  )
    43  
    44  func init() {
    45  	plugins.RegisterPullRequestHandler(PluginName, handlePullRequest, helpProvider)
    46  }
    47  
    48  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    49  	return &pluginhelp.PluginHelp{
    50  			Description: fmt.Sprintf("The verify-owners plugin validates %s files if they are modified in a PR. On validation failure it automatically adds the '%s' label to the PR, and a review comment on the incriminating file(s).", ownersFileName, labels.InvalidOwners),
    51  		},
    52  		nil
    53  }
    54  
    55  type ownersClient interface {
    56  	LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error)
    57  }
    58  
    59  type githubClient interface {
    60  	AddLabel(org, repo string, number int, label string) error
    61  	CreateComment(owner, repo string, number int, comment string) error
    62  	CreateReview(org, repo string, number int, r github.DraftReview) error
    63  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
    64  	RemoveLabel(owner, repo string, number int, label string) error
    65  }
    66  
    67  func handlePullRequest(pc plugins.Agent, pre github.PullRequestEvent) error {
    68  	if pre.Action != github.PullRequestActionOpened && pre.Action != github.PullRequestActionReopened && pre.Action != github.PullRequestActionSynchronize {
    69  		return nil
    70  	}
    71  	return handle(pc.GitHubClient, pc.GitClient, pc.Logger, &pre, pc.PluginConfig.Owners.LabelsBlackList)
    72  }
    73  
    74  type messageWithLine struct {
    75  	line    int
    76  	message string
    77  }
    78  
    79  func handle(ghc githubClient, gc *git.Client, log *logrus.Entry, pre *github.PullRequestEvent, labelsBlackList []string) error {
    80  	org := pre.Repo.Owner.Login
    81  	repo := pre.Repo.Name
    82  	wrongOwnersFiles := map[string]messageWithLine{}
    83  
    84  	// Get changes.
    85  	changes, err := ghc.GetPullRequestChanges(org, repo, pre.Number)
    86  	if err != nil {
    87  		return fmt.Errorf("error getting PR changes: %v", err)
    88  	}
    89  
    90  	// List modified OWNERS files.
    91  	var modifiedOwnersFiles []github.PullRequestChange
    92  	for _, change := range changes {
    93  		if filepath.Base(change.Filename) == ownersFileName {
    94  			modifiedOwnersFiles = append(modifiedOwnersFiles, change)
    95  		}
    96  	}
    97  	if len(modifiedOwnersFiles) == 0 {
    98  		return nil
    99  	}
   100  
   101  	// Clone the repo, checkout the PR.
   102  	r, err := gc.Clone(pre.Repo.FullName)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	defer func() {
   107  		if err := r.Clean(); err != nil {
   108  			log.WithError(err).Error("Error cleaning up repo.")
   109  		}
   110  	}()
   111  	if err := r.CheckoutPullRequest(pre.Number); err != nil {
   112  		return err
   113  	}
   114  	// If we have a specific SHA, use it.
   115  	if pre.PullRequest.Head.SHA != "" {
   116  		if err := r.Checkout(pre.PullRequest.Head.SHA); err != nil {
   117  			return err
   118  		}
   119  	}
   120  
   121  	// Check each OWNERS file.
   122  	for _, c := range modifiedOwnersFiles {
   123  		// Try to load OWNERS file.
   124  		path := filepath.Join(r.Dir, c.Filename)
   125  		b, err := ioutil.ReadFile(path)
   126  		if err != nil {
   127  			log.WithError(err).Warningf("Failed to read %s.", path)
   128  			return nil
   129  		}
   130  		var approvers []string
   131  		var labels []string
   132  		// by default we bind errors to line 1
   133  		lineNumber := 1
   134  		simple, err := repoowners.ParseSimpleConfig(b)
   135  		if err != nil || simple.Empty() {
   136  			full, err := repoowners.ParseFullConfig(b)
   137  			if err != nil {
   138  				lineNumberRe, _ := regexp.Compile(`line (\d+)`)
   139  				lineNumberMatches := lineNumberRe.FindStringSubmatch(err.Error())
   140  				// try to find a line number for the error
   141  				if len(lineNumberMatches) > 1 {
   142  					// we're sure it will convert as it passed the regexp already
   143  					absoluteLineNumber, _ := strconv.Atoi(lineNumberMatches[1])
   144  					// we need to convert it to a line number relative to the patch
   145  					al, err := golint.AddedLines(c.Patch)
   146  					if err != nil {
   147  						log.WithError(err).Errorf("Failed to compute added lines in %s: %v", c.Filename, err)
   148  					} else if val, ok := al[absoluteLineNumber]; ok {
   149  						lineNumber = val
   150  					}
   151  				}
   152  				wrongOwnersFiles[c.Filename] = messageWithLine{
   153  					lineNumber,
   154  					fmt.Sprintf("Cannot parse file: %v.", err),
   155  				}
   156  				continue
   157  			} else {
   158  				// it's a FullConfig
   159  				for _, config := range full.Filters {
   160  					approvers = append(approvers, config.Approvers...)
   161  					labels = append(labels, config.Labels...)
   162  				}
   163  			}
   164  		} else {
   165  			// it's a SimpleConfig
   166  			approvers = simple.Config.Approvers
   167  			labels = simple.Config.Labels
   168  		}
   169  		// Check labels against blacklist
   170  		if sets.NewString(labels...).HasAny(labelsBlackList...) {
   171  			wrongOwnersFiles[c.Filename] = messageWithLine{
   172  				lineNumber,
   173  				fmt.Sprintf("File contains blacklisted labels: %s.", sets.NewString(labels...).Intersection(sets.NewString(labelsBlackList...)).List()),
   174  			}
   175  			continue
   176  		}
   177  		// Check approvers isn't empty
   178  		if filepath.Dir(c.Filename) == "." && len(approvers) == 0 {
   179  			wrongOwnersFiles[c.Filename] = messageWithLine{
   180  				lineNumber,
   181  				fmt.Sprintf("No approvers defined in this root directory %s file.", ownersFileName),
   182  			}
   183  			continue
   184  		}
   185  	}
   186  	// React if we saw something.
   187  	if len(wrongOwnersFiles) > 0 {
   188  		s := "s"
   189  		if len(wrongOwnersFiles) == 1 {
   190  			s = ""
   191  		}
   192  		if err := ghc.AddLabel(org, repo, pre.Number, labels.InvalidOwners); err != nil {
   193  			return err
   194  		}
   195  		log.Debugf("Creating a review for %d %s file%s.", len(wrongOwnersFiles), ownersFileName, s)
   196  		var comments []github.DraftReviewComment
   197  		for errFile, err := range wrongOwnersFiles {
   198  			comments = append(comments, github.DraftReviewComment{
   199  				Path:     errFile,
   200  				Body:     err.message,
   201  				Position: err.line,
   202  			})
   203  		}
   204  		// Make the review body.
   205  		response := fmt.Sprintf("%d invalid %s file%s", len(wrongOwnersFiles), ownersFileName, s)
   206  		draftReview := github.DraftReview{
   207  			Body:     plugins.FormatResponseRaw(pre.PullRequest.Body, pre.PullRequest.HTMLURL, pre.PullRequest.User.Login, response),
   208  			Action:   github.Comment,
   209  			Comments: comments,
   210  		}
   211  		if pre.PullRequest.Head.SHA != "" {
   212  			draftReview.CommitSHA = pre.PullRequest.Head.SHA
   213  		}
   214  		err := ghc.CreateReview(org, repo, pre.Number, draftReview)
   215  		if err != nil {
   216  			return fmt.Errorf("error creating a review for invalid %s file%s: %v", ownersFileName, s, err)
   217  		}
   218  	} else {
   219  		// Don't bother checking if it has the label...it's a race, and we'll have
   220  		// to handle failure due to not being labeled anyway.
   221  		if err := ghc.RemoveLabel(org, repo, pre.Number, labels.InvalidOwners); err != nil {
   222  			return fmt.Errorf("failed removing %s label: %v", labels.InvalidOwners, err)
   223  		}
   224  	}
   225  	return nil
   226  }