blob: 95eb1d75d7af46683fb41778d3938e131b3b5636 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"golang.org/x/build/gerrit"
"golang.org/x/build/maintner/godata"
)
// GerritAccounts holds a mapping of Gerrit account IDs to
// the corresponding gerrit.AccountInfo object.
// A call to Initialize must be made in order for the map to be populated.
type GerritAccounts struct {
accounts map[int64]*gerrit.AccountInfo // Gerrit account ID to AccountInfo.
refreshTime time.Time
}
// ErrNotFound is the error returned when no mapping for a Gerrit email address is available.
var ErrNotFound = errors.New("no mapping found for the given Gerrit email address")
// LookupByGerritEmail translates a Gerrit email address in the format of
// <Gerrit User ID>@<Gerrit server UUID> into the actual email address of the person.
// If the cache is out of date, and fetchUpdates is true, it'll download a fresh mapping from Gerrit,
// and persist it as well. If fetchUpdates is false, then ErrNotFound is returned.
// After downloading a fresh mapping, and a mapping for an account ID is not found,
// then ErrNotFound is returned.
func (ga *GerritAccounts) LookupByGerritEmail(gerritEmail string, fetchUpdates bool) (*gerrit.AccountInfo, error) {
if gerritEmail == "" {
return nil, errors.New("gerritEmail cannot be empty")
}
atIdx := strings.LastIndex(gerritEmail, "@")
if atIdx == -1 {
return nil, fmt.Errorf("LookupByGerritEmail: %q is not a valid email address", gerritEmail)
}
accountId, err := strconv.Atoi(gerritEmail[0:atIdx])
if err != nil {
return nil, fmt.Errorf("LookupByGerritEmail: %q is not of the form <Gerrit User ID>@<Gerrit server UUID>", gerritEmail)
}
account := ga.accounts[int64(accountId)]
if account != nil {
// The cached mapping might be the same as gerritEmail (as it's the default if a mapping is missing).
// Return ErrNotFound in that case.
if account.Email == gerritEmail {
return nil, ErrNotFound
}
return account, nil
}
if !fetchUpdates {
return nil, ErrNotFound
}
// Cache miss, let's sync up with Gerrit.
// We should also add a default value for this email address - in case
// Gerrit doesn't have this account ID (which would be rare - or the account is inactive),
// we don't want to keep making network calls.
// As GerritAccounts holds a map, if Gerrit returns a valid mapping,
// it will be overridden.
ga.accounts[int64(accountId)] = &gerrit.AccountInfo{
Email: gerritEmail,
NumericID: int64(accountId),
Name: gerritEmail,
Username: gerritEmail,
}
// If we've recently hit Gerrit for a fresh mapping already, then skip a network call,
// and persist the default version for this gerritEmail.
if time.Now().Sub(ga.refreshTime).Minutes() < 5 {
log.Println("Skipping Gerrit account info lookup for", gerritEmail)
err = ga.cacheMappingToDisk()
if err != nil {
return nil, err
}
return nil, ErrNotFound
}
if err := ga.fetchAndPersist(); err != nil {
return nil, err
}
if ga.accounts[int64(accountId)].Email == gerritEmail {
return nil, ErrNotFound
}
return ga.accounts[int64(accountId)], nil
}
// refresh makes a call to the Gerrit server, and updates the mapping.
// It also updates refreshTime, after the update has completed.
func (ga *GerritAccounts) refresh() error {
if ga.accounts == nil {
ga.accounts = map[int64]*gerrit.AccountInfo{}
}
c := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
if ctx.Err() != nil {
return ctx.Err()
}
start := 0
for {
accounts, err := c.QueryAccounts(ctx, "is:active",
gerrit.QueryAccountsOpt{Fields: []string{"DETAILS"}, Start: start})
if err != nil {
return ctx.Err()
}
start += len(accounts)
for _, account := range accounts {
ga.accounts[account.NumericID] = account
}
log.Println("Fetched", start, "accounts from Gerrit")
if accounts[len(accounts)-1].MoreAccounts == false {
break
}
}
ga.refreshTime = time.Now()
return nil
}
// cacheMappingToDisk serializes the map and writes it to the cache directory.
func (ga *GerritAccounts) cacheMappingToDisk() error {
cachePath, err := cachePath()
if err != nil {
return err
}
var out bytes.Buffer
encoder := gob.NewEncoder(&out)
err = encoder.Encode(ga.accounts)
if err != nil {
return err
}
err = ioutil.WriteFile(cachePath, out.Bytes(), 0600)
if err != nil {
return err
}
return nil
}
// Initialize does either one of the following two things, in order:
// 1. If a cached mapping exists, then restore the map from the cache and return.
// 2. If the cached mapping does not exist, hit Gerrit (call refresh()), and then persist the mapping.
func (ga *GerritAccounts) Initialize() error {
cachePath, err := cachePath()
if err != nil {
return err
}
if cache, err := ioutil.ReadFile(cachePath); err == nil {
d := gob.NewDecoder(bytes.NewReader(cache))
if err := d.Decode(&ga.accounts); err != nil {
return err
}
log.Println("Read Gerrit accounts information from disk cache")
return nil
}
if err := ga.fetchAndPersist(); err != nil {
return err
}
return nil
}
func (ga *GerritAccounts) fetchAndPersist() error {
log.Println("Fetching accounts mapping from Gerrit. This will take some time...")
err := ga.refresh()
if err != nil {
return err
}
err = ga.cacheMappingToDisk()
if err != nil {
return err
}
return nil
}
func cachePath() (string, error) {
targetDir := godata.XdgCacheDir()
targetDir = filepath.Join(targetDir, "golang-build-cmd-cl")
if err := os.MkdirAll(targetDir, 0700); err != nil {
return "", err
}
return filepath.Join(targetDir, "accounts"), nil
}