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 }