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 }