github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/blockade/blockade.go (about) 1 /* 2 Copyright 2017 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 blockade defines a plugin that adds the 'do-not-merge/blocked-paths' label to PRs that 18 // modify protected file paths. 19 // Protected file paths are defined with the plugins.Blockade struct. A PR is blocked if any file 20 // it changes is blocked by any Blockade. The process for determining if a file is blocked by a 21 // Blockade is as follows: 22 // By default, allow the file. Block if the file path matches any of block regexps, and does not 23 // match any of the exception regexps. 24 package blockade 25 26 import ( 27 "bytes" 28 "fmt" 29 "regexp" 30 "strings" 31 32 "github.com/sirupsen/logrus" 33 "k8s.io/test-infra/prow/github" 34 "k8s.io/test-infra/prow/labels" 35 "k8s.io/test-infra/prow/pluginhelp" 36 "k8s.io/test-infra/prow/plugins" 37 ) 38 39 const ( 40 // PluginName defines this plugin's registered name. 41 PluginName = "blockade" 42 ) 43 44 var blockedPathsBody = fmt.Sprintf("Adding label: `%s` because PR changes a protected file.", labels.BlockedPaths) 45 46 type githubClient interface { 47 GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) 48 GetIssueLabels(org, repo string, number int) ([]github.Label, error) 49 AddLabel(owner, repo string, number int, label string) error 50 RemoveLabel(owner, repo string, number int, label string) error 51 CreateComment(org, repo string, number int, comment string) error 52 } 53 54 type pruneClient interface { 55 PruneComments(func(ic github.IssueComment) bool) 56 } 57 58 func init() { 59 plugins.RegisterPullRequestHandler(PluginName, handlePullRequest, helpProvider) 60 } 61 62 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) { 63 // The {WhoCanUse, Usage, Examples} fields are omitted because this plugin cannot be triggered manually. 64 blockConfig := map[string]string{} 65 for _, repo := range enabledRepos { 66 parts := strings.Split(repo, "/") 67 if len(parts) != 2 { 68 return nil, fmt.Errorf("invalid repo in enabledRepos: %q", repo) 69 } 70 var buf bytes.Buffer 71 fmt.Fprint(&buf, "The following blockades apply in this repository:") 72 for _, blockade := range config.Blockades { 73 if !stringInSlice(parts[0], blockade.Repos) && !stringInSlice(repo, blockade.Repos) { 74 continue 75 } 76 fmt.Fprintf(&buf, "<br>Block reason: '%s'<br>    Block regexps: %q<br>    Exception regexps: %q<br>", blockade.Explanation, blockade.BlockRegexps, blockade.ExceptionRegexps) 77 } 78 blockConfig[repo] = buf.String() 79 } 80 return &pluginhelp.PluginHelp{ 81 Description: "The blockade plugin blocks pull requests from merging if they touch specific files. The plugin applies the '" + labels.BlockedPaths + "' label to pull requests that touch files that match a blockade's block regular expression and none of the corresponding exception regular expressions.", 82 Config: blockConfig, 83 }, 84 nil 85 } 86 87 type blockCalc func([]github.PullRequestChange, []blockade) summary 88 89 type client struct { 90 ghc githubClient 91 log *logrus.Entry 92 93 blockCalc blockCalc 94 } 95 96 func handlePullRequest(pc plugins.Agent, pre github.PullRequestEvent) error { 97 cp, err := pc.CommentPruner() 98 if err != nil { 99 return err 100 } 101 return handle(pc.GitHubClient, pc.Logger, pc.PluginConfig.Blockades, cp, calculateBlocks, &pre) 102 } 103 104 // blockade is a compiled version of a plugins.Blockade config struct. 105 type blockade struct { 106 blockRegexps, exceptionRegexps []*regexp.Regexp 107 explanation string 108 } 109 110 func (bd *blockade) isBlocked(file string) bool { 111 return matchesAny(file, bd.blockRegexps) && !matchesAny(file, bd.exceptionRegexps) 112 } 113 114 type summary map[string][]github.PullRequestChange 115 116 func (s summary) String() string { 117 if len(s) == 0 { 118 return "" 119 } 120 var buf bytes.Buffer 121 fmt.Fprint(&buf, "#### Reasons for blocking this PR:\n") 122 for reason, files := range s { 123 fmt.Fprintf(&buf, "[%s]\n", reason) 124 for _, file := range files { 125 fmt.Fprintf(&buf, "- [%s](%s)\n\n", file.Filename, file.BlobURL) 126 } 127 } 128 return buf.String() 129 } 130 131 func handle(ghc githubClient, log *logrus.Entry, config []plugins.Blockade, cp pruneClient, blockCalc blockCalc, pre *github.PullRequestEvent) error { 132 if pre.Action != github.PullRequestActionSynchronize && 133 pre.Action != github.PullRequestActionOpened && 134 pre.Action != github.PullRequestActionReopened { 135 return nil 136 } 137 138 org := pre.Repo.Owner.Login 139 repo := pre.Repo.Name 140 issueLabels, err := ghc.GetIssueLabels(org, repo, pre.Number) 141 if err != nil { 142 return err 143 } 144 145 labelPresent := hasBlockedLabel(issueLabels) 146 blockades := compileApplicableBlockades(org, repo, log, config) 147 if len(blockades) == 0 && !labelPresent { 148 // Since the label is missing, we assume that we removed any associated comments. 149 return nil 150 } 151 152 var sum summary 153 if len(blockades) > 0 { 154 changes, err := ghc.GetPullRequestChanges(org, repo, pre.Number) 155 if err != nil { 156 return err 157 } 158 sum = blockCalc(changes, blockades) 159 } 160 161 shouldBlock := len(sum) > 0 162 if shouldBlock && !labelPresent { 163 // Add the label and leave a comment explaining why the label was added. 164 if err := ghc.AddLabel(org, repo, pre.Number, labels.BlockedPaths); err != nil { 165 return err 166 } 167 msg := plugins.FormatResponse(pre.PullRequest.User.Login, blockedPathsBody, sum.String()) 168 return ghc.CreateComment(org, repo, pre.Number, msg) 169 } else if !shouldBlock && labelPresent { 170 // Remove the label and delete any comments created by this plugin. 171 if err := ghc.RemoveLabel(org, repo, pre.Number, labels.BlockedPaths); err != nil { 172 return err 173 } 174 cp.PruneComments(func(ic github.IssueComment) bool { 175 return strings.Contains(ic.Body, blockedPathsBody) 176 }) 177 } 178 return nil 179 } 180 181 // compileApplicableBlockades filters the specified blockades and compiles those that apply to the repo. 182 func compileApplicableBlockades(org, repo string, log *logrus.Entry, blockades []plugins.Blockade) []blockade { 183 if len(blockades) == 0 { 184 return nil 185 } 186 187 orgRepo := fmt.Sprintf("%s/%s", org, repo) 188 var compiled []blockade 189 for _, raw := range blockades { 190 // Only consider blockades that apply to this repo. 191 if !stringInSlice(org, raw.Repos) && !stringInSlice(orgRepo, raw.Repos) { 192 continue 193 } 194 b := blockade{} 195 for _, str := range raw.BlockRegexps { 196 if reg, err := regexp.Compile(str); err != nil { 197 log.WithError(err).Errorf("Failed to compile the blockade regexp '%s'.", str) 198 } else { 199 b.blockRegexps = append(b.blockRegexps, reg) 200 } 201 } 202 if len(b.blockRegexps) == 0 { 203 continue 204 } 205 if raw.Explanation == "" { 206 b.explanation = "Files are protected" 207 } else { 208 b.explanation = raw.Explanation 209 } 210 for _, str := range raw.ExceptionRegexps { 211 if reg, err := regexp.Compile(str); err != nil { 212 log.WithError(err).Errorf("Failed to compile the blockade regexp '%s'.", str) 213 } else { 214 b.exceptionRegexps = append(b.exceptionRegexps, reg) 215 } 216 } 217 compiled = append(compiled, b) 218 } 219 return compiled 220 } 221 222 // calculateBlocks determines if a PR should be blocked and returns the summary describing the block. 223 func calculateBlocks(changes []github.PullRequestChange, blockades []blockade) summary { 224 sum := make(summary) 225 for _, change := range changes { 226 for _, b := range blockades { 227 if b.isBlocked(change.Filename) { 228 sum[b.explanation] = append(sum[b.explanation], change) 229 } 230 } 231 } 232 return sum 233 } 234 235 func hasBlockedLabel(githubLabels []github.Label) bool { 236 label := strings.ToLower(labels.BlockedPaths) 237 for _, elem := range githubLabels { 238 if strings.ToLower(elem.Name) == label { 239 return true 240 } 241 } 242 return false 243 } 244 245 func matchesAny(str string, regexps []*regexp.Regexp) bool { 246 for _, reg := range regexps { 247 if reg.MatchString(str) { 248 return true 249 } 250 } 251 return false 252 } 253 254 func stringInSlice(str string, slice []string) bool { 255 for _, elem := range slice { 256 if elem == str { 257 return true 258 } 259 } 260 return false 261 }