blob: 95eb1d75d7af46683fb41778d3938e131b3b5636 [file] [log] [blame]
Jude Pereira11e039e2017-09-24 11:32:22 +05301// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package main
6
7import (
8 "bytes"
9 "context"
10 "encoding/gob"
11 "errors"
12 "fmt"
13 "io/ioutil"
14 "log"
15 "os"
16 "path/filepath"
17 "strconv"
18 "strings"
19 "time"
20
21 "golang.org/x/build/gerrit"
22 "golang.org/x/build/maintner/godata"
23)
24
25// GerritAccounts holds a mapping of Gerrit account IDs to
26// the corresponding gerrit.AccountInfo object.
27// A call to Initialize must be made in order for the map to be populated.
28type GerritAccounts struct {
29 accounts map[int64]*gerrit.AccountInfo // Gerrit account ID to AccountInfo.
30 refreshTime time.Time
31}
32
33// ErrNotFound is the error returned when no mapping for a Gerrit email address is available.
34var ErrNotFound = errors.New("no mapping found for the given Gerrit email address")
35
36// LookupByGerritEmail translates a Gerrit email address in the format of
37// <Gerrit User ID>@<Gerrit server UUID> into the actual email address of the person.
38// If the cache is out of date, and fetchUpdates is true, it'll download a fresh mapping from Gerrit,
39// and persist it as well. If fetchUpdates is false, then ErrNotFound is returned.
40// After downloading a fresh mapping, and a mapping for an account ID is not found,
41// then ErrNotFound is returned.
42func (ga *GerritAccounts) LookupByGerritEmail(gerritEmail string, fetchUpdates bool) (*gerrit.AccountInfo, error) {
43 if gerritEmail == "" {
44 return nil, errors.New("gerritEmail cannot be empty")
45 }
46
47 atIdx := strings.LastIndex(gerritEmail, "@")
48 if atIdx == -1 {
49 return nil, fmt.Errorf("LookupByGerritEmail: %q is not a valid email address", gerritEmail)
50 }
51
52 accountId, err := strconv.Atoi(gerritEmail[0:atIdx])
53 if err != nil {
54 return nil, fmt.Errorf("LookupByGerritEmail: %q is not of the form <Gerrit User ID>@<Gerrit server UUID>", gerritEmail)
55 }
56
57 account := ga.accounts[int64(accountId)]
58 if account != nil {
59 // The cached mapping might be the same as gerritEmail (as it's the default if a mapping is missing).
60 // Return ErrNotFound in that case.
61 if account.Email == gerritEmail {
62 return nil, ErrNotFound
63 }
64 return account, nil
65 }
66
67 if !fetchUpdates {
68 return nil, ErrNotFound
69 }
70
71 // Cache miss, let's sync up with Gerrit.
72
73 // We should also add a default value for this email address - in case
74 // Gerrit doesn't have this account ID (which would be rare - or the account is inactive),
75 // we don't want to keep making network calls.
76 // As GerritAccounts holds a map, if Gerrit returns a valid mapping,
77 // it will be overridden.
78 ga.accounts[int64(accountId)] = &gerrit.AccountInfo{
79 Email: gerritEmail,
80 NumericID: int64(accountId),
81 Name: gerritEmail,
82 Username: gerritEmail,
83 }
84
85 // If we've recently hit Gerrit for a fresh mapping already, then skip a network call,
86 // and persist the default version for this gerritEmail.
87 if time.Now().Sub(ga.refreshTime).Minutes() < 5 {
88 log.Println("Skipping Gerrit account info lookup for", gerritEmail)
89 err = ga.cacheMappingToDisk()
90 if err != nil {
91 return nil, err
92 }
93
94 return nil, ErrNotFound
95 }
96
97 if err := ga.fetchAndPersist(); err != nil {
98 return nil, err
99 }
100
101 if ga.accounts[int64(accountId)].Email == gerritEmail {
102 return nil, ErrNotFound
103 }
104
105 return ga.accounts[int64(accountId)], nil
106}
107
108// refresh makes a call to the Gerrit server, and updates the mapping.
109// It also updates refreshTime, after the update has completed.
110func (ga *GerritAccounts) refresh() error {
111 if ga.accounts == nil {
112 ga.accounts = map[int64]*gerrit.AccountInfo{}
113 }
114
115 c := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
116 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
117 defer cancel()
118
119 if ctx.Err() != nil {
120 return ctx.Err()
121 }
122
123 start := 0
124 for {
125 accounts, err := c.QueryAccounts(ctx, "is:active",
126 gerrit.QueryAccountsOpt{Fields: []string{"DETAILS"}, Start: start})
127
128 if err != nil {
129 return ctx.Err()
130 }
131
132 start += len(accounts)
133
134 for _, account := range accounts {
135 ga.accounts[account.NumericID] = account
136 }
137
138 log.Println("Fetched", start, "accounts from Gerrit")
139
140 if accounts[len(accounts)-1].MoreAccounts == false {
141 break
142 }
143 }
144
145 ga.refreshTime = time.Now()
146
147 return nil
148}
149
150// cacheMappingToDisk serializes the map and writes it to the cache directory.
151func (ga *GerritAccounts) cacheMappingToDisk() error {
152 cachePath, err := cachePath()
153
154 if err != nil {
155 return err
156 }
157
158 var out bytes.Buffer
159 encoder := gob.NewEncoder(&out)
160
161 err = encoder.Encode(ga.accounts)
162
163 if err != nil {
164 return err
165 }
166
167 err = ioutil.WriteFile(cachePath, out.Bytes(), 0600)
168 if err != nil {
169 return err
170 }
171
172 return nil
173}
174
175// Initialize does either one of the following two things, in order:
176// 1. If a cached mapping exists, then restore the map from the cache and return.
177// 2. If the cached mapping does not exist, hit Gerrit (call refresh()), and then persist the mapping.
178func (ga *GerritAccounts) Initialize() error {
179 cachePath, err := cachePath()
180 if err != nil {
181 return err
182 }
183
184 if cache, err := ioutil.ReadFile(cachePath); err == nil {
185 d := gob.NewDecoder(bytes.NewReader(cache))
186
187 if err := d.Decode(&ga.accounts); err != nil {
188 return err
189 }
190 log.Println("Read Gerrit accounts information from disk cache")
191 return nil
192 }
193
194 if err := ga.fetchAndPersist(); err != nil {
195 return err
196 }
197
198 return nil
199}
200
201func (ga *GerritAccounts) fetchAndPersist() error {
202 log.Println("Fetching accounts mapping from Gerrit. This will take some time...")
203
204 err := ga.refresh()
205 if err != nil {
206 return err
207 }
208
209 err = ga.cacheMappingToDisk()
210 if err != nil {
211 return err
212 }
213
214 return nil
215}
216
217func cachePath() (string, error) {
218 targetDir := godata.XdgCacheDir()
219 targetDir = filepath.Join(targetDir, "golang-build-cmd-cl")
220 if err := os.MkdirAll(targetDir, 0700); err != nil {
221 return "", err
222 }
223 return filepath.Join(targetDir, "accounts"), nil
224}