sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/pjutil/filter.go (about) 1 /* 2 Copyright 2019 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 pjutil 18 19 import ( 20 "fmt" 21 "regexp" 22 "strings" 23 24 "github.com/sirupsen/logrus" 25 26 "k8s.io/apimachinery/pkg/util/sets" 27 "sigs.k8s.io/prow/pkg/config" 28 ) 29 30 var TestAllRe = regexp.MustCompile(`(?m)^/test all,?($|\s.*)`) 31 32 // RetestRe provides the regex for `/retest` 33 var RetestRe = regexp.MustCompile(`(?m)^/retest\s*$`) 34 35 // RetestRe provides the regex for `/retest-required` 36 var RetestRequiredRe = regexp.MustCompile(`(?m)^/retest-required\s*$`) 37 38 var OkToTestRe = regexp.MustCompile(`(?m)^/ok-to-test\s*$`) 39 40 // AvailablePresubmits returns 3 sets of presubmits: 41 // 1. presubmits that can be run with '/test all' command. 42 // 2. optional presubmits commands that can be run with their trigger, e.g. '/test job' 43 // 3. required presubmits commands that can be run with their trigger, e.g. '/test job' 44 func AvailablePresubmits(changes config.ChangedFilesProvider, branch string, 45 presubmits []config.Presubmit, logger *logrus.Entry) (sets.Set[string], sets.Set[string], sets.Set[string], error) { 46 runWithTestAllNames := sets.New[string]() 47 optionalJobTriggerCommands := sets.New[string]() 48 requiredJobsTriggerCommands := sets.New[string]() 49 50 runWithTestAll, err := FilterPresubmits(NewTestAllFilter(), changes, branch, presubmits, logger) 51 if err != nil { 52 return runWithTestAllNames, optionalJobTriggerCommands, requiredJobsTriggerCommands, err 53 } 54 55 var triggerFilters []Filter 56 for _, ps := range presubmits { 57 triggerFilters = append(triggerFilters, NewCommandFilter(ps.RerunCommand)) 58 } 59 runWithTrigger, err := FilterPresubmits(NewAggregateFilter(triggerFilters), changes, branch, presubmits, logger) 60 if err != nil { 61 return runWithTestAllNames, optionalJobTriggerCommands, requiredJobsTriggerCommands, err 62 } 63 64 for _, ps := range runWithTestAll { 65 runWithTestAllNames.Insert(ps.Name) 66 } 67 68 for _, ps := range runWithTrigger { 69 if ps.Optional { 70 optionalJobTriggerCommands.Insert(ps.RerunCommand) 71 } else { 72 requiredJobsTriggerCommands.Insert(ps.RerunCommand) 73 } 74 } 75 76 return runWithTestAllNames, optionalJobTriggerCommands, requiredJobsTriggerCommands, nil 77 } 78 79 // Filter digests a presubmit config to determine if: 80 // - the presubmit matched the filter 81 // - we know that the presubmit is forced to run 82 // - what the default behavior should be if the presubmit 83 // runs conditionally and does not match trigger conditions 84 type Filter interface { 85 ShouldRun(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) 86 Name() string 87 } 88 89 // ArbitraryFilter is a lazy filter that can be used ad hoc when consumer 90 // doesn't want to declare a new struct. One of the usage is in unit test. 91 type ArbitraryFilter struct { 92 override func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) 93 name string 94 } 95 96 func NewArbitraryFilter(override func(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool), name string) *ArbitraryFilter { 97 return &ArbitraryFilter{override: override, name: name} 98 } 99 100 func (af *ArbitraryFilter) ShouldRun(p config.Presubmit) (shouldRun bool, forcedToRun bool, defaultBehavior bool) { 101 return af.override(p) 102 } 103 104 func (af *ArbitraryFilter) Name() string { 105 return af.name 106 } 107 108 // CommandFilter builds a filter for `/test foo` 109 type CommandFilter struct { 110 body string 111 // half is used by `.Name`. For large body only the first and last "half" 112 // chars are part of `.Name`. The default is 70, define here for easier unit 113 // test purpose. 114 half int 115 } 116 117 func NewCommandFilter(body string) *CommandFilter { 118 return &CommandFilter{body: body, half: 70} 119 } 120 121 func (cf *CommandFilter) ShouldRun(p config.Presubmit) (bool, bool, bool) { 122 return p.TriggerMatches(cf.body), p.TriggerMatches(cf.body), true 123 } 124 125 func (cf *CommandFilter) Name() string { 126 var body string 127 if len(cf.body) <= cf.half*2 { 128 body = cf.body 129 } else { 130 body = cf.body[:cf.half] + "\n...\n" + cf.body[len(cf.body)-cf.half:] 131 } 132 return "command-filter: " + body 133 } 134 135 // TestAllFilter builds a filter for the automatic behavior of `/test all`. 136 // Jobs that explicitly match `/test all` in their trigger regex will be 137 // handled by a commandFilter for the comment in question. 138 type TestAllFilter struct{} 139 140 func NewTestAllFilter() *TestAllFilter { 141 return &TestAllFilter{} 142 } 143 144 func (tf *TestAllFilter) ShouldRun(p config.Presubmit) (bool, bool, bool) { 145 return !p.NeedsExplicitTrigger(), false, false 146 } 147 148 func (tf *TestAllFilter) Name() string { 149 return "test-all-filter" 150 } 151 152 // AggregateFilter builds a filter that evaluates the child filters in order 153 // and returns the first match 154 type AggregateFilter struct { 155 filters []Filter 156 } 157 158 func NewAggregateFilter(filters []Filter) *AggregateFilter { 159 return &AggregateFilter{filters: filters} 160 } 161 162 func (nf *AggregateFilter) ShouldRun(p config.Presubmit) (bool, bool, bool) { 163 for _, filter := range nf.filters { 164 if shouldRun, forced, defaults := filter.ShouldRun(p); shouldRun { 165 return shouldRun, forced, defaults 166 } 167 } 168 return false, false, false 169 } 170 171 func (nf *AggregateFilter) Name() string { 172 var names []string 173 for _, filter := range nf.filters { 174 names = append(names, filter.Name()) 175 } 176 return strings.Join(names, "::") 177 } 178 179 // FilterPresubmits determines which presubmits should run by evaluating the user-provided filter. 180 func FilterPresubmits(filter Filter, changes config.ChangedFilesProvider, branch string, presubmits []config.Presubmit, logger logrus.FieldLogger) ([]config.Presubmit, error) { 181 182 var toTrigger []config.Presubmit 183 var namesToTrigger []string 184 var noMatch, shouldnotRun int 185 for _, presubmit := range presubmits { 186 matches, forced, defaults := filter.ShouldRun(presubmit) 187 if !matches { 188 noMatch++ 189 continue 190 } 191 shouldRun, err := presubmit.ShouldRun(branch, changes, forced, defaults) 192 if err != nil { 193 return nil, fmt.Errorf("%s: should run: %w", presubmit.Name, err) 194 } 195 if !shouldRun { 196 shouldnotRun++ 197 continue 198 } 199 // Add a trace log for debugging an internal bug. 200 // (TODO: chaodaiG) Remove this once root cause is discovered. 201 logger.WithFields(logrus.Fields{ 202 "pj": presubmit.Name, 203 "trigger": presubmit.Trigger, 204 "filters": filter.Name(), 205 }).Trace("Job should be triggered.") 206 toTrigger = append(toTrigger, presubmit) 207 namesToTrigger = append(namesToTrigger, presubmit.Name) 208 } 209 210 logger.WithFields(logrus.Fields{ 211 "to-trigger": namesToTrigger, 212 "total-count": len(presubmits), 213 "to-trigger-count": len(toTrigger), 214 "no-match-count": noMatch, 215 "should-not-run-count": shouldnotRun, 216 "filters": filter.Name()}).Debug("Filtered complete.") 217 return toTrigger, nil 218 } 219 220 // RetestFilter builds a filter for `/retest` 221 type RetestFilter struct { 222 failedContexts, allContexts sets.Set[string] 223 } 224 225 func NewRetestFilter(failedContexts, allContexts sets.Set[string]) *RetestFilter { 226 return &RetestFilter{ 227 failedContexts: failedContexts, 228 allContexts: allContexts, 229 } 230 } 231 232 func (rf *RetestFilter) ShouldRun(p config.Presubmit) (bool, bool, bool) { 233 failed := rf.failedContexts.Has(p.Context) 234 return failed || (!p.NeedsExplicitTrigger() && !rf.allContexts.Has(p.Context)), false, failed 235 } 236 237 func (rf *RetestFilter) Name() string { 238 return "retest-filter" 239 } 240 241 type RetestRequiredFilter struct { 242 failedContexts, allContexts sets.Set[string] 243 } 244 245 func NewRetestRequiredFilter(failedContexts, allContexts sets.Set[string]) *RetestRequiredFilter { 246 return &RetestRequiredFilter{ 247 failedContexts: failedContexts, 248 allContexts: allContexts, 249 } 250 } 251 252 func (rrf *RetestRequiredFilter) ShouldRun(ps config.Presubmit) (bool, bool, bool) { 253 if ps.Optional { 254 return false, false, false 255 } 256 return NewRetestFilter(rrf.failedContexts, rrf.allContexts).ShouldRun(ps) 257 } 258 259 func (rrf *RetestRequiredFilter) Name() string { 260 return "retest-required-filter" 261 } 262 263 type contextGetter func() (sets.Set[string], sets.Set[string], error) 264 265 // PresubmitFilter creates a filter for presubmits 266 func PresubmitFilter(honorOkToTest bool, contextGetter contextGetter, body string, logger logrus.FieldLogger) (Filter, error) { 267 // the filters determine if we should check whether a job should run, whether 268 // it should run regardless of whether its triggering conditions match, and 269 // what the default behavior should be for that check. Multiple filters 270 // can match a single presubmit, so it is important to order them correctly 271 // as they have precedence -- filters that override the false default should 272 // match before others. We order filters by amount of specificity. 273 var filters []Filter 274 filters = append(filters, NewCommandFilter(body)) 275 if RetestRe.MatchString(body) { 276 logger.Info("Using retest filter.") 277 failedContexts, allContexts, err := contextGetter() 278 if err != nil { 279 return nil, err 280 } 281 filters = append(filters, NewRetestFilter(failedContexts, allContexts)) 282 } 283 if RetestRequiredRe.MatchString(body) { 284 logger.Info("Using retest-required filter") 285 failedContexts, allContexts, err := contextGetter() 286 if err != nil { 287 return nil, err 288 } 289 filters = append(filters, NewRetestRequiredFilter(failedContexts, allContexts)) 290 } 291 if (honorOkToTest && OkToTestRe.MatchString(body)) || TestAllRe.MatchString(body) { 292 logger.Debug("Using test-all filter.") 293 filters = append(filters, NewTestAllFilter()) 294 } 295 return NewAggregateFilter(filters), nil 296 }