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