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