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  }