sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/tide/gerrit.go (about)

     1  /*
     2  Copyright 2022 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 tide
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  	"sync"
    24  	"time"
    25  
    26  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    27  
    28  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    31  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    32  	"sigs.k8s.io/prow/pkg/config"
    33  	gerritadaptor "sigs.k8s.io/prow/pkg/gerrit/adapter"
    34  	"sigs.k8s.io/prow/pkg/gerrit/client"
    35  	"sigs.k8s.io/prow/pkg/git/types"
    36  	"sigs.k8s.io/prow/pkg/git/v2"
    37  	"sigs.k8s.io/prow/pkg/io"
    38  	"sigs.k8s.io/prow/pkg/kube"
    39  	"sigs.k8s.io/prow/pkg/moonraker"
    40  	"sigs.k8s.io/prow/pkg/tide/blockers"
    41  	"sigs.k8s.io/prow/pkg/tide/history"
    42  
    43  	"github.com/andygrunwald/go-gerrit"
    44  	githubql "github.com/shurcooL/githubv4"
    45  	"github.com/sirupsen/logrus"
    46  )
    47  
    48  const (
    49  	// tideEnablementLabel is the Gerrit label that has to be voted for enabling
    50  	// Tide. By default a PR is not considered by Tide unless the author of the
    51  	// PR toggled this label.
    52  	tideEnablementLabel = "Prow-Auto-Submit"
    53  	// ref:
    54  	// https://gerrit-review.googlesource.com/Documentation/user-search.html#_search_operators.
    55  	// Also good to know: `(repo:repo-A OR repo:repo-B)`
    56  	gerritDefaultQueryParam = "status:open+-is:wip+is:submittable"
    57  )
    58  
    59  func gerritQueryParam(optInByDefault bool) string {
    60  	// Whenever a the `Prow-Auto-Submit` label is voted with -1 by anyone, the
    61  	// PR has to be excluded from Tide.
    62  	enablementLabelQueryParam := "+-label:" + tideEnablementLabel + "=-1"
    63  	// By default require `Prow-Auto-Submit` label.
    64  	// If the repo enabled optInByDefault, `Prow-Auto-Submit` is no longer
    65  	// required. But users can still temporarily opting out of merge automation
    66  	// by voting -1 on this label.
    67  	if !optInByDefault {
    68  		// We want `-label:Prow-Auto-Submit=-1 label:Prow-Auto-Submit`
    69  		enablementLabelQueryParam += "+label:" + tideEnablementLabel
    70  	}
    71  	return gerritDefaultQueryParam + enablementLabelQueryParam
    72  }
    73  
    74  // gerritContextChecker implements contextChecker, it's a permissive no-op
    75  // implementation for Gerrit only, as context checking only applies to GitHub.
    76  type gerritContextChecker struct{}
    77  
    78  // IsOptional tells whether a context is optional.
    79  func (gcc *gerritContextChecker) IsOptional(string) bool {
    80  	return true
    81  }
    82  
    83  // MissingRequiredContexts tells if required contexts are missing from the list of contexts provided.
    84  func (gcc *gerritContextChecker) MissingRequiredContexts([]string) []string {
    85  	return nil
    86  }
    87  
    88  type gerritClient interface {
    89  	QueryChangesForProject(instance, project string, lastUpdate time.Time, rateLimit int, additionalFilters ...string) ([]gerrit.ChangeInfo, error)
    90  	GetChange(instance, id string, additionalFields ...string) (*gerrit.ChangeInfo, error)
    91  	GetBranchRevision(instance, project, branch string) (string, error)
    92  	SubmitChange(instance, id string, wait bool) (*gerrit.ChangeInfo, error)
    93  	SetReview(instance, id, revision, message string, _ map[string]string) error
    94  }
    95  
    96  // NewController makes a Controller out of the given clients.
    97  func NewGerritController(
    98  	mgr manager,
    99  	cfgAgent *config.Agent,
   100  	gc git.ClientFactory,
   101  	maxRecordsPerPool int,
   102  	opener io.Opener,
   103  	historyURI,
   104  	statusURI string,
   105  	logger *logrus.Entry,
   106  	configOptions configflagutil.ConfigOptions,
   107  	cookieFilePath string,
   108  	maxQPS, maxBurst int,
   109  ) (*Controller, error) {
   110  	if logger == nil {
   111  		logger = logrus.NewEntry(logrus.StandardLogger())
   112  	}
   113  	hist, err := history.New(maxRecordsPerPool, opener, historyURI)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("error initializing history client from %q: %w", historyURI, err)
   116  	}
   117  
   118  	ctx := context.Background()
   119  	// Shared fields
   120  	statusUpdate := &statusUpdate{
   121  		dontUpdateStatus: &threadSafePRSet{},
   122  		newPoolPending:   make(chan bool),
   123  	}
   124  
   125  	var ircg config.InRepoConfigGetter
   126  	if configOptions.MoonrakerAddress != "" {
   127  		moonrakerClient, err := moonraker.NewClient(configOptions.MoonrakerAddress, cfgAgent)
   128  		if err != nil {
   129  			logrus.WithError(err).Fatal("Error getting Moonraker client.")
   130  		}
   131  		ircg = moonrakerClient
   132  	} else {
   133  		var err error
   134  		ircg, err = config.NewInRepoConfigCache(configOptions.InRepoConfigCacheSize, cfgAgent, gc)
   135  		if err != nil {
   136  			return nil, fmt.Errorf("failed creating inrepoconfig cache: %v", err)
   137  		}
   138  	}
   139  
   140  	provider := newGerritProvider(logger, cfgAgent.Config, mgr.GetClient(), ircg, cookieFilePath, "", maxQPS, maxBurst)
   141  	syncCtrl, err := newSyncController(ctx, logger, mgr, provider, cfgAgent.Config, gc, hist, false, statusUpdate)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	return &Controller{syncCtrl: syncCtrl}, nil
   146  }
   147  
   148  // Enforcing interface implementation check at compile time
   149  var _ provider = (*GerritProvider)(nil)
   150  
   151  // GerritProvider implements provider, used by Tide Controller for
   152  // interacting directly with Gerrit.
   153  //
   154  // Tide Controller should only use GerritProvider for communicating with Gerrit.
   155  type GerritProvider struct {
   156  	cfg         config.Getter
   157  	gc          gerritClient
   158  	pjclientset ctrlruntimeclient.Client
   159  
   160  	cookiefilePath     string
   161  	inRepoConfigGetter config.InRepoConfigGetter
   162  	tokenPathOverride  string
   163  
   164  	logger *logrus.Entry
   165  }
   166  
   167  func newGerritProvider(
   168  	logger *logrus.Entry,
   169  	cfg config.Getter,
   170  	pjclientset ctrlruntimeclient.Client,
   171  	ircg config.InRepoConfigGetter,
   172  	cookiefilePath string,
   173  	tokenPathOverride string,
   174  	maxQPS, maxBurst int,
   175  ) *GerritProvider {
   176  	gerritClient, err := client.NewClient(nil, maxQPS, maxBurst)
   177  	if err != nil {
   178  		logrus.WithError(err).Fatal("Error creating gerrit client.")
   179  	}
   180  	orgRepoConfigGetter := func() *config.GerritOrgRepoConfigs {
   181  		return &cfg().Tide.Gerrit.Queries
   182  	}
   183  	gerritClient.ApplyGlobalConfig(orgRepoConfigGetter, nil, cookiefilePath, tokenPathOverride, nil)
   184  
   185  	return &GerritProvider{
   186  		logger:             logger,
   187  		cfg:                cfg,
   188  		pjclientset:        pjclientset,
   189  		gc:                 gerritClient,
   190  		inRepoConfigGetter: ircg,
   191  		cookiefilePath:     cookiefilePath,
   192  		tokenPathOverride:  tokenPathOverride,
   193  	}
   194  }
   195  
   196  // Query returns all PRs from configured Gerrit org/repos.
   197  func (p *GerritProvider) Query() (map[string]CodeReviewCommon, error) {
   198  	// lastUpdate is used by Gerrit adapter for achieving incremental query. In
   199  	// Tide case we want to get everything so use default time.Time, which
   200  	// should be 1970,1,1.
   201  	var lastUpdate time.Time
   202  
   203  	var wg sync.WaitGroup
   204  	errChan := make(chan error)
   205  	type changesFromProject struct {
   206  		instance string
   207  		project  string
   208  		changes  []gerrit.ChangeInfo
   209  	}
   210  	resChan := make(chan changesFromProject)
   211  	for instance, projs := range p.cfg().Tide.Gerrit.Queries.AllRepos() {
   212  		instance, projs := instance, projs
   213  		for projName, projFilter := range projs {
   214  			wg.Add(1)
   215  			var optInByDefault bool
   216  			if projFilter != nil {
   217  				optInByDefault = projFilter.OptInByDefault
   218  			}
   219  			go func(projName string, optInByDefault bool) {
   220  				changes, err := p.gc.QueryChangesForProject(instance, projName, lastUpdate, p.cfg().Gerrit.RateLimit, gerritQueryParam(optInByDefault))
   221  				if err != nil {
   222  					p.logger.WithFields(logrus.Fields{"instance": instance, "project": projName}).WithError(err).Warn("Querying gerrit project for changes.")
   223  					errChan <- fmt.Errorf("failed querying project '%s' from instance '%s': %v", projName, instance, err)
   224  					return
   225  				}
   226  				resChan <- changesFromProject{instance: instance, project: projName, changes: changes}
   227  			}(projName, optInByDefault)
   228  		}
   229  	}
   230  
   231  	var combinedErrs []error
   232  	res := make(map[string]CodeReviewCommon)
   233  	go func() {
   234  		for {
   235  			select {
   236  			case err := <-errChan:
   237  				combinedErrs = append(combinedErrs, err)
   238  				wg.Done()
   239  			case changes := <-resChan:
   240  				for _, pr := range changes.changes {
   241  					crc := CodeReviewCommonFromGerrit(&pr, changes.instance)
   242  					res[prKey(crc)] = *crc
   243  				}
   244  				wg.Done()
   245  			}
   246  		}
   247  	}()
   248  
   249  	wg.Wait()
   250  
   251  	// Let's not return error unless all queries failed.
   252  	if len(combinedErrs) > 0 && len(res) == 0 {
   253  		return nil, utilerrors.NewAggregate(combinedErrs)
   254  	}
   255  	return res, nil
   256  }
   257  
   258  func (p *GerritProvider) blockers() (blockers.Blockers, error) {
   259  	// This is not supported yet, so return an empty blocker for now.
   260  	return blockers.Blockers{}, nil
   261  }
   262  
   263  func (p *GerritProvider) isAllowedToMerge(crc *CodeReviewCommon) (string, error) {
   264  	// gci.Mergeable is only set if this feature is enabled on the Gerrit Host.
   265  	// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
   266  	if crc.Mergeable == string(githubql.MergeableStateConflicting) {
   267  		return "PR has a merge conflict.", nil
   268  	}
   269  	return "", nil
   270  }
   271  
   272  // GetRef gets the latest revision from org/repo/branch.
   273  func (p *GerritProvider) GetRef(org, repo, ref string) (string, error) {
   274  	return p.gc.GetBranchRevision(org, repo, ref)
   275  }
   276  
   277  // headContexts gets the status contexts for the commit with OID ==
   278  // pr.HeadRefOID
   279  //
   280  // Assuming all submission requirements are already met as the PRs queried are
   281  // already submittable. So the focus here is to ensure that all prowjobs were
   282  // tested against latest baseSHA.
   283  // Prow parses baseSHA from the `Description` field of a context, will make sure
   284  // that all Prow jobs that vote to required labels are represented here.
   285  func (p *GerritProvider) headContexts(crc *CodeReviewCommon) ([]Context, error) {
   286  	var res []Context
   287  
   288  	selector := map[string]string{
   289  		kube.GerritRevision:   crc.HeadRefOID,
   290  		kube.ProwJobTypeLabel: string(prowapi.PresubmitJob),
   291  		kube.OrgLabel:         crc.Org,
   292  		kube.RepoLabel:        crc.Repo,
   293  		kube.PullLabel:        strconv.Itoa(crc.Number),
   294  	}
   295  	var pjs prowapi.ProwJobList
   296  	if err := p.pjclientset.List(context.Background(), &pjs, ctrlruntimeclient.MatchingLabels(selector)); err != nil {
   297  		return nil, fmt.Errorf("Cannot list prowjob with selector %v", selector)
   298  	}
   299  
   300  	// keep track of latest prowjobs only
   301  	latestPjs := make(map[string]*prowapi.ProwJob)
   302  	for _, pj := range pjs.Items {
   303  		pj := pj
   304  		if exist, ok := latestPjs[pj.Spec.Context]; ok && exist.CreationTimestamp.After(pj.CreationTimestamp.Time) {
   305  			continue
   306  		}
   307  		latestPjs[pj.Spec.Context] = &pj
   308  	}
   309  
   310  	for _, pj := range latestPjs {
   311  		res = append(res, Context{
   312  			Context:     githubql.String(pj.Spec.Context),
   313  			Description: githubql.String(config.ContextDescriptionWithBaseSha(pj.Status.Description, pj.Spec.Refs.BaseSHA)),
   314  			State:       githubql.StatusState(pj.Status.State),
   315  		})
   316  	}
   317  
   318  	return res, nil
   319  }
   320  
   321  func (p *GerritProvider) mergePRs(sp subpool, prs []CodeReviewCommon, _ *threadSafePRSet) ([]CodeReviewCommon, error) {
   322  	logger := p.logger.WithFields(logrus.Fields{"repo": sp.repo, "org": sp.org, "branch": sp.branch, "prs": len(prs)})
   323  	logger.Info("Merging subpool.")
   324  
   325  	isBatch := len(prs) > 1
   326  
   327  	var merged []CodeReviewCommon
   328  	var errs []error
   329  	for _, pr := range prs {
   330  		logger := logger.WithField("id", pr.Gerrit.ID)
   331  		logger.Info("Submitting change.")
   332  		_, err := p.gc.SubmitChange(sp.org, pr.Gerrit.ID, true)
   333  		if err != nil {
   334  			errs = append(errs, fmt.Errorf("failed submitting change '%s' from org '%s': %v", sp.org, pr.Gerrit.ID, err))
   335  		} else {
   336  			merged = append(merged, pr)
   337  		}
   338  		// Comment on the PR if it's a batch.
   339  		// In case of flaky tests, Tide triggered prowjobs for highest priority
   340  		// PR might fail even when batch prowjobs passed. And in this case Crier
   341  		// would report this failure on the PR before Tide merges the PR, this
   342  		// might cause confusing to users so comment on the PR explaining that
   343  		// the merge was based on batch testing.
   344  		if isBatch && err != nil {
   345  			msg := fmt.Sprintf("The Tide batch containing current change passed all required prowjobs, so this submission was performed by Tide. See %s/tide-history for record", p.cfg().Gerrit.DeckURL)
   346  			if err := p.gc.SetReview(sp.org, pr.Gerrit.ID, pr.Gerrit.CurrentRevision, msg, nil); err != nil {
   347  				logger.WithError(err).Warn("Failed commenting after batch submission.")
   348  			}
   349  		}
   350  	}
   351  	return merged, utilerrors.NewAggregate(errs)
   352  }
   353  
   354  // GetTideContextPolicy returns an empty config.TideContextPolicy struct.
   355  //
   356  // These information are only for determining whether a PR is ready for merge or
   357  // not, this in Gerrit is handled by Gerrit query filters, so this is not useful
   358  // for Gerrit.
   359  func (p *GerritProvider) GetTideContextPolicy(org, repo, branch string, baseSHAGetter config.RefGetter, crc *CodeReviewCommon) (contextChecker, error) {
   360  	return &gerritContextChecker{}, nil
   361  }
   362  
   363  func (p *GerritProvider) prMergeMethod(crc *CodeReviewCommon) *types.PullRequestMergeType {
   364  	var res types.PullRequestMergeType
   365  	pr := crc.Gerrit
   366  	if pr == nil {
   367  		return nil
   368  	}
   369  
   370  	// Translate merge methods to types that Git could understand. The merge
   371  	// methods for Gerrit are documented at
   372  	// https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#repository.
   373  	// Git can only understand MergeIfNecessary, MergeMerge, MergeRebase, MergeSquash.
   374  	switch pr.SubmitType {
   375  	case "MERGE_IF_NECESSARY":
   376  		res = types.MergeIfNecessary
   377  	case "FAST_FORWARD_ONLY":
   378  		res = types.MergeMerge
   379  	case "REBASE_IF_NECESSARY":
   380  		res = types.MergeRebase
   381  	case "REBASE_ALWAYS":
   382  		res = types.MergeRebase
   383  	case "MERGE_ALWAYS":
   384  		res = types.MergeMerge
   385  	default:
   386  		res = types.MergeMerge
   387  	}
   388  
   389  	return &res
   390  }
   391  
   392  // GetPresubmits gets presubmit jobs for a PR.
   393  //
   394  // (TODO:chaodaiG): deduplicate this with GitHub, which means inrepoconfig
   395  // processing all use cache client.
   396  func (p *GerritProvider) GetPresubmits(identifier, baseBranch string, baseSHAGetter config.RefGetter, headSHAGetters ...config.RefGetter) ([]config.Presubmit, error) {
   397  	// If InRepoConfigCache is provided, then it means that we want to fetch
   398  	// from an inrepoconfig.
   399  	if p.inRepoConfigGetter != nil {
   400  		return p.inRepoConfigGetter.GetPresubmits(identifier, baseBranch, baseSHAGetter, headSHAGetters...)
   401  	}
   402  	// Get presubmits from Config alone.
   403  	return p.cfg().GetPresubmitsStatic(identifier), nil
   404  }
   405  
   406  func (p *GerritProvider) GetChangedFiles(org, repo string, number int) ([]string, error) {
   407  	// "CURRENT_FILES" lists all changed files from current revision, which is
   408  	// what we want, "CURRENT_REVISION" is required for "CURRENT_FILES".
   409  	// according to
   410  	// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes.
   411  	change, err := p.gc.GetChange(org, strconv.Itoa(number), "CURRENT_FILES", "CURRENT_REVISION")
   412  	if err != nil {
   413  		return nil, fmt.Errorf("failed get change: %v", err)
   414  	}
   415  	return client.ChangedFilesProvider(change)()
   416  }
   417  
   418  func (p *GerritProvider) refsForJob(sp subpool, prs []CodeReviewCommon) (prowapi.Refs, error) {
   419  	var changes []client.ChangeInfo
   420  	for _, pr := range prs {
   421  		changes = append(changes, *pr.Gerrit)
   422  	}
   423  	return gerritadaptor.CreateRefs(sp.org, sp.repo, sp.branch, sp.sha, changes...)
   424  }
   425  
   426  func (p *GerritProvider) labelsAndAnnotations(instance string, jobLabels, jobAnnotations map[string]string, prs ...CodeReviewCommon) (labels, annotations map[string]string) {
   427  	var changes []client.ChangeInfo
   428  	for _, pr := range prs {
   429  		changes = append(changes, *pr.Gerrit)
   430  	}
   431  	labels, annotations = gerritadaptor.LabelsAndAnnotations(instance, jobLabels, jobAnnotations, changes...)
   432  	return
   433  }
   434  
   435  func (p *GerritProvider) jobIsRequiredByTide(ps *config.Presubmit, crc *CodeReviewCommon) bool {
   436  	if ps.RunBeforeMerge {
   437  		return true
   438  	}
   439  
   440  	requireLabels := sets.New[string]()
   441  	for l, info := range crc.Gerrit.Labels {
   442  		if !info.Optional {
   443  			requireLabels.Insert(l)
   444  		}
   445  	}
   446  
   447  	val, ok := ps.Labels[kube.GerritReportLabel]
   448  	if !ok {
   449  		return false
   450  	}
   451  	return requireLabels.Has(val)
   452  }