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 }