OLD | NEW |
1 // Copyright 2016 The LUCI Authors. | 1 // Copyright 2016 The LUCI Authors. |
2 // | 2 // |
3 // Licensed under the Apache License, Version 2.0 (the "License"); | 3 // Licensed under the Apache License, Version 2.0 (the "License"); |
4 // you may not use this file except in compliance with the License. | 4 // you may not use this file except in compliance with the License. |
5 // You may obtain a copy of the License at | 5 // You may obtain a copy of the License at |
6 // | 6 // |
7 // http://www.apache.org/licenses/LICENSE-2.0 | 7 // http://www.apache.org/licenses/LICENSE-2.0 |
8 // | 8 // |
9 // Unless required by applicable law or agreed to in writing, software | 9 // Unless required by applicable law or agreed to in writing, software |
10 // distributed under the License is distributed on an "AS IS" BASIS, | 10 // distributed under the License is distributed on an "AS IS" BASIS, |
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 // See the License for the specific language governing permissions and | 12 // See the License for the specific language governing permissions and |
13 // limitations under the License. | 13 // limitations under the License. |
14 | 14 |
15 package buildbot | 15 package buildbot |
16 | 16 |
17 import ( | 17 import ( |
18 "crypto/sha1" | 18 "crypto/sha1" |
19 "encoding/base64" | 19 "encoding/base64" |
20 "errors" | |
21 "fmt" | 20 "fmt" |
22 "sort" | 21 "sort" |
23 "strings" | 22 "strings" |
24 "time" | 23 "time" |
25 | 24 |
26 "github.com/luci/gae/service/datastore" | 25 "github.com/luci/gae/service/datastore" |
27 "github.com/luci/gae/service/memcache" | 26 "github.com/luci/gae/service/memcache" |
28 | 27 |
29 "github.com/luci/luci-go/common/clock" | 28 "github.com/luci/luci-go/common/clock" |
| 29 "github.com/luci/luci-go/common/errors" |
30 "github.com/luci/luci-go/common/logging" | 30 "github.com/luci/luci-go/common/logging" |
31 "github.com/luci/luci-go/milo/api/resp" | 31 "github.com/luci/luci-go/milo/api/resp" |
| 32 "github.com/luci/luci-go/milo/common" |
32 "golang.org/x/net/context" | 33 "golang.org/x/net/context" |
33 ) | 34 ) |
34 | 35 |
35 // builderRef is used for keying specific builds in a master json. | 36 // builderRef is used for keying specific builds in a master json. |
36 type builderRef struct { | 37 type builderRef struct { |
37 builder string | 38 builder string |
38 buildNum int | 39 buildNum int |
39 } | 40 } |
40 | 41 |
41 // buildMap contains all of the current build within a master json. We use this | 42 // buildMap contains all of the current build within a master json. We use this |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
88 }, | 89 }, |
89 Text: mergeText(b.Text), | 90 Text: mergeText(b.Text), |
90 Blame: blame(b), | 91 Blame: blame(b), |
91 Revision: b.Sourcestamp.Revision, | 92 Revision: b.Sourcestamp.Revision, |
92 } | 93 } |
93 } | 94 } |
94 | 95 |
95 // getBuilds fetches all of the recent builds from the . Note that | 96 // getBuilds fetches all of the recent builds from the . Note that |
96 // getBuilds() does not perform ACL checks. | 97 // getBuilds() does not perform ACL checks. |
97 func getBuilds( | 98 func getBuilds( |
98 » c context.Context, masterName, builderName string, finished bool, limit
int, cursor *datastore.Cursor) ( | 99 » c context.Context, masterName, builderName string, finished bool, limit
int, cursor datastore.Cursor) ( |
99 » []*resp.BuildSummary, *datastore.Cursor, error) { | 100 » []*resp.BuildSummary, datastore.Cursor, error) { |
100 | 101 |
101 // TODO(hinoka): Builder specific structs. | 102 // TODO(hinoka): Builder specific structs. |
102 result := []*resp.BuildSummary{} | 103 result := []*resp.BuildSummary{} |
103 q := datastore.NewQuery("buildbotBuild") | 104 q := datastore.NewQuery("buildbotBuild") |
104 q = q.Eq("finished", finished) | 105 q = q.Eq("finished", finished) |
105 q = q.Eq("master", masterName) | 106 q = q.Eq("master", masterName) |
106 q = q.Eq("builder", builderName) | 107 q = q.Eq("builder", builderName) |
107 q = q.Order("-number") | 108 q = q.Order("-number") |
108 if cursor != nil { | 109 if cursor != nil { |
109 » » q = q.Start(*cursor) | 110 » » q = q.Start(cursor) |
110 } | 111 } |
111 buildbots, nextCursor, err := runBuildsQuery(c, q, int32(limit)) | 112 buildbots, nextCursor, err := runBuildsQuery(c, q, int32(limit)) |
112 if err != nil { | 113 if err != nil { |
113 return nil, nil, err | 114 return nil, nil, err |
114 } | 115 } |
115 for _, b := range buildbots { | 116 for _, b := range buildbots { |
116 result = append(result, getBuildSummary(b)) | 117 result = append(result, getBuildSummary(b)) |
117 } | 118 } |
118 return result, nextCursor, nil | 119 return result, nextCursor, nil |
119 } | 120 } |
120 | 121 |
121 // maybeSetGetCursor is a cheesy way to implement bidirectional paging with forw
ard-only | 122 // maybeSetGetCursor is a cheesy way to implement bidirectional paging with forw
ard-only |
122 // datastore cursor by creating a mapping of nextCursor -> thisCursor | 123 // datastore cursor by creating a mapping of nextCursor -> thisCursor |
123 // in memcache. maybeSetGetCursor stores the future mapping, then returns prevC
ursor | 124 // in memcache. maybeSetGetCursor stores the future mapping, then returns prevC
ursor |
124 // in the mapping for thisCursor -> prevCursor, if available. | 125 // in the mapping for thisCursor -> prevCursor, if available. |
125 func maybeSetGetCursor(c context.Context, thisCursor, nextCursor *datastore.Curs
or, limit int) (*datastore.Cursor, bool) { | 126 func maybeSetGetCursor(c context.Context, thisCursor, nextCursor datastore.Curso
r, limit int) (datastore.Cursor, bool) { |
126 key := func(c datastore.Cursor) string { | 127 key := func(c datastore.Cursor) string { |
127 // Memcache key limit is 250 bytes, hash our cursor to get under
this limit. | 128 // Memcache key limit is 250 bytes, hash our cursor to get under
this limit. |
128 blob := sha1.Sum([]byte(c.String())) | 129 blob := sha1.Sum([]byte(c.String())) |
129 return fmt.Sprintf("v2:cursors:buildbot_builders:%d:%s", limit,
base64.StdEncoding.EncodeToString(blob[:])) | 130 return fmt.Sprintf("v2:cursors:buildbot_builders:%d:%s", limit,
base64.StdEncoding.EncodeToString(blob[:])) |
130 } | 131 } |
131 // Set the next cursor to this cursor mapping, if available. | 132 // Set the next cursor to this cursor mapping, if available. |
132 if nextCursor != nil { | 133 if nextCursor != nil { |
133 » » item := memcache.NewItem(c, key(*nextCursor)) | 134 » » item := memcache.NewItem(c, key(nextCursor)) |
134 if thisCursor == nil { | 135 if thisCursor == nil { |
135 // Make sure we know it exists, just empty | 136 // Make sure we know it exists, just empty |
136 item.SetValue([]byte{}) | 137 item.SetValue([]byte{}) |
137 } else { | 138 } else { |
138 » » » item.SetValue([]byte((*thisCursor).String())) | 139 » » » item.SetValue([]byte(thisCursor.String())) |
139 } | 140 } |
140 item.SetExpiration(24 * time.Hour) | 141 item.SetExpiration(24 * time.Hour) |
141 memcache.Set(c, item) | 142 memcache.Set(c, item) |
142 } | 143 } |
143 // Try to get the last cursor, if valid and available. | 144 // Try to get the last cursor, if valid and available. |
144 if thisCursor == nil { | 145 if thisCursor == nil { |
145 return nil, false | 146 return nil, false |
146 } | 147 } |
147 » if item, err := memcache.GetKey(c, key(*thisCursor)); err == nil { | 148 » if item, err := memcache.GetKey(c, key(thisCursor)); err == nil { |
148 if len(item.Value()) == 0 { | 149 if len(item.Value()) == 0 { |
149 return nil, true | 150 return nil, true |
150 } | 151 } |
151 if prevCursor, err := datastore.DecodeCursor(c, string(item.Valu
e())); err == nil { | 152 if prevCursor, err := datastore.DecodeCursor(c, string(item.Valu
e())); err == nil { |
152 » » » return &prevCursor, true | 153 » » » return prevCursor, true |
153 } | 154 } |
154 } | 155 } |
155 return nil, false | 156 return nil, false |
156 } | 157 } |
157 | 158 |
158 var errMasterNotFound = errors.New( | |
159 "Either the request resource was not found or you have insufficient perm
issions") | |
160 var errNotAuth = errors.New("You are not authenticated, try logging in") | |
161 | |
162 type errBuilderNotFound struct { | |
163 master string | |
164 builder string | |
165 available []string | |
166 } | |
167 | |
168 func (e errBuilderNotFound) Error() string { | |
169 avail := strings.Join(e.available, "\n") | |
170 return fmt.Sprintf("Cannot find builder %q in master %q.\nAvailable buil
ders: \n%s", | |
171 e.builder, e.master, avail) | |
172 } | |
173 | |
174 func summarizeSlavePool( | 159 func summarizeSlavePool( |
175 baseURL string, slaves []string, slaveMap map[string]*buildbotSlave) *re
sp.MachinePool { | 160 baseURL string, slaves []string, slaveMap map[string]*buildbotSlave) *re
sp.MachinePool { |
176 | 161 |
177 mp := &resp.MachinePool{ | 162 mp := &resp.MachinePool{ |
178 Total: len(slaves), | 163 Total: len(slaves), |
179 Bots: make([]resp.Bot, 0, len(slaves)), | 164 Bots: make([]resp.Bot, 0, len(slaves)), |
180 } | 165 } |
181 for _, slaveName := range slaves { | 166 for _, slaveName := range slaves { |
182 slave, ok := slaveMap[slaveName] | 167 slave, ok := slaveMap[slaveName] |
183 bot := resp.Bot{ | 168 bot := resp.Bot{ |
(...skipping 13 matching lines...) Expand all Loading... |
197 mp.Busy++ | 182 mp.Busy++ |
198 default: | 183 default: |
199 bot.Status = resp.Idle | 184 bot.Status = resp.Idle |
200 mp.Idle++ | 185 mp.Idle++ |
201 } | 186 } |
202 mp.Bots = append(mp.Bots, bot) | 187 mp.Bots = append(mp.Bots, bot) |
203 } | 188 } |
204 return mp | 189 return mp |
205 } | 190 } |
206 | 191 |
207 // builderImpl is the implementation for getting a milo builder page from buildb
ot. | 192 // GetBuilder is the implementation for getting a milo builder page from |
| 193 // buildbot. |
| 194 // |
208 // This gets: | 195 // This gets: |
209 // * Current Builds from querying the master json from the datastore. | 196 // * Current Builds from querying the master json from the datastore. |
210 // * Recent Builds from a cron job that backfills the recent builds. | 197 // * Recent Builds from a cron job that backfills the recent builds. |
211 func builderImpl( | 198 func GetBuilder(c context.Context, masterName, builderName string, limit int, cu
rsor datastore.Cursor) (*resp.Builder, error) { |
212 » c context.Context, masterName, builderName string, limit int, cursor str
ing) ( | |
213 » *resp.Builder, error) { | |
214 | |
215 » var thisCursor *datastore.Cursor | |
216 » if cursor != "" { | |
217 » » tmpCur, err := datastore.DecodeCursor(c, cursor) | |
218 » » if err != nil { | |
219 » » » return nil, fmt.Errorf("bad cursor: %s", err) | |
220 » » } | |
221 » » thisCursor = &tmpCur | |
222 » } | |
223 | |
224 result := &resp.Builder{ | 199 result := &resp.Builder{ |
225 Name: builderName, | 200 Name: builderName, |
226 } | 201 } |
227 master, internal, t, err := getMasterJSON(c, masterName) | 202 master, internal, t, err := getMasterJSON(c, masterName) |
228 if err != nil { | 203 if err != nil { |
229 return nil, err | 204 return nil, err |
230 } | 205 } |
231 if clock.Now(c).Sub(t) > 2*time.Minute { | 206 if clock.Now(c).Sub(t) > 2*time.Minute { |
232 warning := fmt.Sprintf( | 207 warning := fmt.Sprintf( |
233 "WARNING: Master data is stale (last updated %s)", t) | 208 "WARNING: Master data is stale (last updated %s)", t) |
234 logging.Warningf(c, warning) | 209 logging.Warningf(c, warning) |
235 result.Warning = warning | 210 result.Warning = warning |
236 } | 211 } |
237 | 212 |
238 p, ok := master.Builders[builderName] | 213 p, ok := master.Builders[builderName] |
239 if !ok { | 214 if !ok { |
240 // This long block is just to return a good error message when a
n invalid | 215 // This long block is just to return a good error message when a
n invalid |
241 // buildbot builder is specified. | 216 // buildbot builder is specified. |
242 keys := make([]string, 0, len(master.Builders)) | 217 keys := make([]string, 0, len(master.Builders)) |
243 for k := range master.Builders { | 218 for k := range master.Builders { |
244 keys = append(keys, k) | 219 keys = append(keys, k) |
245 } | 220 } |
246 sort.Strings(keys) | 221 sort.Strings(keys) |
247 » » return nil, errBuilderNotFound{masterName, builderName, keys} | 222 » » // TODO(iannucci): add error-info-helper tags to give the error
page enough |
| 223 » » // information to render link-to-master and link-to-builder. |
| 224 » » builders := strings.Join(keys, "\n") |
| 225 » » return nil, errors.Reason( |
| 226 » » » "Cannot find builder %q in master %q.\nAvailable builder
s: \n%s", |
| 227 » » » builderName, masterName, builders, |
| 228 » » ).Tag(common.CodeNotFound).Err() |
248 } | 229 } |
249 // Extract pending builds out of the master json. | 230 // Extract pending builds out of the master json. |
250 result.PendingBuilds = make([]*resp.BuildSummary, len(p.PendingBuildStat
es)) | 231 result.PendingBuilds = make([]*resp.BuildSummary, len(p.PendingBuildStat
es)) |
251 logging.Debugf(c, "Number of pending builds: %d", len(p.PendingBuildStat
es)) | 232 logging.Debugf(c, "Number of pending builds: %d", len(p.PendingBuildStat
es)) |
252 for i, pb := range p.PendingBuildStates { | 233 for i, pb := range p.PendingBuildStates { |
253 start := time.Unix(int64(pb.SubmittedAt), 0).UTC() | 234 start := time.Unix(int64(pb.SubmittedAt), 0).UTC() |
254 result.PendingBuilds[i] = &resp.BuildSummary{ | 235 result.PendingBuilds[i] = &resp.BuildSummary{ |
255 PendingTime: resp.Interval{ | 236 PendingTime: resp.Interval{ |
256 Started: start, | 237 Started: start, |
257 Duration: clock.Now(c).UTC().Sub(start), | 238 Duration: clock.Now(c).UTC().Sub(start), |
258 }, | 239 }, |
259 } | 240 } |
260 result.PendingBuilds[i].Blame = make([]*resp.Commit, len(pb.Sour
ce.Changes)) | 241 result.PendingBuilds[i].Blame = make([]*resp.Commit, len(pb.Sour
ce.Changes)) |
261 for j, cm := range pb.Source.Changes { | 242 for j, cm := range pb.Source.Changes { |
262 result.PendingBuilds[i].Blame[j] = &resp.Commit{ | 243 result.PendingBuilds[i].Blame[j] = &resp.Commit{ |
263 AuthorEmail: cm.Who, | 244 AuthorEmail: cm.Who, |
264 CommitURL: cm.Revlink, | 245 CommitURL: cm.Revlink, |
265 } | 246 } |
266 } | 247 } |
267 } | 248 } |
268 | 249 |
269 baseURL := "https://build.chromium.org/p/" | 250 baseURL := "https://build.chromium.org/p/" |
270 if internal { | 251 if internal { |
271 baseURL = "https://uberchromegw.corp.google.com/i/" | 252 baseURL = "https://uberchromegw.corp.google.com/i/" |
272 } | 253 } |
273 result.MachinePool = summarizeSlavePool(baseURL+master.Name, p.Slaves, m
aster.Slaves) | 254 result.MachinePool = summarizeSlavePool(baseURL+master.Name, p.Slaves, m
aster.Slaves) |
274 | 255 |
275 // This is CPU bound anyways, so there's no need to do this in parallel. | 256 // This is CPU bound anyways, so there's no need to do this in parallel. |
276 » finishedBuilds, nextCursor, err := getBuilds(c, masterName, builderName,
true, limit, thisCursor) | 257 » finishedBuilds, nextCursor, err := getBuilds(c, masterName, builderName,
true, limit, cursor) |
277 if err != nil { | 258 if err != nil { |
278 return nil, err | 259 return nil, err |
279 } | 260 } |
280 » if prevCursor, ok := maybeSetGetCursor(c, thisCursor, nextCursor, limit)
; ok { | 261 » if prevCursor, ok := maybeSetGetCursor(c, cursor, nextCursor, limit); ok
{ |
281 if prevCursor == nil { | 262 if prevCursor == nil { |
282 // Magic string to signal display prev without cursor | 263 // Magic string to signal display prev without cursor |
283 result.PrevCursor = "EMPTY" | 264 result.PrevCursor = "EMPTY" |
284 } else { | 265 } else { |
285 » » » result.PrevCursor = (*prevCursor).String() | 266 » » » result.PrevCursor = prevCursor.String() |
286 } | 267 } |
287 } | 268 } |
288 if nextCursor != nil { | 269 if nextCursor != nil { |
289 » » result.NextCursor = (*nextCursor).String() | 270 » » result.NextCursor = nextCursor.String() |
290 } | 271 } |
291 // Cursor is not needed for current builds. | 272 // Cursor is not needed for current builds. |
292 currentBuilds, _, err := getBuilds(c, masterName, builderName, false, 0,
nil) | 273 currentBuilds, _, err := getBuilds(c, masterName, builderName, false, 0,
nil) |
293 if err != nil { | 274 if err != nil { |
294 return nil, err | 275 return nil, err |
295 } | 276 } |
296 // currentBuilds is presented in reversed order, so flip it | 277 // currentBuilds is presented in reversed order, so flip it |
297 for i, j := 0, len(currentBuilds)-1; i < j; i, j = i+1, j-1 { | 278 for i, j := 0, len(currentBuilds)-1; i < j; i, j = i+1, j-1 { |
298 currentBuilds[i], currentBuilds[j] = currentBuilds[j], currentBu
ilds[i] | 279 currentBuilds[i], currentBuilds[j] = currentBuilds[j], currentBu
ilds[i] |
299 } | 280 } |
300 result.CurrentBuilds = currentBuilds | 281 result.CurrentBuilds = currentBuilds |
301 | 282 |
302 for _, fb := range finishedBuilds { | 283 for _, fb := range finishedBuilds { |
303 if fb != nil { | 284 if fb != nil { |
304 result.FinishedBuilds = append(result.FinishedBuilds, fb
) | 285 result.FinishedBuilds = append(result.FinishedBuilds, fb
) |
305 } | 286 } |
306 } | 287 } |
307 return result, nil | 288 return result, nil |
308 } | 289 } |
OLD | NEW |