blob: 141062a4c238ac3863085e3a3e13934501b023e4 [file] [log] [blame] [edit]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package branch
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
gerritapi "github.com/andygrunwald/go-gerrit"
"golang.org/x/sync/errgroup"
"go.chromium.org/luci/common/errors"
"go.chromium.org/infra/cros/internal/shared"
)
const (
retriesTimeout = 5 * time.Minute
testRetries = 1
testBaseDelay = 1
readOnlyErrorMsg = "project state READ_ONLY does not permit write"
)
// GerritProjectBranch contains all the details for creating a new Gerrit branch
// based on an existing one.
type GerritProjectBranch struct {
GerritURL string
Project string
ProjectPath string
Branch string
SrcRef string
}
func qpsToPeriod(qps float64) time.Duration {
if qps <= 0 {
// some very generous default duration
return time.Second * 10
}
periodSec := float64(time.Second) / qps
return time.Duration(int64(periodSec))
}
func (c *Client) createRemoteBranch(authedClient *http.Client, b GerritProjectBranch, dryRun bool) error {
if dryRun {
return nil
}
agClient, err := gerritapi.NewClient(b.GerritURL, authedClient)
if err != nil {
return fmt.Errorf("failed to create Gerrit client: %w", err)
}
bi, resp, err := agClient.Projects.CreateBranch(b.Project, b.Branch, &gerritapi.BranchInput{Revision: b.SrcRef})
defer resp.Body.Close()
if err != nil {
body, err2 := io.ReadAll(resp.Body)
if err2 != nil {
// shouldn't happen
return err2
}
if resp.StatusCode == http.StatusConflict && strings.Contains(string(body), "already exists") {
// Branch already exists, so there's nothing to do.
c.LogOut("branch %s already exists for %s/%s, nothing to do here", b.Branch, b.GerritURL, b.Project)
return nil
}
return errors.Annotate(err, "failed to create branch. Got response %v and branch info %v", string(body), bi).Err()
}
return nil
}
// CreateRemoteBranchesAPI creates a bunch of branches on remote Gerrit instances
// for the specified inputs using the Gerrit API.
func (c *Client) CreateRemoteBranchesAPI(authedClient *http.Client, branches []GerritProjectBranch, dryRun bool, gerritQPS float64, skipRetries bool, isTest bool) error {
if c.FakeCreateRemoteBranchesAPI != nil {
return c.FakeCreateRemoteBranchesAPI(authedClient, branches, dryRun, gerritQPS)
}
if dryRun {
c.LogOut("Dry run (no --push): would create remote branches for %v Gerrit repos", len(branches))
} else {
c.LogOut("Creating remote branches for %v Gerrit repos. This will take a few minutes, since otherwise Gerrit would throttle us.", len(branches))
}
var g errgroup.Group
throttle := time.Tick(qpsToPeriod(gerritQPS))
var logPrefix string
if dryRun {
logPrefix = "(Dry run) "
}
var createCount, readOnlyCount int64
for _, b := range branches {
<-throttle
g.Go(func() error {
err := func() error {
if skipRetries {
err := c.createRemoteBranch(authedClient, b, dryRun)
if err != nil {
return err
}
} else {
ctx, cancel := context.WithTimeout(context.Background(), retriesTimeout)
defer cancel()
opts := shared.DefaultOpts
if isTest {
opts.Retries = testRetries
opts.BaseDelay = testBaseDelay
}
err := shared.DoWithRetry(ctx, opts, func() error {
err := c.createRemoteBranch(authedClient, b, dryRun)
return err
})
if err != nil {
return err
}
}
count := atomic.AddInt64(&createCount, 1)
if count%10 == 0 {
c.LogOut("%sCreated %v of %v remote branches", logPrefix, count, len(branches))
}
return nil
}()
if err != nil && strings.Contains(err.Error(), readOnlyErrorMsg) {
readOnlyCount := atomic.AddInt64(&readOnlyCount, 1)
// If the error is widespread we ought to fail.
if float64(readOnlyCount)/float64(len(branches)) > 0.05 {
err := errors.New(">5%% branches have failed with READ_ONLY error.")
c.LogErr("failed due to widespread issue: %s", err.Error())
return err
}
c.LogErr("Warning: Branch for %v failed with '%s' error. Continuing with best-effort branch creation.",
b, readOnlyErrorMsg)
return nil
}
return err
})
}
err := g.Wait()
c.LogOut("%sSuccessfully created %v of %v remote branches", logPrefix, atomic.LoadInt64(&createCount), len(branches))
return err
}
// CheckSelfGroupMembership checks if the authenticated user is in the given
// group on the given Gerrit host. It returns a bool indicating whether or
// not that's the case, or an error if the lookup fails.
func CheckSelfGroupMembership(authedClient *http.Client, gerritURL, expectedGroup string) (bool, error) {
agClient, err := gerritapi.NewClient(gerritURL, authedClient)
if err != nil {
return false, fmt.Errorf("failed to create Gerrit client: %w", err)
}
groups, resp, err := agClient.Accounts.ListGroups("self")
defer resp.Body.Close()
if err != nil {
return false, errors.Annotate(err, "failed to get list of Gerrit groups for self").Err()
}
for _, g := range *groups {
if g.Name == expectedGroup {
return true, nil
}
}
return false, nil
}